mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(ocr): unblock and harden accurate-OCR install (#552)
Two OCR-install fixes surfaced while verifying the accurate-OCR (v3 runtime) path end to end: - Installer timeout must be a safe integer, not a performance.now() float. With the default INSTALL_MAX_MS this failed every accurate-OCR install via the app right after the download (masked by the unpublished runtime; CI drives install_runtime.py directly so it never surfaced). Fixed via remainingInstallerTimeoutMs(). - Classify an absent or forbidden runtime index (401/403/404/410) as OcrRuntimeNotPublishedError with a clear "Fast OCR still works" message instead of a raw HTTP 404, without retrying. Refs #552
This commit is contained in:
@@ -65,6 +65,10 @@ const HARD_MAX_RETRY_ATTEMPTS = 6;
|
||||
const HARD_MAX_RETRY_DELAY_MS = 30_000;
|
||||
const HARD_MAX_RETRY_TOTAL_DELAY_MS = 120_000;
|
||||
const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
||||
// The bundle host answers a runtime index that was never published for this
|
||||
// version with one of these. On a public bundle repository they all mean the
|
||||
// same thing to a user: the accurate runtime is not available to install yet.
|
||||
const RUNTIME_NOT_PUBLISHED_STATUSES = new Set([401, 403, 404, 410]);
|
||||
const RETRYABLE_NETWORK_CODES = new Set([
|
||||
"EAI_AGAIN",
|
||||
"ECONNREFUSED",
|
||||
@@ -185,6 +189,22 @@ export class OcrRuntimeImportValidationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The signed OCR runtime index is not published (or not readable) for this
|
||||
* SnapOtter version. This is a definitive "nothing to install here" answer, not
|
||||
* a transient failure, so it is thrown without retrying and carries a message
|
||||
* the install UI can show verbatim.
|
||||
*/
|
||||
export class OcrRuntimeNotPublishedError extends Error {
|
||||
readonly httpStatus: number;
|
||||
|
||||
constructor(message: string, httpStatus: number, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = "OcrRuntimeNotPublishedError";
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
}
|
||||
|
||||
interface ResolvedRetryPolicy {
|
||||
maxAttempts: number;
|
||||
baseDelayMs: number;
|
||||
@@ -332,6 +352,22 @@ async function assertDownloadResponse(
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an absent (or forbidden) signed runtime index into a clear, terminal
|
||||
* error instead of a raw "HTTP 404". Callers invoke this before the generic
|
||||
* response check so the message reaches the install UI unretried and unwrapped.
|
||||
*/
|
||||
function assertOcrRuntimeIndexPublished(response: Response, version: string): void {
|
||||
if (!RUNTIME_NOT_PUBLISHED_STATUSES.has(response.status)) return;
|
||||
cancelResponseBody(response);
|
||||
throw new OcrRuntimeNotPublishedError(
|
||||
`The accurate OCR runtime for SnapOtter ${version} is not available to install yet ` +
|
||||
`(its signed runtime index returned HTTP ${response.status}). Fast OCR still works; ` +
|
||||
`accurate OCR becomes available once its runtime is published for this version.`,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
|
||||
function retryDelayMs(error: unknown, completedAttempts: number, policy: ResolvedRetryPolicy) {
|
||||
if (error instanceof RetryableOcrDownloadError && error.retryAfterMs !== null) {
|
||||
return Math.min(error.retryAfterMs, policy.maxDelayMs);
|
||||
@@ -1748,6 +1784,7 @@ async function downloadVerifiedRuntimeReleaseUnderLease(
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
assertOcrRuntimeIndexPublished(indexResponse, options.version);
|
||||
await assertDownloadResponse(indexResponse, "OCR runtime index", retryPolicy.now);
|
||||
return readBoundedResponse(
|
||||
indexResponse,
|
||||
@@ -1965,6 +2002,17 @@ async function purgeOcrRuntimeDownloadsUnderLease(aiDataDir: string): Promise<vo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaining installer time budget from an optional deadline. `performance.now()`
|
||||
* is fractional, so a raw `deadline - now` is a float that `runOcrRuntimeInstaller`
|
||||
* rejects as a non-integer timeout. Floor it to a safe integer and keep at least
|
||||
* 1ms so a live install always receives a positive, valid timeout.
|
||||
*/
|
||||
export function remainingInstallerTimeoutMs(deadlineMs: number | undefined, nowMs: number): number {
|
||||
if (deadlineMs === undefined) return 0;
|
||||
return Math.max(1, Math.floor(deadlineMs - nowMs));
|
||||
}
|
||||
|
||||
export function buildOcrRuntimeInstallerCommand(
|
||||
options: RunOcrRuntimeInstallerOptions,
|
||||
): OcrRuntimeInstallerCommand {
|
||||
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
prepareOfflineRuntimeIndex,
|
||||
prepareOfflineRuntimeRelease,
|
||||
purgeOcrRuntimeDownloads,
|
||||
remainingInstallerTimeoutMs,
|
||||
runOcrRuntimeInstaller,
|
||||
runOcrRuntimeMaintenance,
|
||||
waitWithOcrRuntimeHeartbeat,
|
||||
@@ -227,7 +228,7 @@ function startOcrInstall(bundleId: string, jobId: string, installLockFd: number)
|
||||
release,
|
||||
aiDataDir: getAiDir(),
|
||||
installLockFd,
|
||||
timeoutMs: installDeadline ? Math.max(1, installDeadline - performance.now()) : 0,
|
||||
timeoutMs: remainingInstallerTimeoutMs(installDeadline, performance.now()),
|
||||
}),
|
||||
reportActivation,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user