Skip to content

Commit a02a695

Browse files
Merge pull request googleapis#37 from garrettjonesgoogle/master
Supporting bundling limits
2 parents 4dcc19c + 7d7e315 commit a02a695

11 files changed

Lines changed: 317 additions & 99 deletions

‎src/main/java/com/google/api/gax/bundling/AccumulatingBundleReceiver.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void validateItem(T message) {
4848

4949
@Override
5050
public void processBundle(List<T> bundle) {
51-
bundles.add(bundle);
51+
bundles.add(new ArrayList<>(bundle));
5252
}
5353

5454
/**

‎src/main/java/com/google/api/gax/bundling/BundlingThreshold.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
*/
3939
public interface BundlingThreshold<E> {
4040

41+
/**
42+
* Returns true if adding this value would breach the limit for the value in
43+
* this threshold.
44+
*/
45+
boolean canAccept(E e);
46+
4147
/**
4248
* Presents the element to the threshold for the attribute of interest to be accumulated.
4349
*

‎src/main/java/com/google/api/gax/bundling/BundlingThresholds.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class BundlingThresholds {
4545
*/
4646
public static <E> ImmutableList<BundlingThreshold<E>> of(long elementThreshold) {
4747
BundlingThreshold<E> bundlingThreshold =
48-
new NumericThreshold<E>(elementThreshold, new ElementCounter<E>() {
48+
new NumericThreshold<E>(elementThreshold, null, new ElementCounter<E>() {
4949
@Override
5050
public long count(E e) {
5151
return 1;

‎src/main/java/com/google/api/gax/bundling/NumericThreshold.java‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,42 @@
3333

3434
import com.google.common.base.Preconditions;
3535

36+
import javax.annotation.Nullable;
37+
3638
/**
3739
* A threshold which accumulates a count based on the provided
3840
* ElementCounter.
3941
*/
4042
public class NumericThreshold<E> implements BundlingThreshold<E> {
4143
private final long threshold;
44+
private final Long limit;
4245
private final ElementCounter<E> extractor;
4346
private long sum;
4447

4548
/**
4649
* Constructs a NumericThreshold.
50+
*
51+
* @param threshold The value that allows an event to happen.
52+
* @param limit The value that forces an event to happen. If null, then this is
53+
* not enforced.
54+
* @param extractor Object that extracts a numeric value from the value object.
4755
*/
48-
public NumericThreshold(long threshold, ElementCounter<E> extractor) {
56+
public NumericThreshold(long threshold, @Nullable Long limit, ElementCounter<E> extractor) {
4957
this.threshold = threshold;
58+
this.limit = limit;
5059
this.extractor = Preconditions.checkNotNull(extractor);
5160
this.sum = 0;
5261
}
5362

63+
@Override
64+
public boolean canAccept(E e) {
65+
if (limit == null) {
66+
return true;
67+
} else {
68+
return sum + extractor.count(e) <= limit;
69+
}
70+
}
71+
5472
@Override
5573
public void accumulate(E e) {
5674
sum += extractor.count(e);
@@ -63,6 +81,6 @@ public boolean isThresholdReached() {
6381

6482
@Override
6583
public BundlingThreshold<E> copyWithZeroedValue() {
66-
return new NumericThreshold<E>(threshold, extractor);
84+
return new NumericThreshold<E>(threshold, limit, extractor);
6785
}
6886
}

‎src/main/java/com/google/api/gax/bundling/ThresholdBundler.java‎

Lines changed: 126 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,23 @@
5252
*/
5353
public class ThresholdBundler<E> {
5454

55-
private ImmutableList<BundlingThreshold<E>> thresholds;
56-
private ImmutableList<ExternalThreshold<E>> externalThresholds;
55+
private ImmutableList<BundlingThreshold<E>> thresholdPrototypes;
56+
private ImmutableList<ExternalThreshold<E>> externalThresholdPrototypes;
5757
private final Duration maxDelay;
5858

5959
private final Lock lock = new ReentrantLock();
6060
private final Condition bundleCondition = lock.newCondition();
61-
private boolean bundleReady = false;
62-
private BundleHandle currentBundleHandle;
63-
64-
private Stopwatch bundleStopwatch;
65-
private final List<E> data = new ArrayList<>();
61+
private Bundle currentOpenBundle;
62+
private List<Bundle> closedBundles = new ArrayList<>();
6663

6764
private ThresholdBundler(ImmutableList<BundlingThreshold<E>> thresholds,
6865
ImmutableList<ExternalThreshold<E>> externalThresholds,
6966
Duration maxDelay) {
70-
this.thresholds = copyResetThresholds(Preconditions.checkNotNull(thresholds));
71-
this.externalThresholds = copyResetExternalThresholds(
67+
this.thresholdPrototypes = copyResetThresholds(Preconditions.checkNotNull(thresholds));
68+
this.externalThresholdPrototypes = copyResetExternalThresholds(
7269
Preconditions.checkNotNull(externalThresholds));
7370
this.maxDelay = maxDelay;
74-
this.currentBundleHandle = new BundleHandle(externalThresholds);
71+
this.currentOpenBundle = null;
7572
}
7673

7774
/**
@@ -154,32 +151,37 @@ public ThresholdBundleHandle add(E e) {
154151
final Lock lock = this.lock;
155152
lock.lock();
156153
try {
157-
boolean signal = false;
158-
// TODO verify invariant: bundleStopwatch == null iff size() == 0
159-
if (data.size() == 0) {
160-
bundleStopwatch = Stopwatch.createStarted();
161-
// we want to trigger the signal so that we switch the await from an unbounded
162-
// await to a time-bounded await.
163-
signal = true;
164-
for (ExternalThreshold<E> threshold : externalThresholds) {
165-
threshold.startBundle();
166-
}
154+
validateLimits(e);
155+
156+
boolean signalBundleIsReady = false;
157+
Bundle bundleOfAddedItem = null;
158+
if (currentOpenBundle == null) {
159+
currentOpenBundle = new Bundle(thresholdPrototypes, externalThresholdPrototypes, maxDelay);
160+
currentOpenBundle.start();
161+
signalBundleIsReady = true;
167162
}
168-
data.add(e);
169-
if (!bundleReady) {
170-
for (BundlingThreshold<E> threshold : thresholds) {
171-
threshold.accumulate(e);
172-
if (threshold.isThresholdReached()) {
173-
bundleReady = true;
174-
signal = true;
175-
break;
176-
}
163+
164+
if (currentOpenBundle.canAccept(e)) {
165+
currentOpenBundle.add(e);
166+
bundleOfAddedItem = currentOpenBundle;
167+
if (currentOpenBundle.isAnyThresholdReached()) {
168+
signalBundleIsReady = true;
169+
closedBundles.add(currentOpenBundle);
170+
currentOpenBundle = null;
177171
}
172+
} else {
173+
signalBundleIsReady = true;
174+
closedBundles.add(currentOpenBundle);
175+
currentOpenBundle = new Bundle(thresholdPrototypes, externalThresholdPrototypes, maxDelay);
176+
currentOpenBundle.start();
177+
currentOpenBundle.add(e);
178+
bundleOfAddedItem = currentOpenBundle;
178179
}
179-
if (signal) {
180+
181+
if (signalBundleIsReady) {
180182
bundleCondition.signalAll();
181183
}
182-
return currentBundleHandle;
184+
return bundleOfAddedItem;
183185
} finally {
184186
lock.unlock();
185187
}
@@ -193,7 +195,10 @@ public void flush() {
193195
final Lock lock = this.lock;
194196
lock.lock();
195197
try {
196-
bundleReady = true;
198+
if (currentOpenBundle != null) {
199+
closedBundles.add(currentOpenBundle);
200+
currentOpenBundle = null;
201+
}
197202
bundleCondition.signalAll();
198203
} finally {
199204
lock.unlock();
@@ -206,23 +211,24 @@ public void flush() {
206211
*
207212
* @return the number of items added to 'bundle'.
208213
*/
209-
public int drainTo(Collection<? super E> bundle) {
214+
public int drainNextBundleTo(Collection<? super E> outputCollection) {
210215
final Lock lock = this.lock;
211216
lock.lock();
212217
try {
213-
int dataSize = data.size();
214-
215-
bundle.addAll(data);
216-
data.clear();
217-
currentBundleHandle = new BundleHandle(externalThresholds);
218-
219-
thresholds = copyResetThresholds(thresholds);
220-
externalThresholds = copyResetExternalThresholds(externalThresholds);
221-
222-
bundleStopwatch = null;
223-
bundleReady = false;
218+
Bundle outBundle = null;
219+
if (closedBundles.size() > 0) {
220+
outBundle = closedBundles.remove(0);
221+
} else if (currentOpenBundle != null) {
222+
outBundle = currentOpenBundle;
223+
currentOpenBundle = null;
224+
}
224225

225-
return dataSize;
226+
if (outBundle != null) {
227+
outputCollection.addAll(outBundle.getData());
228+
return outputCollection.size();
229+
} else {
230+
return 0;
231+
}
226232
} finally {
227233
lock.unlock();
228234
}
@@ -236,66 +242,61 @@ public List<E> takeBundle() throws InterruptedException {
236242
lock.lockInterruptibly();
237243
try {
238244
while (shouldWait()) {
239-
if (data.size() == 0 || maxDelay == null) {
245+
if (currentOpenBundle == null || maxDelay == null) {
240246
// if an element gets added, this will be signaled, then we will re-check the while-loop
241247
// condition to see if the delay or other thresholds have been exceeded,
242248
// and if none of these are true, then we will arrive at the time-bounded
243249
// await in the else clause.
244250
bundleCondition.await();
245251
} else {
246-
bundleCondition.await(getDelayLeft().getMillis(), TimeUnit.MILLISECONDS);
252+
bundleCondition.await(currentOpenBundle.getDelayLeft().getMillis(),
253+
TimeUnit.MILLISECONDS);
247254
}
248255
}
256+
249257
List<E> bundle = new ArrayList<>();
250-
drainTo(bundle);
258+
drainNextBundleTo(bundle);
251259
return bundle;
252260
} finally {
253261
lock.unlock();
254262
}
255263
}
256264

257-
/**
258-
* Returns the number of elements queued up in the bundler.
259-
*/
260-
public int size() {
265+
public boolean isEmpty() {
261266
final Lock lock = this.lock;
262267
lock.lock();
263268
try {
264-
return data.size();
269+
if (closedBundles.size() > 0) {
270+
return false;
271+
}
272+
if (currentOpenBundle != null) {
273+
return false;
274+
}
275+
return true;
265276
} finally {
266277
lock.unlock();
267278
}
268279
}
269280

270-
/**
271-
* Returns the elements queued up in the bundler.
272-
*/
273-
public Object[] toArray() {
274-
final Lock lock = this.lock;
275-
lock.lock();
276-
try {
277-
return data.toArray();
278-
} finally {
279-
lock.unlock();
281+
private void validateLimits(E e) {
282+
for (BundlingThreshold<E> threshold : thresholdPrototypes) {
283+
if (!threshold.canAccept(e)) {
284+
throw new IllegalArgumentException("Single item too large for bundle");
285+
}
280286
}
281287
}
282288

283289
private boolean shouldWait() {
284-
if (data.size() == 0) {
285-
return true;
286-
}
287-
if (bundleReady) {
290+
if (closedBundles.size() > 0) {
288291
return false;
289292
}
293+
if (currentOpenBundle == null) {
294+
return true;
295+
}
290296
if (maxDelay == null) {
291297
return true;
292298
}
293-
return getDelayLeft().getMillis() > 0;
294-
}
295-
296-
// pre-condition: data.size() > 0 ( === bundleStopwatch != null)
297-
private Duration getDelayLeft() {
298-
return Duration.millis(maxDelay.getMillis() - bundleStopwatch.elapsed(TimeUnit.MILLISECONDS));
299+
return currentOpenBundle.getDelayLeft().getMillis() > 0;
299300
}
300301

301302
private static <E> ImmutableList<BundlingThreshold<E>> copyResetThresholds(
@@ -324,11 +325,59 @@ private static <E> ImmutableList<ExternalThreshold<E>> copyResetExternalThreshol
324325
* a ThresholdBundler, but only if the bundle referenced is still the active
325326
* one.
326327
*/
327-
private class BundleHandle implements ThresholdBundleHandle {
328+
private class Bundle implements ThresholdBundleHandle {
329+
private final ImmutableList<BundlingThreshold<E>> thresholds;
328330
private final ImmutableList<ExternalThreshold<E>> externalThresholds;
331+
private final Duration maxDelay;
332+
private final List<E> data = new ArrayList<>();
333+
private Stopwatch stopwatch;
334+
335+
private Bundle(ImmutableList<BundlingThreshold<E>> thresholds,
336+
ImmutableList<ExternalThreshold<E>> externalThresholds,
337+
Duration maxDelay) {
338+
this.thresholds = copyResetThresholds(thresholds);
339+
this.externalThresholds = copyResetExternalThresholds(externalThresholds);
340+
this.maxDelay = maxDelay;
341+
}
329342

330-
private BundleHandle(ImmutableList<ExternalThreshold<E>> externalThresholds) {
331-
this.externalThresholds = externalThresholds;
343+
private void start() {
344+
stopwatch = Stopwatch.createStarted();
345+
for (ExternalThreshold<E> threshold : externalThresholds) {
346+
threshold.startBundle();
347+
}
348+
}
349+
350+
private boolean canAccept(E e) {
351+
for (BundlingThreshold<E> threshold : thresholds) {
352+
if (!threshold.canAccept(e)) {
353+
return false;
354+
}
355+
}
356+
return true;
357+
}
358+
359+
private void add(E e) {
360+
data.add(e);
361+
for (BundlingThreshold<E> threshold : thresholds) {
362+
threshold.accumulate(e);
363+
}
364+
}
365+
366+
private List<E> getData() {
367+
return data;
368+
}
369+
370+
private Duration getDelayLeft() {
371+
return Duration.millis(maxDelay.getMillis() - stopwatch.elapsed(TimeUnit.MILLISECONDS));
372+
}
373+
374+
private boolean isAnyThresholdReached() {
375+
for (BundlingThreshold<E> threshold : thresholds) {
376+
if (threshold.isThresholdReached()) {
377+
return true;
378+
}
379+
}
380+
return false;
332381
}
333382

334383
@Override
@@ -351,7 +400,7 @@ public void flush() {
351400
lock.lock();
352401

353402
try {
354-
if (ThresholdBundler.this.currentBundleHandle != this) {
403+
if (ThresholdBundler.this.currentOpenBundle != this) {
355404
return;
356405
}
357406
ThresholdBundler.this.flush();

0 commit comments

Comments
 (0)