-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(pubsub): implement publish hedging to reduce tail latency #13735
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
Open
tonyyyycui
wants to merge
45
commits into
googleapis:main
Choose a base branch
from
tonyyyycui:publish-hedging-settings
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
61d0b60
Add hedgeSettings with configurable hedgeDelay
19afd68
Add HedgeTokenBucket class to allow for token operations
49426a9
Add publisher integration with HedgeSettings
22417d1
Restore original @Ignore annotations and code in PublisherImplTest
91706a2
style(pubsub): fix checkstyle violations in HedgeSettings
bd76ede
style(pubsub): fix formatting and checkstyle violations in HedgeSetti…
61aef08
Merge branch 'main' into publish-hedging-settings
tonyyyycui 227f808
Resolve git comments related to access modifiers and constraints
e29eab8
Add CancellationSharer and HedgedRequest helper classes
ed05c60
Integrate CancellationSharer and HedgedRequest into Publisher.java. A…
23b8432
fix lint
3a23d60
Modifying comment line
da4307e
Modified default values, added configurable settings to refill ratio …
336c809
Removed HedgeTokenBucket.java class in favor of using an atomicintege…
59a7a09
Add documentation for scaling and remove unused getAttemptCount() in …
79411a8
Added method overloading to handle OTel span for hedged publish events
e545cd6
Verify that the per-RPC timeout is strictly greater than the hedging …
8543d23
Moved static methods in Publisher.java above the builder
3c48a1a
Configure empty token bucket and fix naming
0929a1b
Change from synchronization to actual lock object for processQueue me…
0295144
Add header injection for hedged publish requests
c8296f5
Cleanup code and fix bug with handling failure attempts in the first …
c8fd6e9
Disable retries for failing rpcs when publish hedging
0b88e33
Change how cancellationSharer propagates cancellations for different …
b6af015
Fixing race condition in unit test setup
be69b98
Merge branch 'main' into publish-hedging-settings
tonyyyycui 1a4644f
Modify header to use an integer count instead of a bool
e87c26f
Merge branch 'publish-hedging-settings' of github.com:tonyyyycui/goog…
dd288c7
Fix lint
8885dc2
Modify scale factor and minimum refillRatio value. Add test annotatio…
2eb687f
Update scale factor, refill ratio constraints, and simplify token buc…
100db18
Rename HedgeSettings to HedgingSettings
ba05985
Inline the hedging attempt count headers map creation
97820e7
Added new subsystem to loggerUtil and adjusted logging usage
58a4f70
Add debugging for token bucket rate limiting
7361f15
Add publish end (hedged) event as well
52a7cb3
Add synchronized locking to resolve race condition in cancellationsharer
5320a56
Clean up unused memory
747e11e
Set deadlineMs for hedged requests
73f61a2
Optimize processQueue() by setting threads free earlier
ecd4ef7
Move hedgingSettings check
e0a20c2
Fixed small bugs and renaming nits
a9bd384
Remove redundant code
ab78c53
Import new files into librarian.yaml
2519a90
Modfied hedging publish start and end events in OTel spans
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
172 changes: 172 additions & 0 deletions
172
...bsub/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/CancellationSharer.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.pubsub.v1; | ||
|
|
||
| import com.google.api.core.AbstractApiFuture; | ||
| import com.google.api.core.ApiFuture; | ||
| import com.google.api.core.ApiFutureCallback; | ||
| import com.google.api.core.ApiFutures; | ||
| import com.google.api.gax.rpc.ApiException; | ||
| import com.google.common.util.concurrent.MoreExecutors; | ||
| import com.google.pubsub.v1.PublishResponse; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.locks.Lock; | ||
| import java.util.concurrent.locks.ReentrantLock; | ||
|
|
||
| /** | ||
| * Coordinates multiple publish attempts for a single batch of messages. | ||
| * | ||
| * <p>Implements {@link ApiFuture} to act as the single future returned to the publisher's client. | ||
| * It manages the lifecycle of the original attempt and any subsequent hedged attempts. | ||
| */ | ||
| class CancellationSharer extends AbstractApiFuture<PublishResponse> { | ||
| private Publisher.OutstandingBatch batch; | ||
| private final Publisher publisher; | ||
| private final long absoluteDeadlineMs; | ||
|
|
||
| // Guarded by lock | ||
| private final Map<Integer, ApiFuture<PublishResponse>> runningAttempts = new HashMap<>(); | ||
| private boolean done = false; | ||
| private Throwable lastError; | ||
|
|
||
| private final Lock lock = new ReentrantLock(); | ||
|
|
||
| private void cleanupLocked() { | ||
| runningAttempts.clear(); | ||
| this.batch = null; | ||
| } | ||
|
|
||
| CancellationSharer( | ||
| final Publisher.OutstandingBatch batch, final Publisher publisher, final long absoluteDeadlineMs) { | ||
| this.batch = batch; | ||
| this.publisher = publisher; | ||
| this.absoluteDeadlineMs = absoluteDeadlineMs; | ||
| } | ||
|
|
||
| void addAttempt(final int attemptNumber, final ApiFuture<PublishResponse> future) { | ||
| lock.lock(); | ||
| try { | ||
| if (done) { | ||
| future.cancel(true); | ||
| return; | ||
| } | ||
| runningAttempts.put(attemptNumber, future); | ||
| } finally { | ||
| lock.unlock(); | ||
| } | ||
|
|
||
| ApiFutures.addCallback( | ||
| future, | ||
| new ApiFutureCallback<PublishResponse>() { | ||
| @Override | ||
| public void onSuccess(final PublishResponse result) { | ||
| handleAttemptSuccess(attemptNumber, result); | ||
| } | ||
|
|
||
| @Override | ||
| public void onFailure(final Throwable t) { | ||
| handleAttemptFailure(attemptNumber, t); | ||
| } | ||
| }, | ||
| MoreExecutors.directExecutor()); | ||
| } | ||
|
|
||
| private void handleAttemptSuccess(final int attemptNumber, final PublishResponse response) { | ||
| lock.lock(); | ||
| try { | ||
| if (done) { | ||
| return; | ||
| } | ||
| done = true; | ||
| batch.successfulAttempt = attemptNumber; | ||
| publisher.refillTokenBucket(); | ||
| set(response); | ||
| cancelAllExceptLocked(attemptNumber); | ||
| cleanupLocked(); | ||
| } finally { | ||
| lock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| private void handleAttemptFailure(final int attemptNumber, final Throwable t) { | ||
| lock.lock(); | ||
| try { | ||
| if (done) { | ||
| return; | ||
| } | ||
| runningAttempts.remove(attemptNumber); | ||
| lastError = t; | ||
| if (attemptNumber == 0 || runningAttempts.isEmpty()) { | ||
| done = true; | ||
| setException(lastError); | ||
| cancelAllLocked(); | ||
| cleanupLocked(); | ||
| } | ||
| } finally { | ||
| lock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public boolean cancel(final boolean mayInterruptIfRunning) { | ||
| boolean cancelled = false; | ||
| lock.lock(); | ||
| try { | ||
| if (super.cancel(mayInterruptIfRunning)) { | ||
| cancelled = true; | ||
| done = true; | ||
| cancelAllLocked(); | ||
| cleanupLocked(); | ||
| } | ||
| } finally { | ||
| lock.unlock(); | ||
| } | ||
| return cancelled; | ||
| } | ||
|
|
||
| private void cancelAllLocked() { | ||
| for (ApiFuture<PublishResponse> future : runningAttempts.values()) { | ||
| future.cancel(true); | ||
| } | ||
| runningAttempts.clear(); | ||
| } | ||
|
|
||
| private void cancelAllExceptLocked(final int successfulAttempt) { | ||
| runningAttempts.forEach( | ||
| (attempt, future) -> { | ||
| if (attempt != successfulAttempt) { | ||
| future.cancel(true); | ||
| } | ||
| }); | ||
| runningAttempts.clear(); | ||
| } | ||
|
|
||
| Publisher.OutstandingBatch getBatchIfActive() { | ||
| lock.lock(); | ||
| try { | ||
| return done ? null : batch; | ||
| } finally { | ||
| lock.unlock(); | ||
| } | ||
| } | ||
|
|
||
| long getAbsoluteDeadlineMs() { | ||
| return absoluteDeadlineMs; | ||
| } | ||
| } |
42 changes: 42 additions & 0 deletions
42
java-pubsub/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/HedgedRequest.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.pubsub.v1; | ||
|
|
||
| /** Represents a pending hedging check in the publisher's queue. */ | ||
| class HedgedRequest { | ||
| private final CancellationSharer coordinator; | ||
| private final int attemptNumber; | ||
| private final long sendAfterMs; | ||
|
|
||
| HedgedRequest(CancellationSharer coordinator, int attemptNumber, long sendAfterMs) { | ||
| this.coordinator = coordinator; | ||
| this.attemptNumber = attemptNumber; | ||
| this.sendAfterMs = sendAfterMs; | ||
| } | ||
|
|
||
| CancellationSharer getCoordinator() { | ||
| return coordinator; | ||
| } | ||
|
|
||
| int getAttemptNumber() { | ||
| return attemptNumber; | ||
| } | ||
|
|
||
| long getSendAfterMs() { | ||
| return sendAfterMs; | ||
| } | ||
| } |
162 changes: 162 additions & 0 deletions
162
...-pubsub/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/HedgingSettings.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.pubsub.v1; | ||
|
|
||
| import com.google.common.base.Preconditions; | ||
| import java.time.Duration; | ||
|
|
||
| /** Settings for configuring publish hedging. */ | ||
| public final class HedgingSettings { | ||
| /** Default hedging delay. */ | ||
| private static final Duration DEFAULT_DELAY = Duration.ofMillis(1000); | ||
|
|
||
| /** Default maximum number of tokens in the bucket. */ | ||
| private static final int DEFAULT_MAX_TOKENS = 50; | ||
|
|
||
| /** Default refill rate (tokens per successful request). */ | ||
| private static final float DEFAULT_REFILL_RATIO = 0.1f; | ||
|
|
||
| /** Minimum refill rate. */ | ||
| private static final float MIN_REFILL_RATIO = 0.001f; | ||
|
|
||
| /** Maximum refill rate. */ | ||
| private static final float MAX_REFILL_RATIO = 0.2f; | ||
|
|
||
| /** Hedging delay. */ | ||
| private final Duration hedgeDelay; | ||
|
|
||
| /** Maximum tokens. */ | ||
| private final int maxTokens; | ||
|
|
||
| /** Refill rate. */ | ||
| private final float refillRatio; | ||
|
|
||
| private HedgingSettings(final Builder builder) { | ||
| this.hedgeDelay = builder.hedgeDelay; | ||
| this.maxTokens = builder.maxTokens; | ||
| this.refillRatio = builder.refillRatio; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the configured hedging delay. | ||
| * | ||
| * @return the hedging delay. | ||
| */ | ||
| public Duration getHedgeDelay() { | ||
| return hedgeDelay; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the maximum number of tokens in the token bucket rate limiter. | ||
| * | ||
| * @return the max tokens. | ||
| */ | ||
| public int getMaxTokens() { | ||
| return maxTokens; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the token bucket refill ratio per successful request. | ||
| * | ||
| * @return the refill ratio. | ||
| */ | ||
| public float getRefillRatio() { | ||
| return refillRatio; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a new builder for {@code HedgingSettings}. | ||
| * | ||
| * @return a new builder. | ||
| */ | ||
| public static Builder newBuilder() { | ||
| return new Builder(); | ||
| } | ||
|
|
||
| /** Builder for {@code HedgingSettings}. */ | ||
| public static final class Builder { | ||
| /** Hedging delay. */ | ||
| private Duration hedgeDelay = DEFAULT_DELAY; | ||
|
|
||
| /** Maximum tokens. */ | ||
| private int maxTokens = DEFAULT_MAX_TOKENS; | ||
|
|
||
| /** Refill rate. */ | ||
| private float refillRatio = DEFAULT_REFILL_RATIO; | ||
|
|
||
| private Builder() {} | ||
|
|
||
| /** | ||
| * Allows hedging delay to be configurable. | ||
| * | ||
| * @param delay the hedging delay, must be 0.1s <= HedgeDelay <= 10s. | ||
| * @return this builder. | ||
| */ | ||
| public Builder setHedgeDelay(final Duration delay) { | ||
| Preconditions.checkNotNull(delay); | ||
| if (delay.toMillis() < 100 || delay.toMillis() > 10000) { | ||
| throw new IllegalArgumentException( | ||
| "hedgeDelay must be greater than or equal to 100ms and less than or equal to 10s"); | ||
| } | ||
| this.hedgeDelay = delay; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Allows the maximum number of tokens in the bucket to be configurable. | ||
| * | ||
| * @param maxTokens the maximum number of tokens, must be 0 < MaxTokens <= 250. | ||
| * @return this builder. | ||
| */ | ||
| public Builder setMaxTokens(final int maxTokens) { | ||
| if (maxTokens <= 0 || maxTokens > 250) { | ||
| throw new IllegalArgumentException( | ||
| "maxTokens must be greater than 0 and less than or equal to 250"); | ||
| } | ||
| this.maxTokens = maxTokens; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Allows the token bucket refill rate to be configurable. | ||
| * | ||
| * @param refillRatio the refill rate (tokens per successful request), must be 0.001 <= | ||
| * RefillRatio <= 0.2. | ||
| * @return this builder. | ||
| */ | ||
| public Builder setRefillRatio(final float refillRatio) { | ||
| if (refillRatio < MIN_REFILL_RATIO || refillRatio > MAX_REFILL_RATIO) { | ||
| throw new IllegalArgumentException( | ||
| "refillRatio must be greater than or equal to " | ||
| + MIN_REFILL_RATIO | ||
| + " and less than or equal to " | ||
| + MAX_REFILL_RATIO); | ||
| } | ||
| this.refillRatio = refillRatio; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Builds an instance of {@code HedgingSettings}. | ||
| * | ||
| * @return the built {@code HedgingSettings} instance. | ||
| */ | ||
| public HedgingSettings build() { | ||
| return new HedgingSettings(this); | ||
| } | ||
| } | ||
| } |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.