Skip to content

Per-identifier locking for the blocking queue rate limiter - #2709

Merged
flodnv merged 26 commits into
mainfrom
danielsilva-662853-blocking-queue-per-identifier-locking
Aug 12, 2026
Merged

Per-identifier locking for the blocking queue rate limiter#2709
flodnv merged 26 commits into
mainfrom
danielsilva-662853-blocking-queue-per-identifier-locking

Conversation

@danieldoglas

@danieldoglas danieldoglas commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Details

This PR changes how we handle the blocking commit thread rate limits.

The logic we applied here is:

  • We have a 180s time window
  • Each command can spend up to 40s in the blocking queue during that period
  • Each accountID can spend up to 20s in the blocking queue during that period
  • If a command or accountID reach their limits, we will block them from going to the blocking queue for 60s.

Every time a command finishes in the blocking queue, we will:

  • add the time spent in that command in _commandStates, keyed by command name
  • add the time spent for that account in _accountStates, keyed by accountID

The StateMap object has a mutex that is briefly held while we retrieve an entry from the map.

The entry in the map is an IdentifierState, which also has a mutex, a blockedUntil property (so the checkes when we're inserting the commands to the blocking queue is O(1)) and a list of <startTime, timeSpent> for commands of that identifier, which we'll use to calculate how long they spent in the blocking queue in the last 180s every time a command is finished.

So the max we'll hold a mutex for someone is if they have several commands going to the blocking queue, or if we have the same command going to the blocking queue too much. But if that happens, there's a high chance we will block them either way.

Fixed Issues

For https://github.com/Expensify/Expensify/issues/662853

Tests

Compiles clean; existing unit + cluster tests unaffected (no behavior change yet).


Internal Testing Reminder: when changing bedrock, please compile auth against your new changes

Base automatically changed from danielsilva-662853-remove-count-rate-limit to main August 6, 2026 19:42
@danieldoglas
danieldoglas force-pushed the danielsilva-662853-blocking-queue-per-identifier-locking branch from 462ed60 to be0c03a Compare August 6, 2026 19:44
@danieldoglas
danieldoglas marked this pull request as ready for review August 7, 2026 14:41
Comment on lines +19 to +22
uint64_t BedrockBlockingCommandQueue::_now() const
{
const string identifier = command->blockingQueueRateLimitIdentifier;
return STimeNow();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is here so we can implement automated tests

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f8eca1c02

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread BedrockBlockingCommandQueue.cpp Outdated
// Hot path: called by push() and by _dequeue() under the base `_queueMutex`. Keep it O(1) by reading only
// the precomputed block deadline. The windowed time is summed in recordExecutionTime, off the blocking thread.
const uint64_t now = _now();
return _isBlocked(_accountStates, accountID, now) || _isBlocked(_commandStates, commandName, now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect disabled thresholds when checking active blocks

When a control command sets AccountThresholdMs or CommandThresholdMs to 0 after an identifier has already been blocked, new traffic for that account/command is still rejected until the old blockedUntil deadline because this check only consults the cached block state and not the current threshold. That contradicts the new “threshold of 0 disables that dimension” behavior and makes the documented emergency disable/reset path ineffective unless operators also know to send ClearBlocks.

Useful? React with 👍 / 👎.

…only branch, blocked-identifier status, MS naming
Comment thread BedrockServer.cpp Outdated
Comment thread BedrockServer.cpp Outdated
response["previousMaxBlockingQueueTimePerIdentifierMs"] = to_string(previousUS / 1000);
SINFO("Setting blocking queue max time per identifier to " << maxTimeMs << "ms");
if (command->request.isSet("WindowMS")) {
int64_t windowMS = command->request.calc64("WindowMS");

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.

You can collapse the above two lines into just int64_t windowMS = command->request.calc64("WindowMS");

it will end up 0 if it's not set. Similar below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think we want that. If you pass identifierThresholdMS or commandThresholdMS as 0, that means we're not gonna check for those anymore, which means disabling the rate limit. I think we want to allow disabling it, so having the check if the property exists makes sense.

// locking each entry to read `blockedUntil`; no code path takes an entry lock before the map mutex, so this
// can't deadlock.
auto inspect = [now](StateMap& map, size_t& tracked, list<string>& blocked) {
lock_guard<decltype(map.mapMutex)> lock(map.mapMutex);

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.

I don't really like this lock wrapping a full walk of the entire map, even more so since it waits for a lock on every single item in the map. It seems like it's only used in Status so maybe performance wise it's not that big of a deal.

However, the two locks are worth considering. Are you sure there's no way to cause a deadlock with this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't see how we would cause a deadlock here... maybe you're seeing something I'm not seeing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

getState only uses the mapMutex, same as clearRateLimits. The other places always use mapMutex, then the state mutex in that order, so I don't think a deadlock would happen.

void BedrockBlockingCommandQueue::push(unique_ptr<BedrockCommand>&& command)
{
if (isBlocked(command->blockingQueueRateLimitIdentifier, command->request.methodLine)) {
STHROW("503 Blocking queue rate limited (time)");

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.

NAB: Auth uses 429 Too Many Requests for something very similar, maybe we should standardize these.

Comment thread BedrockServer.cpp Outdated
Comment thread BedrockServer.cpp
@@ -2097,12 +2099,36 @@ void BedrockServer::_control(unique_ptr<BedrockCommand>& command)
} else if (SIEquals(command->request.methodLine, "SetConflictPageLocks")) {
_enableConflictPageLocks = command->request.test("enable");
} else if (SIEquals(command->request.methodLine, "SetBlockingQueueTimeRateLimit")) {

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do once we approve this and there are not more changes

Comment thread BedrockBlockingCommandQueue.cpp Outdated
Comment thread BedrockBlockingCommandQueue.cpp Outdated
Comment thread BedrockBlockingCommandQueue.cpp Outdated
Comment thread BedrockBlockingCommandQueue.cpp Outdated
it = state->commands.erase(it);
continue;
}
total += min(it->elapsedTime, windowUS - age);

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.

I don't understand this. Why not total += it->elapsedTime?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

let me know if it's clearer or not

Comment thread BedrockBlockingCommandQueue.cpp Outdated
Comment thread BedrockBlockingCommandQueue.cpp Outdated
Comment thread BedrockBlockingCommandQueue.cpp Outdated
danieldoglas and others added 10 commits August 10, 2026 14:03
Co-authored-by: Florent De'Neve <florent@expensify.com>
Takes a `commitCount` and returns the journal hash for that commit. Reads
through the DB pool the same way the Status command does, so it doesn't need
the sync node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers argument validation, that the returned hash matches what's actually in
the journal, and that the command is allowed from a non-localhost source while
other control commands aren't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
We'll verify this one by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread BedrockServer.cpp
Comment thread BedrockServer.cpp Outdated
@danieldoglas
danieldoglas requested a review from flodnv August 11, 2026 17:59
@flodnv
flodnv merged commit 9b71d8c into main Aug 12, 2026
9 of 11 checks passed
@flodnv
flodnv deleted the danielsilva-662853-blocking-queue-per-identifier-locking branch August 12, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants