Fix runtime SQLite contention and inference reliability - #104
Conversation
protostatis
left a comment
There was a problem hiding this comment.
Sky's Code Review
This PR hardens SQLite access (shared 30s busy-timeout, rollback-safe sync/async transactions, staggered scheduler writers, offloading blocking work from event loops), makes OpenRouter embeddings fail-closed with no silent local fallback, batches per-post embedding calls (title + up to 20 segments), and modernizes the experimental inference command to read live user_sentiment_scores/confounders with neutral suppression when social sentiment is stale. The changes are well-motivated, well-tested (249 passing), and generally correct. The write_transaction/sqlite_transaction helpers are clean and correctly guarantee rollback. I have only minor-to-moderate concerns, chiefly a behavior change in migration error handling that flips silent-ignore to fail-hard, and a couple of code-clarity issues around early returns inside transaction context managers.
Verdict: Comment
Comments
insert_sentiment_scorein storage/db.py appears to still use the old rawself.conn.execute(...)+await self.conn.commit()path rather thanwrite_transaction()(the diff shows its signature line but no body change). This leaves one write path un-serialized and non-rollback-safe relative to the others, which is inconsistent with the PR's stated goal. Worth confirming it was intentionally left out of scope or wrapping it too.save_post_scorenow callsget_or_create_useras a SEPARATE committed transaction before opening the insert transaction. This introduces a window where a user_profiles row is created/updated but the subsequent score insert can still fail, leaving an orphan last_seen bump (not a correctness bug since last_seen updates are idempotent, just noting the atomicity boundary changed from the original single-connection design).- The staggered scheduler uses a single
anchor = datetime.now(timezone.utc)and offset-based start dates. Tests verify 8 hours of non-collision, but note that intervals that are exact multiples of each other (e.g. crawl at 60s and price at 300s) can still drift back into alignment if job execution durations exceed the stagger offsets under load — the bounded busy-timeout plusmax_instances=1provides the real backstop, so this is acceptable defense-in-depth. - Overall the fail-closed embedding change is the most impactful and is correctly implemented with regression tests asserting
LocalSentenceTransformerProvideris never called on the OpenRouter error path. No secrets, no schema migration, and no journal-mode change are involved, which reduces rollout risk. Consider a follow-up to emit a clear startup log line whenEMBEDDING_BACKEND=openrouteris selected with an empty key so operators see the reason for the hard failure.
Reviewed by Sky — Unchained Sky engineering agent
| await self._connection.rollback() | ||
| if "duplicate column name" not in str(exc).lower(): | ||
| raise | ||
| except Exception: |
There was a problem hiding this comment.
Behavior change: the previous code silently swallowed ALL migration errors (comment 'Column already exists'). Now only 'duplicate column name' is tolerated and every other exception is re-raised after rollback. This is defensible hardening, but it means any previously-benign migration failure (e.g. an idempotency guard that returns a different message, or a rename/check that already applied) will now crash connect() and take the whole service down on startup. Verify that every existing migration's re-run path is genuinely idempotent or that its error text contains 'duplicate column name', otherwise this could break production rollout.
| cursor = conn.cursor() | ||
| cursor.execute( | ||
| "SELECT id FROM user_sentiment_scores WHERE raw_id = ?", | ||
| (post_score.raw_id,), |
There was a problem hiding this comment.
The if not update_existing: return -1 early-return now sits inside with sqlite_transaction(...), which means an empty transaction is COMMITTED on the 'already exists' path. It's not harmful (nothing was written), but it's a slight misuse of the helper's contract — the intent reads as 'rollback/skip', yet it commits. Consider resolving the existence check before opening the write transaction, or restructuring so the -1 return happens outside the context manager for clarity.
| cfg_backend = (settings.embedding_backend or "local").lower() | ||
| cfg_model = settings.embedding_model or "all-MiniLM-L6-v2" | ||
|
|
||
| if cfg_backend == "openrouter": |
There was a problem hiding this comment.
.lower() is now applied to settings.embedding_backend but the subsequent comparison if cfg_backend == 'openrouter' and the else ValueError use the lowered value, which is correct. However the previous fallback path also accepted any non-'openrouter' value as 'local'; now unknown values raise ValueError. Confirm no existing deployment has a non-lowercase or typo'd EMBEDDING_BACKEND (e.g. 'OpenRouter' or 'open_router') that will now crash startup rather than silently defaulting — the fail-closed intent is right, but the config collision risk is worth a doc/validation note.
| {coin_filter} | ||
| ), | ||
| latest_fear_greed AS ( | ||
| SELECT |
There was a problem hiding this comment.
The coin_filter string is interpolated into the query via f-string, but it only injects static SQL fragments and the coin value itself stays parameterized via ?, so there is no injection risk. Just confirm the params order remains [cutoff, coin?, fear_greed_cutoff] if the coin-filter branch ever adds additional placeholders in future edits, as the positional binding is order-sensitive and easy to break during refactoring.
Summary
Motivation
Production validation found transient
database is lockedfailures when scheduled writers aligned, a silent local fallback path for explicitly configured OpenRouter embeddings, up to 21 serial embedding calls per post, and a legacy inference path reading stalesentiment_scoresrows.Implementation
SQLite/runtime
Embeddings
Experimental inference
sentiment_scoresreads withuser_sentiment_scoresand recentconfoundersValidation
pytest tests/ -q: 249 passed, 1 skippedOperational notes
EMBEDDING_BACKEND=openrouternow requires a valid OpenRouter configuration and never changes embedding spaces silently