GrampsWebApiDb: repair a mirror the history feed can't explain - #1015
Open
dsblank wants to merge 2 commits into
Open
GrampsWebApiDb: repair a mirror the history feed can't explain#1015dsblank wants to merge 2 commits into
dsblank wants to merge 2 commits into
Conversation
Three related fixes to how a session copes with a server that isn't answering, or that answers with a history it cannot back up. An unreachable server logged a full traceback at ERROR on every 10 second poll tick for as long as the outage lasted, and spent a blocking round trip (plus webapi_client's own retry sleep) on the GTK main thread each time. It is now reported once per outage -- one line at WARNING, traceback kept at DEBUG -- with the poll interval doubling up to the new POLL_BACKOFF_MAX_SECONDS while it persists and resetting on the first success. The media poll gets the same once-per-outage treatment (no backoff: 300s is already coarse). A server whose tree was populated without gramps-web-api recording any history has nothing for the incremental feed to replay, so syncing it produced a mirror holding only the handful of edits the history does know about, with nothing logged to say so. https://demo.grampsweb.org is exactly this: 4668 people against a history that was empty until someone edited it through the API. load() now compares the mirror's own get_total() against the server's object_counts after each sync (_mirror_is_short_of_the_server(), via _sync_from_server()'s new verify_totals) and routes a shortfall to the existing _full_resync(). Comparing totals rather than watching for an empty feed is what makes the case detectable at all -- one API edit is enough to hand back a transaction, advance the cursor, and make the sync look like it worked. The check runs at load() only, never on the poll; it is skipped while pushes are queued, and a larger local total is left alone rather than overwritten from the export. Neither of those was visible from the UI, so both modules now log to ".grampswebapidb" ("gramps -d .grampswebapidb") with a per-operation DEBUG trace: one line per HTTP request, one per sync page and sync, plus load, push, queue depth, media transfer counts and the totals compared. No object data, payloads or headers -- a busy feed costs a fixed handful of lines, not one per change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every network round trip this addon makes runs synchronously on the GTK main thread, so a long one -- the initial catch-up, a full-export rebuild, a first media sync of hundreds of files, a backgrounded push being waited on -- is time the main loop spends inside the addon rather than answering. The window stops redrawing and the window manager offers to force-quit Gramps. _pump_main_loop() hands the loop its turn at the boundaries of each of those: between sync pages and once on the way out (so even a poll that found nothing pumps after its round trip), between media files, across the export download's chunks (new on_chunk hook on download_export() / _get_binary()) and the task-poll loop (new on_wait hook on wait_for_task() / push_transaction()), and either side of the rebuild -- before the wipe and after request_rebuild(), never in between, where a dispatched event would be looking at a half-empty tree. It goes through GLib's default main context rather than Gtk.main_iteration() so the module stays importable without a display: this is a DATABASE plugin, loadable from the CLI, and GTK drives that same context anyway. Pumping re-enters, so both poll timeouts now check a _syncing flag and skip their turn rather than starting a second sync underneath the first. Measured on a full rebuild of the 26540-object demo tree: a 100ms heartbeat timeout went from 0 dispatches during load() to 15, leaving two stalls the pump structurally can't cover -- the export request itself (~6.6s, server-side generation, blocked inside urlopen) and ImportXml (~5.2s, which never calls User.step_progress so there is no hook to drive). Moving the sync off-thread remains the real fix for those; this is the version that doesn't restructure every call path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dsblank
marked this pull request as ready for review
August 13, 2026 18:53
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.
Follow-up to #1014, against the same addon.
1. An unreachable server no longer floods the log or stutters the UI
A refused connection logged a full traceback at
ERRORon every 10-second poll tick for as long as the server stayed down, and each tick spent a blocking round trip on the GTK main thread (pluswebapi_client's own one-shot retry sleep):An outage is an expected, self-healing condition here — the mirror stays usable, local edits queue for the next successful push, and the sync cursor is persisted — so it is now reported once per outage (one
WARNINGline with the error text, traceback demoted toDEBUG), and the poll interval doubles fromPOLL_INTERVAL_SECONDSup to a newPOLL_BACKOFF_MAX_SECONDS(300) until the first success restores it. The media poll gets the same once-per-outage reporting without backoff — 300s is already coarse.2. A mirror the transaction history can't account for is now repaired
The history table only records what gramps-web-api itself wrote. A tree populated by any other route — a server-side import straight into the database, a restored dump, a truncated history table — has nothing for the incremental feed to replay, so syncing it produced a mirror holding only the handful of edits the history does know about, with nothing in the log to say why.
https://demo.grampsweb.orgis exactly this case:load()now compares the mirror's ownget_total()against the server'sobject_countsafter each sync (_mirror_is_short_of_the_server(), via_sync_from_server()'s newverify_totals) and routes a shortfall to the existing_full_resync().Comparing totals rather than watching for an empty feed is what makes the case detectable at all: one API edit against such a server is enough to hand back a transaction, advance
sync_last_time, and make the sync look like it worked. Guards:load()only, never on the 10-second poll — repairs happen when a tree is opened, not mid-session, and it costs oneGET /metadata/per open;pending_pushesis non-empty — those are local edits the server hasn't taken yet, so the counts are legitimately out of step;webapi_client.OBJECT_COUNT_KEYSpins the sum to the same ten primary typesDbGeneric.get_total()counts, so a future server bucket can't make a good mirror look permanently short.Verified live against the demo, on a mirror left in the broken state (one person, cursor already advanced):
3. A DEBUG trace, since none of the above is visible from the UI
Both modules now log to
.grampswebapidb, sogramps -d .grampswebapidbturns on a per-operation trace: one line per HTTP request (method, path, status, round-trip time), one per sync page and one per sync (applied/skipped/cursor/elapsed), plus load, push, pending-queue depth, media transfer counts, and the local-vs-server totals compared at load. No object data, payloads, or headers — replaying a busy feed costs a fixed handful of lines rather than one per change. A full repair traces in ~20 lines.Testing
python3 -m unittest GrampsWebApiDb.tests.test_grampswebapidb GrampsWebApiDb.tests.test_webapi_client— 255 tests pass (94 of them new here: backoff, once-per-outage reporting, the totals check and each of its guards, andget_object_count()). Also exercised end to end against the live demo server, as above.🤖 Generated with Claude Code