feat(comicvine): release its connections and sqlite handles on close - #222
Merged
Merged
Conversation
`OnlineSession.close()` and the CLI's end-of-run release covered Metron only. simyan's Comic Vine client is `requests`-backed too, and holds more besides: the response cache's sqlite connection, one rate-limit bucket connection per endpoint pool, and pyrate-limiter's leaker thread. Left to finalization they surface as the `ResourceWarning` that asked for this. Closing the session is not enough on its own, and on its own is worse than nothing. requests-ratelimiter's `close` does ask pyrate-limiter to close the buckets, but `BucketFactory.close()` drops its leaker before iterating `get_buckets()`, which reads that leaker, so the loop closes nothing (pyrate-limiter 4.5.0). What it does do is drop the references that had been keeping those buckets alive, so a session-only close turns a silent leak into one `ResourceWarning` per pool: 0 to 5 measured on a run that touches every endpoint comicbox calls. So the close empties the factory's bucket registry and closes each bucket itself. Emptying first is what keeps a closed bucket from being handed back out to a racing lookup, which would die on its `None` connection; each bucket's own lock covers an acquire already in flight. A closed client stays usable, as Metron's does. The response cache reopens its connection lazily, and the hourly budget lives in the bucket file rather than the bucket object, so a rebuilt bucket resumes where the closed one left off -- `shared_client_rate_limit_status` reads the same numbers across a close. The one sharp edge, documented on `OnlineSession.close()`: a Comic Vine lookup that already holds a bucket can fail outright rather than merely reconnect, so close between files, not under one. tests/util's `close_comicvine_client` had grown the same bucket dance for test hygiene; it now delegates to the production close instead of being a second copy of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Answers F12: a Comic Vine counterpart to
OnlineSession.close(). v5.2.0'srelease call covered Metron only.
What a Comic Vine client holds
simyan's
Comicvine._sessionis aCachedLimiterSession(CacheMixin, LimiterMixin, Session)—requests-backed, like mokkari's. Per credentialset, after a run that touches every endpoint comicbox calls, it holds:
requests_cachesqlite connection (the response cache)pyrate_limiterSQLiteBucketconnections — one per endpoint pool(
issues,volumes,get_issue,get_volume,search), the samebucket_%tables_read_rate_limit_bucketsalready readsComicvineexposes nocloseof its own, so this goes through_session— private simyan surface, as
_maintain_cachealready does.Why the one-liner would have been worse than nothing
session.close()does cascade, and gets the response cache, the sockets andthe leaker thread. It does not get the buckets: requests-ratelimiter asks
pyrate-limiter to close them, but
BucketFactory.close()drops its leakerbefore iterating
get_buckets(), which reads that leaker — so the loop seesan empty list (pyrate-limiter 4.5.0).
What it does do is drop the references that had been keeping the buckets
alive. Measured
ResourceWarning: unclosed databaseat interpreter exit,same workload:
session.close()onlyA session-only close would have made the reported symptom louder, not
quieter.
What this does
close_shared_sessions()in the Comic Vine source, mirroringmetron_api's,wired into
OnlineSession.close()(and its context manager) and the CLI'send-of-run release. Per client: close the session, empty the factory's bucket
registry, then close each bucket. Emptying first is what keeps a closed
bucket from being handed back to a racing lookup, which would die on its
Noneconnection; each bucket's own lock covers an acquire already inflight.
A closed client stays usable, as Metron's does — the response cache reopens
lazily and the hourly budget lives in the bucket file, so a rebuilt bucket
resumes where the closed one left off. The sharp edge, documented on
OnlineSession.close(): a Comic Vine lookup that already holds a bucket canfail outright rather than merely reconnect, so close between files, not under
one. Codex's
_release_metron_connectionscalls this at session teardown,which fits.
Tests
tests/unit/test_comicvine_session_lifecycle.py, mirroring the Metron one:doubles for the ordering and tolerance contracts, and real-client tests that
every sqlite handle is released, that a closed client still works and still
reports its spent budget, and that the close emits no
ResourceWarning.Verified they fail without the bucket close (4 of 11, including the warning
one, which reproduces
unclosed database in <sqlite3.Connection ...>).tests/util'sclose_comicvine_clienthad grown the same dance for testhygiene; it now delegates to the production close.
make fix,make lint,make tyclean; 2252 passed, 1 skipped.Adjacent, not fixed here
mokkari's
close()is sockets-only by design and explicitly leaves acaller-supplied cache alone, and
mokkari.sqlite_cache.SqliteCachehas noclose()at all while holding its connection open for the process. comicboxbuilds that cache itself, so one unclosed sqlite connection remains on the
Metron side — same warning class, one handle instead of five.
🤖 Generated with Claude Code