@@ -26,7 +26,13 @@ trait Batchable {
26
26
27
27
private [concurrent] object BatchingExecutorStatics {
28
28
final val emptyBatchArray : Array [Runnable ] = new Array [Runnable ](0 )
29
- final val marker = " "
29
+
30
+ // Max number of Runnables executed nested before starting to batch (to prevent stack exhaustion)
31
+ final val syncPreBatchDepth = 16
32
+
33
+ // Max number of Runnables processed in one go (to prevent starvation of other tasks on the pool)
34
+ final val runLimit = 1024
35
+
30
36
final object MissingParentBlockContext extends BlockContext {
31
37
override def blockOn [T ](thunk : => T )(implicit permission : CanAwait ): T =
32
38
try thunk finally throw new IllegalStateException (" BUG in BatchingExecutor.Batch: parentBlockContext is null" )
@@ -117,31 +123,24 @@ private[concurrent] trait BatchingExecutor extends Executor {
117
123
this .size = sz + 1
118
124
}
119
125
120
- @ tailrec protected final def runAll (): Unit = // TODO: Impose max limit of number of items (fairness)
121
- (this .size: @ switch) match {
122
- case 0 =>
123
- case 1 =>
124
- val next = this .first
125
- this .first = null
126
- this .size = 0
127
- next.run()
128
- runAll()
129
- case sz =>
130
- val o = this .other
131
- val next = o(sz - 2 )
132
- o(sz - 2 ) = null
133
- this .size = sz - 1 // Important to update prior to `r.run()`
134
- next.run()
135
- runAll()
136
- }
137
-
138
- protected final def runUntilFailureOrDone (): Throwable =
139
- try {
140
- runAll()
141
- null
142
- } catch {
143
- case t : Throwable => t
144
- }
126
+ @ tailrec protected final def runN (n : Int ): Unit =
127
+ if (n > 0 )
128
+ (this .size: @ switch) match {
129
+ case 0 =>
130
+ case 1 =>
131
+ val next = this .first
132
+ this .first = null
133
+ this .size = 0
134
+ next.run()
135
+ runN(n - 1 )
136
+ case sz =>
137
+ val o = this .other
138
+ val next = o(sz - 2 )
139
+ o(sz - 2 ) = null
140
+ this .size = sz - 1
141
+ next.run()
142
+ runN(n - 1 )
143
+ }
145
144
}
146
145
147
146
private [this ] final class AsyncBatch private (_first : Runnable , _other : Array [Runnable ], _size : Int ) extends AbstractBatch (_first, _other, _size) with Runnable with BlockContext with (BlockContext => Throwable ) {
@@ -159,12 +158,15 @@ private[concurrent] trait BatchingExecutor extends Executor {
159
158
}
160
159
161
160
/* LOGIC FOR ASYNCHRONOUS BATCHES */
162
- override final def apply (prevBlockContext : BlockContext ): Throwable = {
161
+ override final def apply (prevBlockContext : BlockContext ): Throwable = try {
163
162
parentBlockContext = prevBlockContext
164
- val failure = runUntilFailureOrDone()
163
+ runN(BatchingExecutorStatics .runLimit)
164
+ null
165
+ } catch {
166
+ case t : Throwable => t // We are handling exceptions on the outside of this method
167
+ } finally {
165
168
parentBlockContext = BatchingExecutorStatics .MissingParentBlockContext
166
169
_tasksLocal.remove()
167
- failure
168
170
}
169
171
170
172
/* Attempts to resubmit this Batch to the underlying ExecutionContext,
@@ -174,7 +176,7 @@ private[concurrent] trait BatchingExecutor extends Executor {
174
176
*/
175
177
private [this ] final def resubmit (cause : Throwable ): Throwable =
176
178
if (this .size > 0 ) {
177
- try { submitAsync (this ); cause } catch {
179
+ try { submitForExecution (this ); cause } catch {
178
180
case inner : Throwable =>
179
181
if (NonFatal (inner)) {
180
182
val e = new ExecutionException (" Non-fatal error occurred and resubmission failed, see suppressed exception." , cause)
@@ -185,7 +187,7 @@ private[concurrent] trait BatchingExecutor extends Executor {
185
187
} else cause // TODO: consider if NonFatals should simply be `reportFailure`:ed rather than rethrown
186
188
187
189
private [this ] final def cloneAndClear (): AsyncBatch = {
188
- val newBatch = new AsyncBatch (first, other, size)
190
+ val newBatch = new AsyncBatch (this . first, this . other, this . size)
189
191
this .first = null
190
192
this .parentBlockContext = BatchingExecutorStatics .MissingParentBlockContext
191
193
this .other = BatchingExecutorStatics .emptyBatchArray
@@ -196,56 +198,70 @@ private[concurrent] trait BatchingExecutor extends Executor {
196
198
override final def blockOn [T ](thunk : => T )(implicit permission : CanAwait ): T = {
197
199
val pbc = parentBlockContext // Store this for later since `cloneAndClear()` will reset it
198
200
199
- if (this .size > 0 ) // If we know there will be blocking, we don't want to keep tasks queued up because it could deadlock.
200
- submitAsync(cloneAndClear()) // If this throws then we have bigger problems
201
+ // If we know there will be blocking, we don't want to keep tasks queued up because it could deadlock.
202
+ if (this .size > 0 )
203
+ submitForExecution(cloneAndClear()) // If this throws then we have bigger problems
201
204
202
205
pbc.blockOn(thunk) // Now delegate the blocking to the previous BC
203
206
}
204
207
}
205
208
206
209
private [this ] final class SyncBatch (runnable : Runnable ) extends AbstractBatch (runnable, BatchingExecutorStatics .emptyBatchArray, 1 ) with Runnable {
207
- @ tailrec private [this ] final def runWithoutResubmit (failure : Throwable ): Throwable =
208
- if (failure != null && (failure.isInstanceOf [InterruptedException ] || NonFatal (failure))) {
209
- reportFailure(failure)
210
- runWithoutResubmit(runUntilFailureOrDone())
211
- } else {
212
- _tasksLocal.set(BatchingExecutorStatics .marker)
213
- failure
210
+ @ tailrec override final def run (): Unit = {
211
+ try runN(BatchingExecutorStatics .runLimit) catch {
212
+ case ie : InterruptedException =>
213
+ reportFailure(ie) // TODO: Handle InterruptedException differently?
214
+ case f if NonFatal (f) =>
215
+ reportFailure(f)
214
216
}
215
217
216
- override final def run (): Unit = {
217
- _tasksLocal.set(this ) // This is later cleared in `runWithoutResubmit`
218
-
219
- val f = runWithoutResubmit(runUntilFailureOrDone())
220
-
221
- if (f != null )
222
- throw f
218
+ if (this .size > 0 )
219
+ run()
223
220
}
224
221
}
225
222
226
- /** SHOULD throw a NullPointerException when `runnable` is null
223
+ /** MUST throw a NullPointerException when `runnable` is null
224
+ * When implementing a sync BatchingExecutor, it is RECOMMENDED
225
+ * to implement this method as `runnable.run()`
227
226
*/
228
- protected def submitAsync (runnable : Runnable ): Unit
227
+ protected def submitForExecution (runnable : Runnable ): Unit
229
228
230
229
/** Reports that an asynchronous computation failed.
231
230
* See `ExecutionContext.reportFailure(throwable: Throwable)`
232
231
*/
233
232
protected def reportFailure (throwable : Throwable ): Unit
234
233
234
+ /**
235
+ * WARNING: Never use both `submitAsyncBatched` and `submitSyncBatched` in the same
236
+ * implementation of `BatchingExecutor`
237
+ */
235
238
protected final def submitAsyncBatched (runnable : Runnable ): Unit = {
236
239
val b = _tasksLocal.get
237
240
if (b.isInstanceOf [AsyncBatch ]) b.asInstanceOf [AsyncBatch ].push(runnable)
238
- else submitAsync (new AsyncBatch (runnable))
241
+ else submitForExecution (new AsyncBatch (runnable))
239
242
}
240
243
244
+ /**
245
+ * WARNING: Never use both `submitAsyncBatched` and `submitSyncBatched` in the same
246
+ * implementation of `BatchingExecutor`
247
+ */
241
248
protected final def submitSyncBatched (runnable : Runnable ): Unit = {
242
249
Objects .requireNonNull(runnable, " runnable is null" )
243
- val b = _tasksLocal.get
250
+ val tl = _tasksLocal
251
+ val b = tl.get
244
252
if (b.isInstanceOf [SyncBatch ]) b.asInstanceOf [SyncBatch ].push(runnable)
245
- else if (b == null ) { // If there is null in _tasksLocal, set a marker and run, inflate the Batch only if needed
246
- _tasksLocal.set(BatchingExecutorStatics .marker) // Set a marker to indicate that we are submitting synchronously
247
- runnable.run() // If we observe a non-null task which isn't a batch here, then allocate a batch
248
- _tasksLocal.remove() // Since we are executing synchronously, we can clear this at the end of execution
249
- } else new SyncBatch (runnable).run()
253
+ else {
254
+ val i = if (b ne null ) b.asInstanceOf [java.lang.Integer ].intValue else 0
255
+ if (i < BatchingExecutorStatics .syncPreBatchDepth) {
256
+ tl.set(java.lang.Integer .valueOf(i + 1 ))
257
+ try submitForExecution(runnable) // User code so needs to be try-finally guarded here
258
+ finally tl.set(b)
259
+ } else {
260
+ val batch = new SyncBatch (runnable)
261
+ tl.set(batch)
262
+ submitForExecution(batch)
263
+ tl.set(b) // Batch only throws fatals so no need for try-finally here
264
+ }
265
+ }
250
266
}
251
267
}
0 commit comments