Repository navigation
Conversation
…aths into functions
psteinroe
force-pushed
the
perf/settlement-pruning
branch
from
October 20, 2026 20:52
b163f50 to
5fb3c46
Compare
This branch has not been deployed
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.
Settlement now plans for one partition, so its cost no longer grows with the number of queues (finding P10). Before,
buildReturnExecutionsreferenced the partitioned executions table about 25 times and none of those references could be pruned. At 106 queues that cost ~520 ms of planning for each of the first 6 calls on every connection and ~30 ms per call after that. With Postgres' defaultjit = on, the statement's estimated cost also crossedjit_above_cost, so every settle JIT-compiled ~10.7k functions and took ~14 s.One queue per statement
returnExecutionsnow takes the worker'squeueName, and results no longer carry aqueueeach. Every reference to the executions table in the statement is narrowed to that queue. The queue is inlined as an escapedE''literal rather than bound as a parameter, because a cached generic plan prunes partitions only by constants. So each queue gets its own prepared statement, planned a few times per connection and then served from the plan cache. With a parameter, Postgres kept choosing custom plans: ~3.5 ms of planning on every call, plus one ~250 ms generic plan per connection at 106 queues. A test covers queue names with a quote and a backslash, and the escaping round-trips under bothstandard_conforming_stringssettings.Parent paths move into functions
Waiting parents can be in any queue, so their lookups now run in two plpgsql functions that settlement calls only when a settled execution has a parent:
_private_complete_children()stores results for waiting parents, wakes them, and returns orphaned children (children no parent waits on any more)._private_fail_waiting_parents()fails the chain of waiting ancestors and returns them for dead-lettering, result waits and the cancel cascade.Their statements can't be pruned, so they use
plan_cache_mode = force_generic_plan(planned once per connection) and look rows up by arrays of ids to stay on indexes:jsonb_to_recordsetis always estimated at 100 rows, which otherwise led to a seq scan of every partition. Both functions read with fresh snapshots, so they run first inwrites, before the statement changes any row they read. That keeps their view identical to the old single snapshot. The old existence check on the parent ininserted_childrenis dropped becausevalid_resultsalready holds that row locked. Behaviour is unchanged otherwise: the full suite passes unchanged apart from the call shape. Two new tests cover the moved paths with parents in other queues: an orphaned child, and a three-level failure that dead-letters the middle ancestor. Both new tests also pass onmain.Measurements
The harness seeds ~1M executions: 93% completed, 3% failed, 3% pending, 1% suspended parents. It runs Postgres 15 with
pg_stat_statements.track_planning. Each settle is one completion plus one retry, sent throughDatabaseClient(postgres.js prepared statements). Times below are DB-side frompg_stat_statements; wall times on this machine were noisy.jit = onWhen every settled child wakes a parent in another queue (an
invokeAndWaitscenario), median wall time per settle drops from 39 ms to 9 ms at 106 queues and from 3.7 ms to 2.8 ms at 6 queues. At 106 queues the first call per connection takes ~170 ms (the functions' generic plans), against ~600–900 ms for each of the first 3 calls before.Trade-off: each settled queue keeps its own cached plan of about 1 MB per connection. Before, there was one plan per connection, but it grew with the partition count: 2 MB at 6 queues, 16 MB at 106.
Follow-ups (out of scope)
jit_above_costas well.