From 3febbe80cfe099d9efa4e10db33fe8fd649af5a1 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Wed, 29 Jul 2026 17:12:02 +0200 Subject: [PATCH 1/7] Classify SQLite disk-pressure errors and stop retrying them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full device disk surfaces as "disk I/O error" (SQLITE_IOERR, shm sizing on open), "unable to open database file" (SQLITE_CANTOPEN), or "cannot rollback - no transaction is active" (failed ROLLBACK masking the original write error). All were UNKNOWN: 5 futile retries per operation plus per-operation alerts. New DISK_PRESSURE class drops the write (cache stays authoritative), skips retries and eviction (neither frees OS-level space), and logs one throttled alert + storage quota snapshot per burst — the snapshot carries free-disk bytes so telemetry can confirm disk pressure as the root cause. Co-Authored-By: Claude Fable 5 --- lib/OnyxUtils.ts | 29 +++++++++++++ lib/storage/errors.ts | 4 ++ lib/storage/providers/classifySQLiteError.ts | 9 ++++ tests/unit/onyxUtilsTest.ts | 44 +++++++++++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 4dc1ba2ce..ad5131c95 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -52,6 +52,16 @@ const METHOD = { // Max number of retries for failed storage operations const MAX_STORAGE_OPERATION_RETRY_ATTEMPTS = 5; +/** Minimum interval between disk-pressure alerts. One disk-pressure burst fails every queued operation + * with the identical error, so per-operation logging would amplify the very storm it reports. */ +const DISK_PRESSURE_LOG_INTERVAL_MS = 60000; +let lastDiskPressureLogTime = 0; + +/** Test-only: clears the disk-pressure log throttle so each test observes its own alert. */ +function resetDiskPressureLogThrottle(): void { + lastDiskPressureLogTime = 0; +} + type OnyxMethod = ValueOf; // Key/value store of Onyx key and arrays of values to merge @@ -783,6 +793,9 @@ function reportStorageQuota(error?: Error): Promise { * - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level * circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making * progress or failures storm — the per-operation budget alone cannot stop a session-wide storm. + * - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither + * retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with + * a single throttled alert + quota snapshot per burst. * - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message + * provider) once so it's visible, then bounded retry without eviction. */ @@ -806,6 +819,21 @@ function retryOperation( return Promise.resolve(); } + // DISK_PRESSURE: the filesystem around the database is out of space or its files are unreadable, so + // neither retries nor in-DB eviction can succeed — every operation fails identically until the OS + // frees space, and the burst recovers on its own once it does. Drop the write (the cache stays + // authoritative) and log one alert + quota snapshot per interval instead of one per operation; the + // quota snapshot carries the free-disk bytes that confirm (or rule out) disk pressure in telemetry. + if (errorClass === StorageErrorClass.DISK_PRESSURE) { + const now = Date.now(); + if (now - lastDiskPressureLogTime < DISK_PRESSURE_LOG_INTERVAL_MS) { + return Promise.resolve(); + } + lastDiskPressureLogTime = now; + Logger.logAlert(`Disk-pressure storage error; skipping retries. provider: ${Storage.getStorageProvider().name}. message: ${error?.message}. onyxMethod: ${onyxMethod.name}.`); + return reportStorageQuota(error); + } + Logger.logInfo( `Failed to save to storage. Error: ${error}. class: ${errorClass}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS}`, ); @@ -1816,6 +1844,7 @@ const OnyxUtils = { getCollectionDataAndSendAsObject, remove, reportStorageQuota, + resetDiskPressureLogThrottle, retryOperation, broadcastUpdate, hasPendingMergeForKey, diff --git a/lib/storage/errors.ts b/lib/storage/errors.ts index 635f55428..631b01535 100644 --- a/lib/storage/errors.ts +++ b/lib/storage/errors.ts @@ -15,6 +15,10 @@ const StorageErrorClass = { TRANSIENT: 'transient', /** Quota exceeded / disk full. Owner: operation layer — evict and retry. */ CAPACITY: 'capacity', + /** Filesystem-level failure around the database files (device disk full, or the files cannot be + * created/read). Owner: operation layer — skip retries and eviction (neither can free OS-level + * space) and log one throttled alert + quota snapshot per burst. */ + DISK_PRESSURE: 'diskPressure', /** Non-serializable payload. Never retriable — the same data will always fail. */ INVALID_DATA: 'invalidData', /** Backing-store corruption. Owner: connection layer — budgeted heal, then give up. */ diff --git a/lib/storage/providers/classifySQLiteError.ts b/lib/storage/providers/classifySQLiteError.ts index 2105764c2..c1cbc3018 100644 --- a/lib/storage/providers/classifySQLiteError.ts +++ b/lib/storage/providers/classifySQLiteError.ts @@ -18,6 +18,15 @@ function classifySQLiteError(error: unknown): ValueOf return StorageErrorClass.CAPACITY; } + // Filesystem-level failures around the database files, seen when the device disk is (nearly) full. + // "disk I/O error" (SQLITE_IOERR) fires on every operation — reads included — after SQLite fails to + // size the -shm file while (re)opening the database on a full disk; "unable to open database file" + // (SQLITE_CANTOPEN) when the -shm file cannot be created at all; "cannot rollback" is a batch-write + // failure whose original error was masked by the ROLLBACK itself failing on the same full disk. + if (message.includes('disk i/o error') || message.includes('unable to open database file') || message.includes('cannot rollback - no transaction is active')) { + return StorageErrorClass.DISK_PRESSURE; + } + return StorageErrorClass.UNKNOWN; } diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 93899cfdc..b4f92309e 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -748,8 +748,12 @@ describe('OnyxUtils', () => { const diskFullError = new Error('database or disk is full'); const nonRetriableIdbError = Object.assign(new Error('Internal error opening backing store for indexedDB.open.'), {name: 'UnknownError'}); - // The circuit breaker is process-scoped, so reset it between tests to avoid state leaking. - beforeEach(() => StorageCircuitBreaker.reset()); + // The circuit breaker and the disk-pressure log throttle are process-scoped, so reset them + // between tests to avoid state leaking. + beforeEach(() => { + StorageCircuitBreaker.reset(); + OnyxUtils.resetDiskPressureLogThrottle(); + }); it('should retry only one time if the operation is firstly failed and then passed', async () => { StorageMock.setItem = jest.fn(StorageMock.setItem).mockRejectedValueOnce(genericError).mockImplementation(StorageMock.setItem); @@ -808,6 +812,42 @@ describe('OnyxUtils', () => { expect(retryOperationSpy).toHaveBeenCalledTimes(1); }); + it.each([ + ['[NativeNitroSQLiteException][SqlExecutionError] disk I/O error'], + ['[NativeNitroSQLiteException][SqlExecutionError] unable to open database file'], + ['[NativeNitroSQLiteException][SqlExecutionError] cannot rollback - no transaction is active'], + ])('should not retry disk-pressure errors (%s)', async (message) => { + StorageMock.setItem = jest.fn().mockRejectedValue(new Error(message)); + + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + + // Called once (initial attempt only): retries cannot succeed while the device disk is full. + expect(retryOperationSpy).toHaveBeenCalledTimes(1); + }); + + it('should log a single throttled alert with a quota snapshot for a disk-pressure burst', async () => { + const logAlertSpy = jest.spyOn(Logger, 'logAlert'); + const logInfoSpy = jest.spyOn(Logger, 'logInfo'); + const diskIOError = new Error('[NativeNitroSQLiteException][SqlExecutionError] disk I/O error'); + StorageMock.setItem = jest.fn().mockRejectedValue(diskIOError); + + // A burst: several operations all failing with the identical error. + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data2'}); + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data3'}); + + // One alert for the whole burst (the rest are throttled within DISK_PRESSURE_LOG_INTERVAL_MS)... + const alerts = logAlertSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].startsWith('Disk-pressure storage error')); + expect(alerts).toHaveLength(1); + expect(alerts[0][0]).toBe(`Disk-pressure storage error; skipping retries. provider: MemoryOnlyProvider. message: ${diskIOError.message}. onyxMethod: setWithRetry.`); + // ...paired with exactly one quota snapshot (carries the free-disk bytes on native platforms). + const quotaLogs = logInfoSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].startsWith('Storage Quota Check')); + expect(quotaLogs).toHaveLength(1); + // And no "failed after N retries" alert: the writes were dropped, not retried to exhaustion. + const retryAlerts = logAlertSpy.mock.calls.filter((call) => typeof call[0] === 'string' && call[0].startsWith('Storage operation failed after')); + expect(retryAlerts).toHaveLength(0); + }); + it('should skip retry quietly (info, not alert) for fatal connection-layer errors', async () => { const logAlertSpy = jest.spyOn(Logger, 'logAlert'); const logInfoSpy = jest.spyOn(Logger, 'logInfo'); From 5de9702d18793a5542491e9fb0b653511a66ba98 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Wed, 29 Jul 2026 17:31:08 +0200 Subject: [PATCH 2/7] Shorten disk-pressure comments Co-Authored-By: Claude Fable 5 --- lib/OnyxUtils.ts | 8 +++----- lib/storage/providers/classifySQLiteError.ts | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index ad5131c95..31c50c38e 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -819,11 +819,9 @@ function retryOperation( return Promise.resolve(); } - // DISK_PRESSURE: the filesystem around the database is out of space or its files are unreadable, so - // neither retries nor in-DB eviction can succeed — every operation fails identically until the OS - // frees space, and the burst recovers on its own once it does. Drop the write (the cache stays - // authoritative) and log one alert + quota snapshot per interval instead of one per operation; the - // quota snapshot carries the free-disk bytes that confirm (or rule out) disk pressure in telemetry. + // DISK_PRESSURE: the device disk is full, so neither retries nor eviction can succeed until the OS + // frees space. Drop the write (cache stays authoritative) and log one alert + quota snapshot per + // interval — the snapshot's free-disk bytes let telemetry confirm (or rule out) disk pressure. if (errorClass === StorageErrorClass.DISK_PRESSURE) { const now = Date.now(); if (now - lastDiskPressureLogTime < DISK_PRESSURE_LOG_INTERVAL_MS) { diff --git a/lib/storage/providers/classifySQLiteError.ts b/lib/storage/providers/classifySQLiteError.ts index c1cbc3018..651fc7ce5 100644 --- a/lib/storage/providers/classifySQLiteError.ts +++ b/lib/storage/providers/classifySQLiteError.ts @@ -18,11 +18,9 @@ function classifySQLiteError(error: unknown): ValueOf return StorageErrorClass.CAPACITY; } - // Filesystem-level failures around the database files, seen when the device disk is (nearly) full. - // "disk I/O error" (SQLITE_IOERR) fires on every operation — reads included — after SQLite fails to - // size the -shm file while (re)opening the database on a full disk; "unable to open database file" - // (SQLITE_CANTOPEN) when the -shm file cannot be created at all; "cannot rollback" is a batch-write - // failure whose original error was masked by the ROLLBACK itself failing on the same full disk. + // Full-disk failures around the database files: SQLITE_IOERR (cannot size the -shm file on reopen, + // fails reads too), SQLITE_CANTOPEN (cannot create it), and a failed ROLLBACK masking the original + // write error. None can succeed until the OS frees space. if (message.includes('disk i/o error') || message.includes('unable to open database file') || message.includes('cannot rollback - no transaction is active')) { return StorageErrorClass.DISK_PRESSURE; } From 2c1c4e8f3ffb5c55beecc0be66b7e95ec4d2b0db Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Wed, 5 Aug 2026 13:15:00 +0200 Subject: [PATCH 3/7] Drop the generic rollback message from disk-pressure classification "cannot rollback - no transaction is active" follows any error that aborted the transaction, not just disk-full, so let it fall through to UNKNOWN's bounded retry; a genuine full disk still surfaces as disk I/O on retry. Co-Authored-By: Claude Fable 5 --- lib/storage/providers/classifySQLiteError.ts | 5 ++--- tests/unit/onyxUtilsTest.ts | 19 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/storage/providers/classifySQLiteError.ts b/lib/storage/providers/classifySQLiteError.ts index 651fc7ce5..3696c106f 100644 --- a/lib/storage/providers/classifySQLiteError.ts +++ b/lib/storage/providers/classifySQLiteError.ts @@ -19,9 +19,8 @@ function classifySQLiteError(error: unknown): ValueOf } // Full-disk failures around the database files: SQLITE_IOERR (cannot size the -shm file on reopen, - // fails reads too), SQLITE_CANTOPEN (cannot create it), and a failed ROLLBACK masking the original - // write error. None can succeed until the OS frees space. - if (message.includes('disk i/o error') || message.includes('unable to open database file') || message.includes('cannot rollback - no transaction is active')) { + // fails reads too) and SQLITE_CANTOPEN (cannot create it). Neither can succeed until the OS frees space. + if (message.includes('disk i/o error') || message.includes('unable to open database file')) { return StorageErrorClass.DISK_PRESSURE; } diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index b4f92309e..0a20a7d21 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -812,18 +812,17 @@ describe('OnyxUtils', () => { expect(retryOperationSpy).toHaveBeenCalledTimes(1); }); - it.each([ - ['[NativeNitroSQLiteException][SqlExecutionError] disk I/O error'], - ['[NativeNitroSQLiteException][SqlExecutionError] unable to open database file'], - ['[NativeNitroSQLiteException][SqlExecutionError] cannot rollback - no transaction is active'], - ])('should not retry disk-pressure errors (%s)', async (message) => { - StorageMock.setItem = jest.fn().mockRejectedValue(new Error(message)); + it.each([['[NativeNitroSQLiteException][SqlExecutionError] disk I/O error'], ['[NativeNitroSQLiteException][SqlExecutionError] unable to open database file']])( + 'should not retry disk-pressure errors (%s)', + async (message) => { + StorageMock.setItem = jest.fn().mockRejectedValue(new Error(message)); - await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); + await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); - // Called once (initial attempt only): retries cannot succeed while the device disk is full. - expect(retryOperationSpy).toHaveBeenCalledTimes(1); - }); + // Called once (initial attempt only): retries cannot succeed while the device disk is full. + expect(retryOperationSpy).toHaveBeenCalledTimes(1); + }, + ); it('should log a single throttled alert with a quota snapshot for a disk-pressure burst', async () => { const logAlertSpy = jest.spyOn(Logger, 'logAlert'); From d7abb2c7b77009990fa7ed62a7b8a609788a8261 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 6 Aug 2026 10:11:46 +0200 Subject: [PATCH 4/7] Document why the rollback message stays UNKNOWN "cannot rollback - no transaction is active" is a mask: nitro-sqlite's rollback-on-error replaced the original failure until 9.7.0, so classifying it as disk pressure would be a guess. Co-Authored-By: Claude Fable 5 --- lib/storage/providers/classifySQLiteError.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/storage/providers/classifySQLiteError.ts b/lib/storage/providers/classifySQLiteError.ts index 3696c106f..f5a67009c 100644 --- a/lib/storage/providers/classifySQLiteError.ts +++ b/lib/storage/providers/classifySQLiteError.ts @@ -24,6 +24,9 @@ function classifySQLiteError(error: unknown): ValueOf return StorageErrorClass.DISK_PRESSURE; } + // "cannot rollback - no transaction is active" lands here deliberately: it is a mask, not a cause — + // nitro-sqlite's rollback-on-error replaced the original failure until 9.7.0. UNKNOWN gives it + // bounded retry + shape logging instead of a disk-pressure guess. return StorageErrorClass.UNKNOWN; } From 994f2eea18f489793bf191253cdad1d43632aecc Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 6 Aug 2026 10:26:00 +0200 Subject: [PATCH 5/7] Regenerate API docs for disk-pressure additions Co-Authored-By: Claude Fable 5 --- API-INTERNAL.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/API-INTERNAL.md b/API-INTERNAL.md index 54dc85d21..bc8c0b467 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -2,9 +2,21 @@ # Internal API Reference +## Constants + +
+
DISK_PRESSURE_LOG_INTERVAL_MS
+

Minimum interval between disk-pressure alerts. One disk-pressure burst fails every queued operation +with the identical error, so per-operation logging would amplify the very storm it reports.

+
+
+ ## Functions
+
resetDiskPressureLogThrottle()
+

Test-only: clears the disk-pressure log throttle so each test observes its own alert.

+
getMergeQueue()

Getter - returns the merge queue.

@@ -89,6 +101,9 @@ and alerted (fatal). Retrying here would only re-amplify, so we skip the write q
  • CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
  • +
  • DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither +retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with +a single throttled alert + quota snapshot per burst.
  • UNKNOWN: the provider couldn't classify it — log the full error shape (name + message + provider) once so it's visible, then bounded retry without eviction.
  • @@ -157,6 +172,19 @@ Retries on failure.

    + + +## DISK\_PRESSURE\_LOG\_INTERVAL\_MS +Minimum interval between disk-pressure alerts. One disk-pressure burst fails every queued operation +with the identical error, so per-operation logging would amplify the very storm it reports. + +**Kind**: global constant + + +## resetDiskPressureLogThrottle() +Test-only: clears the disk-pressure log throttle so each test observes its own alert. + +**Kind**: global function ## getMergeQueue() @@ -333,6 +361,9 @@ capacity recovery (eviction) so that a given failure is retried by exactly one l - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making progress or failures storm — the per-operation budget alone cannot stop a session-wide storm. +- DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither + retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with + a single throttled alert + quota snapshot per burst. - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message + provider) once so it's visible, then bounded retry without eviction. From c78c5786743e1a198908a2921a3fc49effd525b5 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Thu, 6 Aug 2026 11:50:54 +0200 Subject: [PATCH 6/7] Keep the free-disk snapshot when SQLite PRAGMAs fail under disk pressure getDatabaseSize ran the PRAGMAs and getFreeDiskStorage in one Promise.all, so the burst snapshot lost its free-disk bytes exactly when the connection was failing. Degrade bytesUsed to -1 instead and always return the filesystem value. Co-Authored-By: Claude Fable 5 --- lib/storage/providers/SQLiteProvider.ts | 21 ++++++++++++------- .../storage/providers/SQLiteProviderTest.ts | 10 +++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts index c03efb66e..8a0de23bc 100644 --- a/lib/storage/providers/SQLiteProvider.ts +++ b/lib/storage/providers/SQLiteProvider.ts @@ -325,16 +325,21 @@ const provider: StorageProvider = { throw new Error('Store is not initialized!'); } - return Promise.all([provider.store.executeAsync('PRAGMA page_size;'), provider.store.executeAsync('PRAGMA page_count;'), getFreeDiskStorage()]).then( - ([pageSizeResult, pageCountResult, bytesRemaining]) => { + // The PRAGMAs run on the SQLite connection — the very thing that's broken during disk pressure — + // while getFreeDiskStorage() is filesystem-level and still works. Degrade bytesUsed to -1 instead + // of failing the snapshot, so the free-disk bytes (the signal that matters) always get logged. + const bytesUsedPromise = Promise.all([provider.store.executeAsync('PRAGMA page_size;'), provider.store.executeAsync('PRAGMA page_count;')]) + .then(([pageSizeResult, pageCountResult]) => { const pageSize = pageSizeResult.rows?.item(0)?.page_size ?? 0; const pageCount = pageCountResult.rows?.item(0)?.page_count ?? 0; - return { - bytesUsed: pageSize * pageCount, - bytesRemaining, - }; - }, - ); + return pageSize * pageCount; + }) + .catch(() => -1); + + return Promise.all([bytesUsedPromise, getFreeDiskStorage()]).then(([bytesUsed, bytesRemaining]) => ({ + bytesUsed, + bytesRemaining, + })); }, }; diff --git a/tests/unit/storage/providers/SQLiteProviderTest.ts b/tests/unit/storage/providers/SQLiteProviderTest.ts index 4d82b2e31..55da98760 100644 --- a/tests/unit/storage/providers/SQLiteProviderTest.ts +++ b/tests/unit/storage/providers/SQLiteProviderTest.ts @@ -453,5 +453,15 @@ describe('SQLiteProvider', () => { expect(after.bytesUsed).toBeGreaterThan(before.bytesUsed); }); + + it('should still report free-disk bytes when the PRAGMAs fail (disk pressure)', async () => { + // During disk pressure the SQLite connection itself fails, but free-disk comes from the + // filesystem — the snapshot must survive with bytesUsed degraded instead of rejecting. + const executeAsyncSpy = jest.spyOn(SQLiteProvider.store!, 'executeAsync').mockRejectedValue(new Error('[NativeNitroSQLiteException][SqlExecutionError] disk I/O error')); + + await expect(SQLiteProvider.getDatabaseSize()).resolves.toEqual({bytesUsed: -1, bytesRemaining: 12345}); + + executeAsyncSpy.mockRestore(); + }); }); }); From 5378d4deaac30313804b9a7979491945eddd8e8f Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Fri, 7 Aug 2026 12:29:22 +0200 Subject: [PATCH 7/7] Tighten and remove verbose classifier comments per review Co-Authored-By: Claude Fable 5 --- lib/storage/providers/SQLiteProvider.ts | 5 ++--- lib/storage/providers/classifySQLiteError.ts | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts index 8a0de23bc..013139120 100644 --- a/lib/storage/providers/SQLiteProvider.ts +++ b/lib/storage/providers/SQLiteProvider.ts @@ -325,9 +325,8 @@ const provider: StorageProvider = { throw new Error('Store is not initialized!'); } - // The PRAGMAs run on the SQLite connection — the very thing that's broken during disk pressure — - // while getFreeDiskStorage() is filesystem-level and still works. Degrade bytesUsed to -1 instead - // of failing the snapshot, so the free-disk bytes (the signal that matters) always get logged. + // The PRAGMAs need the SQLite connection; getFreeDiskStorage() is filesystem-level. Degrade + // bytesUsed to -1 instead of failing, so the free-disk bytes always get logged. const bytesUsedPromise = Promise.all([provider.store.executeAsync('PRAGMA page_size;'), provider.store.executeAsync('PRAGMA page_count;')]) .then(([pageSizeResult, pageCountResult]) => { const pageSize = pageSizeResult.rows?.item(0)?.page_size ?? 0; diff --git a/lib/storage/providers/classifySQLiteError.ts b/lib/storage/providers/classifySQLiteError.ts index f5a67009c..06d4f7d51 100644 --- a/lib/storage/providers/classifySQLiteError.ts +++ b/lib/storage/providers/classifySQLiteError.ts @@ -18,15 +18,10 @@ function classifySQLiteError(error: unknown): ValueOf return StorageErrorClass.CAPACITY; } - // Full-disk failures around the database files: SQLITE_IOERR (cannot size the -shm file on reopen, - // fails reads too) and SQLITE_CANTOPEN (cannot create it). Neither can succeed until the OS frees space. if (message.includes('disk i/o error') || message.includes('unable to open database file')) { return StorageErrorClass.DISK_PRESSURE; } - // "cannot rollback - no transaction is active" lands here deliberately: it is a mask, not a cause — - // nitro-sqlite's rollback-on-error replaced the original failure until 9.7.0. UNKNOWN gives it - // bounded retry + shape logging instead of a disk-pressure guess. return StorageErrorClass.UNKNOWN; }