Skip to content

chore: Improvements around FiberQueue - #605

Merged
romange merged 1 commit into
masterfrom
Pr2
Jul 5, 2026
Merged

chore: Improvements around FiberQueue#605
romange merged 1 commit into
masterfrom
Pr2

Conversation

@romange

@romange romange commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces FiberQueue's callback storage with a fixed-capacity fu2::function (Tasklet), separates
the producer/consumer EventCounts onto distinct cache lines, and adds a blocked_submitters_ metric.

Changes

  • Replace std::function<void()> (CbFunc) with Tasklet, a fixed-capacity (32 bytes inline, 8-byte
    alignment), non-copyable, non-throwing fu2::function_base. Callbacks up to 32 bytes avoid heap
    allocation. Ported from Epoll: use busy poll cycle limit #595.
  • alignas(64) pull_ec_ and push_ec_ separately — previously adjacent, they shared a cache line
    and ping-ponged between producer-side (notify() per commit) and consumer-side (notifyAll() per
    drained batch) wakeup traffic.
  • Add a blocked_submitters_ thread-local counter, incremented/decremented around
    push_ec_.wait() when a submitter blocks on a full queue.

Summary by CodeRabbit

  • Bug Fixes
    • Improved task submission and execution behavior under heavy load and short queue-full conditions by streamlining batch handling to avoid unnecessary exception overhead.
    • Strengthened backpressure responsiveness via per-thread tracking of blocked submitters.
  • Refactor / Performance
    • Updated the internal task execution pipeline to use a fixed-capacity callable wrapper and tasklet-based queueing for more consistent throughput.
  • New Features
    • Added a new blocked_submitters() metric to monitor per-thread queue contention levels.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

FiberQueue now stores and runs Tasklet callables instead of std::function<void()>, and it tracks per-thread blocked submitters around the slow-path wait in Add().

Changes

FiberQueue Tasklet Migration

Layer / File(s) Summary
Tasklet type definition and include
util/fibers/fiberqueue_threadpool.h
Adds a guarded include of base/function2.hpp and defines Fu2Fun/Tasklet, a fixed-capacity function wrapper with a size check.
Queue storage and blocked submitter tracking
util/fibers/fiberqueue_threadpool.h
Updates the internal queue to store Tasklet, realigns the wake event counters, adds the blocked_submitters() accessor, and increments/decrements the per-thread counter around the blocking wait in Add().
Run loop and TLS definition
util/fibers/fiberqueue_threadpool.cc
Defines the thread-local blocked_submitters_ storage, changes the batch buffer in Run() from CbFunc to Tasklet, and removes the per-task exception handler.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • romange/helio#599: Also changes FiberQueue::Run() batching and queue-related fiber execution flow in util/fibers/fiberqueue_threadpool.cc.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the change, but it is too vague to describe the specific FiberQueue improvements. Use a more specific title, such as mentioning the Tasklet storage change, cache-line alignment, or blocked submitter metric.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Pr2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

FiberQueue: inline task callables and reduce producer/consumer contention

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Replace std::function tasks with fixed-capacity, move-only Tasklet to avoid heap allocations.
• Cacheline-separate producer/consumer EventCounts to reduce wakeup ping-pong under load.
• Track per-thread blocked submitters while waiting on push event notifications.
Diagram

graph TD
P["Producer fibers"] --> A["FiberQueue::Add/TryAdd"] --> Q[("mpmc_bounded_queue<Tasklet>")] --> R["FiberQueue::Run"] --> X["Execute Tasklet"]
A --> PE["pull_ec_ (notify)"]
A --> SE["push_ec_ (wait)"]
R --> SE
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep std::function + custom allocator/PMR
  • ➕ Less invasive type change (keeps copyable callable semantics)
  • ➕ Can still reduce allocations with a monotonic/pool resource
  • ➖ Harder to guarantee no allocations for small callables
  • ➖ May still break trivial-relocation assumptions for queue cells depending on allocator/bookkeeping
2. Use absl::AnyInvocable / folly::Function-like wrapper
  • ➕ Established move-only callable wrappers designed for non-owning/owning invocation
  • ➕ Potentially fewer compiler-specific pragmas than function2
  • ➖ May not be available in this repo/toolchain or may have different size/ABI characteristics
  • ➖ Inline capacity and exception guarantees may differ from current needs
3. Single EventCount with padding instead of alignas separation
  • ➕ Simpler memory layout and fewer members
  • ➖ More risk of false sharing between producer notify traffic and consumer notifyAll traffic under contention

Recommendation: Current approach is reasonable for a perf-sensitive queue: using a fixed-capacity, move-only Tasklet reduces allocator pressure and helps keep queue cells relocatable, while cacheline-separating push/pull EventCounts directly targets producer/consumer wakeup contention. Main things to double-check are callable move semantics with mpmc_bounded_queue (no accidental copies), and that the GCC diagnostic suppression is narrowly scoped and justified.

Files changed (2) +35 / -5

Enhancement (1) +32 / -4
fiberqueue_threadpool.hIntroduce fixed-capacity Tasklet, align EventCounts, and count blocked producers +32/-4

Introduce fixed-capacity Tasklet, align EventCounts, and count blocked producers

• Replaces std::function storage with a function2-based, fixed-capacity, non-copyable Tasklet to avoid heap allocation and keep queue storage friendly to relocation. Separates pull_ec_ and push_ec_ onto distinct cache lines to reduce false sharing, and tracks per-thread blocked submitters around push_ec_ waits.

util/fibers/fiberqueue_threadpool.h

Refactor (1) +3 / -1
fiberqueue_threadpool.ccDefine thread-local blocked counter and switch Run batch to Tasklet +3/-1

Define thread-local blocked counter and switch Run batch to Tasklet

• Adds the thread-local definition for FiberQueue::blocked_submitters_. Updates the consumer batch buffer type from the previous callback type to the new Tasklet wrapper used by the queue.

util/fibers/fiberqueue_threadpool.cc

@romange romange changed the title Pr2 mpmc_bounded_queue: Improvements around FiberQueue Jul 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
util/fibers/fiberqueue_threadpool.h (1)

73-77: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the blocked_submitters_ accounting exception/early-return safe.

If push_ec_.wait(...) unwinds (e.g., fiber cancellation) the blocked_submitters_-- on Line 77 is skipped, permanently leaking the counter. Pair the inc/dec with RAII so it is balanced on every exit path.

♻️ Suggested RAII fix
-      result = true;
-      blocked_submitters_++;
-      push_ec_.wait(key.epoch());
-      blocked_submitters_--;
+      result = true;
+      blocked_submitters_++;
+      absl::Cleanup dec = [] { blocked_submitters_--; };
+      push_ec_.wait(key.epoch());

As per coding guidelines: "Use smart pointers (std::unique_ptr) and RAII (absl::Cleanup) for resource management".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@util/fibers/fiberqueue_threadpool.h` around lines 73 - 77, The
blocked_submitters_ counter in the fiber queue wait path is not
exception/early-return safe because the decrement after
push_ec_.wait(key.epoch()) can be skipped if the wait unwinds. Update the
submit-wait logic in fiberqueue_threadpool.h to use RAII (for example,
absl::Cleanup) around the blocked_submitters_ increment so the decrement is
guaranteed on every exit path, and keep the change localized to the
push_ec_.wait block in the queue submit/await flow.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@util/fibers/fiberqueue_threadpool.h`:
- Around line 73-77: The blocked_submitters_ counter in the fiber queue wait
path is not exception/early-return safe because the decrement after
push_ec_.wait(key.epoch()) can be skipped if the wait unwinds. Update the
submit-wait logic in fiberqueue_threadpool.h to use RAII (for example,
absl::Cleanup) around the blocked_submitters_ increment so the decrement is
guaranteed on every exit path, and keep the change localized to the
push_ec_.wait block in the queue submit/await flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53c59093-f322-4c85-b43a-cf610949199d

📥 Commits

Reviewing files that changed from the base of the PR and between f501d99 and 6771d13.

📒 Files selected for processing (2)
  • util/fibers/fiberqueue_threadpool.cc
  • util/fibers/fiberqueue_threadpool.h

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 11 rules
✅ Cross-repo context
  Explored: repo: dragonflydb/dragonfly (sha: 5dcbdf55)

Grey Divider


Remediation recommended

1. Unused blocked counter 🐞 Bug ⚙ Maintainability
Description
FiberQueue::Add() increments/decrements blocked_submitters_ around push_ec_.wait(), but the counter
is never read anywhere, so it has no functional effect while adding work on the contended submission
path and introducing misleading state for future changes.
Code

util/fibers/fiberqueue_threadpool.h[R74-78]

      result = true;
+      blocked_submitters_++;
      push_ec_.wait(key.epoch());
+      blocked_submitters_--;
    }
Relevance

⭐⭐⭐ High

Team frequently removes dead/unused code/fields (e.g., removed unused task index, removed unneeded
code).

PR-#476
PR-#495
PR-#603

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The counter is modified in FiberQueue::Add(), declared as a static TLS member, and defined in the
.cc file, but a repo-wide search shows no reads/usages beyond these write sites.

util/fibers/fiberqueue_threadpool.h[61-80]
util/fibers/fiberqueue_threadpool.h[111-127]
util/fibers/fiberqueue_threadpool.cc[15-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`FiberQueue::blocked_submitters_` is incremented/decremented in `FiberQueue::Add()` but is never read anywhere. This adds unnecessary instructions on the contended path and leaves dead state in the class.

### Issue Context
- The counter is thread-local and currently has no consumers.
- If it was intended to influence wakeup strategy (e.g., avoiding `notifyAll()` when no one is blocked), that logic is missing; until it exists, the counter should be removed.

### Fix Focus Areas
- Remove the counter increments/decrements:
 - util/fibers/fiberqueue_threadpool.h[61-79]
- Remove the static member declaration:
 - util/fibers/fiberqueue_threadpool.h[111-127]
- Remove the TLS definition:
 - util/fibers/fiberqueue_threadpool.cc[15-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread util/fibers/fiberqueue_threadpool.h
1. Replace the std::function<void()> callback (CbFunc) with Tasklet, a fixed-capacity
fu2::function_base (capacity_fixed<32,8>, non-copyable, non-throwing). Callbacks up to 32 bytes
are stored inline with no heap allocation. Ported from #595.

2. FiberQueue: put pull_ec_ and push_ec_ on separate cache lines
pull_ec_ is written by every producer (notify() after each commit); push_ec_ is written by the
consumer (notifyAll() per drained batch) and by blocked producers. Adjacent in memory they share a
cache line and ping-pong between producer- and consumer-side wakeup traffic. alignas(64) each onto
its own line.

3. Add blocked_submitters_ metric

Signed-off-by: Roman Gershman <romange@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@util/fibers/fiberqueue_threadpool.cc`:
- Around line 53-55: The worker loop in FiberQueueThreadPool currently calls
batch[i]() without any local exception handling, so any throwing Tasklet escapes
the worker thread and loses per-task context. Update the loop in the
worker/dispatch path to either make Tasklet explicitly noexcept if callbacks
must not throw, or keep a thin try/catch around batch[i]() that logs the failure
with the task/queue context before terminating or handling it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2766a83-988d-457a-8697-c7adbced1e3a

📥 Commits

Reviewing files that changed from the base of the PR and between 87e60ee and 247c15a.

📒 Files selected for processing (2)
  • util/fibers/fiberqueue_threadpool.cc
  • util/fibers/fiberqueue_threadpool.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • util/fibers/fiberqueue_threadpool.h

Comment thread util/fibers/fiberqueue_threadpool.cc
@romange romange changed the title mpmc_bounded_queue: Improvements around FiberQueue chore: Improvements around FiberQueue Jul 5, 2026
// enqueue_pos_/dequeue_pos_ internally.
alignas(64) EventCount pull_ec_;
alignas(64) EventCount push_ec_;
std::atomic_bool is_closed_{false};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about is_closed_, it is in the same cache line as push_ec_ so if we read it we get push_ec_

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked it. Since is_closed_ changes only once, it does not really matter. As long as variable does not change it does not affect cache-lines.

@romange
romange merged commit e25ec9e into master Jul 5, 2026
11 checks passed
@romange
romange deleted the Pr2 branch July 5, 2026 19:24
push_ec_.notifyAll();

for (unsigned i = 0; i < count; ++i) {
try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is wrong to remove the try/catch.
It will still throw even with Tasklet - the isThrowing just mark how to act on a specific case:
See function2.hpp:


/// \tparam IsThrowing Defines whether the function throws an exception on
///                    empty function call, `std::abort` is called otherwise.

The signature doesn't have noexcept at the end, so it can still throw. If you remove this try/catch the LOG(FATAL) won't log the error and any exception will be treated as an uncaught exception.

You can return this block back or add noexcept at the end of fu2Fun:

fu2::function_base<true, false, fu2::capacity_fixed<32, 8>,
                   false, false, void() noexcept>;


void Run();

unsigned blocked_submitters() const {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Nit: This is dead code, why do you add it if no one uses it? I assume it is for future use?
  2. If you decide to keep this function, I think you should document it (add a comment above) because the name + non-static signature give the impression a per-queue counter, which it is not (it can be confusing) .
  3. Also, if it stays consider making it static, since it reads thread-global state and has nothing to do with a specific instance - a non-static accessor makes caller think they can read it as per-queue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it should be static

}

result = true;
blocked_submitters_++;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't a "blocked submitter" be accounted also in line 176?
I don't see blocked_submitters_ being updated there.

using Fu2Fun::operator=;
};

static_assert(sizeof(Tasklet) == 48);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider adding a message here in case compilation fail.
e.g:

static_assert(sizeof(Tasklet) == 48,
              "Tasklet size changed: 32B inline buffer + fu2 dispatch overhead. ");

friend class FiberQueueThreadPool;

// Fixed-capacity, non-copyable, non-throwing callable. Callbacks up to 32 bytes are stored
// inline (no heap allocation), unlike std::function. Keeps queue cells trivially relocatable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
I think that "Keeps queue cells trivially relocatable." is not correct.
From my understanding "trivially relocatable" is true if we can move the object to a new address using memcpy - but this is not what i see in function2.hpp. e.g:

  void move(vtable& to_table, data_accessor* from, std::size_t from_capacity,
            data_accessor* to,
            std::size_t to_capacity) noexcept(HasStrongExceptGuarantee) {
    cmd_(&to_table, opcode::op_move, from, from_capacity, to, to_capacity);
     ....

and after that for inline storage that op_move path move-constructs the callable and destroys the source:

  // The object is allocated inplace
          else {
            construct(std::true_type{}, std::move(*box), to_table, to,
                      to_capacity);
            box->~T();
          }

I would just deleted "Keeps queue cells trivially relocatable."

romange added a commit that referenced this pull request Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants