88import java .util .List ;
99import java .util .concurrent .CompletableFuture ;
1010import java .util .concurrent .TimeUnit ;
11- import java .util .concurrent .atomic .AtomicReference ;
1211import java .util .function .Consumer ;
1312import java .util .function .Function ;
1413
@@ -30,91 +29,22 @@ public class ApiRequestBatcher<T> {
3029 /** Executes the batch operation */
3130 private final Consumer <List <T >> executeBatch ;
3231
33- private record Item <T >(T request , CompletableFuture <Void > result ) {}
34-
35- /** Batch accumulator */
36- private class Batch {
37- /** Accumulated requests */
38- private final List <Item <T >> items ;
39- /** Current batch size in bytes */
40- private int totalBytes ;
41-
42- long expireTime ;
43- /** Timer to auto-flush incomplete batch */
44- private final CompletableFuture <Void > flushTimer ;
45-
46- Batch () {
47- this .items = new ArrayList <>();
48- this .totalBytes = 0 ;
49- this .expireTime = System .nanoTime () + MAX_DELAY .toNanos ();
50- this .flushTimer = new CompletableFuture <>();
51- this .flushTimer .thenRunAsync (this ::execute , InternalExecutor .INSTANCE );
52- }
53-
54- /** Adds request to batch and returns its result future */
55- CompletableFuture <Void > add (T request , Duration delay ) {
56- totalBytes += calculateItemSize .apply (request );
57- CompletableFuture <Void > result = new CompletableFuture <>();
58- items .add (new Item <>(request , result ));
59- long newExpireTime = System .nanoTime () + delay .toNanos ();
60- if (expireTime > newExpireTime ) {
61- // the batch needs to be completed earlier than previously scheduled
62- expireTime = newExpireTime ;
63- flushAfterDelay (delay .toNanos ());
64- }
65- return result ;
66- }
32+ /** Accumulated requests */
33+ private final List <Item <T >> items ;
6734
68- /** Returns true if request fits within byte limit */
69- boolean canFit (T request ) {
70- return totalBytes + calculateItemSize .apply (request ) <= maxBatchBytes ;
71- }
72-
73- /** Returns true if batch has reached item count limit */
74- boolean isFull () {
75- return items .size () >= maxItemCount ;
76- }
35+ /** Current batch size in bytes */
36+ private volatile int totalBytes ;
7737
78- void flushAfterDelay (long delayInNanos ) {
79- flushTimer .completeOnTimeout (null , delayInNanos , TimeUnit .NANOSECONDS );
80- }
38+ /** Time when the current batch must be flushed */
39+ private volatile long expireTime ;
8140
82- void flushNow () {
83- flushAfterDelay (0 );
84- }
41+ /** Timer to auto-flush incomplete batch */
42+ private CompletableFuture <Void > flushTimer ;
8543
86- void cancel () {
87- var ex = new IllegalDurableOperationException ("Batch cancelled" );
88- for (Item <T > item : items ) {
89- item .result ().completeExceptionally (ex );
90- }
91- }
92-
93- /** Executes batch and completes all item futures */
94- private void execute () {
95- // detach this from active batch if it's still active
96- detachActiveBatchAndCreateNew (this );
97-
98- List <T > requests = new ArrayList <>(items .size ());
99- for (Item <T > item : items ) {
100- requests .add (item .request ());
101- }
102-
103- try {
104- executeBatch .accept (requests );
105- for (Item <T > item : items ) {
106- item .result ().complete (null );
107- }
108- } catch (Throwable ex ) {
109- for (Item <T > item : items ) {
110- item .result ().completeExceptionally (ex );
111- }
112- }
113- }
114- }
44+ /** Future of flushing previous batch */
45+ private CompletableFuture <Void > previousBatchFuture ;
11546
116- /** Current batch accepting requests */
117- private final AtomicReference <Batch > activeBatchAtom ;
47+ private record Item <T >(T request , CompletableFuture <Void > result ) {}
11848
11949 /**
12050 * Creates a new ApiRequestBatcher with the specified configuration.
@@ -133,7 +63,22 @@ public ApiRequestBatcher(
13363 this .maxBatchBytes = maxBatchBytes ;
13464 this .calculateItemSize = calculateItemSize ;
13565 this .executeBatch = executeBatch ;
136- this .activeBatchAtom = new AtomicReference <>(new Batch ());
66+ this .previousBatchFuture = CompletableFuture .allOf ();
67+ this .items = new ArrayList <>();
68+
69+ initializeBatch ();
70+ }
71+
72+ private void initializeBatch () {
73+ this .items .clear ();
74+ this .totalBytes = 0 ;
75+ this .expireTime = System .nanoTime () + MAX_DELAY .toNanos ();
76+ this .flushTimer = new CompletableFuture <>();
77+ this .flushTimer .thenRun (() -> {
78+ synchronized (items ) {
79+ execute ();
80+ }
81+ });
13782 }
13883
13984 /**
@@ -144,49 +89,90 @@ public ApiRequestBatcher(
14489 */
14590 public CompletableFuture <Void > submit (T request , Duration flushDelay ) {
14691 // Flush the current batch if request doesn't fit
147- while (true ) {
148- Batch activeBatch = activeBatchAtom .get ();
149-
150- if (activeBatch .isFull () || !activeBatch .canFit (request )) {
151- if (!flushActiveBatchAndCreateNew (activeBatch )) {
152- // failed to flush due to a race condition.
153- continue ;
154- }
92+ synchronized (items ) {
93+ if (isFull () || !canFit (request )) {
94+ flushNow ();
15595 }
15696
157- var result = activeBatch . add (request , flushDelay );
97+ var future = add (request , flushDelay );
15898
159- // Flush early if batch is full
160- if ( activeBatch . isFull ()) {
161- flushActiveBatchAndCreateNew ( activeBatch );
99+ if ( isFull ()) {
100+ // Flush early if batch is full
101+ flushNow ( );
162102 }
163- return result ;
103+ return future ;
164104 }
165105 }
166106
167- private Batch detachActiveBatchAndCreateNew (Batch oldBatch ) {
168- if (activeBatchAtom .compareAndSet (oldBatch , new Batch ())) {
169- return oldBatch ;
107+ /** Adds request to batch and returns its result future */
108+ CompletableFuture <Void > add (T request , Duration delay ) {
109+ synchronized (items ) {
110+ totalBytes += calculateItemSize .apply (request );
111+ CompletableFuture <Void > result = new CompletableFuture <>();
112+ items .add (new Item <>(request , result ));
113+ long newExpireTime = System .nanoTime () + delay .toNanos ();
114+ if (expireTime > newExpireTime ) {
115+ // the batch needs to be completed earlier than previously scheduled
116+ expireTime = newExpireTime ;
117+ flushAfterDelay (delay .toNanos ());
118+ }
119+ return result ;
170120 }
121+ }
171122
172- return null ;
123+ /** Returns true if request fits within byte limit */
124+ private boolean canFit (T request ) {
125+ return totalBytes + calculateItemSize .apply (request ) <= maxBatchBytes ;
173126 }
174127
175- /** flushes active batch and crate a new batch. Return true if successful */
176- private boolean flushActiveBatchAndCreateNew (Batch oldBatch ) {
177- Batch activeBatch = detachActiveBatchAndCreateNew (oldBatch );
178- if (activeBatch != null ) {
179- activeBatch .flushNow ();
180- }
181- return activeBatch != null ;
128+ /** Returns true if batch has reached item count limit */
129+ private boolean isFull () {
130+ return items .size () >= maxItemCount ;
131+ }
132+
133+ private void flushAfterDelay (long delayInNanos ) {
134+ flushTimer .completeOnTimeout (null , delayInNanos , TimeUnit .NANOSECONDS );
135+ }
136+
137+ private void flushNow () {
138+ this .flushTimer .cancel (false );
139+ // wait for new batch to be ready
140+ execute ();
182141 }
183142
184143 public void shutdown () {
185- Batch activeBatch = activeBatchAtom .get ();
186- while (!activeBatchAtom .compareAndSet (activeBatch , new Batch ())) {
187- // try again
188- activeBatch = activeBatchAtom .get ();
144+ var ex = new IllegalDurableOperationException ("Batch cancelled" );
145+ synchronized (items ) {
146+ for (Item <T > item : items ) {
147+ item .result ().completeExceptionally (ex );
148+ }
149+ initializeBatch ();
150+ }
151+ }
152+
153+ /** Executes batch and completes all item futures */
154+ private void execute () {
155+ if (items .isEmpty ()) {
156+ return ;
189157 }
190- activeBatchAtom .get ().cancel ();
158+ var copyItems = new ArrayList <>(items );
159+ initializeBatch ();
160+
161+ // append the current batch to the previous one
162+ previousBatchFuture = previousBatchFuture .thenRunAsync (
163+ () -> {
164+ try {
165+ var requests = copyItems .stream ().map (Item ::request ).toList ();
166+ executeBatch .accept (requests );
167+ for (Item <T > item : copyItems ) {
168+ item .result ().complete (null );
169+ }
170+ } catch (Throwable ex ) {
171+ for (Item <T > item : copyItems ) {
172+ item .result ().completeExceptionally (ex );
173+ }
174+ }
175+ },
176+ InternalExecutor .INSTANCE );
191177 }
192178}
0 commit comments