99/// This represents the case where a 'happy path' actor died, or experienced another impediment on the path.
1010module Fc.Inventory.Transaction
1111
12+ let [<Literal>] Category = " InventoryTransaction"
13+ let streamName transactionId = FsCodec.StreamName.create Category ( InventoryTransactionId.toString transactionId)
14+
1215// NB - these types and the union case names reflect the actual storage formats and hence need to be versioned with care
1316[<RequireQualifiedAccess>]
1417module Events =
1518
16- let [<Literal>] CategoryId = " InventoryTransaction"
17- let (| For |) transactionId = FsCodec.StreamName.create CategoryId ( InventoryTransactionId.toString transactionId)
18-
1919 type AdjustmentRequested = { location : LocationId ; quantity : int }
2020 type TransferRequested = { source : LocationId ; destination : LocationId ; quantity : int }
2121 type Removed = { balance : int }
@@ -130,28 +130,28 @@ let decide update (state : Fold.State) : Action * Events.Event list =
130130 let state ' = Fold.fold state events
131131 Fold.nextAction state', events
132132
133- type Service internal ( log , resolve , maxAttempts ) =
134-
135- let resolve ( Events.For streamId ) = Equinox.Stream< Events.Event, Fold.State>( log, resolve streamId, maxAttempts)
133+ type Service internal ( resolve : InventoryTransactionId -> Equinox.Stream < Events.Event , Fold.State >) =
136134
137135 member __.Apply ( transactionId , update ) : Async < Action > =
138136 let stream = resolve transactionId
139137 stream.Transact( decide update)
140138
141- let createService resolve = Service( Serilog.Log.ForContext< Service>(), resolve, maxAttempts = 3 )
139+ let create resolver =
140+ let resolve inventoryTransactionId =
141+ let stream = resolver ( streamName inventoryTransactionId)
142+ Equinox.Stream( Serilog.Log.ForContext< Service>(), stream, maxAttempts = 2 )
143+ Service ( resolve)
142144
143145module Cosmos =
144146
145- open Equinox.Cosmos
146-
147147 // in the happy path case, the event stream will typically be short, and the state cached, so snapshotting is less critical
148148 let accessStrategy = Equinox.Cosmos.AccessStrategy.Unoptimized
149149 // ... and there will generally be a single actor touching it at a given time, so we don't need to do a load (which would be more expensive than normal given the `accessStrategy`) before we sync
150150 let opt = Equinox.AllowStale
151151 let resolve ( context , cache ) =
152- let cacheStrategy = CachingStrategy.SlidingWindow ( cache, System.TimeSpan.FromMinutes 20. )
153- fun id -> Resolver( context, Events.codec, Fold.fold, Fold.initial, cacheStrategy, accessStrategy) .Resolve( id, opt)
154- let createService ( context , cache ) = createService ( resolve ( context, cache))
152+ let cacheStrategy = Equinox.Cosmos. CachingStrategy.SlidingWindow ( cache, System.TimeSpan.FromMinutes 20. )
153+ fun id -> Equinox.Cosmos. Resolver( context, Events.codec, Fold.fold, Fold.initial, cacheStrategy, accessStrategy) .Resolve( id, opt)
154+ let createService ( context , cache ) = create ( resolve ( context, cache))
155155
156156/// Handles requirement to infer when a transaction is 'stuck'
157157/// Note we don't want to couple to the state in a deep manner; thus we track:
@@ -188,3 +188,16 @@ module Watchdog =
188188 | Fold.Active startTime when startTime < cutoffTime -> Stuck
189189 | Fold.Active _ -> Active
190190 | Fold.Completed -> Complete
191+
192+ let fold : Events.TimestampAndEvent seq -> Fold.State =
193+ Fold.fold Fold.initial
194+
195+ let (| FoldToWatchdogState |) events : Fold.State =
196+ events
197+ |> Seq.choose Events.codec.TryDecode
198+ |> fold
199+
200+ let (| Match | _ |) = function
201+ | FsCodec.StreamName.CategoryAndId ( Category, InventoryTransactionId.Parse transId), FoldToWatchdogState state ->
202+ Some ( transId, state)
203+ | _ -> None
0 commit comments