Repository navigation
perf: one round trip per enqueue, and back the result poll off to 1s - #5
Merged
Merged
Conversation
Producers run on Cloud Run in us-east4; the ModelQ Redis is a Vultr host in
Mumbai. Measured RTT is ~271ms, so every reply this library waits for before
sending the next command is a quarter of a second on a caller's request path.
enqueue() sent five commands sequentially -- rPush ml_tasks, zAdd
queued_requests, setex task:, zAdd task_history, setex task_history: -- and
waited for each reply in turn. Five waits. Measured against 5,434 production
requests joined across Redis and Mongo, the gap between queued_at and the
request record landing is a flat 1.355-1.41s regardless of payload size,
which is exactly five round trips plus a Mongo insert.
They now go out as one pipeline. Redis still executes them in order; only the
waiting is removed. Order is corrected at the same time: the task's own state
is written before the id is advertised on ml_tasks, so a worker can no longer
pop a task whose task:{id} key does not exist yet.
getResult() woke every 100ms and issued two GETs per wake -- the cancel flag,
then the result. On a 30s wait against a Redis 271ms away that is ~100 round
trips to learn nothing, with the caller's worker pinned for all of it. On
flux_klein only 5.7% of tasks finish inside 30s, so 94% of those requests paid
the full cost and still had to poll afterwards.
Two changes: the pair of GETs became one pipelined read, and the interval now
starts at 100ms and doubles to a 1s ceiling. Backing off rather than jumping
straight to a flat 1s keeps the fast endpoints fast -- realtime_turbo runs in
1.18s at p50 and 100% of its tasks finish inside 10s, so a flat 1s poll would
add up to a second to a one-second job. A 3s wait now costs 6 polls instead of
30; a 30s wait costs 9 instead of 300.
addToTaskHistory() is removed rather than left unused; it was private and its
only caller was the enqueue path now folded into the pipeline.
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.
Producers run on Cloud Run in
us-east4; the ModelQ Redis is a Vultr host in Mumbai. Measured RTT is ~271ms, so every reply this library waits for before sending the next command is a quarter of a second on a caller's request path.1.
enqueue()was five sequential round tripsFive commands, five waits.
Measured: across 5,434 production requests joined between ModelQ Redis and MongoDB, the gap between
queued_atand the request record landing is a flat 1.355–1.41s — identical regardless of payload size, and identical across queues. Five round trips at 271ms, plus a Mongo insert.Now one pipeline. Redis still executes them in order; only the waiting is removed.
Ordering also fixed:
rPush ml_tasksused to go first, so a worker couldBLPOPa task whosetask:{id}key did not exist yet. State is now written before the id is advertised — free, since it's the same pipeline.2. The result poll woke 10× a second and asked twice each time
On a 30s wait that's ~100 round trips to learn nothing, with the caller's Octane worker pinned for all of it. On
flux_kleinonly 5.7% of tasks finish inside 30s, so 94% of those requests paid the full cost and then had to poll/fetchanyway.Two changes:
Why backoff rather than a flat 1s
A flat 1s would regress the fast endpoints. Measured GPU time:
realtime_turboz_image_turboflux_kleinA flat 1s poll adds up to a second to a one-second job. Starting tight and backing off gives sub-second tasks the loop they need and long ones a quiet one:
Both constants are public (
Task::POLL_MIN_INTERVAL_US,Task::POLL_MAX_INTERVAL_US) if you'd rather pin it flat.Compatibility
isCancelled()semantics preserved exactly, including the edge case where a stored"0"still counts as cancelled (a naive(bool)cast on the pipelined reply would have got that wrong).addToTaskHistory()removed — it wasprivateand its only caller is now the pipeline.Tests
20 new tests.
tests/Unit/RoundTripTest.phppins the round-trip count (not the command list — the commands were never the problem) with a recording Redis double.tests/Integration/PipelinedEnqueueTest.phpruns the real phpredis against a live Redis on a dedicated DB index, because a mock cannot catch a misuse of the actual pipeline API.main— they're payload-shape assertions unrelated to this change)Mutation-tested:
testEnqueueCostsOneRoundTrip+testEnqueueStillWritesAllFiveKeysfailtestPollingBacksOff→ "poll did not back off"Deploying
The app that consumes this pins
modelslab/modelq: @dev; production is currently running a build that still writes the payload three times (before #4). Worth landing both.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.