Skip to content

Commit 7504008

Browse files
committed
Minor tweaks
1 parent 6106905 commit 7504008

6 files changed

Lines changed: 905 additions & 361 deletions

File tree

src/main/java/groovy/concurrent/AsyncStream.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,20 @@ static <T> AsyncStream<T> empty() {
113113
return (AsyncStream<T>) EMPTY;
114114
}
115115

116+
/**
117+
* Cached awaitable for {@code moveNext()} returning {@code true}.
118+
* Eliminates per-call allocation on the hot path. Shared by all
119+
* {@code AsyncStream} implementations (e.g. via
120+
* {@link org.apache.groovy.runtime.async.AbstractAsyncStream AbstractAsyncStream}).
121+
*/
122+
Awaitable<Boolean> MOVE_NEXT_TRUE = Awaitable.of(Boolean.TRUE);
123+
124+
/**
125+
* Cached awaitable for {@code moveNext()} returning {@code false}.
126+
* Eliminates per-call allocation on the stream-end path.
127+
*/
128+
Awaitable<Boolean> MOVE_NEXT_FALSE = Awaitable.of(Boolean.FALSE);
129+
116130
/**
117131
* Singleton empty stream instance.
118132
* <p>
@@ -121,7 +135,7 @@ static <T> AsyncStream<T> empty() {
121135
* referencing this field directly.
122136
*/
123137
AsyncStream<Object> EMPTY = new AsyncStream<>() {
124-
@Override public Awaitable<Boolean> moveNext() { return Awaitable.of(false); }
138+
@Override public Awaitable<Boolean> moveNext() { return MOVE_NEXT_FALSE; }
125139
@Override public Object getCurrent() { return null; }
126140
};
127141
}
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.groovy.runtime.async;
20+
21+
import groovy.concurrent.AsyncStream;
22+
import groovy.concurrent.Awaitable;
23+
24+
import java.util.Objects;
25+
import java.util.concurrent.BlockingQueue;
26+
import java.util.concurrent.CancellationException;
27+
import java.util.concurrent.atomic.AtomicBoolean;
28+
29+
/**
30+
* Template base class for queue-based {@link AsyncStream} implementations.
31+
*
32+
* <p>This class implements the
33+
* <a href="https://en.wikipedia.org/wiki/Template_method_pattern">Template Method</a>
34+
* pattern, centralising the signal dispatch logic, lifecycle management, and
35+
* interrupt handling that is common to all queue-based async streams.
36+
* Concrete subclasses only need to supply a {@link BlockingQueue} and override
37+
* a small number of hook methods to customise behaviour.</p>
38+
*
39+
* <h2>Signal protocol</h2>
40+
* <p>Elements flowing through the queue are wrapped in one of three signal types:</p>
41+
* <ul>
42+
* <li>{@link ValueSignal} — carries a data element (may wrap {@code null})</li>
43+
* <li>{@link ErrorSignal} — carries a {@link Throwable} to propagate</li>
44+
* <li>{@link #COMPLETE} — singleton sentinel indicating normal end-of-stream</li>
45+
* </ul>
46+
* <p>The template's {@link #moveNext()} dispatches on these signals with a fixed
47+
* sequence: value → set current + {@link #afterValueConsumed()} + return {@code true};
48+
* error → set closed + sneaky-throw; complete → set closed + return {@code false}.</p>
49+
*
50+
* <h2>Hook methods (override points)</h2>
51+
* <table>
52+
* <caption>Hook methods and their defaults</caption>
53+
* <tr><th>Hook</th><th>Default</th><th>Typical override</th></tr>
54+
* <tr><td>{@link #beforeTake()}</td><td>return {@code MOVE_NEXT_FALSE} if closed</td>
55+
* <td>thread registration, double-check, or drain check</td></tr>
56+
* <tr><td>{@link #afterValueConsumed()}</td><td>no-op</td>
57+
* <td>request more items from upstream (back-pressure)</td></tr>
58+
* <tr><td>{@link #afterMoveNext()}</td><td>no-op</td>
59+
* <td>unregister consumer thread</td></tr>
60+
* <tr><td>{@link #onMoveNextInterrupted(InterruptedException)}</td>
61+
* <td>set closed, restore interrupt, throw {@link CancellationException}</td>
62+
* <td>return {@code MOVE_NEXT_FALSE} if already closed</td></tr>
63+
* <tr><td>{@link #onClose()}</td><td><em>abstract</em></td>
64+
* <td>interrupt threads, cancel subscriptions, drain queue</td></tr>
65+
* </table>
66+
*
67+
* <h2>Thread safety</h2>
68+
* <p>The {@link #closed} flag is an {@link AtomicBoolean} shared between the
69+
* producer (subclass-managed) and consumer ({@code moveNext()}) sides.
70+
* The {@link #close()} method uses CAS to guarantee exactly-once semantics.
71+
* The {@link #current} field is {@code volatile} for safe cross-thread visibility.</p>
72+
*
73+
* <p>This class is an internal implementation detail and should not be referenced
74+
* directly by user code.</p>
75+
*
76+
* @param <T> the element type
77+
* @see AsyncStreamGenerator
78+
* @see FlowPublisherAdapter
79+
* @since 6.0.0
80+
*/
81+
public abstract class AbstractAsyncStream<T> implements AsyncStream<T> {
82+
83+
// ---- Unified signal types ----
84+
85+
/**
86+
* Wraps a data element for transport through the signal queue.
87+
* The wrapper is necessary because the queue element type is {@code Object},
88+
* and the actual value may be {@code null}.
89+
*/
90+
protected record ValueSignal(Object value) { }
91+
92+
/**
93+
* Wraps an error for transport through the signal queue.
94+
* When dispatched by {@link #moveNext()}, the wrapped throwable is
95+
* re-thrown via {@link AsyncSupport#sneakyThrow(Throwable)}.
96+
*/
97+
protected record ErrorSignal(Throwable error) { }
98+
99+
/**
100+
* Singleton sentinel indicating normal stream completion.
101+
* Identity comparison ({@code ==}) is used in the dispatch logic.
102+
*/
103+
protected static final Object COMPLETE = new Object();
104+
105+
// ---- Shared state ----
106+
107+
/** The signal queue bridging producer and consumer. */
108+
protected final BlockingQueue<Object> queue;
109+
110+
/** Lifecycle flag: set exactly once when the stream is closed. */
111+
protected final AtomicBoolean closed = new AtomicBoolean(false);
112+
113+
/** Most recently consumed value, set by {@link #moveNext()} on value signals. */
114+
private volatile T current;
115+
116+
/**
117+
* @param queue the blocking queue used for producer→consumer signal delivery;
118+
* must not be {@code null}
119+
*/
120+
protected AbstractAsyncStream(BlockingQueue<Object> queue) {
121+
this.queue = Objects.requireNonNull(queue, "queue");
122+
}
123+
124+
// ---- Template method: moveNext ----
125+
126+
/**
127+
* Template method implementing the {@link AsyncStream} iteration protocol.
128+
*
129+
* <p>Execution sequence:</p>
130+
* <ol>
131+
* <li>{@link #beforeTake()} — may short-circuit with an early return</li>
132+
* <li>{@code queue.take()} — blocks until a signal is available</li>
133+
* <li>Signal dispatch: value / error / complete</li>
134+
* <li>{@link #afterMoveNext()} — always runs (finally block)</li>
135+
* </ol>
136+
*
137+
* <p>If {@code queue.take()} throws {@link InterruptedException},
138+
* {@link #onMoveNextInterrupted(InterruptedException)} handles it.</p>
139+
*
140+
* @return an {@code Awaitable<Boolean>} — {@code true} if a new element is
141+
* available via {@link #getCurrent()}, {@code false} if the stream
142+
* is exhausted or closed
143+
*/
144+
@Override
145+
@SuppressWarnings("unchecked")
146+
public final Awaitable<Boolean> moveNext() {
147+
Awaitable<Boolean> earlyReturn = beforeTake();
148+
if (earlyReturn != null) {
149+
return earlyReturn;
150+
}
151+
try {
152+
Object signal = queue.take();
153+
154+
if (signal instanceof ValueSignal vs) {
155+
current = (T) vs.value;
156+
afterValueConsumed();
157+
return MOVE_NEXT_TRUE;
158+
}
159+
if (signal instanceof ErrorSignal es) {
160+
closed.set(true);
161+
Throwable cause = es.error;
162+
if (cause instanceof Error err) throw err;
163+
throw AsyncSupport.sneakyThrow(cause);
164+
}
165+
// COMPLETE sentinel — end-of-stream
166+
closed.set(true);
167+
return MOVE_NEXT_FALSE;
168+
} catch (InterruptedException e) {
169+
return onMoveNextInterrupted(e);
170+
} finally {
171+
afterMoveNext();
172+
}
173+
}
174+
175+
// ---- Final implementations ----
176+
177+
/**
178+
* {@inheritDoc}
179+
* <p>
180+
* Returns the most recently consumed element, set by the last successful
181+
* {@link #moveNext()} call.
182+
*
183+
* @return the current element, or {@code null} before the first successful
184+
* {@code moveNext()} call
185+
*/
186+
@Override
187+
public final T getCurrent() {
188+
return current;
189+
}
190+
191+
/**
192+
* {@inheritDoc}
193+
* <p>
194+
* Atomically marks this stream as closed via CAS on the {@link #closed}
195+
* flag, then delegates to {@link #onClose()} for subclass-specific cleanup.
196+
* Idempotent: only the first invocation triggers {@code onClose()}.
197+
*/
198+
@Override
199+
public final void close() {
200+
if (!closed.compareAndSet(false, true)) {
201+
return;
202+
}
203+
onClose();
204+
}
205+
206+
// ---- Abstract methods ----
207+
208+
/**
209+
* Subclass-specific cleanup, called exactly once from {@link #close()}.
210+
* Typical actions include interrupting blocked threads, cancelling
211+
* upstream subscriptions, and draining the queue.
212+
*/
213+
protected abstract void onClose();
214+
215+
// ---- Hook methods with defaults ----
216+
217+
/**
218+
* Pre-take hook invoked at the start of {@link #moveNext()}.
219+
* <p>
220+
* Return a non-{@code null} {@link Awaitable} to short-circuit
221+
* {@code moveNext()} without blocking on the queue. Return {@code null}
222+
* to proceed with the normal take-and-dispatch sequence.
223+
* <p>
224+
* The default implementation returns {@link #MOVE_NEXT_FALSE} when
225+
* the stream is closed, and {@code null} otherwise.
226+
*
227+
* @return an early-return value, or {@code null} to continue
228+
*/
229+
protected Awaitable<Boolean> beforeTake() {
230+
return closed.get() ? MOVE_NEXT_FALSE : null;
231+
}
232+
233+
/**
234+
* Post-value hook invoked after a {@link ValueSignal} has been consumed
235+
* and the {@link #current} field updated.
236+
* <p>
237+
* Subclasses may use this to signal demand to an upstream source
238+
* (e.g. {@code Subscription.request(1)} for reactive streams).
239+
* <p>
240+
* The default implementation is a no-op.
241+
*/
242+
protected void afterValueConsumed() { }
243+
244+
/**
245+
* Finally hook invoked after every {@link #moveNext()} attempt,
246+
* regardless of outcome (value, error, completion, or interrupt).
247+
* <p>
248+
* Subclasses may use this to unregister the consumer thread.
249+
* <p>
250+
* The default implementation is a no-op.
251+
*/
252+
protected void afterMoveNext() { }
253+
254+
/**
255+
* Interrupt handler invoked when {@code queue.take()} throws
256+
* {@link InterruptedException} inside {@link #moveNext()}.
257+
* <p>
258+
* The default implementation sets {@link #closed} to {@code true},
259+
* restores the interrupt flag, and throws a {@link CancellationException}.
260+
* Subclasses may override to return {@link #MOVE_NEXT_FALSE} instead
261+
* of throwing (e.g. when an external {@code close()} caused the interrupt).
262+
*
263+
* @param e the interrupt exception
264+
* @return an {@link Awaitable} to return from {@code moveNext()}, or
265+
* the method may throw instead
266+
*/
267+
protected Awaitable<Boolean> onMoveNextInterrupted(InterruptedException e) {
268+
closed.set(true);
269+
Thread.currentThread().interrupt();
270+
throw newCancellationException("Interrupted during moveNext", e);
271+
}
272+
273+
// ---- Utilities for subclasses ----
274+
275+
/**
276+
* Creates a {@link CancellationException} with the given message and cause.
277+
*
278+
* @param message the detail message
279+
* @param cause the interrupt that triggered the cancellation
280+
* @return a new {@code CancellationException}
281+
*/
282+
protected static CancellationException newCancellationException(String message, InterruptedException cause) {
283+
CancellationException ce = new CancellationException(message);
284+
ce.initCause(cause);
285+
return ce;
286+
}
287+
}

0 commit comments

Comments
 (0)