Skip to content

Comments

Fix SQLiteDiskIOException crash from disk storage exhaustion#15347

Draft
JorgeMucientes wants to merge 3 commits intotrunkfrom
issue/woomob-2170-fix-sqlite-disk-io-crash
Draft

Fix SQLiteDiskIOException crash from disk storage exhaustion#15347
JorgeMucientes wants to merge 3 commits intotrunkfrom
issue/woomob-2170-fix-sqlite-disk-io-crash

Conversation

@JorgeMucientes
Copy link
Contributor

@JorgeMucientes JorgeMucientes commented Feb 13, 2026

Closes WOOMOB-2170

Summary

Fixes the SQLiteDiskIOException: disk I/O error (code 4874 SQLITE_IOERR_SHMSIZE) crash that occurs when the device runs out of storage. The crash chain is:

  1. Unbounded log file growthLogFileWriter had no per-file size limit. On days with cascading errors, a single log file could grow indefinitely.
  2. Device storage exhaustion — Between log files, databases, caches, and other apps, the device hits 0 bytes free. Sentry confirms free_storage: 0.
  3. SQLite SHM allocation failure — Room executes PRAGMA journal_mode to configure WAL mode, which requires a .db-shm file. With no storage left, this fails with SQLITE_IOERR_SHMSIZE and the app crashes on launch.

Changes

Three complementary fixes across two modules:

1. Cap log file size and check disk space (LogFileWriter.kt)

  • Adds a 2 MB per-file size cap — writes are silently skipped once a log file reaches the limit. Combined with the existing 7-file rotation, total log footprint is capped at ~14 MB.
  • Adds a disk space check before writing — if available storage drops below 50 MB, log writes are skipped and the oldest log files are proactively deleted to free space.
  • availableDiskBytes is injected as a lambda to allow testing without real filesystem checks.

2. Graceful database recovery (WCDatabaseModule.kt)

  • Wraps WCAndroidDatabase.buildDb() in a try-catch for SQLiteDiskIOException.
  • On failure, clears log files (filtered by log_ prefix) under context.filesDir/logs to free space, then retries the database open exactly once.
  • If the retry also fails, the exception propagates normally (no infinite retry loop).
  • Uses AppLog for error logging, consistent with the rest of the fluxc-plugin module.

3. Unit tests (LogFileWriterTest.kt)

  • Tests that writes are skipped when a log file reaches the 2 MB cap.
  • Tests that writes succeed when the file is below the cap.
  • Tests that low disk space triggers oldest-log cleanup and skips the write.

Testing instructions

  1. Install the app and verify logs are written normally under typical conditions (check files/logs/ directory via Device File Explorer in Android Studio).
  2. Fill device storage to near-zero:
    adb shell fallocate -l 10G /data/local/tmp/fill_disk.bin
  3. Open the app and verify it does not crash on launch.
  4. With low storage, check that old log files under files/logs/ are cleaned up (only today's file should remain).
  5. Free space and verify logging resumes normally:
    adb shell rm /data/local/tmp/fill_disk.bin

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical crash (SQLiteDiskIOException: SQLITE_IOERR_SHMSIZE) caused by device storage exhaustion. The fix addresses unbounded log file growth that could consume all available disk space, preventing SQLite from creating necessary shared memory files. The solution adds defensive measures at both the log writing and database initialization layers.

Changes:

  • Implements size cap (2 MB per file) and disk space checks (50 MB minimum) in LogFileWriter to prevent unbounded log growth
  • Adds recovery logic in WCDatabaseModule to catch disk I/O exceptions, clear log files, and retry database initialization
  • Adds unit tests for the new size cap and disk space checking behavior

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.

File Description
libs/commons/src/main/java/com/woocommerce/android/util/logs/LogFileWriter.kt Adds per-file size cap, disk space checking, and proactive log cleanup when storage is low
libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/di/WCDatabaseModule.kt Wraps database initialization with exception handling to recover from disk I/O errors by clearing logs
libs/commons/src/test/kotlin/com/woocommerce/android/util/logs/LogFileWriterTest.kt Adds tests for size cap enforcement and low disk space handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 45 to 49
Log.e(
"WCDatabaseModule",
"Database open failed due to disk I/O error, freeing space and retrying",
e
)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The codebase convention in fluxc-plugin is to use AppLog for logging instead of android.util.Log. For example, see WCProductModel.kt:16-17, WooNetwork.kt:149, and WCCurrencyUtils.kt:67. Consider using AppLog.e() instead of Log.e() for consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.

private fun clearLogFiles(context: Context) {
val logsDir = File(context.filesDir, "logs")
logsDir.listFiles()?.forEach { it.delete() }
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clearLogFiles function deletes all files in the logs directory without checking if they are actually log files. If other files happen to be in this directory, they would also be deleted. Consider adding a filter to only delete files that match the log file pattern (e.g., files starting with "log_" prefix), similar to how it's done in LogFileWriter's rotateLogFilesIfNeeded method.

Suggested change
logsDir.listFiles()?.forEach { it.delete() }
logsDir.listFiles()
?.filter { it.isFile && it.name.startsWith("log_") }
?.forEach { it.delete() }

Copilot uses AI. Check for mistakes.
const val DATE_FORMAT_PATTERN = "yyyy-MM-dd"

private const val MAX_LOG_FILE_SIZE_BYTES = 2L * 1024 * 1024 // 2 MB per file
private const val MIN_DISK_SPACE_BYTES = 50L * 1024 * 1024 // 50 MB
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 50 MB minimum disk space threshold is quite high and might trigger cleanup unnecessarily on devices with limited storage. Users with 100-200 MB of free space would constantly have their logs cleared, even though there's still adequate space for normal app operation. Consider lowering this threshold (e.g., 10-20 MB) or making it configurable. Additionally, this check happens on every log write, which could impact performance - consider caching the result for a short period.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +46
if (!hasEnoughDiskSpace()) {
deleteOldestLogFiles()
return
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disk space check (hasEnoughDiskSpace) is called on every single log write operation. StatFs operations can be relatively expensive as they involve system calls. For high-frequency logging scenarios, this could impact performance. Consider caching the disk space check result for a short duration (e.g., 5-10 seconds) to reduce overhead while still catching low disk space conditions reasonably quickly.

Copilot uses AI. Check for mistakes.

private fun clearLogFiles(context: Context) {
val logsDir = File(context.filesDir, "logs")
logsDir.listFiles()?.forEach { it.delete() }
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clearLogFiles function should handle potential SecurityException or other exceptions that could occur during file deletion. While unlikely, file deletion can fail due to permission issues or files being locked. Consider wrapping the deletion in a try-catch block and logging any failures.

Suggested change
logsDir.listFiles()?.forEach { it.delete() }
logsDir.listFiles()?.forEach { file ->
try {
if (!file.delete()) {
Log.w(
"WCDatabaseModule",
"Failed to delete log file: ${file.absolutePath}"
)
}
} catch (e: SecurityException) {
Log.w(
"WCDatabaseModule",
"SecurityException while deleting log file: ${file.absolutePath}",
e
)
} catch (e: Exception) {
Log.w(
"WCDatabaseModule",
"Unexpected exception while deleting log file: ${file.absolutePath}",
e
)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +41
val exceedsMaxSize = withContext(dispatchers.io) {
logFile.length() >= MAX_LOG_FILE_SIZE_BYTES
}

if (exceedsMaxSize) {
return
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The file size check occurs outside the mutex, but the write happens inside it. This means multiple threads could check the size concurrently when the file is at 1.9 MB, both see it's below the limit, and both proceed to write, causing the file to exceed the 2 MB cap. The size check should be moved inside the mutex block (lines 48-52) to ensure atomicity between the size check and the write operation.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +137
private suspend fun deleteOldestLogFiles() {
withContext(dispatchers.io) {
mutex.withLock {
val logFiles = logsDirectory
.listFiles { file -> file.isFile && file.name.startsWith(LOG_FILE_NAME_PREFIX) }
?.sortedByDescending { it.lastModified() }
?: return@withLock

logFiles.drop(1).forEach { it.delete() }
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When disk space is low, this function deletes all log files except the most recent one. While this frees maximum space, it's very aggressive and results in losing potentially useful debugging information. Consider a more gradual approach: delete only the oldest file(s) until enough space is freed, or set a minimum number of files to preserve (e.g., keep at least 2-3 recent log files). This would balance freeing space with retaining useful logs for debugging.

Copilot uses AI. Check for mistakes.
Comment on lines 44 to 56
} catch (e: SQLiteDiskIOException) {
Log.e(
"WCDatabaseModule",
"Database open failed due to disk I/O error, freeing space and retrying",
e
)
clearLogFiles(context)
WCAndroidDatabase.buildDb(
context,
currencyPositionConverter,
statsGranularityConverter,
)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception handler catches SQLiteDiskIOException, but SQLite can throw this exception for various disk I/O errors, not just storage exhaustion (e.g., permission errors, filesystem corruption). Deleting log files and retrying may not help in those cases. Consider checking available disk space before clearing logs, or catching a more specific exception if possible. Also, the retry could fail for the same or a different reason, and that exception would propagate uncaught, potentially causing confusion about what actually failed.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +25
private val availableDiskBytes: () -> Long = {
runCatching { StatFs(logsDirectory.absolutePath).availableBytes }.getOrDefault(Long.MAX_VALUE)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production code in WooFileLogger instantiates LogFileWriter without passing the availableDiskBytes parameter (see WooFileLogger.kt:50-54). While this works because there's a default value, it means the disk space check will use the default StatFs implementation. Since this is the intended behavior for production, this is fine. However, consider documenting this in the class documentation to clarify that the parameter is primarily for testing.

Copilot uses AI. Check for mistakes.
private val dispatchers: CoroutineDispatchers
private val dispatchers: CoroutineDispatchers,
private val availableDiskBytes: () -> Long = {
runCatching { StatFs(logsDirectory.absolutePath).availableBytes }.getOrDefault(Long.MAX_VALUE)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StatFs constructor can throw IllegalArgumentException if the path doesn't exist. While runCatching handles this, the getOrDefault(Long.MAX_VALUE) fallback could mask real errors. If the logs directory doesn't exist, returning Long.MAX_VALUE makes the code think there's infinite disk space, which could be misleading. Consider creating the directory first or handling the case more explicitly.

Suggested change
runCatching { StatFs(logsDirectory.absolutePath).availableBytes }.getOrDefault(Long.MAX_VALUE)
if (!logsDirectory.exists()) {
logsDirectory.mkdirs()
}
runCatching { StatFs(logsDirectory.absolutePath).availableBytes }.getOrElse { 0L }

Copilot uses AI. Check for mistakes.
@codecov-commenter
Copy link

codecov-commenter commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 68.18182% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.90%. Comparing base (a19b4f8) to head (8636328).
⚠️ Report is 383 commits behind head on trunk.

Files with missing lines Patch % Lines
...com/woocommerce/android/util/logs/LogFileWriter.kt 68.18% 1 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##              trunk   #15347   +/-   ##
=========================================
  Coverage     38.89%   38.90%           
- Complexity    10829    10835    +6     
=========================================
  Files          2227     2227           
  Lines        127224   127246   +22     
  Branches      17684    17692    +8     
=========================================
+ Hits          49488    49508   +20     
+ Misses        72690    72688    -2     
- Partials       5046     5050    +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JorgeMucientes JorgeMucientes marked this pull request as draft February 20, 2026 11:22
@JorgeMucientes JorgeMucientes force-pushed the issue/woomob-2170-fix-sqlite-disk-io-crash branch from cefeaf8 to 8636328 Compare February 20, 2026 13:41
@dangermattic
Copy link
Collaborator

1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants