Skip to content

Commit 08fd2d0

Browse files
committed
refactor(Batching)!: Relax arbitrary arrays constraint
1 parent 856c655 commit 08fd2d0

File tree

2 files changed

+12
-12
lines changed

2 files changed

+12
-12
lines changed

src/Equinox.Core/Batching.fs

+8-8
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ type internal AsyncBatch<'Req, 'Res>() =
1818
// sadly there's no way to detect without a try/catch
1919
try queue.TryAdd(item)
2020
with :? InvalidOperationException -> false
21-
let mutable attempt = Unchecked.defaultof<Lazy<Task<'Res[]>>>
21+
let mutable attempt = Unchecked.defaultof<Lazy<Task<'Res>>>
2222

2323
/// Attempt to add a request to the flight
2424
/// Succeeds during linger interval (which commences when the first caller triggers the workflow via AwaitResult)
2525
/// Fails if this flight has closed (caller should initialize a fresh Batch, potentially holding off until the current attempt completes)
26-
member _.TryAdd(req, dispatch: Func<'Req[], CancellationToken, Task<'Res[]>>, lingerMs: int, limiter: System.Threading.SemaphoreSlim voption, ct) =
26+
member _.TryAdd(req, dispatch: Func<'Req[], CancellationToken, Task<'Res>>, lingerMs: int, limiter: System.Threading.SemaphoreSlim voption, ct) =
2727
if not (tryEnqueue req) then false else
2828

29-
// Prepare a new instance, with cancellation under our control (it won't start until the Force triggers it though)
30-
let newInstance: Lazy<Task<'Res[]>> = lazy task {
29+
// Prepare a new instance, with cancellation under our control (it won't start until the .Value triggers it though)
30+
let newInstance: Lazy<Task<'Res>> = lazy task {
3131
do! Task.Delay(lingerMs, ct)
3232
match limiter with ValueNone -> () | ValueSome s -> do! s.WaitAsync(ct)
3333
try queue.CompleteAdding()
@@ -45,12 +45,12 @@ type internal AsyncBatch<'Req, 'Res>() =
4545
/// Requests are added to pending batch during the wait period, which consists of two phases:
4646
/// 1. a defined linger period (min 1ms)
4747
/// 2. (optionally) a wait to acquire capacity on a limiter semaphore (e.g. one might have a limit on concurrent dispatches across a pool)
48-
type Batcher<'Req, 'Res> private (tryInclude: Func<AsyncBatch<_, _>, 'Req, CancellationToken, bool>) =
48+
type Batcher<'Req, 'Res> private (tryInclude: Func<AsyncBatch<'Req, 'Res>, 'Req, CancellationToken, bool>) =
4949
let mutable cell = AsyncBatch<'Req, 'Res>()
50-
new(dispatch: Func<'Req[], CancellationToken, Task<'Res[]>>, lingerMs, limiter) =
50+
new(dispatch: Func<'Req[], CancellationToken, Task<'Res>>, lingerMs, limiter) =
5151
if lingerMs < 1 then invalidArg (nameof(lingerMs)) "Minimum linger period is 1ms" // concurrent waiters need to add work to the batch across their threads
5252
Batcher(fun cell req ct -> cell.TryAdd(req, dispatch, lingerMs, limiter, ct = ct))
53-
new(dispatch: 'Req[] -> Async<'Res[]>, ?linger : TimeSpan,
53+
new(dispatch: 'Req[] -> Async<'Res>, ?linger: TimeSpan,
5454
// Extends the linger phase to include a period during which we await capacity on an externally managed Semaphore
5555
// The Batcher doesn't care, but a typical use is to enable limiting the number of concurrent in-flight dispatches
5656
?limiter) =
@@ -99,7 +99,7 @@ type BatcherCache<'Id, 'Entry>(cache: Cache<'Entry>, toKey: Func<'Id, string>, c
9999
let mapKey = Func<'Id, string>(fun id -> "$Batcher-" + string id)
100100
BatcherCache(Cache cache, mapKey, createEntry, ?cacheWindow = cacheWindow)
101101

102-
member _.GetOrAdd(id : 'Id) : 'Entry =
102+
member _.GetOrAdd(id: 'Id) : 'Entry =
103103
// Optimise for low allocations on happy path
104104
let key = toKey.Invoke(id)
105105
match cache.TryGet key with

tests/Equinox.Core.Tests/BatchingTests.fs

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ open Xunit
1212
let ``Batcher correctness`` () = async {
1313
let mutable batches = 0
1414
let mutable active = 0
15-
let dispatch (reqs : int[]) = async {
15+
let dispatch (reqs: int[]) = async {
1616
let concurrency = Interlocked.Increment &active
1717
1 =! concurrency
1818
Interlocked.Increment &batches |> ignore
@@ -23,15 +23,15 @@ let ``Batcher correctness`` () = async {
2323
}
2424
let cell = Batcher(dispatch, linger = TimeSpan.FromMilliseconds 40)
2525
let! results = [1 .. 100] |> Seq.map cell.Execute |> Async.Parallel
26-
test <@ set (Seq.collect id results) = set [1 .. 100] @>
26+
test <@ set (Seq.concat results) = set [1 .. 100] @>
2727
// Linger of 40ms makes this tend strongly to only be 1 batch, but no guarantees
2828
test <@ 1 <= batches && batches < 3 @>
2929
}
3030

3131
[<Property>]
3232
let ``Batcher error handling`` shouldFail = async {
3333
let fails = ConcurrentBag() // Could be a ResizeArray per spec, but this removes all doubt
34-
let dispatch (reqs : int[]) = async {
34+
let dispatch (reqs: int[]) = async {
3535
if shouldFail () then
3636
reqs |> Seq.iter fails.Add
3737
failwith $"failing %A{reqs}"
@@ -43,7 +43,7 @@ let ``Batcher error handling`` shouldFail = async {
4343
let oks = results |> Array.choose (function Choice1Of2 r -> Some r | _ -> None)
4444
// Note extraneous exceptions we encounter (e.g. if we remove the catch in TryAdd)
4545
let cancels = results |> Array.choose (function Choice2Of2 e when not (e.Message.Contains "failing") -> Some e | _ -> None)
46-
let inputs, outputs = set input, set (Seq.collect id oks |> Seq.append fails)
46+
let inputs, outputs = set input, set (Seq.concat oks |> Seq.append fails)
4747
test <@ inputs.Count = outputs.Count
4848
&& Array.isEmpty cancels
4949
&& inputs = outputs @>

0 commit comments

Comments
 (0)