Skip to content

Commit 36b4036

Browse files
authored
Update to FsCodec 3rc10, Equinox 4rc10, Propulsion 3rc5 (#130)
1 parent 819ae68 commit 36b4036

170 files changed

Lines changed: 1920 additions & 1975 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ The `Unreleased` section name is replaced by the expected version of next releas
1010

1111
### Added
1212
### Changed
13+
14+
- Target `Equinox` v `4.0.0-rc.9`, `Propulsion` v `3.0.0-rc.3` [#128](https://github.com/jet/dotnet-templates/pull/128)
15+
- `module Config` -> `module Store`/`module Factory` [#128](https://github.com/jet/dotnet-templates/pull/128)
16+
1317
### Removed
1418
### Fixed
1519

equinox-patterns/Domain.Tests/ExactlyOnceIngesterTests.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ let linger, maxItemsPerEpoch = System.TimeSpan.FromMilliseconds 1., 5
1111

1212
let createSut =
1313
// While we use ~ 200ms when hitting Cosmos, there's no value in doing so in the context of these property based tests
14-
ListIngester.Config.create_ linger maxItemsPerEpoch
14+
ListIngester.Factory.create_ linger maxItemsPerEpoch
1515

1616
type GuidStringN<[<Measure>]'m> = GuidStringN of string<'m> with static member op_Explicit(GuidStringN x) = x
1717
let (|Ids|) = Array.map (function GuidStringN x -> x)
@@ -28,7 +28,7 @@ type Custom =
2828

2929
let [<Property>] properties shouldUseSameSut (Gap gap) (initialEpochId, NonEmptyArray (Ids initialItems)) (NonEmptyArray (Ids items)) = async {
3030

31-
let store = Equinox.MemoryStore.VolatileStore() |> Config.Store.Memory
31+
let store = Equinox.MemoryStore.VolatileStore() |> Store.Context.Memory
3232

3333
let mutable nextEpochId = initialEpochId
3434
for _ in 1 .. gap do nextEpochId <- ExactlyOnceIngester.Internal.next nextEpochId

equinox-patterns/Domain.Tests/PeriodsCarryingForward.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ open Xunit
99

1010
[<Fact>]
1111
let ``Happy path`` () =
12-
let store = Equinox.MemoryStore.VolatileStore() |> Config.Store.Memory
13-
let service = Config.create store
12+
let store = Equinox.MemoryStore.VolatileStore() |> Store.Context.Memory
13+
let service = Factory.create store
1414
let decide items _state =
1515
let apply = Array.truncate 2 items
1616
let overflow = Array.skip apply.Length items

equinox-patterns/Domain/Domain.fsproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<ItemGroup>
88
<Compile Include="ExactlyOnceIngester.fs" />
99
<Compile Include="Infrastructure.fs" />
10-
<Compile Include="Config.fs" />
10+
<Compile Include="Store.fs" />
1111
<Compile Include="Types.fs" />
1212
<Compile Include="Period.fs" />
1313
<Compile Include="ListEpoch.fs" />
@@ -16,9 +16,9 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<PackageReference Include="Equinox.MemoryStore" Version="4.0.0-rc.7" />
20-
<PackageReference Include="Equinox.CosmosStore" Version="4.0.0-rc.7" />
21-
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.9" />
19+
<PackageReference Include="Equinox.MemoryStore" Version="4.0.0-rc.10" />
20+
<PackageReference Include="Equinox.CosmosStore" Version="4.0.0-rc.10" />
21+
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.10" />
2222
</ItemGroup>
2323

2424
</Project>

equinox-patterns/Domain/ExactlyOnceIngester.fs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,32 @@ module Patterns.Domain.ExactlyOnceIngester
77

88
open FSharp.UMX // %
99

10-
type IngestResult<'req, 'res> = { accepted : 'res[]; closed : bool; residual : 'req[] }
10+
type IngestResult<'req, 'res> = { accepted: 'res[]; closed: bool; residual: 'req[] }
1111

1212
module Internal =
1313

1414
let unknown<[<Measure>]'m> = UMX.tag -1
15-
let next<[<Measure>]'m> (value : int<'m>) = UMX.tag<'m>(UMX.untag value + 1)
15+
let next<[<Measure>]'m> (value: int<'m>) = UMX.tag<'m>(UMX.untag value + 1)
1616

1717
/// Ensures any given item is only added to the series exactly once by virtue of the following protocol:
1818
/// 1. Caller obtains an origin epoch via ActiveIngestionEpochId, storing that alongside the source item
1919
/// 2. Caller deterministically obtains that origin epoch to supply to Ingest/TryIngest such that retries can be idempotent
2020
type Service<[<Measure>]'id, 'req, 'res, 'outcome> internal
21-
( log : Serilog.ILogger,
22-
readActiveEpoch : unit -> Async<int<'id>>,
23-
markActiveEpoch : int<'id> -> Async<unit>,
24-
ingest : int<'id> * 'req [] -> Async<IngestResult<'req, 'res>>,
25-
mapResults : 'res [] -> 'outcome seq,
21+
( log: Serilog.ILogger,
22+
readActiveEpoch: unit -> Async<int<'id>>,
23+
markActiveEpoch: int<'id> -> Async<unit>,
24+
ingest: int<'id> * 'req [] -> Async<IngestResult<'req, 'res>>,
25+
mapResults: 'res [] -> 'outcome seq,
2626
linger) =
2727

28-
let uninitializedSentinel : int = %Internal.unknown
28+
let uninitializedSentinel: int = %Internal.unknown
2929
let mutable currentEpochId_ = uninitializedSentinel
3030
let currentEpochId () = if currentEpochId_ <> uninitializedSentinel then Some %currentEpochId_ else None
3131

32-
let tryIngest (reqs : (int<'id> * 'req)[][]) =
32+
let tryIngest (reqs: (int<'id> * 'req)[][]) =
3333
let rec aux ingestedItems items = async {
3434
let epochId = items |> Seq.map fst |> Seq.min
35-
let epochItems, futureEpochItems = items |> Array.partition (fun (e, _ : 'req) -> e = epochId)
35+
let epochItems, futureEpochItems = items |> Array.partition (fun (e, _: 'req) -> e = epochId)
3636
let! res = ingest (epochId, Array.map snd epochItems)
3737
let ingestedItemIds = Array.append ingestedItems res.accepted
3838
let logLevel =
@@ -56,7 +56,7 @@ type Service<[<Measure>]'id, 'req, 'res, 'outcome> internal
5656

5757
/// In the overall processing using an Ingester, we frequently have a Scheduler running N streams concurrently
5858
/// If each thread works in isolation, they'll conflict with each other as they feed the Items into the batch in epochs.Ingest
59-
/// Instead, we enable concurrent requests to coalesce by having requests converge in this AsyncBatchingGate
59+
/// Instead, we enable concurrent requests to coalesce by having requests converge in this Batcher
6060
/// This has the following critical effects:
6161
/// - Traffic to CosmosDB is naturally constrained to a single flight in progress
6262
/// (BatchingGate does not release next batch for execution until current has succeeded or throws)
@@ -65,11 +65,11 @@ type Service<[<Measure>]'id, 'req, 'res, 'outcome> internal
6565
/// a) back-off, re-read and retry if there's a concurrent write Optimistic Concurrency Check failure when writing the stream
6666
/// b) enter a prolonged period of retries if multiple concurrent writes trigger rate limiting and 429s from CosmosDB
6767
/// c) readers will less frequently encounter sustained 429s on the batch
68-
let batchedIngest = Equinox.Core.AsyncBatchingGate(tryIngest, linger)
68+
let batchedIngest = Equinox.Core.Batching.Batcher(tryIngest, linger)
6969

7070
/// Run the requests over a chain of epochs.
7171
/// Returns the subset that actually got handled this time around (i.e., exclusive of items that did not trigger writes per the idempotency rules).
72-
member _.IngestMany(originEpoch, reqs) : Async<'outcome seq> = async {
72+
member _.IngestMany(originEpoch, reqs): Async<'outcome seq> = async {
7373
if Array.isEmpty reqs then return Seq.empty else
7474

7575
let! results = batchedIngest.Execute [| for x in reqs -> originEpoch, x |]
@@ -80,7 +80,7 @@ type Service<[<Measure>]'id, 'req, 'res, 'outcome> internal
8080
/// The fact that any Ingest call for a given item (or set of items) always commences from the same origin is key to exactly once insertion guarantee.
8181
/// Caller should first store this alongside the item in order to deterministically be able to start from the same origin in idempotent retry cases.
8282
/// Uses cached values as epoch transitions are rare, and caller needs to deal with the inherent race condition in any case
83-
member _.ActiveIngestionEpochId() : Async<int<'id>> =
83+
member _.ActiveIngestionEpochId(): Async<int<'id>> =
8484
match currentEpochId () with
8585
| Some currentEpochId -> async { return currentEpochId }
8686
| None -> readActiveEpoch()

equinox-patterns/Domain/Infrastructure.fs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,31 @@
22
module Patterns.Domain.Infrastructure
33

44
/// Buffers events accumulated from a series of decisions while evolving the presented `state` to reflect said proposed `Events`
5-
type Accumulator<'e, 's>(originState : 's, fold : 's -> seq<'e> -> 's) =
5+
type Accumulator<'e, 's>(originState: 's, fold: 's -> seq<'e> -> 's) =
66
let mutable state = originState
77
let pendingEvents = ResizeArray<'e>()
8-
let (|Apply|) (xs : #seq<'e>) = state <- fold state xs; pendingEvents.AddRange xs
8+
let (|Apply|) (xs: #seq<'e>) = state <- fold state xs; pendingEvents.AddRange xs
99

1010
/// Run an Async interpret function that does not yield a result
11-
member _.Transact(interpret : 's -> Async<#seq<'e>>) : Async<unit> = async {
11+
member _.Transact(interpret: 's -> Async<#seq<'e>>): Async<unit> = async {
1212
let! Apply = interpret state in return () }
1313

1414
/// Run an Async decision function, buffering and applying any Events yielded
15-
member _.Transact(decide : 's -> Async<'r * #seq<'e>>) : Async<'r> = async {
15+
member _.Transact(decide: 's -> Async<'r * #seq<'e>>): Async<'r> = async {
1616
let! r, Apply = decide state in return r }
1717

1818
/// Run a decision function, buffering and applying any Events yielded
19-
member _.Transact(decide : 's -> 'r * #seq<'e>) : 'r =
19+
member _.Transact(decide: 's -> 'r * #seq<'e>): 'r =
2020
let r, Apply = decide state in r
2121

2222
/// Accumulated events based on the Decisions applied to date
23-
member _.Events : 'e list =
23+
member _.Events: 'e list =
2424
List.ofSeq pendingEvents
2525

2626
// /// Run a decision function that does not yield a result
27-
// member x.Transact(interpret) : unit =
27+
// member x.Transact(interpret): unit =
2828
// x.Transact(fun state -> (), interpret state)
2929

3030
// /// Projects from the present state including accumulated events
31-
// member _.Query(render : 's -> 'r) : 'r =
31+
// member _.Query(render: 's -> 'r): 'r =
3232
// render state

equinox-patterns/Domain/ListEpoch.fs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ let streamId = Equinox.StreamId.gen ListEpochId.toString
88
module Events =
99

1010
type Event =
11-
| Ingested of {| ids : ItemId[] |}
11+
| Ingested of {| ids: ItemId[] |}
1212
| Closed
13-
| Snapshotted of {| ids : ItemId[]; closed : bool |}
13+
| Snapshotted of {| ids: ItemId[]; closed: bool |}
1414
interface TypeShape.UnionContract.IUnionContract
15-
let codec, codecJe = Config.EventCodec.gen<Event>, Config.EventCodec.genJsonElement<Event>
15+
let codec, codecJe = Store.Codec.gen<Event>, Store.Codec.genJsonElement<Event>
1616

1717
module Fold =
1818

@@ -22,7 +22,7 @@ module Fold =
2222
| Events.Ingested e -> Array.append e.ids ids, closed
2323
| Events.Closed -> (ids, true)
2424
| Events.Snapshotted e -> (e.ids, e.closed)
25-
let fold : State -> Events.Event seq -> State = Seq.fold evolve
25+
let fold: State -> Events.Event seq -> State = Seq.fold evolve
2626

2727
let isOrigin = function Events.Snapshotted _ -> true | _ -> false
2828
let toSnapshot (ids, closed) = Events.Snapshotted {| ids = ids; closed = closed |}
@@ -41,35 +41,35 @@ let decide shouldClose candidateIds = function
4141
let ingestEvent = Events.Ingested {| ids = news |}
4242
news, if closing then [ ingestEvent ; Events.Closed ] else [ ingestEvent ]
4343
let _, closed = Fold.fold state events
44-
let res : ExactlyOnceIngester.IngestResult<_, _> = { accepted = added; closed = closed; residual = [||] }
44+
let res: ExactlyOnceIngester.IngestResult<_, _> = { accepted = added; closed = closed; residual = [||] }
4545
res, events
4646
| currentIds, true ->
4747
{ accepted = [||]; closed = true; residual = candidateIds |> Array.except currentIds (*|> Array.distinct*) }, []
4848

4949
// NOTE see feedSource for example of separating Service logic into Ingestion and Read Services in order to vary the folding and/or state held
5050
type Service internal
51-
( shouldClose : ItemId[] -> ItemId[] -> bool, // let outer layers decide whether ingestion should trigger closing of the batch
52-
resolve : ListEpochId -> Equinox.Decider<Events.Event, Fold.State>) =
51+
( shouldClose: ItemId[] -> ItemId[] -> bool, // let outer layers decide whether ingestion should trigger closing of the batch
52+
resolve: ListEpochId -> Equinox.Decider<Events.Event, Fold.State>) =
5353

5454
/// Ingest the supplied items. Yields relevant elements of the post-state to enable generation of stats
5555
/// and facilitate deduplication of incoming items in order to avoid null store round-trips where possible
56-
member _.Ingest(epochId, items) : Async<ExactlyOnceIngester.IngestResult<_, _>> =
56+
member _.Ingest(epochId, items): Async<ExactlyOnceIngester.IngestResult<_, _>> =
5757
let decider = resolve epochId
5858
// NOTE decider which will initially transact against potentially stale cached state, which will trigger a
5959
// resync if another writer has gotten in before us. This is a conscious decision in this instance; the bulk
6060
// of writes are presumed to be coming from within this same process
61-
decider.Transact(decide shouldClose items, load = Equinox.AllowStale)
61+
decider.Transact(decide shouldClose items, load = Equinox.AnyCachedValue)
6262

63-
/// Returns all the items currently held in the stream (Not using AllowStale on the assumption this needs to see updates from other apps)
64-
member _.Read epochId : Async<Fold.State> =
63+
/// Returns all the items currently held in the stream (Not using AnyCachedValue on the assumption this needs to see updates from other apps)
64+
member _.Read epochId: Async<Fold.State> =
6565
let decider = resolve epochId
66-
decider.Query id
66+
decider.Query(id, Equinox.AllowStale (System.TimeSpan.FromSeconds 1))
6767

68-
module Config =
68+
module Factory =
6969

7070
let private (|Category|) = function
71-
| Config.Store.Memory store -> Config.Memory.create Events.codec Fold.initial Fold.fold store
72-
| Config.Store.Cosmos (context, cache) -> Config.Cosmos.createSnapshotted Events.codecJe Fold.initial Fold.fold (Fold.isOrigin, Fold.toSnapshot) (context, cache)
71+
| Store.Context.Memory store -> Store.Memory.create Events.codec Fold.initial Fold.fold store
72+
| Store.Context.Cosmos (context, cache) -> Store.Cosmos.createSnapshotted Events.codecJe Fold.initial Fold.fold (Fold.isOrigin, Fold.toSnapshot) (context, cache)
7373
let create maxItemsPerEpoch (Category cat) =
7474
let shouldClose candidateItems currentItems = Array.length currentItems + Array.length candidateItems >= maxItemsPerEpoch
75-
Service(shouldClose, streamId >> Config.resolveDecider cat Category)
75+
Service(shouldClose, streamId >> Store.resolveDecider cat Category)

equinox-patterns/Domain/ListIngester.fs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
module Patterns.Domain.ListIngester
22

3-
type Service internal (ingester : ExactlyOnceIngester.Service<_, _, _, _>) =
3+
type Service internal (ingester: ExactlyOnceIngester.Service<_, _, _, _>) =
44

55
/// Slot the item into the series of epochs.
66
/// Returns items that actually got added (i.e. may be empty if it was an idempotent retry).
7-
member _.IngestItems(originEpochId, items : ItemId[]) : Async<seq<ItemId>>=
7+
member _.IngestItems(originEpochId, items: ItemId[]): Async<seq<ItemId>>=
88
ingester.IngestMany(originEpochId, items)
99

1010
/// Efficiently determine a valid ingestion origin epoch
1111
member _.ActiveIngestionEpochId() =
1212
ingester.ActiveIngestionEpochId()
1313

14-
module Config =
14+
module Factory =
1515

1616
let create_ linger maxItemsPerEpoch store =
1717
let log = Serilog.Log.ForContext<Service>()
18-
let series = ListSeries.Config.create store
19-
let epochs = ListEpoch.Config.create maxItemsPerEpoch store
18+
let series = ListSeries.Factory.create store
19+
let epochs = ListEpoch.Factory.create maxItemsPerEpoch store
2020
let ingester = ExactlyOnceIngester.create log linger (series.ReadIngestionEpochId, series.MarkIngestionEpochId) (epochs.Ingest, Array.toSeq)
2121
Service(ingester)
2222
let create store =

equinox-patterns/Domain/ListSeries.fs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ let streamId () = Equinox.StreamId.gen ListSeriesId.toString ListSeriesId.wellKn
1313
module Events =
1414

1515
type Event =
16-
| Started of {| epochId : ListEpochId |}
17-
| Snapshotted of {| active : ListEpochId |}
16+
| Started of {| epochId: ListEpochId |}
17+
| Snapshotted of {| active: ListEpochId |}
1818
interface TypeShape.UnionContract.IUnionContract
19-
let codec, codecJe = Config.EventCodec.gen<Event>, Config.EventCodec.genJsonElement<Event>
19+
let codec, codecJe = Store.Codec.gen<Event>, Store.Codec.genJsonElement<Event>
2020

2121
module Fold =
2222

@@ -25,33 +25,33 @@ module Fold =
2525
let private evolve _state = function
2626
| Events.Started e -> Some e.epochId
2727
| Events.Snapshotted e -> Some e.active
28-
let fold : State -> Events.Event seq -> State = Seq.fold evolve
28+
let fold: State -> Events.Event seq -> State = Seq.fold evolve
2929

3030
let isOrigin = function Events.Snapshotted _ -> true | _ -> false
3131
let toSnapshot s = Events.Snapshotted {| active = Option.get s |}
3232

33-
let interpret epochId (state : Fold.State) =
33+
let interpret epochId (state: Fold.State) =
3434
[if state |> Option.forall (fun cur -> cur < epochId) && epochId >= ListEpochId.initial then
3535
yield Events.Started {| epochId = epochId |}]
3636

37-
type Service internal (resolve : unit -> Equinox.Decider<Events.Event, Fold.State>) =
37+
type Service internal (resolve: unit -> Equinox.Decider<Events.Event, Fold.State>) =
3838

3939
/// Determines the current active epoch
4040
/// Uses cached values as epoch transitions are rare, and caller needs to deal with the inherent race condition in any case
41-
member _.ReadIngestionEpochId() : Async<ListEpochId> =
41+
member _.ReadIngestionEpochId(): Async<ListEpochId> =
4242
let decider = resolve ()
4343
decider.Query(Option.defaultValue ListEpochId.initial)
4444

4545
/// Mark specified `epochId` as live for the purposes of ingesting
4646
/// Writers are expected to react to having writes to an epoch denied (due to it being Closed) by anointing a successor via this
47-
member _.MarkIngestionEpochId epochId : Async<unit> =
47+
member _.MarkIngestionEpochId epochId: Async<unit> =
4848
let decider = resolve ()
49-
decider.Transact(interpret epochId, load = Equinox.AllowStale)
49+
decider.Transact(interpret epochId, load = Equinox.AnyCachedValue)
5050

51-
module Config =
51+
module Factory =
5252

5353
let private (|Category|) = function
54-
| Config.Store.Memory store -> Config.Memory.create Events.codec Fold.initial Fold.fold store
55-
| Config.Store.Cosmos (context, cache) ->
56-
Config.Cosmos.createSnapshotted Events.codecJe Fold.initial Fold.fold (Fold.isOrigin, Fold.toSnapshot) (context, cache)
57-
let create (Category cat) = Service(streamId >> Config.resolveDecider cat Category)
54+
| Store.Context.Memory store -> Store.Memory.create Events.codec Fold.initial Fold.fold store
55+
| Store.Context.Cosmos (context, cache) ->
56+
Store.Cosmos.createSnapshotted Events.codecJe Fold.initial Fold.fold (Fold.isOrigin, Fold.toSnapshot) (context, cache)
57+
let create (Category cat) = Service(streamId >> Store.resolveDecider cat Category)

0 commit comments

Comments
 (0)