Skip to content

Commit 98ceff0

Browse files
fix: simplify AckSetTrackerImpl and make acks after shutdown not cause a permanent error (#872)
* fix: simplify AckSetTrackerImpl and make acks after shutdown not cause a permanent error * fix: comment
1 parent 23765cb commit 98ceff0

File tree

3 files changed

+92
-76
lines changed

3 files changed

+92
-76
lines changed

google-cloud-pubsublite/clirr-ignored-differences.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@
132132
<differenceType>8001</differenceType>
133133
<className>com/google/cloud/pubsublite/internal/**</className>
134134
</difference>
135+
<difference>
136+
<differenceType>4001</differenceType>
137+
<className>com/google/cloud/pubsublite/cloudpubsub/internal/**</className>
138+
<to>**</to>
139+
</difference>
140+
<difference>
141+
<differenceType>5001</differenceType>
142+
<className>com/google/cloud/pubsublite/cloudpubsub/internal/**</className>
143+
<to>**</to>
144+
</difference>
135145
<difference>
136146
<differenceType>6000</differenceType>
137147
<className>com/google/cloud/pubsublite/cloudpubsub/internal/**</className>

google-cloud-pubsublite/src/main/java/com/google/cloud/pubsublite/cloudpubsub/internal/AckSetTrackerImpl.java

Lines changed: 73 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -24,120 +24,117 @@
2424
import com.google.cloud.pubsublite.Offset;
2525
import com.google.cloud.pubsublite.SequencedMessage;
2626
import com.google.cloud.pubsublite.internal.CheckedApiException;
27-
import com.google.cloud.pubsublite.internal.CloseableMonitor;
2827
import com.google.cloud.pubsublite.internal.ExtractStatus;
29-
import com.google.cloud.pubsublite.internal.TrivialProxyService;
28+
import com.google.cloud.pubsublite.internal.ProxyService;
3029
import com.google.cloud.pubsublite.internal.wire.Committer;
31-
import com.google.common.collect.ImmutableList;
30+
import com.google.common.flogger.GoogleLogger;
3231
import com.google.errorprone.annotations.concurrent.GuardedBy;
3332
import java.util.ArrayDeque;
3433
import java.util.Deque;
35-
import java.util.List;
3634
import java.util.Optional;
3735
import java.util.PriorityQueue;
36+
import java.util.concurrent.atomic.AtomicBoolean;
3837

39-
public class AckSetTrackerImpl extends TrivialProxyService implements AckSetTracker {
40-
// Receipt represents an unacked message. It can be cleared, which will cause the ack to be
41-
// ignored.
38+
public class AckSetTrackerImpl extends ProxyService implements AckSetTracker {
39+
private static final GoogleLogger LOGGER = GoogleLogger.forEnclosingClass();
40+
41+
// Receipt represents an unacked message. If the tracker generation is incremented, the ack will
42+
// be ignored.
4243
private static class Receipt {
4344
final Offset offset;
45+
final long generation;
4446

45-
private final CloseableMonitor m = new CloseableMonitor();
46-
47-
@GuardedBy("m.monitor")
48-
private boolean wasAcked = false;
47+
private final AtomicBoolean wasAcked = new AtomicBoolean();
4948

50-
@GuardedBy("m.monitor")
51-
private Optional<AckSetTrackerImpl> tracker;
49+
private final AckSetTrackerImpl tracker;
5250

53-
Receipt(Offset offset, AckSetTrackerImpl tracker) {
51+
Receipt(Offset offset, long generation, AckSetTrackerImpl tracker) {
5452
this.offset = offset;
55-
this.tracker = Optional.of(tracker);
56-
}
57-
58-
void clear() {
59-
try (CloseableMonitor.Hold h = m.enter()) {
60-
tracker = Optional.empty();
61-
}
53+
this.generation = generation;
54+
this.tracker = tracker;
6255
}
6356

64-
void onAck() {
65-
try (CloseableMonitor.Hold h = m.enter()) {
66-
if (!tracker.isPresent()) {
67-
return;
68-
}
69-
if (wasAcked) {
70-
CheckedApiException e =
71-
new CheckedApiException("Duplicate acks are not allowed.", Code.FAILED_PRECONDITION);
72-
tracker.get().onPermanentError(e);
73-
throw e.underlying;
74-
}
75-
wasAcked = true;
76-
tracker.get().onAck(offset);
57+
void onAck() throws ApiException {
58+
if (wasAcked.getAndSet(true)) {
59+
CheckedApiException e =
60+
new CheckedApiException("Duplicate acks are not allowed.", Code.FAILED_PRECONDITION);
61+
tracker.onPermanentError(e);
62+
throw e.underlying;
7763
}
64+
tracker.onAck(offset, generation);
7865
}
7966
}
8067

81-
private final CloseableMonitor monitor = new CloseableMonitor();
82-
83-
@GuardedBy("monitor.monitor")
68+
@GuardedBy("this")
8469
private final Committer committer;
8570

86-
@GuardedBy("monitor.monitor")
71+
@GuardedBy("this")
8772
private final Deque<Receipt> receipts = new ArrayDeque<>();
8873

89-
@GuardedBy("monitor.monitor")
74+
@GuardedBy("this")
9075
private final PriorityQueue<Offset> acks = new PriorityQueue<>();
9176

77+
@GuardedBy("this")
78+
private long generation = 0L;
79+
80+
@GuardedBy("this")
81+
private boolean shutdown = false;
82+
9283
public AckSetTrackerImpl(Committer committer) throws ApiException {
93-
super(committer);
9484
this.committer = committer;
85+
addServices(committer);
9586
}
9687

9788
// AckSetTracker implementation.
9889
@Override
99-
public Runnable track(SequencedMessage message) throws CheckedApiException {
100-
final Offset messageOffset = message.offset();
101-
try (CloseableMonitor.Hold h = monitor.enter()) {
102-
checkArgument(
103-
receipts.isEmpty() || receipts.peekLast().offset.value() < messageOffset.value());
104-
Receipt receipt = new Receipt(messageOffset, this);
105-
receipts.addLast(receipt);
106-
return receipt::onAck;
107-
}
90+
public synchronized Runnable track(SequencedMessage message) throws CheckedApiException {
91+
checkArgument(
92+
receipts.isEmpty() || receipts.peekLast().offset.value() < message.offset().value());
93+
Receipt receipt = new Receipt(message.offset(), generation, this);
94+
receipts.addLast(receipt);
95+
return receipt::onAck;
10896
}
10997

11098
@Override
111-
public void waitUntilCommitted() throws CheckedApiException {
112-
List<Receipt> receiptsCopy;
113-
try (CloseableMonitor.Hold h = monitor.enter()) {
114-
receiptsCopy = ImmutableList.copyOf(receipts);
99+
public synchronized void waitUntilCommitted() throws CheckedApiException {
100+
++generation;
101+
receipts.clear();
102+
acks.clear();
103+
committer.waitUntilEmpty();
104+
}
105+
106+
private synchronized void onAck(Offset offset, long generation) {
107+
if (shutdown) {
108+
LOGGER.atFine().log("Dropping ack after tracker shutdown.");
109+
return;
115110
}
116-
// Clearing receipts here avoids deadlocks due to locks acquired in different order.
117-
receiptsCopy.forEach(Receipt::clear);
118-
try (CloseableMonitor.Hold h = monitor.enter()) {
119-
receipts.clear();
120-
acks.clear();
121-
committer.waitUntilEmpty();
111+
if (generation != this.generation) {
112+
LOGGER.atFine().log("Dropping ack from wrong generation (admin seek occurred).");
113+
return;
114+
}
115+
acks.add(offset);
116+
Optional<Offset> prefixAckedOffset = Optional.empty();
117+
while (!receipts.isEmpty()
118+
&& !acks.isEmpty()
119+
&& receipts.peekFirst().offset.value() == acks.peek().value()) {
120+
prefixAckedOffset = Optional.of(acks.remove());
121+
receipts.removeFirst();
122+
}
123+
// Convert from last acked to first unacked.
124+
if (prefixAckedOffset.isPresent()) {
125+
ApiFuture<?> future = committer.commitOffset(Offset.of(prefixAckedOffset.get().value() + 1));
126+
ExtractStatus.addFailureHandler(future, this::onPermanentError);
122127
}
123128
}
124129

125-
private void onAck(Offset offset) {
126-
try (CloseableMonitor.Hold h = monitor.enter()) {
127-
acks.add(offset);
128-
Optional<Offset> prefixAckedOffset = Optional.empty();
129-
while (!receipts.isEmpty()
130-
&& !acks.isEmpty()
131-
&& receipts.peekFirst().offset.value() == acks.peek().value()) {
132-
prefixAckedOffset = Optional.of(acks.remove());
133-
receipts.removeFirst();
134-
}
135-
// Convert from last acked to first unacked.
136-
if (prefixAckedOffset.isPresent()) {
137-
ApiFuture<?> future =
138-
committer.commitOffset(Offset.of(prefixAckedOffset.get().value() + 1));
139-
ExtractStatus.addFailureHandler(future, this::onPermanentError);
140-
}
141-
}
130+
@Override
131+
protected void start() throws CheckedApiException {}
132+
133+
@Override
134+
protected synchronized void stop() throws CheckedApiException {
135+
shutdown = true;
142136
}
137+
138+
@Override
139+
protected void handlePermanentError(CheckedApiException error) {}
143140
}

google-cloud-pubsublite/src/test/java/com/google/cloud/pubsublite/cloudpubsub/internal/AckSetTrackerImplTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,13 @@ public void waitUntilCommittedDiscardsPendingAcks() throws Exception {
131131
ack.run();
132132
verify(committer, never()).commitOffset(any());
133133
}
134+
135+
@Test
136+
public void ackAfterShutdown() throws Exception {
137+
Runnable ack = tracker.track(messageForOffset(1));
138+
139+
tracker.stopAsync().awaitTerminated();
140+
ack.run();
141+
verify(committer, never()).commitOffset(any());
142+
}
134143
}

0 commit comments

Comments
 (0)