Skip to content

Commit 33e4a9b

Browse files
committed
Process Manager wip
1 parent 66faf2b commit 33e4a9b

6 files changed

Lines changed: 155 additions & 79 deletions

File tree

equinox-fc/Domain/Inventory.fs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ type internal IdsCache<'Id>() =
1212

1313
/// Maintains active Epoch Id in a thread-safe manner while ingesting items into the `series` of `epochs`
1414
/// Prior to first add, reads `lookBack` epochs to seed the cache, in order to minimize the number of duplicated Ids we ingest
15-
type Service internal (inventoryId, series : Series.Service, epochs : Epoch.Service, lookBack, capacity) =
15+
type Service2 internal (inventoryId, series : Series.Service, epochs : Epoch.Service, lookBack, capacity) =
1616

17-
let log = Serilog.Log.ForContext<Service>()
17+
let log = Serilog.Log.ForContext<Service2>()
1818

1919
// Maintains what we believe to be the currently open EpochId
2020
// Guaranteed to be set only after `previousIds.AwaitValue()`
@@ -74,7 +74,7 @@ module internal Helpers =
7474
let remainingEpochCapacity (state: Epoch.Fold.State) =
7575
let currentLen = state.ids.Count
7676
max 0 (maxTransactionsPerEpoch - currentLen)
77-
Service(inventoryId, series, epochs, lookBack = lookBackLimit, capacity = remainingEpochCapacity)
77+
Service2(inventoryId, series, epochs, lookBack = lookBackLimit, capacity = remainingEpochCapacity)
7878

7979
module Cosmos =
8080

@@ -85,15 +85,35 @@ module Cosmos =
8585

8686
module Processor =
8787

88-
type Service(transactions : Transaction.Service, locations : Fc.Location.Service, inventory : Service) =
89-
90-
member __.Apply(inventoryId, transactionId, update) = async {
91-
let! action = transactions.Apply(transactionId, update)
92-
match action with
93-
| Transaction.Adjust (loc, qty) -> locations.Execute
94-
| Remove of LocationId * int
95-
| Add of LocationId * int
96-
| Log of TerminalState
97-
| Finish
98-
service.Ingest
99-
}
88+
type Service(transactions : Transaction.Service, locations : Fc.Location.Service, inventory : Service2) =
89+
90+
let execute transactionId =
91+
let f = Fc.Location.Epoch.decide transactionId
92+
let rec aux update = async {
93+
let! action = transactions.Apply(transactionId, update)
94+
match action with
95+
| Transaction.Adjust (loc, bal) ->
96+
match! locations.Execute(loc, f (Fc.Location.Epoch.Reset bal)) with
97+
| Fc.Location.Epoch.Accepted _ -> do! aux (Transaction.Events.Adjusted)
98+
| Fc.Location.Epoch.DupFromPreviousEpoch -> failwith "TODO walk back to previous epoch"
99+
| Transaction.Remove (loc, delta) ->
100+
match! locations.Execute(loc, f (Fc.Location.Epoch.Remove delta)) with
101+
| Fc.Location.Epoch.Accepted bal -> do! aux (Transaction.Events.Removed { balance = bal })
102+
| Fc.Location.Epoch.DupFromPreviousEpoch -> failwith "TODO walk back to previous epoch"
103+
| Transaction.Add (loc, delta) ->
104+
match! locations.Execute(loc, f (Fc.Location.Epoch.Add delta)) with
105+
| Fc.Location.Epoch.Accepted bal -> do! aux (Transaction.Events.Added { balance = bal })
106+
| Fc.Location.Epoch.DupFromPreviousEpoch -> failwith "TODO walk back to previous epoch"
107+
| Transaction.Log (Transaction.Adjusted e) ->
108+
let! _count = inventory.Ingest([Fc.Inventory.Epoch.Events.Adjusted { transactionId = transactionId }])
109+
do! aux Transaction.Events.Logged
110+
| Transaction.Log (Transaction.Transferred e) ->
111+
let! _count = inventory.Ingest([Fc.Inventory.Epoch.Events.Transferred { transactionId = transactionId }])
112+
do! aux Transaction.Events.Logged
113+
| Transaction.Finish ->
114+
()
115+
}
116+
aux
117+
118+
member __.Apply(transactionId, update) =
119+
execute transactionId update

equinox-fc/Domain/InventoryEpoch.fs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ module Events =
1010
let [<Literal>] CategoryId = "InventoryEpoch"
1111
let (|For|) (inventoryId, epochId) = FsCodec.StreamName.compose CategoryId [InventoryId.toString inventoryId; InventoryEpochId.toString epochId]
1212

13-
type TransactionInfo = { transactionId : InventoryTransactionId }
14-
13+
type TransactionRef = { transactionId : InventoryTransactionId }
1514
type Snapshotted = { closed: bool; ids : InventoryTransactionId[] }
1615

1716
type Event =
18-
| Adjusted of TransactionInfo
19-
| Transferred of TransactionInfo
17+
| Adjusted of TransactionRef
18+
| Transferred of TransactionRef
2019
| Closed
2120
| Snapshotted of Snapshotted
2221
interface TypeShape.UnionContract.IUnionContract

equinox-fc/Domain/InventoryTransaction.fs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,34 @@ module Events =
3838
interface TypeShape.UnionContract.IUnionContract
3939
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()
4040

41-
type TerminalState =
42-
| Adjusted of Events.AdjustmentRequested
43-
| Transferred of Added
44-
| TransferFailed of Events.TransferRequested
45-
and Added = { request : Events.TransferRequested; removed : Events.Removed; added : Events.Added }
4641
type Action =
4742
| Adjust of LocationId * int
4843
| Remove of LocationId * int
4944
| Add of LocationId * int
50-
| Log of TerminalState
45+
| Log of LoggingState
5146
| Finish
47+
and LoggingState =
48+
| Adjusted of Events.AdjustmentRequested
49+
| Transferred of Added
50+
and Added = { request : Events.TransferRequested; removed : Events.Removed; added : Events.Added }
5251

5352
module Fold =
5453

5554
type State =
5655
| Initial
5756
| Running of RunningState
58-
| Logging of TerminalState
57+
| Logging of LoggingState
5958
| Completed of TerminalState
6059
and RunningState =
6160
| Adjust of Events.AdjustmentRequested
6261
| Transfer of TransferState
6362
and TransferState =
6463
| Requested of Events.TransferRequested
6564
| Adding of Removed
65+
and TerminalState =
66+
| Adjusted of Events.AdjustmentRequested
67+
| Transferred of Added
68+
| TransferFailed of Events.TransferRequested
6669
and Removed = { request : Events.TransferRequested; removed : Events.Removed }
6770
let initial = Initial
6871
let evolve state event =
@@ -71,7 +74,7 @@ module Fold =
7174
| Initial, Events.AdjustmentRequested r ->
7275
Running (Adjust r)
7376
| Running (Adjust r), Events.Adjusted ->
74-
Logging (Adjusted r)
77+
Logging (LoggingState.Adjusted r)
7578

7679
(* Transfer Process *)
7780
| Initial, Events.TransferRequested e ->
@@ -83,11 +86,13 @@ module Fold =
8386
| Running (Transfer (Requested s)), Events.Removed e ->
8487
Running (Transfer (Adding { request = s; removed = e }))
8588
| Running (Transfer (Adding s)), Events.Added e ->
86-
Logging (Transferred { request = s.request; removed = s.removed; added = e })
89+
Logging (LoggingState.Transferred { request = s.request; removed = s.removed; added = e })
8790

8891
(* Log result *)
89-
| Logging s, Events.Logged ->
90-
Completed s
92+
| Logging (LoggingState.Adjusted s), Events.Logged ->
93+
Completed (Adjusted s)
94+
| Logging (LoggingState.Transferred s), Events.Logged ->
95+
Completed (Transferred s)
9196

9297
(* Any disallowed state changes represent gaps in the model, so we fail fast *)
9398
| state, event -> failwithf "Unexpected %A when %A" event state

equinox-fc/Domain/Location.fs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,28 @@ namespace Fc.Location
22

33
[<NoComparison; NoEquality>]
44
type Wip<'R> =
5-
| Pending of decide : (Epoch.Fold.Balance -> 'R * Epoch.Events.Event list)
5+
| Pending of decide : (Epoch.Fold.State -> 'R * Epoch.Events.Event list)
66
| Complete of 'R
77

88
/// Manages Reads and Writes for a Series of Epochs, with a running total being carried forward to the next Epoch when it's marked Closed
9-
type Service internal (zeroBalance, shouldClose, series : Series.Service, epochs : Epoch.Service) =
9+
type Service internal (zeroBalance, toBalanceCarriedForward, shouldClose, series : Series.Service, epochs : Epoch.Service) =
1010

11-
let rec execute locationId originEpochId =
11+
let execute locationId originEpochId =
1212
let rec aux epochId balanceToCarryForward wip = async {
1313
let decide state = match wip with Complete r -> r, [] | Pending decide -> decide state
1414
match! epochs.Sync(locationId, epochId, balanceToCarryForward, decide, shouldClose) with
15-
| { balance = bal; result = Some res; isOpen = true } ->
15+
| { result = Some res; isOpen = true } ->
1616
if originEpochId <> epochId then
1717
do! series.AdvanceIngestionEpoch(locationId, epochId)
18-
return bal, res
19-
| { balance = bal; result = Some res } ->
18+
return res
19+
| { history = history; result = Some res } ->
2020
let successorEpochId = LocationEpochId.next epochId
21-
return! aux successorEpochId (Some bal) (Complete res)
22-
| { balance = bal } ->
21+
let cf = toBalanceCarriedForward history
22+
return! aux successorEpochId (Some cf) (Complete res)
23+
| { history = history } ->
2324
let successorEpochId = LocationEpochId.next epochId
24-
return! aux successorEpochId (Some bal) wip }
25+
let cf = toBalanceCarriedForward history
26+
return! aux successorEpochId (Some cf) wip }
2527
aux
2628

2729
member __.Execute(locationId, decide) = async {
@@ -35,12 +37,12 @@ type Service internal (zeroBalance, shouldClose, series : Series.Service, epochs
3537
[<AutoOpen>]
3638
module Helpers =
3739

38-
let create (zeroBalance, shouldClose) (series, epochs) =
39-
Service(zeroBalance, shouldClose, series, epochs)
40+
let create (zeroBalance, toBalanceCarriedForward, shouldClose) (series, epochs) =
41+
Service(zeroBalance, toBalanceCarriedForward, shouldClose, series, epochs)
4042

4143
module Cosmos =
4244

43-
let createService (zeroBalance, shouldClose) (context, cache, maxAttempts) =
45+
let createService (zeroBalance, toBalanceCarriedForward, shouldClose) (context, cache, maxAttempts) =
4446
let series = Series.Cosmos.createService (context, cache, maxAttempts)
4547
let epochs = Epoch.Cosmos.createService (context, cache, maxAttempts)
46-
create (zeroBalance, shouldClose) (series, epochs)
48+
create (zeroBalance, toBalanceCarriedForward, shouldClose) (series, epochs)

equinox-fc/Domain/LocationEpoch.fs

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,71 +10,121 @@ module Events =
1010
let [<Literal>] CategoryId = "LocationEpoch"
1111
let (|For|) (locationId, epochId) = FsCodec.StreamName.compose CategoryId [LocationId.toString locationId; LocationEpochId.toString epochId]
1212

13-
type CarriedForward = { initial : int }
14-
type Delta = { delta : int; transaction : InventoryTransactionId }
15-
type Value = { value : int; transaction : InventoryTransactionId }
13+
type CarriedForward = { initial : int; recentTransactions : InventoryTransactionId[] }
1614
type Event =
1715
| CarriedForward of CarriedForward
16+
| Added of {| delta : int; id : InventoryTransactionId |}
17+
| Removed of {| delta : int; id : InventoryTransactionId |}
18+
| Reset of {| value : int; id : InventoryTransactionId |}
1819
| Closed
19-
| Added of Delta
20-
| Removed of Delta
21-
| Reset of Value
2220
interface TypeShape.UnionContract.IUnionContract
2321
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()
2422

2523
module Fold =
2624

27-
type Balance = int
28-
type OpenState = { count : int; value : Balance }
29-
type State = Initial | Open of OpenState | Closed of Balance
25+
type State =
26+
| Initial
27+
| Open of Record list // reverse order, i.e. most revent first
28+
| Closed of Record list // trimmed
29+
and Record =
30+
| Init of Events.CarriedForward
31+
| Step of Step
32+
and Step = { balance : Balance; id : InventoryTransactionId }
33+
and Balance = int
3034
let initial = Initial
35+
let (|Current|) = function
36+
| (Init { initial = bal } | Step { balance = bal }) :: _ -> bal
37+
| [] -> failwith "Cannot transact when no CarriedForward"
3138
let evolve state event =
3239
match event, state with
33-
| Events.CarriedForward e, Initial -> Open { count = 0; value = e.initial }
34-
| Events.Added e, Open bal -> Open { count = bal.count + 1; value = bal.value + e.delta }
35-
| Events.Removed e, Open bal -> Open { count = bal.count + 1; value = bal.value - e.delta }
36-
| Events.Reset e, Open bal -> Open { count = bal.count + 1; value = e.value }
37-
| Events.Closed, Open { value = bal } -> Closed bal
38-
| Events.CarriedForward _, (Open _|Closed _ as x) -> failwithf "CarriedForward : Unexpected %A" x
39-
| (Events.Added _|Events.Removed _|Events.Reset _|Events.Closed) as e, (Initial|Closed _ as s) -> failwithf "Unexpected %A when %A" e s
40+
| Events.CarriedForward e, Initial -> Open [Init e]
41+
| Events.Added e, Open (Current cur as log) -> Open (Step { id = e.id ; balance = cur + e.delta } :: log)
42+
| Events.Removed e, Open (Current cur as log) -> Open (Step { id = e.id ; balance = cur - e.delta } :: log)
43+
| Events.Reset e, Open log -> Open (Step { id = e.id ; balance = e.value } :: log)
44+
| Events.Closed, Open log -> Closed log
45+
| Events.CarriedForward _, (Open _ | Closed _ as x) -> failwithf "CarriedForward : Unexpected %A" x
46+
| (Events.Added _ | Events.Removed _ | Events.Reset _ | Events.Closed) as e, (Initial | Closed _ as s) ->
47+
failwithf "Unexpected %A when %A" e s
4048
let fold = Seq.fold evolve
4149

4250
/// Holds events accumulated from a series of decisions while also evolving the presented `state` to reflect the pended events
4351
type private Accumulator() =
4452
let acc = ResizeArray()
4553
member __.Ingest state : 'res * Events.Event list -> 'res * Fold.State = function
46-
| res, [] -> res, state
47-
| res, [e] -> acc.Add e; res, Fold.evolve state e
48-
| res, xs -> acc.AddRange xs; res, Fold.fold state (Seq.ofList xs)
54+
| res, [] -> res, state
55+
| res, [e] -> acc.Add e; res, Fold.evolve state e
56+
| res, xs -> acc.AddRange xs; res, Fold.fold state (Seq.ofList xs)
4957
member __.Accumulated = List.ofSeq acc
5058

51-
type Result<'t> = { balance : Fold.Balance; result : 't option; isOpen : bool }
59+
type Result<'t> = { history : Fold.Record list; result : 't option; isOpen : bool }
5260

53-
let sync (balanceCarriedForward : Fold.Balance option) (decide : Fold.Balance -> 't*Events.Event list) shouldClose state : Result<'t>*Events.Event list =
61+
let sync (carriedForward : Events.CarriedForward option)
62+
(decide : Fold.State -> 't * Events.Event list)
63+
shouldClose
64+
state
65+
: Result<'t> * Events.Event list =
5466

5567
let acc = Accumulator()
56-
// We require a CarriedForward event at the start of any Epoch's event stream
68+
// 1. Guarantee a CarriedForward event at the start of any Epoch's event stream
5769
let (), state =
5870
acc.Ingest state <|
5971
match state with
60-
| Fold.Initial -> (), [Events.CarriedForward { initial = Option.get balanceCarriedForward }]
72+
| Fold.Initial -> (), [Events.CarriedForward (Option.get carriedForward )]
6173
| Fold.Open _ | Fold.Closed _ -> (), []
62-
// Run, unless we determine we're in Closed state
74+
// 2. Transact (unless we determine we're in Closed state)
6375
let result, state =
6476
acc.Ingest state <|
6577
match state with
66-
| Fold.Initial -> failwith "We've just guaranteed not Initial"
67-
| Fold.Open { value = bal } -> let r, es = decide bal in Some r, es
68-
| Fold.Closed _ -> None, []
69-
// Finally (iff we're `Open`, have run a `decide` and `shouldClose`), we generate a Closed event
70-
let (balance, isOpen), _ =
78+
| Fold.Initial -> failwith "We've just guaranteed not Initial"
79+
| Fold.Open history -> let r, es = decide state in Some r, es
80+
| Fold.Closed _ -> None, []
81+
// 3. Finally (iff we're `Open`, have run a `decide` and `shouldClose`), we generate a Closed event
82+
let (history, isOpen), _ =
7183
acc.Ingest state <|
7284
match state with
73-
| Fold.Initial -> failwith "Can't be Initial"
74-
| Fold.Open ({ value = bal } as openState) when shouldClose openState -> (bal, false), [Events.Closed]
75-
| Fold.Open { value = bal } -> (bal, true), []
76-
| Fold.Closed bal -> (bal, false), []
77-
{ balance = balance; result = result; isOpen = isOpen }, acc.Accumulated
85+
| Fold.Initial -> failwith "Can't be Initial"
86+
| Fold.Open history ->
87+
if shouldClose history then (history, false), [Events.Closed]
88+
else (history, true), []
89+
| Fold.Closed history -> (history, false), []
90+
{ history = history; result = result; isOpen = isOpen }, acc.Accumulated
91+
92+
type DupCheckResult = NotDuplicate | IdempotentInsert of Fold.Balance | DupCarriedForward
93+
let private tryFindDup transactionId (history : Fold.Record list) =
94+
let tryMatch : Fold.Record -> Fold.Balance option option = function
95+
| Fold.Step { balance = bal; id = id } when id = transactionId -> Some (Some bal)
96+
| Fold.Init { recentTransactions = prevs } when prevs |> Array.contains transactionId -> Some None
97+
| _ -> None
98+
match history |> Seq.tryPick tryMatch with
99+
| None -> NotDuplicate
100+
| Some None -> DupCarriedForward
101+
| Some (Some bal) -> IdempotentInsert bal
102+
103+
type Command =
104+
| Reset of value : int
105+
| Add of delta : int
106+
| Remove of delta : int
107+
108+
type Result = Accepted of Fold.Balance | DupFromPreviousEpoch
109+
110+
let decide transactionId command (state: Fold.State) =
111+
match state with
112+
| Fold.Closed _ | Fold.Initial -> failwithf "Cannot apply in state %A" state
113+
| Fold.Open history ->
114+
115+
match tryFindDup transactionId history with
116+
| IdempotentInsert bal -> Accepted bal, []
117+
| DupCarriedForward -> DupFromPreviousEpoch, []
118+
| NotDuplicate ->
119+
120+
let e =
121+
match command with
122+
| Reset value -> Events.Reset {| value = value; id = transactionId |}
123+
| Add delta -> Events.Added {| delta = delta; id = transactionId |}
124+
| Remove delta -> Events.Removed {| delta = delta; id = transactionId |}
125+
match Fold.evolve state e with
126+
| Fold.Open (Fold.Current cur) -> Accepted cur, []
127+
| s -> failwithf "Unexpected state %A" s
78128

79129
type Service internal (log, resolve, maxAttempts) =
80130

global.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "2.1.500"
3+
"version": "3.1.101"
44
}
5-
}
5+
}

0 commit comments

Comments
 (0)