@@ -7,7 +7,7 @@ open Equinox
7
7
open Equinox.Core
8
8
open System.Runtime .InteropServices
9
9
10
- /// Equivalent to GetEventStore 's in purpose; signals a conflict has been detected and reprocessing of the decision will be necessary
10
+ /// Equivalent to EventStoreDB 's in purpose; signals a conflict has been detected and reprocessing of the decision will be necessary
11
11
exception private WrongVersionException of streamName : string * expected : int * value : obj
12
12
13
13
/// Internal result used to reflect the outcome of syncing with the entry in the inner ConcurrentDictionary
@@ -20,10 +20,21 @@ type ConcurrentArraySyncResult<'t> = Written of 't | Conflict of 't
20
20
21
21
/// Maintains a dictionary of ITimelineEvent<'Format>[] per stream-name, allowing one to vary the encoding used to match that of a given concrete store, or optimize test run performance
22
22
type VolatileStore < 'Format >() =
23
- let streams = System.Collections.Concurrent.ConcurrentDictionary< string, FsCodec.ITimelineEvent< 'Format>[]>()
23
+
24
+ let streams = System.Collections.Concurrent.ConcurrentDictionary< string, FsCodec.ITimelineEvent< 'Format>[]>()
25
+
26
+ // Where TrySync attempts overlap on the same stream, there's a race to raise the Committed event for each 'commit' resulting from a successful Sync
27
+ // If we don't serialize the publishing of the events, its possible for handlers to observe the Events out of order
24
28
let committed = Event<_>()
29
+ // Here we neuter that effect - the BatchingGate can end up with commits submitted out of order, but we serialize the raising of the events per stream
30
+ let publishBatches ( commits : ( FsCodec.StreamName * FsCodec.ITimelineEvent < 'Format >[])[]) = async {
31
+ for streamName, events in commits |> Seq.groupBy fst do
32
+ committed.Trigger( streamName, events |> Seq.collect snd |> Seq.sortBy ( fun x -> x.Index) |> Seq.toArray) }
33
+ let publishCommit = AsyncBatchingGate( publishBatches, System.TimeSpan.FromMilliseconds 2. )
25
34
26
35
[<CLIEvent>]
36
+ /// Notifies of a batch of events being committed to a given Stream. Guarantees no out of order and/or overlapping raising of the event<br/>
37
+ /// NOTE in some cases, two or more overlapping commits can be coalesced into a single <c>Committed</c> event
27
38
member __.Committed : IEvent < FsCodec.StreamName * FsCodec.ITimelineEvent < 'Format >[]> = committed.Publish
28
39
29
40
/// Loads state from a given stream
@@ -33,20 +44,22 @@ type VolatileStore<'Format>() =
33
44
member __.TrySync
34
45
( streamName , trySyncValue : FsCodec.ITimelineEvent < 'Format >[] -> ConcurrentDictionarySyncResult < FsCodec.ITimelineEvent < 'Format >[]>,
35
46
events : FsCodec.ITimelineEvent < 'Format >[])
36
- : ConcurrentArraySyncResult < FsCodec.ITimelineEvent < 'Format >[]> =
47
+ : Async < ConcurrentArraySyncResult < FsCodec.ITimelineEvent < 'Format >[]>> = async {
37
48
let seedStream _streamName = events
38
49
let updateValue streamName ( currentValue : FsCodec.ITimelineEvent < 'Format >[]) =
39
50
match trySyncValue currentValue with
40
51
| ConcurrentDictionarySyncResult.Conflict expectedVersion -> raise <| WrongVersionException ( streamName, expectedVersion, box currentValue)
41
52
| ConcurrentDictionarySyncResult.Written value -> value
42
- try let res = streams.AddOrUpdate( streamName, seedStream, updateValue) |> Written
43
- committed.Trigger(( FsCodec.StreamName.parse streamName, events)) // raise here, once, as updateValue can conceptually be invoked multiple times
44
- res
45
- with WrongVersionException(_, _, conflictingValue) -> unbox conflictingValue |> Conflict
53
+ try let res = streams.AddOrUpdate( streamName, seedStream, updateValue)
54
+ // we publish the event here, once, as `updateValue` can be invoked multiple times
55
+ do ! publishCommit.Execute(( FsCodec.StreamName.parse streamName, events))
56
+ return Written res
57
+ with WrongVersionException(_, _, conflictingValue) ->
58
+ return Conflict ( unbox conflictingValue) }
46
59
47
60
type Token = { streamVersion: int ; streamName: string }
48
61
49
- /// Internal implementation detail of MemoryStreamStore
62
+ /// Internal implementation detail of MemoryStore
50
63
module private Token =
51
64
52
65
let private streamTokenOfIndex streamName ( streamVersion : int ) : StreamToken =
@@ -64,36 +77,37 @@ module private Token =
64
77
65
78
/// Represents the state of a set of streams in a style consistent withe the concrete Store types - no constraints on memory consumption (but also no persistence!).
66
79
type Category < 'event , 'state , 'context , 'Format >( store : VolatileStore < 'Format >, codec : FsCodec.IEventCodec < 'event , 'Format , 'context >, fold , initial ) =
67
- let (| Decode |) = Array.choose codec.TryDecode
68
80
interface ICategory< 'event, 'state, string, 'context> with
69
81
member __.Load ( _log , streamName , _opt ) = async {
70
82
match store.TryLoad streamName with
71
83
| None -> return Token.ofEmpty streamName initial
72
- | Some ( Decode events) -> return Token.ofEventArray streamName fold initial events }
84
+ | Some events -> return Token.ofEventArray streamName fold initial ( events |> Array.choose codec.TryDecode ) }
73
85
member __.TrySync ( _log , Token.Unpack token , state , events : 'event list , context : 'context option ) = async {
74
86
let inline map i ( e : FsCodec.IEventData < 'Format >) =
75
87
FsCodec.Core.TimelineEvent.Create( int64 i, e.EventType, e.Data, e.Meta, e.EventId, e.CorrelationId, e.CausationId, e.Timestamp)
76
- let encoded : FsCodec.ITimelineEvent < _ >[] = events |> Seq.mapi ( fun i e -> map ( token.streamVersion+ i + 1 ) ( codec.Encode( context, e))) |> Array.ofSeq
88
+ let encoded = events |> Seq.mapi ( fun i e -> map ( token.streamVersion + i + 1 ) ( codec.Encode( context, e))) |> Array.ofSeq
77
89
let trySyncValue currentValue =
78
90
if Array.length currentValue <> token.streamVersion + 1 then ConcurrentDictionarySyncResult.Conflict ( token.streamVersion)
79
91
else ConcurrentDictionarySyncResult.Written ( Seq.append currentValue encoded |> Array.ofSeq)
80
- match store.TrySync( token.streamName, trySyncValue, encoded) with
92
+ match ! store.TrySync( token.streamName, trySyncValue, encoded) with
93
+ | ConcurrentArraySyncResult.Written _ ->
94
+ return SyncResult.Written <| Token.ofEventArrayAndKnownState token.streamName fold state events
81
95
| ConcurrentArraySyncResult.Conflict conflictingEvents ->
82
96
let resync = async {
83
97
let version = Token.tokenOfArray token.streamName conflictingEvents
84
98
let successorEvents = conflictingEvents |> Seq.skip ( token.streamVersion + 1 ) |> List.ofSeq
85
99
return version, fold state ( successorEvents |> Seq.choose codec.TryDecode) }
86
- return SyncResult.Conflict resync
87
- | ConcurrentArraySyncResult.Written _ -> return SyncResult.Written <| Token.ofEventArrayAndKnownState token.streamName fold state events }
100
+ return SyncResult.Conflict resync }
88
101
89
102
type Resolver < 'event , 'state , 'Format , 'context >( store : VolatileStore < 'Format >, codec : FsCodec.IEventCodec < 'event , 'Format , 'context >, fold , initial ) =
90
103
let category = Category< 'event, 'state, 'context, 'Format>( store, codec, fold, initial)
91
104
let resolveStream streamName context = Stream.create category streamName None context
105
+
92
106
member __.Resolve ( streamName : FsCodec.StreamName , [<Optional; DefaultParameterValue null >] ? option , [<Optional; DefaultParameterValue null >] ? context : 'context ) =
93
107
match FsCodec.StreamName.toString streamName, option with
94
- | sn, ( None| Some AllowStale) -> resolveStream sn context
108
+ | sn, ( None | Some AllowStale) -> resolveStream sn context
95
109
| sn, Some AssumeEmpty -> Stream.ofMemento ( Token.ofEmpty sn initial) ( resolveStream sn context)
96
110
97
111
/// Resolve from a Memento being used in a Continuation [based on position and state typically from Stream.CreateMemento]
98
112
member __.FromMemento ( Token.Unpack stream as streamToken , state , ? context ) =
99
- Stream.ofMemento ( streamToken, state) ( resolveStream stream.streamName context)
113
+ Stream.ofMemento ( streamToken, state) ( resolveStream stream.streamName context)
0 commit comments