Repository navigation
Open caches read-only when their database or directory is not writable or when asked, and stop rewriting unchanged settings on open - #17
Merged
Conversation
…e or when asked, and stop rewriting unchanged settings, triggers and the tag index on open
avgvi
force-pushed
the
feat/read-only-caches
branch
from
October 11, 2026 05:44
56148fe to
a2846bd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Upstream issue 113, open since 2019 with 14 comments: a
cache.dbsnapshot used only for reads was modified on every run, and a cache on a read-only file system or container layer could not be opened at all, because every open rewrote all settings, re-issuedPRAGMA journal_mode, dropped and recreated the tag index and the statistics counters wrote on reads.No writes on open when nothing changed. Settings are written only when missing or different from the stored row, metadata rows only when missing, the tag index is created or dropped only when
sqlite_masterdisagrees with the setting, and the trigger rewrite already compared first. A test reopens a cache, reads every value type, iterates, checks and closes, twice, and asserts thecache.dbhash and mtime are unchanged with no side files. What still writes on a writable cache: the hit and miss counters withstatistics=1, the access columns for the LRU and LFU policies, andpeek/peekitemdeleting expired rows they encounter.Read-only mode.
Cache(directory, read_only=True), a keyword-only constructor argument that is never persisted and passes throughFanoutCacheand the DjangoOPTIONS, or automatically whencache.dbexists and the file or its directory is not writable. The connection opens with SQLite'smode=roURI, addingimmutable=1only when the directory cannot hold the WAL side files, which is safe because nothing can write there. Reads,iterkeys, iteration,len,volume,check(fix=False), memoize hits andDeque,Index,FanoutCacheandDjangoCachereads all work;getskips the statistics counters and the eviction access update;peekandpeekitemfilter expired rows in SQL instead of deleting them;stats()reads the stored counters; persistent pragmas are not issued. Every writing method raisesReadOnlyErrorbefore any side effect; it subclassessqlite3.OperationalError, the error SQLite itself raises for this case, so existing handlers keep working. Transactions use a deferredBEGINin read-only mode.read_only=Trueon a directory without a database raises instead of creating one. ACache.read_onlyproperty reports the mode.Refactor for pylint: the read-only decision, the settings loop and the DDL moved into three small methods;
max-public-methodsgoes from 30 to 32.Tests (
tests/test_readonly.py, 15): no-write reopen; only changed settings write; tag index only when changed; statistics write on reads but not when read-only; explicit read-only with every write raising and the database unchanged; missing database; the error type; all eviction policies; a write insidetransact(); a read-only file (runs on Windows too); a read-only directory with immutable mode (POSIX, skipped as root); a read-only directory without aSettingstable;FanoutCache,Deque,IndexandDjangoCache; a snapshot without metadata rows. Tutorial section "Read-only caches" and the Cache docstring.Verified locally: 372 tests pass at 98.89 % on Python 3.12 with Django 4.2 and 373 on 3.14 with Django 6.0 (reverse-direction golden test enabled); bluecheck, isortcheck, flake8, mypy, pylint, doc8, rstcheck and the Sphinx build pass. No schema change, no new persisted key, writes byte-identical to before. Rebased onto PR 16 once that merges; one expected conflict in
Cache.get.