11namespace Fc.Domain.Inventory
22
33open Equinox.Core // we use Equinox's AsyncCacheCell helper below
4+ open FSharp.UMX
45
56type internal IdsCache < 'Id >() =
67 let all = System.Collections.Concurrent.ConcurrentDictionary< 'Id, unit>() // Bounded only by relatively low number of physical pick tickets IRL
78 static member Create init = let x = IdsCache() in x.Add init; x
89 member __.Add ids = for x in ids do all.[ x] <- ()
910 member __.Contains id = all.ContainsKey id
1011
11- /// Ingests items into a log of items, making a best effort at deduplicating as it writes
12- /// Prior to first add, reads recent ids , in order to minimize the number of duplicated Ids we ingest
13- type Service internal ( inventoryId , epochs : Epoch.Service ) =
12+ /// Maintains active Epoch Id in a thread-safe manner while ingesting items into the ` series ` of ` epochs `
13+ /// Prior to first add, reads ` lookBack ` epochs to seed the cache , in order to minimize the number of duplicated Ids we ingest
14+ type Service internal ( inventoryId , series : Series.Service , epochs : Epoch.Service , lookBack , capacity ) =
1415
15- static let log = Serilog.Log.ForContext< Service>()
16+ let log = Serilog.Log.ForContext< Service>()
17+
18+ // Maintains what we believe to be the currently open EpochId
19+ // Guaranteed to be set only after `previousIds.AwaitValue()`
20+ let mutable activeEpochId = Unchecked.defaultof<_>
1621
1722 // We want max one request in flight to establish the pre-existing Events from which the TransactionIds cache will be seeded
18- let previousIds : AsyncCacheCell < Set < InventoryTransactionId >> =
19- let read = async { let! r = epochs.TryIngest( inventoryId, Seq.empty) in return r.transactionIds }
20- AsyncCacheCell read
23+ let previousEpochs = AsyncCacheCell< AsyncCacheCell< Set< InventoryTransactionId>> list> <| async {
24+ let! startingId = series.ReadIngestionEpoch( inventoryId)
25+ activeEpochId <- % startingId
26+ let read epochId = async { let! r = epochs.TryIngest( inventoryId, epochId, ( fun _ -> 1 ), Seq.empty) in return r.transactionIds }
27+ return [ for epoch in ( max 0 (% startingId - lookBack)) .. (% startingId - 1 ) -> AsyncCacheCell( read % epoch) ] }
2128
2229 // TransactionIds cache - used to maintain a list of transactions that have already been ingested in order to avoid db round-trips
2330 let previousIds : AsyncCacheCell < IdsCache < _ >> = AsyncCacheCell <| async {
24- let! previousIds = previousIds.AwaitValue()
25- return IdsCache.Create( previousIds) }
31+ let! previousEpochs = previousEpochs.AwaitValue()
32+ let! ids = seq { for x in previousEpochs -> x.AwaitValue() } |> Async.Parallel
33+ return IdsCache.Create( Seq.concat ids) }
2634
2735 let tryIngest events = async {
2836 let! previousIds = previousIds.AwaitValue()
37+ let initialEpochId = % activeEpochId
2938
30- let rec aux totalIngested items = async {
39+ let rec aux epochId totalIngested items = async {
3140 let SeqPartition f = Seq.toArray >> Array.partition f
3241 let dup , fresh = items |> SeqPartition ( Epoch.Events.chooseInventoryTransactionId >> Option.exists previousIds.Contains)
3342 let fullCount = List.length items
3443 let dropping = fullCount - Array.length fresh
35- if dropping <> 0 then log.Information( " Ignoring {count}/{fullCount} duplicate ids: {ids}" , dropping, fullCount, dup)
44+ if dropping <> 0 then log.Information( " Ignoring {count}/{fullCount} duplicate ids: {ids} for {epochId} " , dropping, fullCount, dup, epochId )
3645 if Array.isEmpty fresh then
3746 return totalIngested
3847 else
39- let! res = epochs.TryIngest( inventoryId, fresh)
40- log.Information( " Added {count} items to {inventoryId:l}" , res.added, inventoryId)
48+ let! res = epochs.TryIngest( inventoryId, epochId , capacity , fresh)
49+ log.Information( " Added {count} items to {inventoryId:l}/{epochId} " , res.added, inventoryId, epochId )
4150 // The adding is potentially redundant; we don't care
4251 previousIds.Add res.transactionIds
52+ // Any writer noticing we've moved to a new epoch shares the burden of marking it active
53+ if not res.isClosed && activeEpochId < % epochId then
54+ log.Information( " Marking {inventoryId:l}/{epochId} active" , inventoryId, epochId)
55+ do ! series.AdvanceIngestionEpoch( inventoryId, epochId)
56+ System.Threading.Interlocked.CompareExchange(& activeEpochId, % epochId, activeEpochId) |> ignore
4357 let totalIngestedTransactions = totalIngested + res.added
44- return totalIngestedTransactions }
45- return ! aux 0 events
58+ match res.rejected with
59+ | [] -> return totalIngestedTransactions
60+ | rej -> return ! aux ( InventoryEpochId.next epochId) totalIngestedTransactions rej }
61+ return ! aux initialEpochId 0 events
4662 }
4763
4864 /// Upon startup, we initialize the TransactionIds cache with recent epochs; we want to kick that process off before our first ingest
@@ -53,11 +69,22 @@ type Service internal (inventoryId, epochs : Epoch.Service) =
5369
5470module internal Helpers =
5571
56- let create inventoryId epochs =
57- Service( inventoryId, epochs)
72+ let create inventoryId ( maxTransactionsPerEpoch , lookBackLimit ) ( series , epochs ) =
73+ let remainingEpochCapacity ( state : Epoch.Fold.State ) =
74+ let currentLen = state.ids.Count
75+ max 0 ( maxTransactionsPerEpoch - currentLen)
76+ Service( inventoryId, series, epochs, lookBack= lookBackLimit, capacity= remainingEpochCapacity)
77+
78+ module Cosmos =
79+
80+ let create inventoryId ( maxTransactionsPerEpoch , lookBackLimit ) ( context , cache ) =
81+ let series = Series.Cosmos.create ( context, cache)
82+ let epochs = Epoch.Cosmos.create ( context, cache)
83+ Helpers.create inventoryId ( maxTransactionsPerEpoch, lookBackLimit) ( series, epochs)
5884
5985module EventStore =
6086
61- let create inventoryId ( context , cache ) =
87+ let create inventoryId ( maxTransactionsPerEpoch , lookBackLimit ) ( context , cache ) =
88+ let series = Series.EventStore.create ( context, cache)
6289 let epochs = Epoch.EventStore.create ( context, cache)
63- Helpers.create inventoryId epochs
90+ Helpers.create inventoryId ( maxTransactionsPerEpoch , lookBackLimit ) ( series , epochs)
0 commit comments