Skip to content

[worker] breakdown the action effect enum handling into a trait#5082

Closed
MohamedBassem wants to merge 5 commits into
mainfrom
pr5082
Closed

[worker] breakdown the action effect enum handling into a trait#5082
MohamedBassem wants to merge 5 commits into
mainfrom
pr5082

Conversation

…tead of taking actions inline

As part of the work on centeralizes writes to the self proposer inside one scheduler, this PR refactors the RPC handlers to return their decisions to the main PP loop where they get executed there. This is in prep for shipping the proposals to a channel in the next PR.

This PR is mostly mechanical and does the following:
1. Changes the handlers to no longer take the "Actuator" trait and instead return their `Decision` from the function.
2. The decision can be either: Propose a command (with its different modes), reply now, or the pre-vqueues invoker poking.

Tests are much simpler as they no longer need to mock the leadership state. Only meaningful change was a bug in the restart_as_new test `copy_prefix_zero_with_no_version_uses_service_invocation_command` where it was using the default `patch_deployment_id` and it didn't actually hit the assertions inside the mock handlers as it was failing with `CannotPatchDeploymentId`. The tests where not setting expectations on num calls to the mock, so it didn't fail when the call to `append_and_respond_asynchronously` didn't happen. So this was fixed along the way by using `PatchDeploymentId::PinToLatest` to hit the correct assertions. This PR anyways removes `mockall` altogether from that worker crate.
This PR changes the handling of the PP RPCs and ingestion requests from happening inside the main PP loop and into the leader state run loop. This is done by introducing a new channel that gets consumed in the large stream_select of `LeaderState` run. With that all main callsites to `SelfProposer` are encapsulated inside `LeaderState` and in a lter PR will be dispatched by a new Self proposer scheduler (*).

A couple of caveats to be aware of:
1. The new channel is of size one to maintain the semantics of keeping only one inflight RPC not enqueued in self proposer. I don't forsee any problems in increasing it though if needed.
2. The network service arm is now guarded by having a permit into this new channel. This means that although we could service stuff like `DedupQquery` (which doesn't propose commands), it'll currenly be blocked until `SelfProposer` is unblocked. This is not very different from now though.
3. `ActionEffects` is no longer effects, and I renamed it to `LeaderEvents` now that it carries also network service actions. Now the new `NetworkServiceEvent` is huge (~0.5KB) and it made the entire `LeaderEvent` wider. Luckily, we only keep 10 of those in memory at a time. In a later PR, I'm getting rid of this enum altogether.


(*) Two other callsites to self proposer remain: the one callsite when candidate to send the `AnnounceLeader` command, and after becoming the leader to do any `VersionBarrier` command. Those two are rare and will be excluded from the SelfProposer backpressure.
This PR adds support for overdrafts in `MemoryPool` via the `force_reserve` API. This allows the memory pool to issue leases for more capacity than it holds. It also introduces a new `overdraft` API to query how much in the negative the memory pool is. Also, it introduces a new `wait_until_available` which waits until the pool is out of the overdraft mode (just as a notification without reserving anything).
…d of record count bound

Context:

Before this PR, the background appender's channel was a bounded channel of size 50. This has two caveats:
1. Large commands (e.g. large schema upserts) can inflate the size of this channel in the worst case to 1.5GB (per partition). A coordinated event (e.g. schema change) can end accumulating 10s of GBs of unaccounted memory across all partitions causing the node to OOM.
2. On the other hands, if the append latency is high, and the records are tiny (as shown later in the benchmarks), this 50 records arbitrary limits hinders the batching efficiency quite a bit.

Solution:

The main problem is that the number of records is just hard to configure correctly across different workloads. We want reasonable batching efficiency with bounded memory growth. As such, this PR switches the background appender channel to be memory-bound instead of record-count bound. The memory bound is configurable and will default to 64MB (i.e. allowing the background appender to do up to two full batches per run, though we might want to tune how large we want a single batch to be).

Implementation wise:
1. We're switching the underlying channel to be unbounded channel with a `MemoryPool` on top.
2. Because we don't know how much to reserve from the memory pool beforehand, we're going to allow overcomitting the memory pool by at most once `enqueueXX` call.
  a. To simplify reasoning about the overcomitting, I'm dropping the `Clone` support for the `LogSender`. Otherwise, we might overcomitt by more than one enqueue calls across concurrent senders. We don't currently need it to be `Clone`, so this was a no-op.
3. Once the memory pool is fully exhusted, the background appender is going to start rejecting future appends. As such, the `LeaderState` stops polling the effects stream once the pool is exhusted, and waits for it to become available again. Because leader state has an exclusive reference over the self proposer (and its underlying sender), it can be sure that if it sees that the pool has capacity, it'll be able to send without getting any reservation.
4. As a nice side effect for all the self proposer APIs becoming `sync`, we no longer need to worry about cancellation safety, so the events are handled inline inside `LeaderState::run`.

Benchmarks (Steady State):

Started a local one node cluster comparing base against this commit. The workfload is a 30s of Counter::get on a virtual object coming from 1000 concurrent connections.

```
# Base

❯❯❯ wrk -t1 -c1000 --latency -d30s -s ./scripts/wrk/counter.lua http://localhost:8080                                                                                                                                                                                                                                 ✘ 130 main
thread 1784573163 started
Running 30s test @ http://localhost:8080
  1 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    56.97ms   89.58ms   1.36s    98.26%
    Req/Sec    21.23k     2.93k   29.85k    76.59%
  Latency Distribution
     50%   45.84ms
     75%   53.83ms
     90%   62.58ms
     99%  581.58ms
  631995 requests in 30.04s, 168.16MB read
Requests/sec:  21036.54
Transfer/sec:      5.60MB
thread 1784573163 made 632995 requests and got 631995 responses

```

```
# This commit

❯❯❯ wrk -t1 -c1000 --latency -d30s -s ./scripts/wrk/counter.lua http://localhost:8080                                                      main
thread 1784573695 started
Running 30s test @ http://localhost:8080
  1 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    55.14ms   88.14ms   1.17s    98.28%
    Req/Sec    22.02k     2.79k   31.03k    76.92%
  Latency Distribution
     50%   44.09ms
     75%   51.81ms
     90%   60.31ms
     99%  574.92ms
  655652 requests in 30.07s, 174.45MB read
Requests/sec:  21805.57
Transfer/sec:      5.80MB
thread 1784573695 made 656652 requests and got 655652 responses
```

So there's no degradation perf wise for this whole branch.

Benchmarks (higher append latency):

Where this becomes interesting is when append latency is higher because the 50recs bounded channel gets filled faster, and starts pushing back specially with tiny records, and due to the lack of pipelining, the throughput of the system collapses. So doing the same benchmarks but with a 200ms sleep in the background appender:

```
# Base

❯❯❯ wrk -t1 -c1000 --latency -d30s -s ./scripts/wrk/counter.lua http://localhost:8080                                                      main
thread 1784574220 started
Running 30s test @ http://localhost:8080
  1 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.04s   308.07ms   2.00s    68.70%
    Req/Sec     0.87k   592.52     2.87k    65.53%
  Latency Distribution
     50%  916.07ms
     75%    1.14s
     90%    1.52s
     99%    1.97s
  25328 requests in 30.08s, 6.74MB read
  Socket errors: connect 0, read 0, write 0, timeout 1437
Requests/sec:    842.15
Transfer/sec:    229.45KB
thread 1784574220 made 26329 requests and got 25328 responses

```

vs:

```
# This commit

❯❯❯ wrk -t1 -c1000 --latency -d30s -s ./scripts/wrk/counter.lua http://localhost:8080                                                      main
thread 1784573956 started
Running 30s test @ http://localhost:8080
  1 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   590.43ms  136.85ms   1.85s    81.02%
    Req/Sec     1.72k   679.51     3.67k    71.75%
  Latency Distribution
     50%  605.69ms
     75%  611.69ms
     90%  617.10ms
     99%    1.44s
  50304 requests in 30.00s, 13.38MB read
Requests/sec:   1676.56
Transfer/sec:    456.80KB
thread 1784573956 made 51305 requests and got 50304 responses
```

This commit was able to have twice the throughput of the base branch under high latency.
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Test Results

  8 files  ±0    8 suites  ±0   4m 58s ⏱️ -9s
 61 tests ±0   61 ✅ ±0  0 💤 ±0  0 ❌ ±0 
268 runs  ±0  268 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 598d0c9. ± Comparison against base commit 340b316.

♻️ This comment has been updated with latest results.

@github-actions github-actions Bot locked and limited conversation to collaborators Jul 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant