-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAsyncSemaphore.java
More file actions
152 lines (141 loc) · 5.47 KB
/
Copy pathAsyncSemaphore.java
File metadata and controls
152 lines (141 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package com.marketdata.sdk;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
/**
* Async-safe concurrency limiter. Replaces {@link java.util.concurrent.Semaphore} in the HTTP path
* so that {@code executeAsync} never parks the caller's thread when the pool is at capacity — it
* returns a {@link CompletableFuture} that completes when a permit is released by an in-flight
* request. See ADR-007 for the rationale.
*
* <p>Two invariants:
*
* <ol>
* <li>Every permit is accounted for exactly once — it is either in {@link #availablePermits()}
* (free), held by an in-flight caller (and will be released via {@link #release()}), or
* pending in the waiter queue (and will be released by completing the waiter's future).
* <li>{@link CompletableFuture#complete} of a transferred permit always runs <em>outside</em> the
* lock. Completing a future runs the caller's attached callbacks synchronously on the
* releasing thread, and we never want those running while our lock is held.
* </ol>
*
* <p>Cancelled or otherwise-completed waiters are skipped on {@link #release()} so a cancelled
* {@code acquire} doesn't burn a permit.
*/
final class AsyncSemaphore {
private final Object lock = new Object();
private final Deque<CompletableFuture<Void>> waiters = new ArrayDeque<>();
private int available;
private boolean closed;
AsyncSemaphore(int permits) {
if (permits < 0) {
throw new IllegalArgumentException("permits must be >= 0, was " + permits);
}
this.available = permits;
}
/**
* Asynchronously claim a permit.
*
* <p>Fast path: a permit is available, returns an already-completed future. Slow path: pool is
* exhausted, returns a pending future enqueued FIFO; it completes when some in-flight caller
* calls {@link #release()}. Either way, the caller's thread is never parked.
*
* <p>After {@link #close()} every acquire fails immediately with {@link CancellationException};
* waiters queued before the close were already drained with the same exception.
*/
CompletableFuture<Void> acquire() {
synchronized (lock) {
if (closed) {
return CompletableFuture.failedFuture(closedException());
}
if (available > 0) {
available--;
return CompletableFuture.completedFuture(null);
}
CompletableFuture<Void> waiter = new CompletableFuture<>();
waiters.addLast(waiter);
return waiter;
}
}
/**
* Release a permit. If a live waiter is enqueued, the permit is transferred to it (its future is
* completed) without going through the counter. Otherwise the counter is incremented.
*/
void release() {
// Retry while a transfer attempt loses the TOCTOU race (a polled waiter is cancelled between
// leaving the lock and complete(null)); the empty body re-runs the attempt. Exits once the
// permit is handed to a live waiter or returned to the counter.
while (!tryTransfer()) {
// retry with the next live waiter
}
}
/**
* One transfer attempt: hand the permit to the first live waiter, or return it to the counter
* when none remain. Returns {@code false} only when the polled waiter was cancelled in the gap
* between leaving the lock and {@code complete(null)} — the caller then retries.
*/
private boolean tryTransfer() {
CompletableFuture<Void> next = null;
synchronized (lock) {
while (!waiters.isEmpty()) {
CompletableFuture<Void> w = waiters.pollFirst();
if (!w.isDone()) {
next = w;
break;
}
}
if (next == null) {
available++;
return true;
}
}
return next.complete(null);
}
/** Permits not currently held nor pending in the queue. */
int availablePermits() {
synchronized (lock) {
return available;
}
}
/** Number of pending waiters on the slow path. Useful for diagnostics and tests. */
int queueLength() {
synchronized (lock) {
return waiters.size();
}
}
/**
* Drain the waiter queue and reject future {@link #acquire()} calls. All currently-queued waiters
* are completed exceptionally with {@link CancellationException} so the {@code thenCompose} chain
* downstream of the dispatcher fails cleanly instead of leaving futures pending forever when the
* owning client is closed mid-flight.
*
* <p>Idempotent: subsequent calls are no-ops. Permits already held by in-flight callers can still
* be {@link #release()}d (the counter accepts it harmlessly) — this matters because cancellation
* of a dispatched future cancels its permit, and that cancel-then-release path must continue to
* work even after close.
*
* <p>Completion of drained waiters runs <em>outside</em> the lock for the same reason {@link
* #release()} does it that way: completing a future runs callbacks synchronously, and we never
* want those running with our lock held.
*/
void close() {
List<CompletableFuture<Void>> drained;
synchronized (lock) {
if (closed) {
return;
}
closed = true;
drained = new ArrayList<>(waiters);
waiters.clear();
}
for (CompletableFuture<Void> w : drained) {
w.completeExceptionally(closedException());
}
}
private static CancellationException closedException() {
return new CancellationException("AsyncSemaphore is closed");
}
}