Refresh table metadata when psycodict announces schema changes#45
Draft
roed-math wants to merge 12 commits into
Draft
Refresh table metadata when psycodict announces schema changes#45roed-math wants to merge 12 commits into
roed-math wants to merge 12 commits into
Conversation
Follow-up to LMFDB#7064, which enabled TCP keepalives on the PostgreSQL connection with hardcoded values. Expose them as command-line / config.ini options instead, so each site can tune or disable them: --postgresql-keepalives (0|1, default 1) --postgresql-keepalives-idle (seconds, default 30) --postgresql-keepalives-interval (seconds, default 10) --postgresql-keepalives-count (default 5) The defaults are unchanged, so behaviour is identical out of the box, but a production or development site can now pass --postgresql-keepalives 0 to fall back to the OS defaults, or adjust the timings, without editing code. These land in options["postgresql"] via the normal argparse/config machinery, which also fixes the previous approach: the hardcoded setdefault() ran after the config was parsed, so a value in config.ini was silently ignored (contrary to the comment claiming it took precedence). Now a config.ini or command-line value is genuinely respected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last LMFDB tables using the search/extras split (maass_newforms, maass_rigor) have had their extras columns merged into the search table, and psycodict is removing support for the mechanism entirely before its 1.0 release (roed314/psycodict#59). - lmfdb_database.py: column_description and drop_table no longer append self.extra_cols (empty for every table now); create_extra_table and move_column leave _nolog_changetypes, since neither operation exists - knowledge/main.py: the missing-column-knowls report drops extra_cols - api/api.py: stop building extra_schema for the collection and data pages, and stop passing it to the templates - api/templates/apischema.html: drop the extra-columns rows This is compatible with both the current psycodict and the version that removes extras support, since extra_cols is [] for every table today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stop referring to psycodict extras tables
psycodict is switching from psycopg2 to psycopg3 (roed314/psycodict#88). Query fragments composed in the LMFDB are executed by psycodict's _execute, so they must come from whichever driver psycodict is built on -- and since both drivers can be installed at once, try/except imports are not a valid probe. The new lmfdb/utils/psycopg_compat.py keys off psycodict's re-exported SQL and provides SQL, Identifier, Placeholder, Literal, Composable, Composed plus DatabaseError, DataError, NumericValueOutOfRange and QueryCanceledError from the matching driver. All sixteen direct psycopg2 imports (search/verify/knowledge/users/api/groups/ecnf, the dynamic-knowls test and the old belyi script) now import from it, so the LMFDB works unchanged on either side of psycodict's transition; once psycodict is pinned past it, the module body can shrink to plain psycopg imports. requirements.txt now asks for psycodict[pgbinary], which installs whichever driver the checked-out psycodict needs (psycopg2-binary today, psycopg[binary] after the port) instead of naming psycopg2 directly. Verified by loading the compat module against both psycodict versions: all names resolve from the matching driver, composed statements execute through _execute, and a statement_timeout raises the exported QueryCanceledError under both -- that exception is what turns timeouts into the 'search took too long' page rather than a 500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
psycodict is underscore-prefixing methods that are not part of its public API. Rename the log_db_change override to _log_db_change (keeping the old name bound so the hook works under either psycodict), and call is_alive / can_read_write_knowls / can_read_write_userdb through version-tolerant getattr fallbacks, so this can merge before or after the psycodict change. Simplify once psycodict is pinned. The db.cursor() calls in riemann/stieltjes and zeros/zeta are sqlite3 connections, not psycodict, and are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Track psycodict's driver instead of importing psycopg2 directly
Follow psycodict's API narrowing (psycodict#92)
…nfig Make the database TCP keepalive settings configurable
…hecks check_external intended to tolerate unreachable external servers, but the errno check never fired: urllib wraps the underlying socket error, so URLError.errno is always None (the real errno lives on e.reason.errno). Connection timeouts therefore failed CI even though refused connections were tolerated via a string fallback. Look at e.reason instead, catch bare TimeoutError/ConnectionError too (read timeouts are not wrapped in URLError), skip visibly rather than passing silently, and bound each request with a 30s timeout instead of the ~130s kernel connect timeout. Also remove the two downloads from beta.lmfdb.org in test_mf_hecke_cc_dataset: they exercised the deployed beta site rather than the code under test, and their connect timeouts were the single largest cause of CI failures. External link checking is tracked in LMFDB#6225. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix flaky CI: tolerate unreachable external servers, remove beta download checks
Companion to roed314/psycodict#111: each web worker keeps a NotificationListener subscribed to the psycodict_schema channel and, on a non-blocking poll from a before_request hook, calls db.refresh_tables() when a schema change is announced, so column and table changes become visible without restarting workers. Reconnects with a catch-up refresh after listener failures, and is a no-op (one log line) when psycodict does not provide the notification API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A server in recovery refuses LISTEN outright (SQLSTATE 25006) and can never deliver notifications (NOTIFY is not WAL-logged), so retrying every 30s would warn forever. Verified against devmirror, which is a physical replica (PG 18.1, pg_is_in_recovery() = true) -- this is the situation for development copies of the website. Also document why abandoning an inherited listener without close() is safe (psycopg's pid-guarded GC). Co-Authored-By: Claude Fable 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.
Companion PR to roed314/psycodict#111 (LISTEN/NOTIFY), showing what the consumer side looks like: the website picks up schema changes without restarting every worker. Draft until psycodict 1.0 (with LMFDB#111) is released — but safe to merge early, since the refresher disables itself (with one log line) when psycodict lacks the API, and requirements.txt already installs psycodict from git master, so the feature lights up on its own once LMFDB#111 merges. To try it today:
pip install "git+https://github.com/roed314/psycodict.git@listen-notify".What it does. Each web worker owns a
SchemaRefresher(new modulelmfdb/schema_refresh.py), driven by abefore_requesthook. The refresher keeps adb.listener()subscribed to thepsycodict_schemachannel; each request does a non-blockingpoll()— a socket read on an idle dedicated connection. When notifications have arrived, it callsdb.refresh_tables()(psycodict#99, already merged) once per batch, so added/dropped/renamed columns and tables become visible before the request is handled. Log lines record each refresh with the table names from the notification payloads.Policies this PR chooses — the things psycodict#111 deliberately leaves to the application:
refresh_tables()on every (re)subscription, because notifications sent while unsubscribed are lost (LISTENonly delivers what is sent after it). The same catch-up covers the window between worker start and first subscription, and a failed refresh drops the listener so the next resubscription retries it.refresh_tables()re-reads everything anyway, which handles create/drop/rename uniformly. Per-table refresh via the payload is an easy later optimization if the full refresh ever gets expensive.gunicorn --preload, an inherited listener is abandoned (not closed — its socket is shared with the parent) and each worker builds its own.Cost. One extra idle PG connection per worker, one non-blocking poll per request, one catch-up refresh per worker per (re)subscription; refreshes otherwise happen only when the schema actually changes.
Tests.
lmfdb/tests/test_schema_refresh.pyunit-tests the control flow with stub db/listener objects (subscribe + catch-up, batch dedupe + channel filtering, subscription-failure backoff and recovery, lost-listener resubscribe, failed-refresh retry, hot-standby permanent-disable, fork handling) and passes against any psycodict version; the live NOTIFY→LISTEN round trip is covered by psycodict's own tests in LMFDB#111. Verified locally: 7 passed undersage -python -m pytest, and the site serves normally with today's psycodict (the hook logs the one-line "not available" notice exactly once and stays out of the way).Deployment topology (verified live). devmirror is a physical hot standby (PG 18.1,
pg_is_in_recovery() = true): a server in recovery refusesLISTENoutright (cannot execute LISTEN during recovery, SQLSTATE 25006), andNOTIFYis not WAL-logged, so notifications cannot reach websites pointing at it no matter what the sync does — no trigger or event trigger on devmirror can help (triggers don't fire during WAL replay, and a standby cannotNOTIFY). The refresher therefore treats SQLSTATE 25006 at subscribe time as permanent and disables itself for the life of the process, so development copies of the website (which read devmirror) keep today's behavior — restart to pick up schema changes — with a single info log line instead of a retry loop. Production/beta webservers, whose schema changes are applied by psycodict-driven processes on the primaries they read from, get the live refresh.We've decided not to pursue automatic refresh for dev copies, since prod/beta don't need it. For the record, the workable design would be a replicated schema-generation marker:
_notify_schema_changeadditionally bumps a one-rowmeta_schema_gentable in the same transaction on the primary. The row replicates through WAL in commit order — so by the time a website sees the bump, the schema change itself has already replayed on the standby (a guarantee primary-side LISTEN cannot give) — and a website that cannotLISTENpolls that single-row SELECT at most every few seconds. That is a small psycodict follow-up plus ~15 lines here; the refresh-on-UndefinedColumnfallback discussed in psycodict#99 remains a complementary option.🤖 Generated with Claude Code