perf: cap query time, bound discussion recursion, isolate health checks - #375
Merged
Conversation
Address the aiopg connection-pool exhaustion that causes periodic steemit-production-beta-hivemind-001 health degradation (34-48% 4xx, EB instance replacement every 12-24h). - db.py: set statement_timeout=30s via server_settings so a single runaway query (observed up to 160s) is cancelled server-side and releases its connection immediately. The pool acquire timeout only bounds waiting for a free connection, not execution time. - db.py: add an isolated maxsize=1 health engine + query_row_health() so /health and /head_age cannot be starved by a saturated main pool (root cause of ELB marking the instance unhealthy). - thread.py: bound _load_discussion with MAX_DEPTH=50 and MAX_THREAD_POSTS=500; previously a deep/wide thread issued an unbounded number of sequential _child_ids queries. - thread.py: cache _get_author_hide_id / _check_posts_hide_id (300s) to drop two per-request connections from every get_discussion call. - Add tests/bridge_thread/ pure-logic unit tests (no live DB needed).
…pat)
The runtime image ships an older psycopg2 whose make_dsn() rejects the
newer 'server_settings' dict keyword with 'invalid connection option
"server_settings"', crashing init_db on startup and preventing the
server from coming up (verified in steemit-dev-hivemind-001 deploy).
Switch to the standard libpq 'options' connection string
('-c statement_timeout=30000'), accepted by every psycopg2/libpq
version. Merge into conf.query so a DATABASE_URL that already carries
its own options= param does not trigger a duplicate-keyword error;
existing options are preserved and the timeout is appended.
kuny0707
approved these changes
Aug 1, 2026
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.
Summary
Address the aiopg connection-pool exhaustion that causes periodic
steemit-production-beta-hivemind-001health degradation (34–48% HTTP 4xx, EB instance replacement every 12–24h). Related: PR #374 (partial fix already deployed).Root cause: all 25 pool connections per instance get consumed by slow
get_discussionqueries on deep nested threads. The/healthcheck (which also needs a DB connection) then times out → ELB marks the instance unhealthy → EB replaces it.Changes
db.py— statement_timeout=30s (P0): Set via the standard libpqoptionsstring (-c statement_timeout=30000) so PostgreSQL cancels any runaway query server side (observed up to 160s) and releases its connection immediately. The pool acquiretimeoutonly bounds waiting for a free connection, NOT execution time. (optionsis used rather than the newerserver_settingsdict — see the deployment note below: the runtime image's older psycopg2 rejectsserver_settingsas an invalid DSN option.)db.py— isolated health engine (P1): Add a dedicatedmaxsize=1engine +query_row_health()so/healthand/head_agecannot be starved by a saturated main pool — the direct cause of the ELB killing healthy instances.thread.py— bound recursion (P0): Cap_load_discussionwithMAX_DEPTH=50/MAX_THREAD_POSTS=500. Previously a deep/wide thread issued an unbounded number of sequential_child_idsqueries (one per depth level).thread.py— cache hide-id lookups (P2): Cache_get_author_hide_id/_check_posts_hide_id(300s TTL) to drop two per-request connections from everyget_discussioncall.tests/bridge_thread/pure-logic unit tests (no live DB needed) — 6 tests covering the depth/post caps, leaf termination, and cache-param forwarding. Placed outsidetests/server/becausetests/server/__init__.pyeagerly opens a real DB connection.Verification
Unit + lint
6 passed(run in an isolated venv with project deps installed)make test-lint): zero new warnings vs. master baseline on changed codemake testnot run in this environment (Python 3.14 + deps not preinstalled; existingtests/server/condenser integration tests require a live populated DB and are pre-existing/stale)Dev deployment + live API tests (steemit-dev-hivemind-001)
Deployed as image
steemit/hivemind:pr375-test2(versionpr375-test2-20260731055551). EB health Green/Ok/Ready, VersionLabel correctly switched. Live tests via SSM port-forward (dev ELB is internal/VPC-only):/healthendpointstatus:OK,db_head_age:6sbridge.get_discussionfunctionalityMAX_THREAD_POSTS=500cap/healthisolation under pool loadget_discussion, each ~17s),/healthstill returns in 251ms→913ms — never hits the 10s acquire timeout. Old behavior here was an ELB-killing timeout.The ~660ms
/healthslowdown under load is I/O contention on the small dev instance, not pool starvation (that would push it to the 10s timeout). The fix keeps the instance alive in exactly the scenario that used to kill it.Deployment note: psycopg2 version mismatch (fixed in 45cab86)
The first dev deploy (
pr375-test1) failed to start. The runtime image is Ubuntu 18.04 / Python 3.6 with an older psycopg2 whosemake_dsn()rejects the newerserver_settingsdict keyword:This crashed
init_dbon startup → server never came up → ELB healthcheck failed → deploy rolled back. A local venv (newer psycopg2) acceptedserver_settingsand hid the problem. Fix (45cab86): switched to the libpqoptionsconnection string (-c statement_timeout=30000), accepted by every psycopg2/libpq version, merged intoconf.queryto avoid clashing with a DATABASE_URL that carries its ownoptions=. Lesson: DB-connection code changes must be validated by actually building the image and deploying to dev, not only by a local venv.Decision: index check NOT included (original P1)
The incident doc suggested a missing
(author, permlink)composite index. Investigation found this is not the case:hive_postsalready has unique constrainthive_posts_ux1 (author, permlink)(backed by a B-tree index), andhive_posts_statusalready hasidx_hive_posts_status_list_type_author (list_type, author)andidx_hive_posts_status_list_type_post_id (list_type, post_id)since migration v21. The 160s query was therefore caused by pool/I/O contention, not a missing index. No schema change (DB_VERSION bump) is warranted. Reviewers can confirm with:Out of scope
Risk
statement_timeout=30s: rare legitimate long queries will start failing fast. Those are exactly the queries exhausting the pool; fail-fast is preferable to taking down the instance. Tunable viaSTATEMENT_TIMEOUT_MS.