Skip to content

Commit 52a7cb3

Browse files
author
Tony Cui
committed
Add synchronized locking to resolve race condition in cancellationsharer
1 parent 7361f15 commit 52a7cb3

1 file changed

Lines changed: 95 additions & 77 deletions

File tree

java-pubsub/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/CancellationSharer.java

Lines changed: 95 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -23,56 +23,45 @@
2323
import com.google.api.gax.rpc.ApiException;
2424
import com.google.common.util.concurrent.MoreExecutors;
2525
import com.google.pubsub.v1.PublishResponse;
26+
import java.util.HashMap;
2627
import java.util.Map;
27-
import java.util.concurrent.ConcurrentHashMap;
2828
import java.util.concurrent.atomic.AtomicBoolean;
29-
import java.util.concurrent.atomic.AtomicReference;
29+
import java.util.concurrent.locks.Lock;
30+
import java.util.concurrent.locks.ReentrantLock;
3031

3132
/**
3233
* Coordinates multiple publish attempts for a single batch of messages.
3334
*
34-
* <p>Implements {@link ApiFuture} to act as the single future returned to the
35-
* publisher's client. It manages the lifecycle of the original attempt and any
36-
* subsequent hedged attempts.
35+
* <p>Implements {@link ApiFuture} to act as the single future returned to the publisher's client.
36+
* It manages the lifecycle of the original attempt and any subsequent hedged attempts.
3737
*/
3838
class CancellationSharer extends AbstractApiFuture<PublishResponse> {
39-
/** The message batch being coordinated. */
4039
private final Publisher.OutstandingBatch batch;
41-
42-
/** The publisher instance. */
4340
private final Publisher publisher;
4441

45-
/** Map of active attempt numbers to their respective gRPC futures. */
46-
private final Map<Integer, ApiFuture<PublishResponse>> runningAttempts =
47-
new ConcurrentHashMap<>();
48-
49-
/** Boolean indicating whether the overall batch has resolved or failed. */
50-
private final AtomicBoolean done = new AtomicBoolean(false);
42+
// Guarded by lock
43+
private final Map<Integer, ApiFuture<PublishResponse>> runningAttempts = new HashMap<>();
44+
private boolean done = false;
45+
private Throwable lastError;
5146

52-
/** Boolean indicating whether the batch is currently waiting in the hedging queue. */
47+
private final Lock lock = new ReentrantLock();
5348
private final AtomicBoolean isInQueue = new AtomicBoolean(false);
5449

55-
/** The last error encountered by any failed attempt. */
56-
private final AtomicReference<Throwable> lastError = new AtomicReference<>();
57-
5850
CancellationSharer(final Publisher.OutstandingBatch batch, final Publisher publisher) {
5951
this.batch = batch;
6052
this.publisher = publisher;
6153
}
6254

63-
/**
64-
* Adds an attempt to be tracked by this coordinator.
65-
*
66-
* @param attemptNumber the 1-based index of the attempt (1 is original, 2+ are hedged)
67-
* @param future the future representing the gRPC call for this attempt
68-
*/
6955
void addAttempt(final int attemptNumber, final ApiFuture<PublishResponse> future) {
70-
runningAttempts.put(attemptNumber, future);
71-
72-
if (done.get()) {
73-
future.cancel(true);
74-
runningAttempts.remove(attemptNumber);
75-
return;
56+
lock.lock();
57+
try {
58+
if (done) {
59+
future.cancel(true);
60+
return;
61+
}
62+
runningAttempts.put(attemptNumber, future);
63+
} finally {
64+
lock.unlock();
7665
}
7766

7867
ApiFutures.addCallback(
@@ -92,80 +81,109 @@ public void onFailure(final Throwable t) {
9281
}
9382

9483
private void handleAttemptSuccess(final int attemptNumber, final PublishResponse response) {
95-
if (done.compareAndSet(false, true)) {
84+
lock.lock();
85+
try {
86+
if (done) {
87+
return;
88+
}
89+
done = true;
9690
batch.successfulAttempt = attemptNumber;
97-
set(response); // Resolve parent future
98-
cancelAllExcept(attemptNumber);
99-
publisher.refillTokenBucket();
91+
set(response);
92+
cancelAllExceptLocked(attemptNumber);
93+
} finally {
94+
lock.unlock();
10095
}
96+
publisher.refillTokenBucket();
10197
}
10298

10399
private void handleAttemptFailure(final int attemptNumber, final Throwable t) {
104-
runningAttempts.remove(attemptNumber);
100+
boolean shouldRemoveFromQueue = false;
101+
lock.lock();
102+
try {
103+
if (done) {
104+
return; // <-- Exit early before modifying runningAttempts to avoid
105+
// ConcurrentModificationException
106+
}
107+
runningAttempts.remove(attemptNumber);
108+
lastError = t;
105109

106-
if (done.get()) {
107-
return;
108-
}
109-
lastError.set(t);
110-
111-
boolean isRetryable = true;
112-
if (t instanceof ApiException) {
113-
isRetryable =
114-
publisher
115-
.getRetryableCodes()
116-
.contains(((ApiException) t).getStatusCode().getCode());
117-
}
110+
boolean isRetryable = true;
111+
if (t instanceof ApiException) {
112+
isRetryable =
113+
publisher.getRetryableCodes().contains(((ApiException) t).getStatusCode().getCode());
114+
}
118115

119-
if (runningAttempts.isEmpty() || !isRetryable) {
120-
if (done.compareAndSet(false, true)) {
121-
setException(lastError.get());
122-
cancelAll();
116+
if (runningAttempts.isEmpty() || !isRetryable) {
117+
done = true;
118+
setException(lastError);
119+
cancelAllLocked();
123120
if (isInQueue.get()) {
124-
publisher.removeFromHedgingQueue(this);
121+
shouldRemoveFromQueue = true;
125122
}
126123
}
124+
} finally {
125+
lock.unlock();
127126
}
128-
}
129127

130-
private void cancelAll() {
131-
for (ApiFuture<PublishResponse> future : runningAttempts.values()) {
132-
future.cancel(true);
128+
if (shouldRemoveFromQueue) {
129+
publisher.removeFromHedgingQueue(this);
133130
}
134131
}
135132

136133
void checkCompletionOnQueueExit() {
137-
if (!done.get() && runningAttempts.isEmpty() && !isInQueue.get()) {
138-
if (done.compareAndSet(false, true)) {
139-
Throwable error = lastError.get();
134+
lock.lock();
135+
try {
136+
if (!done && runningAttempts.isEmpty() && !isInQueue.get()) {
137+
done = true;
140138
setException(
141-
error != null
142-
? error
139+
lastError != null
140+
? lastError
143141
: new RuntimeException("Hedging failed with no active attempts"));
144142
}
143+
} finally {
144+
lock.unlock();
145145
}
146146
}
147147

148-
private void cancelAllExcept(final int successfulAttempt) {
149-
for (Map.Entry<Integer, ApiFuture<PublishResponse>> entry : runningAttempts.entrySet()) {
150-
if (entry.getKey() != successfulAttempt) {
151-
entry.getValue().cancel(true);
148+
@Override
149+
public boolean cancel(final boolean mayInterruptIfRunning) {
150+
boolean cancelled = false;
151+
boolean shouldRemoveFromQueue = false;
152+
lock.lock();
153+
try {
154+
if (super.cancel(mayInterruptIfRunning)) {
155+
cancelled = true;
156+
done = true;
157+
if (isInQueue.get()) {
158+
shouldRemoveFromQueue = true;
159+
}
160+
cancelAllLocked();
152161
}
162+
} finally {
163+
lock.unlock();
153164
}
165+
166+
if (shouldRemoveFromQueue) {
167+
publisher.removeFromHedgingQueue(this);
168+
}
169+
return cancelled;
154170
}
155171

156-
@Override
157-
public boolean cancel(final boolean mayInterruptIfRunning) {
158-
if (super.cancel(mayInterruptIfRunning)) {
159-
done.set(true);
160-
if (isInQueue.get()) {
161-
publisher.removeFromHedgingQueue(this);
162-
}
163-
for (ApiFuture<PublishResponse> future : runningAttempts.values()) {
164-
future.cancel(mayInterruptIfRunning);
165-
}
166-
return true;
172+
private void cancelAllLocked() {
173+
for (ApiFuture<PublishResponse> future : runningAttempts.values()) {
174+
future.cancel(true);
167175
}
168-
return false;
176+
runningAttempts.clear();
177+
}
178+
179+
private void cancelAllExceptLocked(final int successfulAttempt) {
180+
runningAttempts.forEach(
181+
(attempt, future) -> {
182+
if (attempt != successfulAttempt) {
183+
future.cancel(true);
184+
}
185+
});
186+
runningAttempts.clear();
169187
}
170188

171189
AtomicBoolean isInQueue() {

0 commit comments

Comments
 (0)