-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[fix][broker] Prevent dispatch rate overshoot when sharing global limiter #25061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,12 +131,11 @@ public int filterEntriesForConsumer(@Nullable MessageMetadata[] metadataArray, i | |
| long totalBytes = 0; | ||
| int totalChunkedMessages = 0; | ||
| int totalEntries = 0; | ||
| int filteredMessageCount = 0; | ||
| int filteredEntryCount = 0; | ||
| long filteredBytesCount = 0; | ||
| List<Position> entriesToFiltered = hasFilter ? new ArrayList<>() : null; | ||
| List<Position> entriesToRedeliver = hasFilter ? new ArrayList<>() : null; | ||
| for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) { | ||
| List<Position> entriesToRedeliver = new ArrayList<>(entries.size()); | ||
| int i; | ||
| int entriesSize = entries.size(); | ||
| for (i = 0; i < entriesSize; i++) { | ||
| final Entry entry = entries.get(i); | ||
| if (entry == null) { | ||
| continue; | ||
|
|
@@ -160,30 +159,35 @@ public int filterEntriesForConsumer(@Nullable MessageMetadata[] metadataArray, i | |
| this.filterProcessedMsgs.add(entryMsgCnt); | ||
| } | ||
|
|
||
| boolean filtered = false; | ||
| EntryFilter.FilterResult filterResult = runFiltersForEntry(entry, msgMetadata, consumer); | ||
| if (filterResult == EntryFilter.FilterResult.REJECT) { | ||
| entriesToFiltered.add(entry.getPosition()); | ||
| entries.set(i, null); | ||
| // FilterResult will be always `ACCEPTED` when there is No Filter | ||
| // dont need to judge whether `hasFilter` is true or not. | ||
| this.filterRejectedMsgs.add(entryMsgCnt); | ||
| filteredEntryCount++; | ||
| filteredMessageCount += entryMsgCnt; | ||
| filteredBytesCount += metadataAndPayload.readableBytes(); | ||
| entry.release(); | ||
| continue; | ||
| filtered = true; | ||
| } else if (filterResult == EntryFilter.FilterResult.RESCHEDULE) { | ||
| entriesToRedeliver.add(entry.getPosition()); | ||
| entries.set(i, null); | ||
| // FilterResult will be always `ACCEPTED` when there is No Filter | ||
| // dont need to judge whether `hasFilter` is true or not. | ||
| this.filterRescheduledMsgs.add(entryMsgCnt); | ||
| filteredEntryCount++; | ||
| filteredMessageCount += entryMsgCnt; | ||
| filteredBytesCount += metadataAndPayload.readableBytes(); | ||
| filtered = true; | ||
| } | ||
|
|
||
| if (filtered) { | ||
| int readableBytes = metadataAndPayload.readableBytes(); | ||
| entries.set(i, null); | ||
| entry.release(); | ||
| if (serviceConfig.isDispatchThrottlingForFilteredEntriesEnabled()) { | ||
| if (!tryAcquirePermitsForDeliveredMessages(subscription.getTopic(), cursor, entryMsgCnt, | ||
| readableBytes)) { | ||
| break; // do not process further entries | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| if (msgMetadata != null && msgMetadata.hasTxnidMostBits() | ||
| && msgMetadata.hasTxnidLeastBits()) { | ||
| if (Markers.isTxnMarker(msgMetadata)) { | ||
|
|
@@ -283,6 +287,11 @@ public int filterEntriesForConsumer(@Nullable MessageMetadata[] metadataArray, i | |
| } | ||
| } | ||
|
|
||
| if (!tryAcquirePermitsForDeliveredMessages(subscription.getTopic(), cursor, entryMsgCnt, | ||
| metadataAndPayload.readableBytes())) { | ||
| break; // do not process further entries | ||
| } | ||
|
|
||
| totalEntries++; | ||
| totalMessages += batchSize; | ||
| totalBytes += metadataAndPayload.readableBytes(); | ||
|
|
@@ -296,6 +305,19 @@ public int filterEntriesForConsumer(@Nullable MessageMetadata[] metadataArray, i | |
| interceptor.beforeSendMessage(subscription, entry, ackSet, msgMetadata, consumer); | ||
| } | ||
| } | ||
|
|
||
| if (i < entriesSize) { | ||
| // throttled, release the unprocessed entries | ||
| for (int j = i; j < entriesSize; j++) { | ||
| Entry entry = entries.get(j); | ||
| if (entry != null) { | ||
| entriesToRedeliver.add(entry.getPosition()); | ||
| entries.set(j, null); | ||
| entry.release(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (CollectionUtils.isNotEmpty(entriesToFiltered)) { | ||
| individualAcknowledgeMessageIfNeeded(entriesToFiltered, Collections.emptyMap()); | ||
|
|
||
|
|
@@ -315,11 +337,6 @@ public int filterEntriesForConsumer(@Nullable MessageMetadata[] metadataArray, i | |
|
|
||
| } | ||
|
|
||
| if (serviceConfig.isDispatchThrottlingForFilteredEntriesEnabled()) { | ||
| acquirePermitsForDeliveredMessages(subscription.getTopic(), cursor, filteredEntryCount, | ||
| filteredMessageCount, filteredBytesCount); | ||
| } | ||
|
|
||
| sendMessageInfo.setTotalMessages(totalMessages); | ||
| sendMessageInfo.setTotalBytes(totalBytes); | ||
| sendMessageInfo.setTotalChunkedMessages(totalChunkedMessages); | ||
|
|
@@ -332,17 +349,23 @@ private void individualAcknowledgeMessageIfNeeded(List<Position> positions, Map< | |
| } | ||
| } | ||
|
|
||
| protected void acquirePermitsForDeliveredMessages(Topic topic, ManagedCursor cursor, long totalEntries, | ||
| long totalMessagesSent, long totalBytesSent) { | ||
| private boolean tryAcquirePermitsForDeliveredMessages( | ||
| Topic topic, ManagedCursor cursor, long totalMessagesSent, long totalBytesSent) { | ||
|
Comment on lines
+352
to
+353
|
||
| boolean permitsAcquired = true; | ||
| if (serviceConfig.isDispatchThrottlingOnNonBacklogConsumerEnabled() | ||
| || (cursor != null && !cursor.isActive())) { | ||
| long permits = dispatchThrottlingOnBatchMessageEnabled ? totalEntries : totalMessagesSent; | ||
| topic.getBrokerDispatchRateLimiter().ifPresent(rateLimiter -> | ||
| rateLimiter.consumeDispatchQuota(permits, totalBytesSent)); | ||
| topic.getDispatchRateLimiter().ifPresent(rateLimter -> | ||
| rateLimter.consumeDispatchQuota(permits, totalBytesSent)); | ||
| getRateLimiter().ifPresent(rateLimiter -> rateLimiter.consumeDispatchQuota(permits, totalBytesSent)); | ||
| long permits = dispatchThrottlingOnBatchMessageEnabled ? 1 : totalMessagesSent; | ||
| permitsAcquired &= topic.getBrokerDispatchRateLimiter() | ||
| .map(l -> l.tryConsumeDispatchQuota(permits, totalBytesSent)) | ||
| .orElse(true); | ||
| permitsAcquired &= topic.getDispatchRateLimiter() | ||
| .map(l -> l.tryConsumeDispatchQuota(permits, totalBytesSent)) | ||
| .orElse(true); | ||
| permitsAcquired &= getRateLimiter() | ||
| .map(l -> l.tryConsumeDispatchQuota(permits, totalBytesSent)) | ||
| .orElse(true); | ||
| } | ||
| return permitsAcquired; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -84,6 +84,14 @@ protected DispatchRateLimiter(BrokerService brokerService) { | |||||||||||
| */ | ||||||||||||
| public abstract void consumeDispatchQuota(long numberOfMessages, long byteSize); | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * It tries to acquire msg and bytes permits from rate-limiter and returns if acquired permits succeed. | ||||||||||||
| * | ||||||||||||
| * @param numberOfMessages | ||||||||||||
| * @param byteSize | ||||||||||||
|
Comment on lines
+90
to
+91
|
||||||||||||
| * @param numberOfMessages | |
| * @param byteSize | |
| * @param numberOfMessages the number of messages to acquire permits for | |
| * @param byteSize the number of bytes to acquire permits for | |
| * @return true if permits were successfully acquired, false otherwise |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Javadoc is incomplete and missing parameter descriptions. The @param tags should have descriptions for what numberOfMessages and byteSize represent.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -73,14 +73,21 @@ public long getAvailableDispatchRateLimitOnByte() { | |||||||||||||||||||
| */ | ||||||||||||||||||||
| @Override | ||||||||||||||||||||
| public void consumeDispatchQuota(long numberOfMessages, long byteSize) { | ||||||||||||||||||||
| tryConsumeDispatchQuota(numberOfMessages, byteSize); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
||||||||||||||||||||
| /** | |
| * Tries to acquire message and byte permits from the rate-limiter and returns whether the acquisition succeeded. | |
| * | |
| * @param numberOfMessages the number of message permits to acquire | |
| * @param byteSize the number of byte permits to acquire | |
| * @return true if permits were successfully acquired, false otherwise | |
| */ |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method consumes tokens from multiple limiters sequentially, which means if the message limiter succeeds but the byte limiter fails, tokens have already been consumed from the message limiter. This creates a conservative behavior where some quota is consumed without dispatching messages. Consider documenting this behavior in the method's Javadoc, or alternatively, check all limiters' availability before consuming any tokens to avoid unnecessary quota consumption.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -74,14 +74,26 @@ public long getAvailableDispatchRateLimitOnByte() { | |||||||||||
| */ | ||||||||||||
| @Override | ||||||||||||
| public void consumeDispatchQuota(long numberOfMessages, long byteSize) { | ||||||||||||
| tryConsumeDispatchQuota(numberOfMessages, byteSize); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * It acquires msg and bytes permits from rate-limiter and returns if acquired permits succeed. | ||||||||||||
| * @param numberOfMessages | ||||||||||||
| * @param byteSize | ||||||||||||
|
Comment on lines
+82
to
+83
|
||||||||||||
| * @param numberOfMessages | |
| * @param byteSize | |
| * @param numberOfMessages the number of messages to acquire permits for | |
| * @param byteSize the number of bytes to acquire permits for | |
| * @return true if permits were successfully acquired, false otherwise |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method consumes tokens from multiple limiters sequentially, which means if the message limiter succeeds but the byte limiter fails, tokens have already been consumed from the message limiter. This creates a conservative behavior where some quota is consumed without dispatching messages. Consider documenting this behavior in the method's Javadoc, or alternatively, check all limiters' availability before consuming any tokens to avoid unnecessary quota consumption.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the previous parameter,
totalBytesSentis misleading since this method is now called per-entry rather than once after processing all entries. Consider renaming tobyteCountornumBytesto better reflect that it represents the count for a single entry, not a cumulative total.