Repository navigation
fix(http): make the sqlite request cache thread-safe - #1200
Merged
Baz (bazarnov) merged 2 commits intoOct 6, 2026
Merged
Conversation
requests_cache shares one sqlite3 connection between threads and locks only writes. All partitions of a stream share one HttpClient, so cache reads raced other threads on that connection and failed with EOFError or "no more rows available", or crashed the interpreter on SQLite builds without serialized mode. Without REQUEST_CACHE_PATH every cache also shares one in-memory database, where concurrent connections fail with "database table is locked". - Hold the backend lock while reading in the existing SQLiteDict.__getitem__ monkeypatch and deserialize after releasing it. - Give every cache backend HttpClient creates one process-wide RLock. Fixes #1150
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This CDK VersionYou can test this version of the CDK using the following: # Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@bazarnov/fix-http-cache-sqlite-thread-safety#egg=airbyte-python-cdk[dev]' --help
# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch bazarnov/fix-http-cache-sqlite-thread-safetyPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
Daryna Ishchenko (darynaishchenko)
approved these changes
Oct 6, 2026
Baz (bazarnov)
deleted the
bazarnov/fix-http-cache-sqlite-thread-safety
branch
October 6, 2026 09:31
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.
Overview
👉 TL;DR: Stops the HTTP response cache from failing or crashing a sync when several worker threads use it at once. Every read and write of the cache now goes through one lock, so only one thread uses the cache database at a time.
Specifically,
monkey_patched_get_itemnow holds the backend lock while it reads, andHttpClient._request_sessiongives everyrequests_cacheSQLiteCacheit builds one process-wide_SQLITE_CACHE_LOCK.Changes
monkey_patched_get_item, the CDK's existing patch ofrequests_cache.SQLiteDict.__getitem__, runs itsSELECTunderself._lockand deserializes after releasing it. The docstring now explains the race and when the patch can go. The inline-key query from fix: monkey patch SQLiteDict in order to fix sqlite3.InterfaceError #725 is unchanged._SQLITE_CACHE_LOCK(threading.RLock).HttpClient._request_sessionbuilds eachSQLiteCacheunder it, since the constructor creates the tables, and assigns it to the backend'sresponsesandredirects. One shared lock also covers runs withoutREQUEST_CACHE_PATH, where every cache uses one in-memory database and a connection touching a table another one is writing fails at once withdatabase table is locked. Aponytail:comment names the ceiling: one lock per process, one lock per sqlite path if contention ever shows.test_http_client.py:test_cache_read_holds_the_lock_shared_by_every_cache(deterministic): two clients share one lock, and a read holds it throughexecute,fetchoneandclose.test_concurrent_cached_sends_do_not_fail[file|in_memory]: 2 clients with the same name, 8 threads, 50 requests each, mixing cache hits and writes.Review Spotlight
Reviewers with limited time, please review first:
http_client.py#L84-L101: reads hold the lock, deserialization stays outside ithttp_client.py#L106-L110: why every backend shares one lock, and its ceilinghttp_client.py#L225-L228: the backend is built under the lock and wired to ittest_http_client.py#L769-L801: deterministic test; each half of the fix alone still fails itRoot cause
The race is in
requests_cache: each table has onesqlite3connection shared by every thread (check_same_thread=False), and only writes take the lock, so reads run on a connection another thread is using (requests-cache#1190, still open). SeveralSQLiteCacheinstances on one file, which the issue also blames, are not a cause on their own: one client used by 8 threads fails by itself, while 2 clients with one thread each never fail.Production syncs surfaced it as
EOFError: Ran out of input,sqlite3.DatabaseError: no more rows availableandsqlite3.OperationalError: database table is locked. SQLite builds without serialized mode, such as the macOS system library, crash the interpreter instead.Risks
mastergo through a cachedHttpClient. 78 of them are directly exposed to the race (a cached parent that is also a top-level stream with more than one partition), for examplesource-github,source-gitlab,source-jiraandsource-zendesk-support. Counted by resolving all 535 connectormanifest.yamlfiles with the CDKManifestReferenceResolver(240 connectors with an auto-cachedSubstreamPartitionRouterparent), plusgit grepfor explicituse_cache: true(9),PropertiesFromEndpoint(1) and Pythonuse_cache(4).requests-cache1.3.x (58 of 163 connectorpoetry.lockfiles onmaster; the CDK locks 1.2.1): a write that fails with anything other thanOperationalErrorleaves its transaction open, and later writes on that connection retry forever. That already hangs the sync today; with one lock it blocks every cache in the process rather than the caches on one file. The race fixed here was the realistic source of such errors.Test plan
poetry run pytest unit_tests/sources/streams/http/test_http_client.py -k "lock_shared_by_every_cache or concurrent_cached_sends"on macOS (Python 3.11, system SQLite 3.51 without serialized mode):mainsource (git checkout origin/main -- airbyte_cdk/sources/streams/http/http_client.py): the deterministic test fails on the shared-lock assertion. The stress test failed in all 28 runs (14 per case): it crashes the interpreter (SIGSEGV, SIGILL, SIGBUS, SIGABRT), hangs, or fails with errors such asDatabaseError('file is not a database').first.responses._lock is second.responses._lock, shared lock only failsassert [False, False, False] == [True, True, True]. Movingfetchone()andclose()out of the locked block fails it with[True, False, False].The same three tests in the
airbyte/source-declarative-manifest:7.30.0image (Python 3.13.14, SQLite 3.40.1 serialized,requests-cache1.2.1), 10 runs each:maintest_cache_read_holds_the_lock_shared_by_every_cachetest_concurrent_cached_sends_do_not_fail[file]EOFError('Ran out of input'),DatabaseError('no more rows available')test_concurrent_cached_sends_do_not_fail[in_memory]OperationalError('database table is locked')poetry run pytest unit_tests/sources/declarative/test_concurrent_declarative_source.py -k test_read_with_concurrent_and_synchronous_streams, 5 runs on macOS:maincrashed the interpreter in 4 and failed in 1; the fix passed all 5.Ad-hoc stress script (cached clients with the same name, 8 threads, 60 mixed requests each, 10 runs): 0 failed runs with the fix, file and in-memory each: 4 clients on macOS, and 2 and 4 clients in the Linux image with
requests-cache1.2.1 and 1.3.3.poetry run pytest unit_tests/sources/streams/http unit_tests/sources/streams/test_call_rate.py unit_tests/test_requests_cache_monkeypatch_version.py: 556 passed.poetry run pytest unit_tests/sources/declarative/test_concurrent_declarative_source.py: 89 passed.ruff format,ruff checkandmypyare clean on the changed files.Fixes #1150