From 81e2dd3028f4bffe2bdf81189a3de3a7c1afe615 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 5 Feb 2020 14:08:52 +0000 Subject: [PATCH 01/29] grpc --- Equinox.sln | 6 + .../Equinox.EventStore.Grpc.fsproj | 32 + src/Equinox.EventStore.Grpc/EventStoreGrpc.fs | 750 ++++++++++++++++++ 3 files changed, 788 insertions(+) create mode 100644 src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj create mode 100755 src/Equinox.EventStore.Grpc/EventStoreGrpc.fs diff --git a/Equinox.sln b/Equinox.sln index d88f176aa..a7d94f8aa 100644 --- a/Equinox.sln +++ b/Equinox.sln @@ -91,6 +91,8 @@ EndProjectSection EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.CosmosStore.Prometheus", "src\Equinox.CosmosStore.Prometheus\Equinox.CosmosStore.Prometheus.fsproj", "{3107BBC1-2BCB-4750-AED0-42B1F4CD1909}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.EventStore.Grpc", "src\Equinox.EventStore.Grpc\Equinox.EventStore.Grpc.fsproj", "{C828E360-6FE8-47F9-9415-0A21869D7A93}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -197,6 +199,10 @@ Global {3107BBC1-2BCB-4750-AED0-42B1F4CD1909}.Debug|Any CPU.Build.0 = Debug|Any CPU {3107BBC1-2BCB-4750-AED0-42B1F4CD1909}.Release|Any CPU.ActiveCfg = Release|Any CPU {3107BBC1-2BCB-4750-AED0-42B1F4CD1909}.Release|Any CPU.Build.0 = Release|Any CPU + {C828E360-6FE8-47F9-9415-0A21869D7A93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C828E360-6FE8-47F9-9415-0A21869D7A93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C828E360-6FE8-47F9-9415-0A21869D7A93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C828E360-6FE8-47F9-9415-0A21869D7A93}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj new file mode 100644 index 000000000..4fd2a770f --- /dev/null +++ b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj @@ -0,0 +1,32 @@ + + + + netcoreapp3.1 + 5 + false + true + true + $(DefineConstants);NET461 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs new file mode 100755 index 000000000..775dbe6dd --- /dev/null +++ b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs @@ -0,0 +1,750 @@ +namespace Equinox.EventStore + +open Equinox +open Equinox.Core +open EventStore.Client +open Serilog +open System + +[] +type Direction = Forward | Backward with + override this.ToString() = match this with Forward -> "Forward" | Backward -> "Backward" + +module Log = + [] + type Measurement = { stream: string; interval: StopwatchInterval; bytes: int; count: int } + + [] + type Event = + | WriteSuccess of Measurement + | WriteConflict of Measurement + | Slice of Direction * Measurement + | Batch of Direction * slices: int * Measurement + + let prop name value (log : ILogger) = log.ForContext(name, value) + + let propEvents name (kvps : System.Collections.Generic.KeyValuePair seq) (log : ILogger) = + let items = seq { for kv in kvps do yield sprintf "{\"%s\": %s}" kv.Key kv.Value } + log.ForContext(name, sprintf "[%s]" (String.concat ",\n\r" items)) + + let propEventData name (events : EventData[]) (log : ILogger) = + log |> propEvents name (seq { + for x in events do + if x.IsJson then + yield System.Collections.Generic.KeyValuePair<_,_>(x.Type, System.Text.Encoding.UTF8.GetString x.Data) }) + + let propResolvedEvents name (events : ResolvedEvent[]) (log : ILogger) = + log |> propEvents name (seq { + for x in events do + let e = x.Event + if e.IsJson then + yield System.Collections.Generic.KeyValuePair<_,_>(e.EventType, System.Text.Encoding.UTF8.GetString e.Data) }) + + open Serilog.Events + + /// Attach a property to the log context to hold the metrics + // Sidestep Log.ForContext converting to a string; see https://github.com/serilog/serilog/issues/1124 + let event (value : Event) (log : ILogger) = + let enrich (e : LogEvent) = e.AddPropertyIfAbsent(LogEventProperty("esEvt", ScalarValue(value))) + log.ForContext({ new Serilog.Core.ILogEventEnricher with member __.Enrich(evt,_) = enrich evt }) + + let withLoggedRetries<'t> retryPolicy (contextLabel : string) (f : ILogger -> Async<'t>) log : Async<'t> = + match retryPolicy with + | None -> f log + | Some retryPolicy -> + let withLoggingContextWrapping count = + let log = if count = 1 then log else log |> prop contextLabel count + f log + retryPolicy withLoggingContextWrapping + + let (|BlobLen|) = function null -> 0 | (x : byte[]) -> x.Length + + /// NB Caveat emptor; this is subject to unlimited change without the major version changing - while the `dotnet-templates` repo will be kept in step, and + /// the ChangeLog will mention changes, it's critical to not assume that the presence or nature of these helpers be considered stable + module InternalMetrics = + + module Stats = + let inline (|Stats|) ({ interval = i } : Measurement) = let e = i.Elapsed in int64 e.TotalMilliseconds + + let (|Read|Write|Resync|Rollup|) = function + | Slice (_, (Stats s)) -> Read s + | WriteSuccess (Stats s) -> Write s + | WriteConflict (Stats s) -> Resync s + // slices are rolled up into batches so be sure not to double-count + | Batch (_, _, (Stats s)) -> Rollup s + + let (|SerilogScalar|_|) : LogEventPropertyValue -> obj option = function + | (:? ScalarValue as x) -> Some x.Value + | _ -> None + + let (|EsMetric|_|) (logEvent : LogEvent) : Event option = + match logEvent.Properties.TryGetValue("esEvt") with + | true, SerilogScalar (:? Event as e) -> Some e + | _ -> None + + type Counter = + { mutable count : int64; mutable ms : int64 } + static member Create() = { count = 0L; ms = 0L } + member __.Ingest(ms) = + System.Threading.Interlocked.Increment(&__.count) |> ignore + System.Threading.Interlocked.Add(&__.ms, ms) |> ignore + + type LogSink() = + static let epoch = System.Diagnostics.Stopwatch.StartNew() + static member val Read = Counter.Create() with get, set + static member val Write = Counter.Create() with get, set + static member val Resync = Counter.Create() with get, set + static member Restart() = + LogSink.Read <- Counter.Create() + LogSink.Write <- Counter.Create() + LogSink.Resync <- Counter.Create() + let span = epoch.Elapsed + epoch.Restart() + span + interface Serilog.Core.ILogEventSink with + member __.Emit logEvent = logEvent |> function + | EsMetric (Read stats) -> LogSink.Read.Ingest stats + | EsMetric (Write stats) -> LogSink.Write.Ingest stats + | EsMetric (Resync stats) -> LogSink.Resync.Ingest stats + | EsMetric (Rollup _) -> () + | _ -> () + + /// Relies on feeding of metrics from Log through to Stats.LogSink + /// Use Stats.LogSink.Restart() to reset the start point (and stats) where relevant + let dump (log : Serilog.ILogger) = + let stats = + [ "Read", Stats.LogSink.Read + "Write", Stats.LogSink.Write + "Resync", Stats.LogSink.Resync ] + let logActivity name count lat = + log.Information("{name}: {count:n0} requests; Average latency: {lat:n0}ms", + name, count, (if count = 0L then Double.NaN else float lat/float count)) + let mutable rows, totalCount, totalMs = 0, 0L, 0L + for name, stat in stats do + if stat.count <> 0L then + totalCount <- totalCount + stat.count + totalMs <- totalMs + stat.ms + logActivity name stat.count stat.ms + rows <- rows + 1 + // Yes, there's a minor race here between the use of the values and the reset + let duration = Stats.LogSink.Restart() + if rows > 1 then logActivity "TOTAL" totalCount totalMs + let measures : (string * (TimeSpan -> float)) list = [ "s", fun x -> x.TotalSeconds(*; "m", fun x -> x.TotalMinutes; "h", fun x -> x.TotalHours*) ] + let logPeriodicRate name count = log.Information("rp{name} {count:n0}", name, count) + for uom, f in measures do let d = f duration in if d <> 0. then logPeriodicRate uom (float totalCount/d |> int64) + +[] +type EsSyncResult = Written of EventStore.ClientAPI.WriteResult | Conflict of actualVersion: int64 + +module private Write = + /// Yields `EsSyncResult.Written` or `EsSyncResult.Conflict` to signify WrongExpectedVersion + let private writeEventsAsync (log : ILogger) (conn : IEventStoreConnection) (streamName : string) (version : int64) (events : EventData[]) + : Async = async { + try + let! wr = conn.AppendToStreamAsync(streamName, version, events) |> Async.AwaitTaskCorrect + return EsSyncResult.Written wr + with :? EventStore.ClientAPI.Exceptions.WrongExpectedVersionException as ex -> + log.Information(ex, "Ges TrySync WrongExpectedVersionException writing {EventTypes}, actual {ActualVersion}", + [| for x in events -> x.Type |], ex.ActualVersion) + return EsSyncResult.Conflict (let v = ex.ActualVersion in v.Value) } + + let eventDataBytes events = + let eventDataLen (x : EventData) = match x.Data, x.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes + events |> Array.sumBy eventDataLen + + let private writeEventsLogged (conn : IEventStoreConnection) (streamName : string) (version : int64) (events : EventData[]) (log : ILogger) + : Async = async { + let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propEventData "Json" events + let bytes, count = eventDataBytes events, events.Length + let log = log |> Log.prop "bytes" bytes + let writeLog = log |> Log.prop "stream" streamName |> Log.prop "expectedVersion" version |> Log.prop "count" count + let! t, result = writeEventsAsync writeLog conn streamName version events |> Stopwatch.Time + let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} + let resultLog, evt = + match result, reqMetric with + | EsSyncResult.Conflict actualVersion, m -> + log |> Log.prop "actualVersion" actualVersion, Log.WriteConflict m + | EsSyncResult.Written x, m -> + log |> Log.prop "nextExpectedVersion" x.NextExpectedVersion |> Log.prop "logPosition" x.LogPosition, Log.WriteSuccess m + (resultLog |> Log.event evt).Information("Ges{action:l} count={count} conflict={conflict}", + "Write", events.Length, match evt with Log.WriteConflict _ -> true | _ -> false) + return result } + + let writeEvents (log : ILogger) retryPolicy (conn : IEventStoreConnection) (streamName : string) (version : int64) (events : EventData[]) + : Async = + let call = writeEventsLogged conn streamName version events + Log.withLoggedRetries retryPolicy "writeAttempt" call log + +module private Read = + open FSharp.Control + + let private readSliceAsync (conn : IEventStoreConnection) (streamName : string) (direction : Direction) (batchSize : int) (startPos : int64) + : Async = async { + let call = + match direction with + | Direction.Forward -> conn.ReadStreamEventsForwardAsync(streamName, startPos, batchSize, resolveLinkTos = false) + | Direction.Backward -> conn.ReadStreamEventsBackwardAsync(streamName, startPos, batchSize, resolveLinkTos = false) + return! call |> Async.AwaitTaskCorrect } + + let (|ResolvedEventLen|) (x : ResolvedEvent) = match x.Event.Data, x.Event.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes + + let private loggedReadSlice conn streamName direction batchSize startPos (log : ILogger) : Async = async { + let! t, slice = readSliceAsync conn streamName direction batchSize startPos |> Stopwatch.Time + let bytes, count = slice.Events |> Array.sumBy (|ResolvedEventLen|), slice.Events.Length + let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} + let evt = Log.Slice (direction, reqMetric) + let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" slice.Events + (log |> Log.prop "startPos" startPos |> Log.prop "bytes" bytes |> Log.event evt).Information("Ges{action:l} count={count} version={version}", + "Read", count, slice.LastEventNumber) + return slice } + + let private readBatches (log : ILogger) (readSlice : int64 -> ILogger -> Async) + (maxPermittedBatchReads : int option) (startPosition : int64) + : AsyncSeq = + let rec loop batchCount pos : AsyncSeq = asyncSeq { + match maxPermittedBatchReads with + | Some mpbr when batchCount >= mpbr -> log.Information "batch Limit exceeded"; invalidOp "batch Limit exceeded" + | _ -> () + + let batchLog = log |> Log.prop "batchIndex" batchCount + let! slice = readSlice pos batchLog + match slice.Status with + | SliceReadStatus.StreamDeleted -> raise <| EventStore.ClientAPI.Exceptions.StreamDeletedException(slice.Stream) + | SliceReadStatus.StreamNotFound -> yield Some (int64 ExpectedVersion.EmptyStream), Array.empty // EmptyStream must = -1 + | SliceReadStatus.Success -> + let version = if batchCount = 0 then Some slice.LastEventNumber else None + yield version, slice.Events + if not slice.IsEndOfStream then + yield! loop (batchCount + 1) slice.NextEventNumber + | x -> raise <| System.ArgumentOutOfRangeException("SliceReadStatus", x, "Unknown result value") } + loop 0 startPosition + + let resolvedEventBytes events = events |> Array.sumBy (|ResolvedEventLen|) + + let logBatchRead direction streamName t events batchSize version (log : ILogger) = + let bytes, count = resolvedEventBytes events, events.Length + let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} + let batches = (events.Length - 1) / batchSize + 1 + let action = match direction with Direction.Forward -> "LoadF" | Direction.Backward -> "LoadB" + let evt = Log.Event.Batch (direction, batches, reqMetric) + (log |> Log.prop "bytes" bytes |> Log.event evt).Information( + "Ges{action:l} stream={stream} count={count}/{batches} version={version}", + action, streamName, count, batches, version) + + let loadForwardsFrom (log : ILogger) retryPolicy conn batchSize maxPermittedBatchReads streamName startPosition + : Async = async { + let mergeBatches (batches: AsyncSeq) = async { + let mutable versionFromStream = None + let! (events : ResolvedEvent[]) = + batches + |> AsyncSeq.map (function None, events -> events | (Some _) as reportedVersion, events -> versionFromStream <- reportedVersion; events) + |> AsyncSeq.concatSeq + |> AsyncSeq.toArrayAsync + let version = match versionFromStream with Some version -> version | None -> invalidOp "no version encountered in event batch stream" + return version, events } + + let call pos = loggedReadSlice conn streamName Direction.Forward batchSize pos + let retryingLoggingReadSlice pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) + let direction = Direction.Forward + let log = log |> Log.prop "batchSize" batchSize |> Log.prop "direction" direction |> Log.prop "stream" streamName + let batches : AsyncSeq = readBatches log retryingLoggingReadSlice maxPermittedBatchReads startPosition + let! t, (version, events) = mergeBatches batches |> Stopwatch.Time + log |> logBatchRead direction streamName t events batchSize version + return version, events } + + let partitionPayloadFrom firstUsedEventNumber : ResolvedEvent[] -> int * int = + let acc (tu, tr) ((ResolvedEventLen bytes) as y) = if y.Event.EventNumber < firstUsedEventNumber then tu, tr + bytes else tu + bytes, tr + Array.fold acc (0, 0) + + let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy conn batchSize maxPermittedBatchReads streamName (tryDecode, isOrigin) + : Async = async { + let mergeFromCompactionPointOrStartFromBackwardsStream (log : ILogger) (batchesBackward : AsyncSeq) + : Async = async { + let versionFromStream, lastBatch = ref None, ref None + let! tempBackward = + batchesBackward + |> AsyncSeq.map (fun batch -> + match batch with + | None, events -> lastBatch := Some events; events + | (Some _) as reportedVersion, events -> versionFromStream := reportedVersion; lastBatch := Some events; events + |> Array.map (fun e -> e, tryDecode e)) + |> AsyncSeq.concatSeq + |> AsyncSeq.takeWhileInclusive (function + | x, Some e when isOrigin e -> + match !lastBatch with + | None -> log.Information("GesStop stream={stream} at={eventNumber}", streamName, x.Event.EventNumber) + | Some batch -> + let used, residual = batch |> partitionPayloadFrom x.Event.EventNumber + log.Information("GesStop stream={stream} at={eventNumber} used={used} residual={residual}", streamName, x.Event.EventNumber, used, residual) + false + | _ -> true) // continue the search + |> AsyncSeq.toArrayAsync + let eventsForward = Array.Reverse(tempBackward); tempBackward // sic - relatively cheap, in-place reverse of something we own + let version = match !versionFromStream with Some version -> version | None -> invalidOp "no version encountered in event batch stream" + return version, eventsForward } + + let call pos = loggedReadSlice conn streamName Direction.Backward batchSize pos + let retryingLoggingReadSlice pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) + let log = log |> Log.prop "batchSize" batchSize |> Log.prop "stream" streamName + let startPosition = int64 StreamPosition.End + let direction = Direction.Backward + let readlog = log |> Log.prop "direction" direction + let batchesBackward : AsyncSeq = readBatches readlog retryingLoggingReadSlice maxPermittedBatchReads startPosition + let! t, (version, events) = mergeFromCompactionPointOrStartFromBackwardsStream log batchesBackward |> Stopwatch.Time + log |> logBatchRead direction streamName t (Array.map fst events) batchSize version + return version, events } + +module UnionEncoderAdapters = + let encodedEventOfResolvedEvent (x : ResolvedEvent) : FsCodec.ITimelineEvent = + let e = x.Event + // Inspecting server code shows both Created and CreatedEpoch are set; taking this as it's less ambiguous than DateTime in the general case + let ts = DateTimeOffset.FromUnixTimeMilliseconds(e.CreatedEpoch) + // TOCONSIDER wire e.Metadata.["$correlationId"] and .["$causationId"] into correlationId and causationId + // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata + FsCodec.Core.TimelineEvent.Create(e.EventNumber, e.EventType, e.Data, e.Metadata, correlationId = null, causationId = null, timestamp = ts) + + let eventDataOfEncodedEvent (x : FsCodec.IEventData) = + // TOCONSIDER wire x.CorrelationId, x.CausationId into x.Meta.["$correlationId"] and .["$causationId"] + // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata + EventData(Guid.NewGuid(), x.EventType, isJson = true, data = x.Data, metadata = x.Meta) + +type Stream = { name : string } +type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } +type Token = { stream : Stream; pos : Position } + +module Token = + let private create compactionEventNumber batchCapacityLimit streamName streamVersion : StreamToken = + { value = box { + stream = { name = streamName} + pos = { streamVersion = streamVersion; compactionEventNumber = compactionEventNumber; batchCapacityLimit = batchCapacityLimit } } + version = streamVersion } + + /// No batching / compaction; we only need to retain the StreamVersion + let ofNonCompacting streamName streamVersion : StreamToken = + create None None streamName streamVersion + + // headroom before compaction is necessary given the stated knowledge of the last (if known) `compactionEventNumberOption` + let private batchCapacityLimit compactedEventNumberOption unstoredEventsPending (batchSize : int) (streamVersion : int64) : int = + match compactedEventNumberOption with + | Some (compactionEventNumber : int64) -> (batchSize - unstoredEventsPending) - int (streamVersion - compactionEventNumber + 1L) |> max 0 + | None -> (batchSize - unstoredEventsPending) - (int streamVersion + 1) - 1 |> max 0 + + let (*private*) ofCompactionEventNumber compactedEventNumberOption unstoredEventsPending batchSize streamName streamVersion : StreamToken = + let batchCapacityLimit = batchCapacityLimit compactedEventNumberOption unstoredEventsPending batchSize streamVersion + create compactedEventNumberOption (Some batchCapacityLimit) streamName streamVersion + + /// Assume we have not seen any compaction events; use the batchSize and version to infer headroom + let ofUncompactedVersion batchSize streamName streamVersion : StreamToken = + ofCompactionEventNumber None 0 batchSize streamName streamVersion + + let (|Unpack|) (x : StreamToken) : Token = unbox x.value + + /// Use previousToken plus the data we are adding and the position we are adding it to infer a headroom + let ofPreviousTokenAndEventsLength (Unpack previousToken) eventsLength batchSize streamVersion : StreamToken = + let compactedEventNumber = previousToken.pos.compactionEventNumber + ofCompactionEventNumber compactedEventNumber eventsLength batchSize previousToken.stream.name streamVersion + + /// Use an event just read from the stream to infer headroom + let ofCompactionResolvedEventAndVersion (compactionEvent: ResolvedEvent) batchSize streamName streamVersion : StreamToken = + ofCompactionEventNumber (Some compactionEvent.Event.EventNumber) 0 batchSize streamName streamVersion + + /// Use an event we are about to write to the stream to infer headroom + let ofPreviousStreamVersionAndCompactionEventDataIndex (Unpack token) compactionEventDataIndex eventsLength batchSize streamVersion' : StreamToken = + ofCompactionEventNumber (Some (token.pos.streamVersion + 1L + int64 compactionEventDataIndex)) eventsLength batchSize token.stream.name streamVersion' + + let (|StreamPos|) (Unpack token) : Stream * Position = token.stream, token.pos + + let supersedes (Unpack current) (Unpack x) = + let currentVersion, newVersion = current.pos.streamVersion, x.pos.streamVersion + newVersion > currentVersion + +type Connection(readConnection, [] ?writeConnection, [] ?readRetryPolicy, [] ?writeRetryPolicy) = + member __.ReadConnection = readConnection + member __.ReadRetryPolicy = readRetryPolicy + member __.WriteConnection = defaultArg writeConnection readConnection + member __.WriteRetryPolicy = writeRetryPolicy + +type BatchingPolicy(getMaxBatchSize : unit -> int, [] ?batchCountLimit) = + new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) + member __.BatchSize = getMaxBatchSize() + member __.MaxBatches = batchCountLimit + +[] +type GatewaySyncResult = Written of StreamToken | ConflictUnknown of StreamToken + +type Context(conn : Connection, batching : BatchingPolicy) = + let isResolvedEventEventType (tryDecode, predicate) (x : ResolvedEvent) = predicate (tryDecode (x.Event.Data)) + let tryIsResolvedEventEventType predicateOption = predicateOption |> Option.map isResolvedEventEventType + + member internal __.LoadEmpty streamName = Token.ofUncompactedVersion batching.BatchSize streamName -1L + + member __.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { + let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize batching.MaxBatches streamName 0L + match tryIsResolvedEventEventType isCompactionEventType with + | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events + | Some isCompactionEvent -> + match events |> Array.tryFindBack isCompactionEvent with + | None -> return Token.ofUncompactedVersion batching.BatchSize streamName version, Array.choose tryDecode events + | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose tryDecode events } + + member __.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin) : Async = async { + let! version, events = + Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize batching.MaxBatches streamName (tryDecode, isOrigin) + match Array.tryHead events |> Option.filter (function _, Some e -> isOrigin e | _ -> false) with + | None -> return Token.ofUncompactedVersion batching.BatchSize streamName version, Array.choose snd events + | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose snd events } + + member __.LoadFromToken useWriteConn streamName log (Token.Unpack token as streamToken) (tryDecode, isCompactionEventType) + : Async = async { + let streamPosition = token.pos.streamVersion + 1L + let connToUse = if useWriteConn then conn.WriteConnection else conn.ReadConnection + let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy connToUse batching.BatchSize batching.MaxBatches streamName streamPosition + match isCompactionEventType with + | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events + | Some isCompactionEvent -> + match events |> Array.tryFindBack (fun re -> match tryDecode re with Some e -> isCompactionEvent e | _ -> false) with + | None -> return Token.ofPreviousTokenAndEventsLength streamToken events.Length batching.BatchSize version, Array.choose tryDecode events + | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose tryDecode events } + + member __.TrySync log (Token.Unpack token as streamToken) (events, encodedEvents: EventData array) (isCompactionEventType) : Async = async { + let streamVersion = token.pos.streamVersion + let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection token.stream.name streamVersion encodedEvents + match wr with + | EsSyncResult.Conflict actualVersion -> + return GatewaySyncResult.ConflictUnknown (Token.ofNonCompacting token.stream.name actualVersion) + | EsSyncResult.Written wr -> + let version' = wr.NextExpectedVersion + let token = + match isCompactionEventType with + | None -> Token.ofNonCompacting token.stream.name version' + | Some isCompactionEvent -> + match events |> Array.ofList |> Array.tryFindIndexBack isCompactionEvent with + | None -> Token.ofPreviousTokenAndEventsLength streamToken encodedEvents.Length batching.BatchSize version' + | Some compactionEventIndex -> + Token.ofPreviousStreamVersionAndCompactionEventDataIndex streamToken compactionEventIndex encodedEvents.Length batching.BatchSize version' + return GatewaySyncResult.Written token } + + member __.Sync(log, streamName, streamVersion, events: FsCodec.IEventData[]) : Async = async { + let encodedEvents : EventData[] = events |> Array.map UnionEncoderAdapters.eventDataOfEncodedEvent + let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection streamName streamVersion encodedEvents + match wr with + | EsSyncResult.Conflict actualVersion -> + return GatewaySyncResult.ConflictUnknown (Token.ofNonCompacting streamName actualVersion) + | EsSyncResult.Written wr -> + let version' = wr.NextExpectedVersion + let token = Token.ofNonCompacting streamName version' + return GatewaySyncResult.Written token } + +[] +type AccessStrategy<'event,'state> = + /// Load only the single most recent event defined in 'event` and trust that doing a fold from any such event + /// will yield a correct and complete state + /// In other words, the fold function should not need to consider either the preceding 'state or 'events. + | LatestKnownEvent + /// Ensures a snapshot/compaction event from which the state can be reconstituted upon decoding is always present + /// (embedded in the stream as an event), generated every batchSize events using the supplied toSnapshot function + /// Scanning for events concludes when any event passes the isOrigin test. + /// See https://eventstore.org/docs/event-sourcing-basics/rolling-snapshots/index.html + | RollingSnapshots of isOrigin : ('event -> bool) * toSnapshot : ('state -> 'event) + +type private CompactionContext(eventsLen : int, capacityBeforeCompaction : int) = + /// Determines whether writing a Compaction event is warranted (based on the existing state and the current accumulated changes) + member __.IsCompactionDue = eventsLen > capacityBeforeCompaction + +type private Category<'event, 'state, 'context>(context : Context, codec : FsCodec.IEventCodec<_, _, 'context>, ?access : AccessStrategy<'event, 'state>) = + let tryDecode (e : ResolvedEvent) = e |> UnionEncoderAdapters.encodedEventOfResolvedEvent |> codec.TryDecode + + let compactionPredicate = + match access with + | None -> None + | Some AccessStrategy.LatestKnownEvent -> Some (fun _ -> true) + | Some (AccessStrategy.RollingSnapshots (isValid, _)) -> Some isValid + + let isOrigin = + match access with + | None | Some AccessStrategy.LatestKnownEvent -> fun _ -> true + | Some (AccessStrategy.RollingSnapshots (isValid, _)) -> isValid + + let loadAlgorithm load streamName initial log = + let batched = load initial (context.LoadBatched streamName log (tryDecode,None)) + let compacted = load initial (context.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin)) + match access with + | None -> batched + | Some AccessStrategy.LatestKnownEvent + | Some (AccessStrategy.RollingSnapshots _) -> compacted + + let load (fold : 'state -> 'event seq -> 'state) initial f = async { + let! token, events = f + return token, fold initial events } + + member __.Load (fold : 'state -> 'event seq -> 'state) (initial : 'state) (streamName : string) (log : ILogger) : Async = + loadAlgorithm (load fold) streamName initial log + + member __.LoadFromToken (fold : 'state -> 'event seq -> 'state) (state : 'state) (streamName : string) token (log : ILogger) : Async = + (load fold) state (context.LoadFromToken false streamName log token (tryDecode, compactionPredicate)) + + member __.TrySync<'context> + ( log : ILogger, fold: 'state -> 'event seq -> 'state, + (Token.StreamPos (stream, pos) as streamToken), state : 'state, events : 'event list, ctx : 'context option) : Async> = async { + let encode e = codec.Encode(ctx, e) + let events = + match access with + | None | Some AccessStrategy.LatestKnownEvent -> events + | Some (AccessStrategy.RollingSnapshots (_, compact)) -> + let cc = CompactionContext(List.length events, pos.batchCapacityLimit.Value) + if cc.IsCompactionDue then events @ [fold state events |> compact] else events + + let encodedEvents : EventData[] = events |> Seq.map (encode >> UnionEncoderAdapters.eventDataOfEncodedEvent) |> Array.ofSeq + let! syncRes = context.TrySync log streamToken (events, encodedEvents) compactionPredicate + match syncRes with + | GatewaySyncResult.ConflictUnknown _ -> + return SyncResult.Conflict (load fold state (context.LoadFromToken true stream.name log streamToken (tryDecode, compactionPredicate))) + | GatewaySyncResult.Written token' -> + return SyncResult.Written (token', fold state (Seq.ofList events)) } + +module Caching = + /// Forwards all state changes in all streams of an ICategory to a `tee` function + type CategoryTee<'event, 'state, 'context>(inner: ICategory<'event, 'state, string, 'context>, tee : string -> StreamToken * 'state -> Async) = + let intercept streamName tokenAndState = async { + let! _ = tee streamName tokenAndState + return tokenAndState } + + let loadAndIntercept load streamName = async { + let! tokenAndState = load + return! intercept streamName tokenAndState } + + interface ICategory<'event, 'state, string, 'context> with + member __.Load(log, streamName : string, opt) : Async = + loadAndIntercept (inner.Load(log, streamName, opt)) streamName + + member __.TrySync(log : ILogger, (Token.StreamPos (stream,_) as token), state, events : 'event list, context) : Async> = async { + let! syncRes = inner.TrySync(log, token, state, events, context) + match syncRes with + | SyncResult.Conflict resync -> return SyncResult.Conflict (loadAndIntercept resync stream.name) + | SyncResult.Written (token', state') -> + let! intercepted = intercept stream.name (token', state') + return SyncResult.Written intercepted } + + let applyCacheUpdatesWithSlidingExpiration + (cache : ICache) + (prefix : string) + (slidingExpiration : TimeSpan) + (category : ICategory<'event, 'state, string, 'context>) + : ICategory<'event, 'state, string, 'context> = + let mkCacheEntry (initialToken : StreamToken, initialState : 'state) = new CacheEntry<'state>(initialToken, initialState, Token.supersedes) + let options = CacheItemOptions.RelativeExpiration slidingExpiration + let addOrUpdateSlidingExpirationCacheEntry streamName value = cache.UpdateIfNewer(prefix + streamName, options, mkCacheEntry value) + CategoryTee<'event, 'state, 'context>(category, addOrUpdateSlidingExpirationCacheEntry) :> _ + +type private Folder<'event, 'state, 'context>(category : Category<'event, 'state, 'context>, fold: 'state -> 'event seq -> 'state, initial: 'state, ?readCache) = + let batched log streamName = category.Load fold initial streamName log + interface ICategory<'event, 'state, string, 'context> with + member __.Load(log, streamName, opt) : Async = + match readCache with + | None -> batched log streamName + | Some (cache : ICache, prefix : string) -> async { + match! cache.TryGet(prefix + streamName) with + | None -> return! batched log streamName + | Some tokenAndState when opt = Some AllowStale -> return tokenAndState + | Some (token, state) -> return! category.LoadFromToken fold state streamName token log } + + member __.TrySync(log : ILogger, token, initialState, events : 'event list, context) : Async> = async { + let! syncRes = category.TrySync(log, fold, token, initialState, events, context) + match syncRes with + | SyncResult.Conflict resync -> return SyncResult.Conflict resync + | SyncResult.Written (token',state') -> return SyncResult.Written (token',state') } + +[] +type CachingStrategy = + | SlidingWindow of ICache * window : TimeSpan + /// Prefix is used to segregate multiple folds per stream when they are stored in the cache + | SlidingWindowPrefixed of ICache * window : TimeSpan * prefix : string + +type Resolver<'event, 'state, 'context> + ( context : Context, codec : FsCodec.IEventCodec<_, _, 'context>, fold, initial, + /// Caching can be overkill for EventStore esp considering the degree to which its intrinsic caching is a first class feature + /// e.g., A key benefit is that reads of streams more than a few pages long get completed in constant time after the initial load + [] ?caching, + [] ?access) = + + do match access with + | Some AccessStrategy.LatestKnownEvent when Option.isSome caching -> + "Equinox.EventStore does not support (and it would make things _less_ efficient even if it did)" + + "mixing AccessStrategy.LatestKnownEvent with Caching at present." + |> invalidOp + | _ -> () + + let inner = Category<'event, 'state, 'context>(context, codec, ?access = access) + let readCacheOption = + match caching with + | None -> None + | Some (CachingStrategy.SlidingWindow(cache, _)) -> Some(cache, null) + | Some (CachingStrategy.SlidingWindowPrefixed(cache, _, prefix)) -> Some(cache, prefix) + + let folder = Folder<'event, 'state, 'context>(inner, fold, initial, ?readCache = readCacheOption) + + let category : ICategory<_, _, _, 'context> = + match caching with + | None -> folder :> _ + | Some (CachingStrategy.SlidingWindow(cache, window)) -> + Caching.applyCacheUpdatesWithSlidingExpiration cache null window folder + | Some (CachingStrategy.SlidingWindowPrefixed(cache, window, prefix)) -> + Caching.applyCacheUpdatesWithSlidingExpiration cache prefix window folder + + let resolveStream = Stream.create category + let loadEmpty sn = context.LoadEmpty sn, initial + + member __.Resolve(streamName : FsCodec.StreamName, [] ?option, [] ?context) = + match FsCodec.StreamName.toString streamName, option with + | sn, (None|Some AllowStale) -> resolveStream sn option context + | sn, Some AssumeEmpty -> Stream.ofMemento (loadEmpty sn) (resolveStream sn option context) + + /// Resolve from a Memento being used in a Continuation [based on position and state typically from Stream.CreateMemento] + member __.FromMemento(Token.Unpack token as streamToken, state, ?context) = + Stream.ofMemento (streamToken, state) (resolveStream token.stream.name context None) + +type private SerilogAdapter(log : ILogger) = + interface EventStore.ClientAPI.ILogger with + member __.Debug(format : string, args : obj []) = log.Debug(format, args) + member __.Debug(ex : exn, format : string, args : obj []) = log.Debug(ex, format, args) + member __.Info(format : string, args : obj []) = log.Information(format, args) + member __.Info(ex : exn, format : string, args : obj []) = log.Information(ex, format, args) + member __.Error(format : string, args : obj []) = log.Error(format, args) + member __.Error(ex : exn, format : string, args : obj []) = log.Error(ex, format, args) + +[] +type Logger = + | SerilogVerbose of ILogger + | SerilogNormal of ILogger + | CustomVerbose of EventStore.ClientAPI.ILogger + | CustomNormal of EventStore.ClientAPI.ILogger + member log.Configure(b : ConnectionSettingsBuilder) = + match log with + | SerilogVerbose logger -> b.EnableVerboseLogging().UseCustomLogger(SerilogAdapter(logger)) + | SerilogNormal logger -> b.UseCustomLogger(SerilogAdapter(logger)) + | CustomVerbose logger -> b.EnableVerboseLogging().UseCustomLogger(logger) + | CustomNormal logger -> b.UseCustomLogger(logger) + +[] +type NodePreference = + /// Track master via gossip, writes direct, reads should immediately reflect writes, resync without backoff (highest load on master, good write perf) + | Master + /// Take the first connection that comes along, ideally a master, but do not track master changes + | PreferMaster + /// Prefer slave node, writes normally need forwarding, often can't read writes, resync requires backoff (kindest to master, writes and resyncs expensive) + | PreferSlave + /// Take random node, writes may need forwarding, sometimes can't read writes, resync requires backoff (balanced load on master, balanced write perf) + | Random + +[] +type Discovery = + // Allow Uri-based connection definition (discovery://, tcp:// or + | Uri of Uri + /// Supply a set of pre-resolved EndPoints instead of letting Gossip resolution derive from the DNS outcome + | GossipSeeded of seedManagerEndpoints : System.Net.IPEndPoint [] + // Standard Gossip-based discovery based on Dns query and standard manager port + | GossipDns of clusterDns : string + // Standard Gossip-based discovery based on Dns query (with manager port overriding default 30778) + | GossipDnsCustomPort of clusterDns : string * managerPortOverride : int + +module private Discovery = + let buildDns np (f : DnsClusterSettingsBuilder -> DnsClusterSettingsBuilder) = + ClusterSettings.Create().DiscoverClusterViaDns().KeepDiscovering() + |> fun s -> match np with NodePreference.Random -> s.PreferRandomNode() | NodePreference.PreferSlave -> s.PreferSlaveNode() | _ -> s + |> f |> fun s -> s.Build() + + let buildSeeded np (f : GossipSeedClusterSettingsBuilder -> GossipSeedClusterSettingsBuilder) = + ClusterSettings.Create().DiscoverClusterViaGossipSeeds().KeepDiscovering() + |> fun s -> match np with NodePreference.Random -> s.PreferRandomNode() | NodePreference.PreferSlave -> s.PreferSlaveNode() | _ -> s + |> f |> fun s -> s.Build() + + let configureDns clusterDns maybeManagerPort (x : DnsClusterSettingsBuilder) = + x.SetClusterDns(clusterDns) + |> fun s -> match maybeManagerPort with Some port -> s.SetClusterGossipPort(port) | None -> s + + let inline configureSeeded (seedEndpoints : System.Net.IPEndPoint []) (x : GossipSeedClusterSettingsBuilder) = + x.SetGossipSeedEndPoints(seedEndpoints) + + // converts a Discovery mode to a ClusterSettings or a Uri as appropriate + let (|DiscoverViaUri|DiscoverViaGossip|) : Discovery * NodePreference -> Choice = function + | (Discovery.Uri uri), _ -> DiscoverViaUri uri + | (Discovery.GossipSeeded seedEndpoints), np -> DiscoverViaGossip (buildSeeded np (configureSeeded seedEndpoints)) + | (Discovery.GossipDns clusterDns), np -> DiscoverViaGossip (buildDns np (configureDns clusterDns None)) + | (Discovery.GossipDnsCustomPort (dns, port)), np ->DiscoverViaGossip (buildDns np (configureDns dns (Some port))) + +// see https://github.com/EventStore/EventStore/issues/1652 +[] +type ConnectionStrategy = + /// Pair of master and slave connections, writes direct, often can't read writes, resync without backoff (kind to master, writes+resyncs optimal) + | ClusterTwinPreferSlaveReads + /// Single connection, with resync backoffs appropriate to the NodePreference + | ClusterSingle of NodePreference + +type Connector + ( username, password, reqTimeout: TimeSpan, reqRetries: int, + [] ?log : Logger, [] ?heartbeatTimeout: TimeSpan, [] ?concurrentOperationsLimit, + [] ?readRetryPolicy, [] ?writeRetryPolicy, + [] ?gossipTimeout, [] ?clientConnectionTimeout, + /// Additional strings identifying the context of this connection; should provide enough context to disambiguate all potential connections to a cluster + /// NB as this will enter server and client logs, it should not contain sensitive information + [] ?tags : (string*string) seq) = + let connSettings node = + ConnectionSettings.Create().SetDefaultUserCredentials(SystemData.UserCredentials(username, password)) + .KeepReconnecting() // ES default: .LimitReconnectionsTo(10) + .SetQueueTimeoutTo(reqTimeout) // ES default: Zero/unlimited + .FailOnNoServerResponse() // ES default: DoNotFailOnNoServerResponse() => wait forever; retry and/or log + .SetOperationTimeoutTo(reqTimeout) // ES default: 7s + .LimitRetriesForOperationTo(reqRetries) // ES default: 10 + |> fun s -> + match node with + | NodePreference.Master -> s.PerformOnMasterOnly() // explicitly use ES default of requiring master, use default Node preference of Master + | NodePreference.PreferMaster -> s.PerformOnAnyNode() // override default [implied] PerformOnMasterOnly(), use default Node preference of Master + // NB .PreferSlaveNode/.PreferRandomNode setting is ignored if using EventStoreConneciton.Create(ConnectionSettings, ClusterSettings) overload but + // this code is necessary for cases where people are using the discover :// and related URI schemes + | NodePreference.PreferSlave -> s.PerformOnAnyNode().PreferSlaveNode() // override default PerformOnMasterOnly(), override Master Node preference + | NodePreference.Random -> s.PerformOnAnyNode().PreferRandomNode() // override default PerformOnMasterOnly(), override Master Node preference + |> fun s -> match concurrentOperationsLimit with Some col -> s.LimitConcurrentOperationsTo(col) | None -> s // ES default: 5000 + |> fun s -> match heartbeatTimeout with Some v -> s.SetHeartbeatTimeout v | None -> s // default: 1500 ms + |> fun s -> match gossipTimeout with Some v -> s.SetGossipTimeout v | None -> s // default: 1000 ms + |> fun s -> match clientConnectionTimeout with Some v -> s.WithConnectionTimeoutOf v | None -> s // default: 1000 ms + |> fun s -> match log with Some log -> log.Configure s | None -> s + |> fun s -> s.Build() + + /// Yields an IEventStoreConnection configured and Connect()ed to a node (or the cluster) per the supplied `discovery` and `clusterNodePrefence` preference + member __.Connect + ( /// Name should be sufficient to uniquely identify this connection within a single app instance's logs + name, + discovery : Discovery, ?clusterNodePreference) : Async = async { + if name = null then nullArg "name" + let clusterNodePreference = defaultArg clusterNodePreference NodePreference.Master + let name = String.concat ";" <| seq { + yield name + yield string clusterNodePreference + match tags with None -> () | Some tags -> for key, value in tags do yield sprintf "%s=%s" key value } + let sanitizedName = name.Replace('\'','_').Replace(':','_') // ES internally uses `:` and `'` as separators in log messages and ... people regex logs + let conn = + match discovery, clusterNodePreference with + | Discovery.DiscoverViaUri uri -> + // This overload picks up the discovery settings via ConnectionSettingsBuilder.PreferSlaveNode/.PreferRandomNode + EventStoreConnection.Create(connSettings clusterNodePreference, uri, sanitizedName) + | Discovery.DiscoverViaGossip clusterSettings -> + // NB This overload's implementation ignores the calls to ConnectionSettingsBuilder.PreferSlaveNode/.PreferRandomNode and + // requires equivalent ones on the GossipSeedClusterSettingsBuilder or ClusterSettingsBuilder + EventStoreConnection.Create(connSettings clusterNodePreference, clusterSettings, sanitizedName) + do! conn.ConnectAsync() |> Async.AwaitTaskCorrect + return conn } + + /// Yields a Connection (which may internally be twin connections) configured per the specified strategy + member __.Establish + ( /// Name should be sufficient to uniquely identify this (aggregate) connection within a single app instance's logs + name, + discovery : Discovery, strategy : ConnectionStrategy) : Async = async { + match strategy with + | ConnectionStrategy.ClusterSingle nodePreference -> + let! conn = __.Connect(name, discovery, nodePreference) + return Connection(conn, ?readRetryPolicy=readRetryPolicy, ?writeRetryPolicy=writeRetryPolicy) + | ConnectionStrategy.ClusterTwinPreferSlaveReads -> + let! masterInParallel = Async.StartChild (__.Connect(name + "-TwinW", discovery, NodePreference.Master)) + let! slave = __.Connect(name + "-TwinR", discovery, NodePreference.PreferSlave) + let! master = masterInParallel + return Connection(readConnection = slave, writeConnection = master, ?readRetryPolicy = readRetryPolicy, ?writeRetryPolicy = writeRetryPolicy) } From 5e1f73f7007f60f9d17bbb5128b7374bf6c1dba0 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 5 Feb 2020 22:50:43 +0000 Subject: [PATCH 02/29] WIP up to L184 --- .../Equinox.EventStore.Grpc.fsproj | 2 +- src/Equinox.EventStore.Grpc/EventStoreGrpc.fs | 37 ++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj index 4fd2a770f..f5bd1a682 100644 --- a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj +++ b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj @@ -26,7 +26,7 @@ - + \ No newline at end of file diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs index 775dbe6dd..792465389 100755 --- a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs +++ b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs @@ -6,10 +6,6 @@ open EventStore.Client open Serilog open System -[] -type Direction = Forward | Backward with - override this.ToString() = match this with Forward -> "Forward" | Backward -> "Backward" - module Log = [] type Measurement = { stream: string; interval: StopwatchInterval; bytes: int; count: int } @@ -134,16 +130,16 @@ module Log = for uom, f in measures do let d = f duration in if d <> 0. then logPeriodicRate uom (float totalCount/d |> int64) [] -type EsSyncResult = Written of EventStore.ClientAPI.WriteResult | Conflict of actualVersion: int64 +type EsSyncResult = Written of EventStore.Client.WriteResult | Conflict of actualVersion: int64 module private Write = /// Yields `EsSyncResult.Written` or `EsSyncResult.Conflict` to signify WrongExpectedVersion - let private writeEventsAsync (log : ILogger) (conn : IEventStoreConnection) (streamName : string) (version : int64) (events : EventData[]) + let private writeEventsAsync (log : ILogger) (conn : EventStoreClient) (streamName : string) (version : int64) (events : EventData[]) : Async = async { - try - let! wr = conn.AppendToStreamAsync(streamName, version, events) |> Async.AwaitTaskCorrect + try let! ct = Async.CancellationToken + let! wr = conn.AppendToStreamAsync(streamName, StreamRevision.FromInt64 version, events, cancellationToken=ct) |> Async.AwaitTaskCorrect return EsSyncResult.Written wr - with :? EventStore.ClientAPI.Exceptions.WrongExpectedVersionException as ex -> + with :? EventStore.Client.WrongExpectedVersionException as ex -> log.Information(ex, "Ges TrySync WrongExpectedVersionException writing {EventTypes}, actual {ActualVersion}", [| for x in events -> x.Type |], ex.ActualVersion) return EsSyncResult.Conflict (let v = ex.ActualVersion in v.Value) } @@ -152,7 +148,7 @@ module private Write = let eventDataLen (x : EventData) = match x.Data, x.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes events |> Array.sumBy eventDataLen - let private writeEventsLogged (conn : IEventStoreConnection) (streamName : string) (version : int64) (events : EventData[]) (log : ILogger) + let private writeEventsLogged (conn : EventStoreClient) (streamName : string) (version : int64) (events : EventData[]) (log : ILogger) : Async = async { let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propEventData "Json" events let bytes, count = eventDataBytes events, events.Length @@ -170,7 +166,7 @@ module private Write = "Write", events.Length, match evt with Log.WriteConflict _ -> true | _ -> false) return result } - let writeEvents (log : ILogger) retryPolicy (conn : IEventStoreConnection) (streamName : string) (version : int64) (events : EventData[]) + let writeEvents (log : ILogger) retryPolicy (conn : EventStoreClient) (streamName : string) (version : int64) (events : EventData[]) : Async = let call = writeEventsLogged conn streamName version events Log.withLoggedRetries retryPolicy "writeAttempt" call log @@ -178,17 +174,14 @@ module private Write = module private Read = open FSharp.Control - let private readSliceAsync (conn : IEventStoreConnection) (streamName : string) (direction : Direction) (batchSize : int) (startPos : int64) - : Async = async { - let call = - match direction with - | Direction.Forward -> conn.ReadStreamEventsForwardAsync(streamName, startPos, batchSize, resolveLinkTos = false) - | Direction.Backward -> conn.ReadStreamEventsBackwardAsync(streamName, startPos, batchSize, resolveLinkTos = false) - return! call |> Async.AwaitTaskCorrect } + let private readSliceAsync (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : int64) + : AsyncSeq = + // TODO ct + conn.ReadStreamAsync(direction, streamName, StreamRevision.FromInt64 startPos, uint64 batchSize, resolveLinkTos = false) |> AsyncSeq.ofAsyncEnum let (|ResolvedEventLen|) (x : ResolvedEvent) = match x.Event.Data, x.Event.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes - let private loggedReadSlice conn streamName direction batchSize startPos (log : ILogger) : Async = async { + let private loggedReadSlice conn streamName direction batchSize startPos (log : ILogger) : Async> = async { let! t, slice = readSliceAsync conn streamName direction batchSize startPos |> Stopwatch.Time let bytes, count = slice.Events |> Array.sumBy (|ResolvedEventLen|), slice.Events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} @@ -225,7 +218,7 @@ module private Read = let bytes, count = resolvedEventBytes events, events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} let batches = (events.Length - 1) / batchSize + 1 - let action = match direction with Direction.Forward -> "LoadF" | Direction.Backward -> "LoadB" + let action = match direction with Direction.Forwards -> "LoadF" | Direction.Backwards -> "LoadB" let evt = Log.Event.Batch (direction, batches, reqMetric) (log |> Log.prop "bytes" bytes |> Log.event evt).Information( "Ges{action:l} stream={stream} count={count}/{batches} version={version}", @@ -243,9 +236,9 @@ module private Read = let version = match versionFromStream with Some version -> version | None -> invalidOp "no version encountered in event batch stream" return version, events } - let call pos = loggedReadSlice conn streamName Direction.Forward batchSize pos + let call pos = loggedReadSlice conn streamName Direction.Forwards batchSize pos let retryingLoggingReadSlice pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) - let direction = Direction.Forward + let direction = Direction.Forwards let log = log |> Log.prop "batchSize" batchSize |> Log.prop "direction" direction |> Log.prop "stream" streamName let batches : AsyncSeq = readBatches log retryingLoggingReadSlice maxPermittedBatchReads startPosition let! t, (version, events) = mergeBatches batches |> Stopwatch.Time From d30ae84bcbdd44bbcf20726a9953106b606edc7d Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 10 Feb 2020 23:53:41 +0000 Subject: [PATCH 03/29] Handle SliceStatus --- src/Equinox.EventStore.Grpc/EventStoreGrpc.fs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs index 792465389..f41d9f61a 100755 --- a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs +++ b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs @@ -200,16 +200,13 @@ module private Read = | _ -> () let batchLog = log |> Log.prop "batchIndex" batchCount - let! slice = readSlice pos batchLog - match slice.Status with - | SliceReadStatus.StreamDeleted -> raise <| EventStore.ClientAPI.Exceptions.StreamDeletedException(slice.Stream) - | SliceReadStatus.StreamNotFound -> yield Some (int64 ExpectedVersion.EmptyStream), Array.empty // EmptyStream must = -1 - | SliceReadStatus.Success -> + try let! slice = readSlice pos batchLog let version = if batchCount = 0 then Some slice.LastEventNumber else None yield version, slice.Events if not slice.IsEndOfStream then yield! loop (batchCount + 1) slice.NextEventNumber - | x -> raise <| System.ArgumentOutOfRangeException("SliceReadStatus", x, "Unknown result value") } + with StreamNotFoundException -> + yield Some -1L, Array.empty } // EmptyStream must = -1 loop 0 startPosition let resolvedEventBytes events = events |> Array.sumBy (|ResolvedEventLen|) From b183c6e1f64325fe2495c5cd29c32890498dd729 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 17 Feb 2020 13:12:39 +0000 Subject: [PATCH 04/29] Migrate StreamSlice -> IAsyncEnumerable --- .../Equinox.EventStore.Grpc.fsproj | 1 + src/Equinox.EventStore.Grpc/EventStoreGrpc.fs | 153 ++++++++---------- 2 files changed, 64 insertions(+), 90 deletions(-) diff --git a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj index f5bd1a682..7e3fe4e1e 100644 --- a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj +++ b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj @@ -6,6 +6,7 @@ false true true + $(DefineConstants);NO_ASYNCSEQ $(DefineConstants);NET461 diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs index f41d9f61a..a485c7df1 100755 --- a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs +++ b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs @@ -174,40 +174,35 @@ module private Write = module private Read = open FSharp.Control - let private readSliceAsync (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : int64) - : AsyncSeq = - // TODO ct - conn.ReadStreamAsync(direction, streamName, StreamRevision.FromInt64 startPos, uint64 batchSize, resolveLinkTos = false) |> AsyncSeq.ofAsyncEnum + let private readAsyncEnum (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamRevision) + : Async> = async { + let! ct = Async.CancellationToken + return conn.ReadStreamAsync(direction, streamName, startPos, uint64 batchSize, resolveLinkTos = false, cancellationToken = ct) + |> AsyncSeq.ofAsyncEnum } + + let readAsyncEnumVer (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamRevision) : Async = async { + try let! res = readAsyncEnum conn streamName direction batchSize startPos + let! events = res |> AsyncSeq.toArrayAsync + let version = + match events with + | [||] when direction = Direction.Backwards -> -1L // When reading backwards, the startPos is End, which is not directly convertible + | [||] -> startPos.ToInt64() + | xs when direction = Direction.Backwards -> xs.[0].Event.EventNumber.ToInt64() + | xs -> xs.[xs.Length - 1].Event.EventNumber.ToInt64() + return version, events + with :? StreamNotFoundException -> return -1L, [||] } let (|ResolvedEventLen|) (x : ResolvedEvent) = match x.Event.Data, x.Event.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes - let private loggedReadSlice conn streamName direction batchSize startPos (log : ILogger) : Async> = async { - let! t, slice = readSliceAsync conn streamName direction batchSize startPos |> Stopwatch.Time - let bytes, count = slice.Events |> Array.sumBy (|ResolvedEventLen|), slice.Events.Length + let private loggedReadAsyncEnumVer conn streamName direction batchSize startPos (log : ILogger) : Async = async { + let! t, (version, events) = readAsyncEnumVer conn streamName direction batchSize startPos |> Stopwatch.Time + let bytes, count = events |> Array.sumBy (|ResolvedEventLen|), events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} let evt = Log.Slice (direction, reqMetric) - let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" slice.Events + let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" events (log |> Log.prop "startPos" startPos |> Log.prop "bytes" bytes |> Log.event evt).Information("Ges{action:l} count={count} version={version}", - "Read", count, slice.LastEventNumber) - return slice } - - let private readBatches (log : ILogger) (readSlice : int64 -> ILogger -> Async) - (maxPermittedBatchReads : int option) (startPosition : int64) - : AsyncSeq = - let rec loop batchCount pos : AsyncSeq = asyncSeq { - match maxPermittedBatchReads with - | Some mpbr when batchCount >= mpbr -> log.Information "batch Limit exceeded"; invalidOp "batch Limit exceeded" - | _ -> () - - let batchLog = log |> Log.prop "batchIndex" batchCount - try let! slice = readSlice pos batchLog - let version = if batchCount = 0 then Some slice.LastEventNumber else None - yield version, slice.Events - if not slice.IsEndOfStream then - yield! loop (batchCount + 1) slice.NextEventNumber - with StreamNotFoundException -> - yield Some -1L, Array.empty } // EmptyStream must = -1 - loop 0 startPosition + "Read", count, version) + return version, events } let resolvedEventBytes events = events |> Array.sumBy (|ResolvedEventLen|) @@ -221,66 +216,44 @@ module private Read = "Ges{action:l} stream={stream} count={count}/{batches} version={version}", action, streamName, count, batches, version) - let loadForwardsFrom (log : ILogger) retryPolicy conn batchSize maxPermittedBatchReads streamName startPosition + let loadForwardsFrom (log : ILogger) retryPolicy conn batchSize streamName startPosition : Async = async { - let mergeBatches (batches: AsyncSeq) = async { - let mutable versionFromStream = None - let! (events : ResolvedEvent[]) = - batches - |> AsyncSeq.map (function None, events -> events | (Some _) as reportedVersion, events -> versionFromStream <- reportedVersion; events) - |> AsyncSeq.concatSeq - |> AsyncSeq.toArrayAsync - let version = match versionFromStream with Some version -> version | None -> invalidOp "no version encountered in event batch stream" - return version, events } - - let call pos = loggedReadSlice conn streamName Direction.Forwards batchSize pos - let retryingLoggingReadSlice pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) + let call pos = loggedReadAsyncEnumVer conn streamName Direction.Forwards batchSize pos + let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) let direction = Direction.Forwards let log = log |> Log.prop "batchSize" batchSize |> Log.prop "direction" direction |> Log.prop "stream" streamName - let batches : AsyncSeq = readBatches log retryingLoggingReadSlice maxPermittedBatchReads startPosition - let! t, (version, events) = mergeBatches batches |> Stopwatch.Time + let! t, (version, events) = retryingLoggingRead startPosition log |> Stopwatch.Time log |> logBatchRead direction streamName t events batchSize version return version, events } - let partitionPayloadFrom firstUsedEventNumber : ResolvedEvent[] -> int * int = - let acc (tu, tr) ((ResolvedEventLen bytes) as y) = if y.Event.EventNumber < firstUsedEventNumber then tu, tr + bytes else tu + bytes, tr - Array.fold acc (0, 0) - - let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy conn batchSize maxPermittedBatchReads streamName (tryDecode, isOrigin) + let takeWhileInclusive (predicate: 'T -> bool) (source: 'T seq) = seq { + let enumerator = source.GetEnumerator() + let mutable fin = false + while not fin && enumerator.MoveNext() do + yield enumerator.Current + if not (predicate enumerator.Current) then + fin <- true } + let mergeFromCompactionPointOrStartFromBackwardsStream streamName (tryDecode, isOrigin) (log : ILogger) (itemsBackward : ResolvedEvent[]) + : (ResolvedEvent * 'event option)[] = + itemsBackward + |> Seq.map (fun x -> x, tryDecode x) + |> takeWhileInclusive (function + | x, Some e when isOrigin e -> + log.Information("GesStop stream={stream} at={eventNumber}", streamName, x.Event.EventNumber) + false + | _ -> true) // continue the search + |> Seq.rev + |> Seq.toArray + let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy conn batchSize streamName (tryDecode, isOrigin) : Async = async { - let mergeFromCompactionPointOrStartFromBackwardsStream (log : ILogger) (batchesBackward : AsyncSeq) - : Async = async { - let versionFromStream, lastBatch = ref None, ref None - let! tempBackward = - batchesBackward - |> AsyncSeq.map (fun batch -> - match batch with - | None, events -> lastBatch := Some events; events - | (Some _) as reportedVersion, events -> versionFromStream := reportedVersion; lastBatch := Some events; events - |> Array.map (fun e -> e, tryDecode e)) - |> AsyncSeq.concatSeq - |> AsyncSeq.takeWhileInclusive (function - | x, Some e when isOrigin e -> - match !lastBatch with - | None -> log.Information("GesStop stream={stream} at={eventNumber}", streamName, x.Event.EventNumber) - | Some batch -> - let used, residual = batch |> partitionPayloadFrom x.Event.EventNumber - log.Information("GesStop stream={stream} at={eventNumber} used={used} residual={residual}", streamName, x.Event.EventNumber, used, residual) - false - | _ -> true) // continue the search - |> AsyncSeq.toArrayAsync - let eventsForward = Array.Reverse(tempBackward); tempBackward // sic - relatively cheap, in-place reverse of something we own - let version = match !versionFromStream with Some version -> version | None -> invalidOp "no version encountered in event batch stream" - return version, eventsForward } - - let call pos = loggedReadSlice conn streamName Direction.Backward batchSize pos - let retryingLoggingReadSlice pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) + let call pos = loggedReadAsyncEnumVer conn streamName Direction.Backwards batchSize pos + let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) let log = log |> Log.prop "batchSize" batchSize |> Log.prop "stream" streamName - let startPosition = int64 StreamPosition.End - let direction = Direction.Backward - let readlog = log |> Log.prop "direction" direction - let batchesBackward : AsyncSeq = readBatches readlog retryingLoggingReadSlice maxPermittedBatchReads startPosition - let! t, (version, events) = mergeFromCompactionPointOrStartFromBackwardsStream log batchesBackward |> Stopwatch.Time + let startPosition = StreamRevision.End + let direction = Direction.Backwards + let readLog = log |> Log.prop "direction" direction + let! t, (version, eventsBackward) = retryingLoggingRead startPosition readLog |> Stopwatch.Time + let events = mergeFromCompactionPointOrStartFromBackwardsStream streamName (tryDecode, isOrigin) log eventsBackward log |> logBatchRead direction streamName t (Array.map fst events) batchSize version return version, events } @@ -288,15 +261,16 @@ module UnionEncoderAdapters = let encodedEventOfResolvedEvent (x : ResolvedEvent) : FsCodec.ITimelineEvent = let e = x.Event // Inspecting server code shows both Created and CreatedEpoch are set; taking this as it's less ambiguous than DateTime in the general case - let ts = DateTimeOffset.FromUnixTimeMilliseconds(e.CreatedEpoch) + let ts = DateTimeOffset(e.Created) + // TODO something like let ts = DateTimeOffset.FromUnixTimeMilliseconds(e.CreatedEpoch) // TOCONSIDER wire e.Metadata.["$correlationId"] and .["$causationId"] into correlationId and causationId // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - FsCodec.Core.TimelineEvent.Create(e.EventNumber, e.EventType, e.Data, e.Metadata, correlationId = null, causationId = null, timestamp = ts) + FsCodec.Core.TimelineEvent.Create(e.EventNumber.ToInt64(), e.EventType, e.Data, e.Metadata, correlationId = null, causationId = null, timestamp = ts) let eventDataOfEncodedEvent (x : FsCodec.IEventData) = // TOCONSIDER wire x.CorrelationId, x.CausationId into x.Meta.["$correlationId"] and .["$causationId"] // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - EventData(Guid.NewGuid(), x.EventType, isJson = true, data = x.Data, metadata = x.Meta) + EventData(Uuid.NewUuid(), x.EventType, isJson = true, data = x.Data, metadata = x.Meta) type Stream = { name : string } type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } @@ -336,7 +310,7 @@ module Token = /// Use an event just read from the stream to infer headroom let ofCompactionResolvedEventAndVersion (compactionEvent: ResolvedEvent) batchSize streamName streamVersion : StreamToken = - ofCompactionEventNumber (Some compactionEvent.Event.EventNumber) 0 batchSize streamName streamVersion + ofCompactionEventNumber (Some (compactionEvent.Event.EventNumber.ToInt64())) 0 batchSize streamName streamVersion /// Use an event we are about to write to the stream to infer headroom let ofPreviousStreamVersionAndCompactionEventDataIndex (Unpack token) compactionEventDataIndex eventsLength batchSize streamVersion' : StreamToken = @@ -354,10 +328,9 @@ type Connection(readConnection, [] ?writeConnection, [] member __.WriteConnection = defaultArg writeConnection readConnection member __.WriteRetryPolicy = writeRetryPolicy -type BatchingPolicy(getMaxBatchSize : unit -> int, [] ?batchCountLimit) = +type BatchingPolicy(getMaxBatchSize : unit -> int) = new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) member __.BatchSize = getMaxBatchSize() - member __.MaxBatches = batchCountLimit [] type GatewaySyncResult = Written of StreamToken | ConflictUnknown of StreamToken @@ -369,7 +342,7 @@ type Context(conn : Connection, batching : BatchingPolicy) = member internal __.LoadEmpty streamName = Token.ofUncompactedVersion batching.BatchSize streamName -1L member __.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { - let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize batching.MaxBatches streamName 0L + let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName StreamRevision.Start match tryIsResolvedEventEventType isCompactionEventType with | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events | Some isCompactionEvent -> @@ -379,16 +352,16 @@ type Context(conn : Connection, batching : BatchingPolicy) = member __.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin) : Async = async { let! version, events = - Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize batching.MaxBatches streamName (tryDecode, isOrigin) + Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName (tryDecode, isOrigin) match Array.tryHead events |> Option.filter (function _, Some e -> isOrigin e | _ -> false) with | None -> return Token.ofUncompactedVersion batching.BatchSize streamName version, Array.choose snd events | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose snd events } member __.LoadFromToken useWriteConn streamName log (Token.Unpack token as streamToken) (tryDecode, isCompactionEventType) : Async = async { - let streamPosition = token.pos.streamVersion + 1L + let streamPosition = StreamRevision.FromInt64(token.pos.streamVersion + 1L) let connToUse = if useWriteConn then conn.WriteConnection else conn.ReadConnection - let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy connToUse batching.BatchSize batching.MaxBatches streamName streamPosition + let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy connToUse batching.BatchSize streamName streamPosition match isCompactionEventType with | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events | Some isCompactionEvent -> From acd15f65539a7d24734c9777159b5c65025dd641 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 17 Feb 2020 13:34:23 +0000 Subject: [PATCH 05/29] Package --- build.proj | 1 + 1 file changed, 1 insertion(+) diff --git a/build.proj b/build.proj index 15f52d847..7ca2f8064 100644 --- a/build.proj +++ b/build.proj @@ -19,6 +19,7 @@ + From 054ac9604a64ee92fbe83910123b9b7b968f6b11 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 17 Feb 2020 13:51:37 +0000 Subject: [PATCH 06/29] Comment out logging until preview 3 --- src/Equinox.EventStore.Grpc/EventStoreGrpc.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs index a485c7df1..2aef77a05 100755 --- a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs +++ b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs @@ -566,6 +566,7 @@ type Resolver<'event, 'state, 'context> member __.FromMemento(Token.Unpack token as streamToken, state, ?context) = Stream.ofMemento (streamToken, state) (resolveStream token.stream.name context None) +#if PREVIEW3 // - there's no logging implemented in V3 as yet, so disabling for now type private SerilogAdapter(log : ILogger) = interface EventStore.ClientAPI.ILogger with member __.Debug(format : string, args : obj []) = log.Debug(format, args) @@ -587,6 +588,7 @@ type Logger = | SerilogNormal logger -> b.UseCustomLogger(SerilogAdapter(logger)) | CustomVerbose logger -> b.EnableVerboseLogging().UseCustomLogger(logger) | CustomNormal logger -> b.UseCustomLogger(logger) +#endif [] type NodePreference = From f923399bd461f93fb945305e933dd194fb02f779 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 17 Feb 2020 14:30:46 +0000 Subject: [PATCH 07/29] Comment version inferences --- src/Equinox.EventStore.Grpc/EventStoreGrpc.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs index 2aef77a05..045477fe7 100755 --- a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs +++ b/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs @@ -186,9 +186,9 @@ module private Read = let version = match events with | [||] when direction = Direction.Backwards -> -1L // When reading backwards, the startPos is End, which is not directly convertible - | [||] -> startPos.ToInt64() - | xs when direction = Direction.Backwards -> xs.[0].Event.EventNumber.ToInt64() - | xs -> xs.[xs.Length - 1].Event.EventNumber.ToInt64() + | [||] -> startPos.ToInt64() // e.g. if reading forward from (verified existing) event 10 and there are none, the version is still 10 + | xs when direction = Direction.Backwards -> xs.[0].Event.EventNumber.ToInt64() // the events arrive backwards, so first is the 'version' + | xs -> xs.[xs.Length - 1].Event.EventNumber.ToInt64() // In normal case, the last event represents the version of the stream return version, events with :? StreamNotFoundException -> return -1L, [||] } From 7724851f5b5169759f5da1347d63484bbcb45ba1 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 12 Mar 2020 16:02:57 +0000 Subject: [PATCH 08/29] Preview 3 starting --- src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj index 7e3fe4e1e..b7d33674f 100644 --- a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj +++ b/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj @@ -6,8 +6,6 @@ false true true - $(DefineConstants);NO_ASYNCSEQ - $(DefineConstants);NET461 @@ -30,4 +28,4 @@ - \ No newline at end of file + From 9db7c0c65a56424f7966a0b8f5235b5a958e8294 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sat, 1 Jan 2022 16:08:33 +0000 Subject: [PATCH 09/29] Target EventStore.Client 21.2 --- Equinox.sln | 2 +- README.md | 1 + .../Equinox.EventStoreDb.fsproj} | 6 ++-- .../EventStoreDb.fs} | 31 ++++++++++--------- 4 files changed, 21 insertions(+), 19 deletions(-) rename src/{Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj => Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj} (83%) rename src/{Equinox.EventStore.Grpc/EventStoreGrpc.fs => Equinox.EventStoreDb/EventStoreDb.fs} (97%) diff --git a/Equinox.sln b/Equinox.sln index a7d94f8aa..c72f971c6 100644 --- a/Equinox.sln +++ b/Equinox.sln @@ -91,7 +91,7 @@ EndProjectSection EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.CosmosStore.Prometheus", "src\Equinox.CosmosStore.Prometheus\Equinox.CosmosStore.Prometheus.fsproj", "{3107BBC1-2BCB-4750-AED0-42B1F4CD1909}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.EventStore.Grpc", "src\Equinox.EventStore.Grpc\Equinox.EventStore.Grpc.fsproj", "{C828E360-6FE8-47F9-9415-0A21869D7A93}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.EventStoreDb", "src\Equinox.EventStoreDb\Equinox.EventStoreDb.fsproj", "{C828E360-6FE8-47F9-9415-0A21869D7A93}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/README.md b/README.md index 44e49d6fb..9f2552135 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ The components within this repository are delivered as multi-targeted Nuget pack - `Equinox.CosmosStore` [![CosmosStore NuGet](https://img.shields.io/nuget/v/Equinox.CosmosStore.svg)](https://www.nuget.org/packages/Equinox.CosmosStore/): Azure CosmosDB Adapter with integrated 'unfolds' feature, facilitating optimal read performance in terms of latency and RU costs, instrumented to meet Jet's production monitoring requirements. ([depends](https://www.fuget.org/packages/Equinox.CosmosStore) on `Equinox.Core`, `Microsoft.Azure.Cosmos >= 3.25`, `FsCodec`, `System.Text.Json`, `FSharp.Control.AsyncSeq >= 2.0.23`) - `Equinox.CosmosStore.Prometheus` [![CosmosStore.Prometheus NuGet](https://img.shields.io/nuget/v/Equinox.CosmosStore.Prometheus.svg)](https://www.nuget.org/packages/Equinox.CosmosStore.Prometheus/): Integration package providing a `Serilog.Core.ILogEventSink` that extracts detailed metrics information attached to the `LogEvent`s and feeds them to the `prometheus-net`'s `Prometheus.Metrics` static instance. ([depends](https://www.fuget.org/packages/Equinox.CosmosStore.Prometheus) on `Equinox.CosmosStore`, `prometheus-net >= 3.6.0`) - `Equinox.EventStore` [![EventStore NuGet](https://img.shields.io/nuget/v/Equinox.EventStore.svg)](https://www.nuget.org/packages/Equinox.EventStore/): [EventStoreDB](https://eventstore.org/) Adapter designed to meet Jet's production monitoring requirements. ([depends](https://www.fuget.org/packages/Equinox.EventStore) on `Equinox.Core`, `EventStore.Client >= 22.0.0-preview`, `FSharp.Control.AsyncSeq >= 2.0.23`), EventStore Server version `21.10` or later) +- `Equinox.EventStoreDb` [![EventStoreDb NuGet](https://img.shields.io/nuget/v/Equinox.EventStoreDb.svg)](https://www.nuget.org/packages/Equinox.EventStoreDb/): Production-strength [EventStoreDB](https://eventstore.org/) Adapter. ([depends](https://www.fuget.org/packages/Equinox.EventStoreDb) on `Equinox.Core`, `EventStore.Client.Grpc.Streams` >= `21.2.0`, `FSharp.Control.AsyncSeq` v `2.0.23`, EventStore Server version `21.10` or later) - `Equinox.SqlStreamStore` [![SqlStreamStore NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore/): [SqlStreamStore](https://github.com/SQLStreamStore/SQLStreamStore) Adapter derived from `Equinox.EventStore` - provides core facilities (but does not connect to a specific database; see sibling `SqlStreamStore`.* packages). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore) on `Equinox.Core`, `FsCodec`, `SqlStreamStore >= 1.2.0-beta.8`, `FSharp.Control.AsyncSeq`) - `Equinox.SqlStreamStore.MsSql` [![MsSql NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.MsSql.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore.MsSql/): [SqlStreamStore.MsSql](https://sqlstreamstore.readthedocs.io/en/latest/sqlserver) Sql Server `Connector` implementation for `Equinox.SqlStreamStore` package). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore.MsSql) on `Equinox.SqlStreamStore`, `SqlStreamStore.MsSql >= 1.2.0-beta.8`) - `Equinox.SqlStreamStore.MySql` [![MySql NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.MySql.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore.MySql/): `SqlStreamStore.MySql` MySQL `Connector` implementation for `Equinox.SqlStreamStore` package). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore.MySql) on `Equinox.SqlStreamStore`, `SqlStreamStore.MySql >= 1.2.0-beta.8`) diff --git a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj similarity index 83% rename from src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj rename to src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj index b7d33674f..514deb0f2 100644 --- a/src/Equinox.EventStore.Grpc/Equinox.EventStore.Grpc.fsproj +++ b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj @@ -10,7 +10,7 @@ - + @@ -23,8 +23,8 @@ - - + + diff --git a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs similarity index 97% rename from src/Equinox.EventStore.Grpc/EventStoreGrpc.fs rename to src/Equinox.EventStoreDb/EventStoreDb.fs index 045477fe7..8d92b646e 100755 --- a/src/Equinox.EventStore.Grpc/EventStoreGrpc.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -26,15 +26,15 @@ module Log = let propEventData name (events : EventData[]) (log : ILogger) = log |> propEvents name (seq { for x in events do - if x.IsJson then - yield System.Collections.Generic.KeyValuePair<_,_>(x.Type, System.Text.Encoding.UTF8.GetString x.Data) }) + if x.ContentType = "application/json" then + yield let d = x.Data in System.Collections.Generic.KeyValuePair<_,_>(x.Type, System.Text.Encoding.UTF8.GetString d.Span) }) let propResolvedEvents name (events : ResolvedEvent[]) (log : ILogger) = log |> propEvents name (seq { for x in events do let e = x.Event - if e.IsJson then - yield System.Collections.Generic.KeyValuePair<_,_>(e.EventType, System.Text.Encoding.UTF8.GetString e.Data) }) + if e.ContentType = "application/json" then + yield let d = e.Data in System.Collections.Generic.KeyValuePair<_,_>(e.EventType, System.Text.Encoding.UTF8.GetString d.Span) }) open Serilog.Events @@ -53,7 +53,7 @@ module Log = f log retryPolicy withLoggingContextWrapping - let (|BlobLen|) = function null -> 0 | (x : byte[]) -> x.Length + let (|BlobLen|) (x : ReadOnlyMemory) = x.Length /// NB Caveat emptor; this is subject to unlimited change without the major version changing - while the `dotnet-templates` repo will be kept in step, and /// the ChangeLog will mention changes, it's critical to not assume that the presence or nature of these helpers be considered stable @@ -130,14 +130,14 @@ module Log = for uom, f in measures do let d = f duration in if d <> 0. then logPeriodicRate uom (float totalCount/d |> int64) [] -type EsSyncResult = Written of EventStore.Client.WriteResult | Conflict of actualVersion: int64 +type EsSyncResult = Written of EventStore.Client.ConditionalWriteResult | Conflict of actualVersion: int64 module private Write = /// Yields `EsSyncResult.Written` or `EsSyncResult.Conflict` to signify WrongExpectedVersion let private writeEventsAsync (log : ILogger) (conn : EventStoreClient) (streamName : string) (version : int64) (events : EventData[]) : Async = async { try let! ct = Async.CancellationToken - let! wr = conn.AppendToStreamAsync(streamName, StreamRevision.FromInt64 version, events, cancellationToken=ct) |> Async.AwaitTaskCorrect + let! wr = conn.ConditionalAppendToStreamAsync(streamName, StreamRevision.FromInt64 version, events, cancellationToken=ct) |> Async.AwaitTaskCorrect return EsSyncResult.Written wr with :? EventStore.Client.WrongExpectedVersionException as ex -> log.Information(ex, "Ges TrySync WrongExpectedVersionException writing {EventTypes}, actual {ActualVersion}", @@ -174,13 +174,13 @@ module private Write = module private Read = open FSharp.Control - let private readAsyncEnum (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamRevision) + let private readAsyncEnum (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) : Async> = async { let! ct = Async.CancellationToken - return conn.ReadStreamAsync(direction, streamName, startPos, uint64 batchSize, resolveLinkTos = false, cancellationToken = ct) + return conn.ReadStreamAsync(direction, streamName, startPos, int64 batchSize, resolveLinkTos = false, cancellationToken = ct) |> AsyncSeq.ofAsyncEnum } - let readAsyncEnumVer (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamRevision) : Async = async { + let readAsyncEnumVer (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) : Async = async { try let! res = readAsyncEnum conn streamName direction batchSize startPos let! events = res |> AsyncSeq.toArrayAsync let version = @@ -249,7 +249,7 @@ module private Read = let call pos = loggedReadAsyncEnumVer conn streamName Direction.Backwards batchSize pos let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) let log = log |> Log.prop "batchSize" batchSize |> Log.prop "stream" streamName - let startPosition = StreamRevision.End + let startPosition = StreamPosition.End let direction = Direction.Backwards let readLog = log |> Log.prop "direction" direction let! t, (version, eventsBackward) = retryingLoggingRead startPosition readLog |> Stopwatch.Time @@ -265,12 +265,13 @@ module UnionEncoderAdapters = // TODO something like let ts = DateTimeOffset.FromUnixTimeMilliseconds(e.CreatedEpoch) // TOCONSIDER wire e.Metadata.["$correlationId"] and .["$causationId"] into correlationId and causationId // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - FsCodec.Core.TimelineEvent.Create(e.EventNumber.ToInt64(), e.EventType, e.Data, e.Metadata, correlationId = null, causationId = null, timestamp = ts) + let n, d, m = e.EventNumber, e.Data, e.Metadata + FsCodec.Core.TimelineEvent.Create(n.ToInt64(), e.EventType, d.ToArray(), m.ToArray(), correlationId = null, causationId = null, timestamp = ts) let eventDataOfEncodedEvent (x : FsCodec.IEventData) = // TOCONSIDER wire x.CorrelationId, x.CausationId into x.Meta.["$correlationId"] and .["$causationId"] // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - EventData(Uuid.NewUuid(), x.EventType, isJson = true, data = x.Data, metadata = x.Meta) + EventData(Uuid.NewUuid(), x.EventType, contentType = "application/json", data = ReadOnlyMemory(x.Data), metadata = ReadOnlyMemory(x.Meta)) type Stream = { name : string } type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } @@ -342,7 +343,7 @@ type Context(conn : Connection, batching : BatchingPolicy) = member internal __.LoadEmpty streamName = Token.ofUncompactedVersion batching.BatchSize streamName -1L member __.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { - let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName StreamRevision.Start + let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName StreamPosition.Start match tryIsResolvedEventEventType isCompactionEventType with | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events | Some isCompactionEvent -> @@ -359,7 +360,7 @@ type Context(conn : Connection, batching : BatchingPolicy) = member __.LoadFromToken useWriteConn streamName log (Token.Unpack token as streamToken) (tryDecode, isCompactionEventType) : Async = async { - let streamPosition = StreamRevision.FromInt64(token.pos.streamVersion + 1L) + let streamPosition = StreamPosition.FromInt64(token.pos.streamVersion + 1L) let connToUse = if useWriteConn then conn.WriteConnection else conn.ReadConnection let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy connToUse batching.BatchSize streamName streamPosition match isCompactionEventType with From 235965f485603c25fa23b2b858529188ea8b7001 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 25 Jan 2022 09:43:57 +0000 Subject: [PATCH 10/29] Sycn with breaking chances to source --- src/Equinox.EventStoreDb/EventStoreDb.fs | 248 ++++++++++++----------- 1 file changed, 132 insertions(+), 116 deletions(-) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 8d92b646e..57c0fab4d 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -1,6 +1,5 @@ namespace Equinox.EventStore -open Equinox open Equinox.Core open EventStore.Client open Serilog @@ -11,7 +10,7 @@ module Log = type Measurement = { stream: string; interval: StopwatchInterval; bytes: int; count: int } [] - type Event = + type Metric = | WriteSuccess of Measurement | WriteConflict of Measurement | Slice of Direction * Measurement @@ -40,9 +39,9 @@ module Log = /// Attach a property to the log context to hold the metrics // Sidestep Log.ForContext converting to a string; see https://github.com/serilog/serilog/issues/1124 - let event (value : Event) (log : ILogger) = + let event (value : Metric) (log : ILogger) = let enrich (e : LogEvent) = e.AddPropertyIfAbsent(LogEventProperty("esEvt", ScalarValue(value))) - log.ForContext({ new Serilog.Core.ILogEventEnricher with member __.Enrich(evt,_) = enrich evt }) + log.ForContext({ new Serilog.Core.ILogEventEnricher with member _.Enrich(evt, _) = enrich evt }) let withLoggedRetries<'t> retryPolicy (contextLabel : string) (f : ILogger -> Async<'t>) log : Async<'t> = match retryPolicy with @@ -63,27 +62,27 @@ module Log = let inline (|Stats|) ({ interval = i } : Measurement) = let e = i.Elapsed in int64 e.TotalMilliseconds let (|Read|Write|Resync|Rollup|) = function - | Slice (_, (Stats s)) -> Read s + | Slice (_, Stats s) -> Read s | WriteSuccess (Stats s) -> Write s | WriteConflict (Stats s) -> Resync s // slices are rolled up into batches so be sure not to double-count - | Batch (_, _, (Stats s)) -> Rollup s + | Batch (_, _, Stats s) -> Rollup s let (|SerilogScalar|_|) : LogEventPropertyValue -> obj option = function - | (:? ScalarValue as x) -> Some x.Value + | :? ScalarValue as x -> Some x.Value | _ -> None - let (|EsMetric|_|) (logEvent : LogEvent) : Event option = + let (|EsMetric|_|) (logEvent : LogEvent) : Metric option = match logEvent.Properties.TryGetValue("esEvt") with - | true, SerilogScalar (:? Event as e) -> Some e + | true, SerilogScalar (:? Metric as e) -> Some e | _ -> None type Counter = { mutable count : int64; mutable ms : int64 } static member Create() = { count = 0L; ms = 0L } - member __.Ingest(ms) = - System.Threading.Interlocked.Increment(&__.count) |> ignore - System.Threading.Interlocked.Add(&__.ms, ms) |> ignore + member x.Ingest(ms) = + System.Threading.Interlocked.Increment(&x.count) |> ignore + System.Threading.Interlocked.Add(&x.ms, ms) |> ignore type LogSink() = static let epoch = System.Diagnostics.Stopwatch.StartNew() @@ -98,7 +97,7 @@ module Log = epoch.Restart() span interface Serilog.Core.ILogEventSink with - member __.Emit logEvent = logEvent |> function + member _.Emit logEvent = logEvent |> function | EsMetric (Read stats) -> LogSink.Read.Ingest stats | EsMetric (Write stats) -> LogSink.Write.Ingest stats | EsMetric (Resync stats) -> LogSink.Resync.Ingest stats @@ -107,7 +106,7 @@ module Log = /// Relies on feeding of metrics from Log through to Stats.LogSink /// Use Stats.LogSink.Restart() to reset the start point (and stats) where relevant - let dump (log : Serilog.ILogger) = + let dump (log : ILogger) = let stats = [ "Read", Stats.LogSink.Read "Write", Stats.LogSink.Write @@ -130,7 +129,7 @@ module Log = for uom, f in measures do let d = f duration in if d <> 0. then logPeriodicRate uom (float totalCount/d |> int64) [] -type EsSyncResult = Written of EventStore.Client.ConditionalWriteResult | Conflict of actualVersion: int64 +type EsSyncResult = Written of ConditionalWriteResult | Conflict of actualVersion: int64 module private Write = /// Yields `EsSyncResult.Written` or `EsSyncResult.Conflict` to signify WrongExpectedVersion @@ -139,7 +138,7 @@ module private Write = try let! ct = Async.CancellationToken let! wr = conn.ConditionalAppendToStreamAsync(streamName, StreamRevision.FromInt64 version, events, cancellationToken=ct) |> Async.AwaitTaskCorrect return EsSyncResult.Written wr - with :? EventStore.Client.WrongExpectedVersionException as ex -> + with :? WrongExpectedVersionException as ex -> log.Information(ex, "Ges TrySync WrongExpectedVersionException writing {EventTypes}, actual {ActualVersion}", [| for x in events -> x.Type |], ex.ActualVersion) return EsSyncResult.Conflict (let v = ex.ActualVersion in v.Value) } @@ -187,8 +186,8 @@ module private Read = match events with | [||] when direction = Direction.Backwards -> -1L // When reading backwards, the startPos is End, which is not directly convertible | [||] -> startPos.ToInt64() // e.g. if reading forward from (verified existing) event 10 and there are none, the version is still 10 - | xs when direction = Direction.Backwards -> xs.[0].Event.EventNumber.ToInt64() // the events arrive backwards, so first is the 'version' - | xs -> xs.[xs.Length - 1].Event.EventNumber.ToInt64() // In normal case, the last event represents the version of the stream + | xs when direction = Direction.Backwards -> let le = xs.[0].Event.EventNumber in le.ToInt64() // the events arrive backwards, so first is the 'version' + | xs -> let le = xs.[xs.Length - 1].Event.EventNumber in le.ToInt64() // In normal case, the last event represents the version of the stream return version, events with :? StreamNotFoundException -> return -1L, [||] } @@ -210,8 +209,8 @@ module private Read = let bytes, count = resolvedEventBytes events, events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} let batches = (events.Length - 1) / batchSize + 1 - let action = match direction with Direction.Forwards -> "LoadF" | Direction.Backwards -> "LoadB" - let evt = Log.Event.Batch (direction, batches, reqMetric) + let action = if direction = Direction.Forwards then "LoadF" else "LoadB" + let evt = Log.Metric.Batch (direction, batches, reqMetric) (log |> Log.prop "bytes" bytes |> Log.event evt).Information( "Ges{action:l} stream={stream} count={count}/{batches} version={version}", action, streamName, count, batches, version) @@ -273,20 +272,20 @@ module UnionEncoderAdapters = // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata EventData(Uuid.NewUuid(), x.EventType, contentType = "application/json", data = ReadOnlyMemory(x.Data), metadata = ReadOnlyMemory(x.Meta)) -type Stream = { name : string } + type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } -type Token = { stream : Stream; pos : Position } +type Token = { pos : Position } module Token = - let private create compactionEventNumber batchCapacityLimit streamName streamVersion : StreamToken = - { value = box { - stream = { name = streamName} - pos = { streamVersion = streamVersion; compactionEventNumber = compactionEventNumber; batchCapacityLimit = batchCapacityLimit } } - version = streamVersion } + let private create compactionEventNumber batchCapacityLimit streamVersion : StreamToken = + { value = box { pos = { streamVersion = streamVersion; compactionEventNumber = compactionEventNumber; batchCapacityLimit = batchCapacityLimit } } + // In this impl, the StreamVersion matches the EventStore StreamVersion in being -1-based + // Version however is the representation that needs to align with ISyncContext.Version + version = streamVersion + 1L } /// No batching / compaction; we only need to retain the StreamVersion - let ofNonCompacting streamName streamVersion : StreamToken = - create None None streamName streamVersion + let ofNonCompacting streamVersion : StreamToken = + create None None streamVersion // headroom before compaction is necessary given the stated knowledge of the last (if known) `compactionEventNumberOption` let private batchCapacityLimit compactedEventNumberOption unstoredEventsPending (batchSize : int) (streamVersion : int64) : int = @@ -294,93 +293,91 @@ module Token = | Some (compactionEventNumber : int64) -> (batchSize - unstoredEventsPending) - int (streamVersion - compactionEventNumber + 1L) |> max 0 | None -> (batchSize - unstoredEventsPending) - (int streamVersion + 1) - 1 |> max 0 - let (*private*) ofCompactionEventNumber compactedEventNumberOption unstoredEventsPending batchSize streamName streamVersion : StreamToken = + let (*private*) ofCompactionEventNumber compactedEventNumberOption unstoredEventsPending batchSize streamVersion : StreamToken = let batchCapacityLimit = batchCapacityLimit compactedEventNumberOption unstoredEventsPending batchSize streamVersion - create compactedEventNumberOption (Some batchCapacityLimit) streamName streamVersion + create compactedEventNumberOption (Some batchCapacityLimit) streamVersion /// Assume we have not seen any compaction events; use the batchSize and version to infer headroom - let ofUncompactedVersion batchSize streamName streamVersion : StreamToken = - ofCompactionEventNumber None 0 batchSize streamName streamVersion + let ofUncompactedVersion batchSize streamVersion : StreamToken = + ofCompactionEventNumber None 0 batchSize streamVersion - let (|Unpack|) (x : StreamToken) : Token = unbox x.value + let (|Unpack|) (x : StreamToken) : Position = let t = unbox x.value in t.pos /// Use previousToken plus the data we are adding and the position we are adding it to infer a headroom let ofPreviousTokenAndEventsLength (Unpack previousToken) eventsLength batchSize streamVersion : StreamToken = - let compactedEventNumber = previousToken.pos.compactionEventNumber - ofCompactionEventNumber compactedEventNumber eventsLength batchSize previousToken.stream.name streamVersion + let compactedEventNumber = previousToken.compactionEventNumber + ofCompactionEventNumber compactedEventNumber eventsLength batchSize streamVersion /// Use an event just read from the stream to infer headroom - let ofCompactionResolvedEventAndVersion (compactionEvent: ResolvedEvent) batchSize streamName streamVersion : StreamToken = - ofCompactionEventNumber (Some (compactionEvent.Event.EventNumber.ToInt64())) 0 batchSize streamName streamVersion + let ofCompactionResolvedEventAndVersion (compactionEvent : ResolvedEvent) batchSize streamVersion : StreamToken = + let e = compactionEvent.Event.EventNumber in ofCompactionEventNumber (Some (e.ToInt64())) 0 batchSize streamVersion /// Use an event we are about to write to the stream to infer headroom let ofPreviousStreamVersionAndCompactionEventDataIndex (Unpack token) compactionEventDataIndex eventsLength batchSize streamVersion' : StreamToken = - ofCompactionEventNumber (Some (token.pos.streamVersion + 1L + int64 compactionEventDataIndex)) eventsLength batchSize token.stream.name streamVersion' - - let (|StreamPos|) (Unpack token) : Stream * Position = token.stream, token.pos + ofCompactionEventNumber (Some (token.streamVersion + 1L + int64 compactionEventDataIndex)) eventsLength batchSize streamVersion' let supersedes (Unpack current) (Unpack x) = - let currentVersion, newVersion = current.pos.streamVersion, x.pos.streamVersion + let currentVersion, newVersion = current.streamVersion, x.streamVersion newVersion > currentVersion -type Connection(readConnection, [] ?writeConnection, [] ?readRetryPolicy, [] ?writeRetryPolicy) = - member __.ReadConnection = readConnection - member __.ReadRetryPolicy = readRetryPolicy - member __.WriteConnection = defaultArg writeConnection readConnection - member __.WriteRetryPolicy = writeRetryPolicy +type EventStoreConnection(readConnection, [] ?writeConnection, [] ?readRetryPolicy, [] ?writeRetryPolicy) = + member _.ReadConnection = readConnection + member _.ReadRetryPolicy = readRetryPolicy + member _.WriteConnection = defaultArg writeConnection readConnection + member _.WriteRetryPolicy = writeRetryPolicy -type BatchingPolicy(getMaxBatchSize : unit -> int) = +type BatchingPolicy(getMaxBatchSize : unit -> int, [] ?batchCountLimit) = new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) - member __.BatchSize = getMaxBatchSize() + member _.BatchSize = getMaxBatchSize() +// member _.MaxBatches = batchCountLimit [] type GatewaySyncResult = Written of StreamToken | ConflictUnknown of StreamToken -type Context(conn : Connection, batching : BatchingPolicy) = - let isResolvedEventEventType (tryDecode, predicate) (x : ResolvedEvent) = predicate (tryDecode (x.Event.Data)) +type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = + let isResolvedEventEventType (tryDecode, predicate) (x : ResolvedEvent) = predicate (tryDecode x.Event.Data) let tryIsResolvedEventEventType predicateOption = predicateOption |> Option.map isResolvedEventEventType - member internal __.LoadEmpty streamName = Token.ofUncompactedVersion batching.BatchSize streamName -1L - - member __.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { + member _.TokenEmpty = Token.ofUncompactedVersion batching.BatchSize -1L + member _.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName StreamPosition.Start match tryIsResolvedEventEventType isCompactionEventType with - | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events + | None -> return Token.ofNonCompacting version, Array.choose tryDecode events | Some isCompactionEvent -> match events |> Array.tryFindBack isCompactionEvent with - | None -> return Token.ofUncompactedVersion batching.BatchSize streamName version, Array.choose tryDecode events - | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose tryDecode events } + | None -> return Token.ofUncompactedVersion batching.BatchSize version, Array.choose tryDecode events + | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose tryDecode events } - member __.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin) : Async = async { + member _.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin) : Async = async { let! version, events = Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName (tryDecode, isOrigin) match Array.tryHead events |> Option.filter (function _, Some e -> isOrigin e | _ -> false) with - | None -> return Token.ofUncompactedVersion batching.BatchSize streamName version, Array.choose snd events - | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose snd events } + | None -> return Token.ofUncompactedVersion batching.BatchSize version, Array.choose snd events + | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose snd events } - member __.LoadFromToken useWriteConn streamName log (Token.Unpack token as streamToken) (tryDecode, isCompactionEventType) + member _.LoadFromToken useWriteConn streamName log (Token.Unpack token as streamToken) (tryDecode, isCompactionEventType) : Async = async { - let streamPosition = StreamPosition.FromInt64(token.pos.streamVersion + 1L) + let streamPosition = StreamPosition.FromInt64(token.streamVersion + 1L) let connToUse = if useWriteConn then conn.WriteConnection else conn.ReadConnection let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy connToUse batching.BatchSize streamName streamPosition match isCompactionEventType with - | None -> return Token.ofNonCompacting streamName version, Array.choose tryDecode events + | None -> return Token.ofNonCompacting version, Array.choose tryDecode events | Some isCompactionEvent -> match events |> Array.tryFindBack (fun re -> match tryDecode re with Some e -> isCompactionEvent e | _ -> false) with | None -> return Token.ofPreviousTokenAndEventsLength streamToken events.Length batching.BatchSize version, Array.choose tryDecode events - | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize streamName version, Array.choose tryDecode events } + | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose tryDecode events } - member __.TrySync log (Token.Unpack token as streamToken) (events, encodedEvents: EventData array) (isCompactionEventType) : Async = async { - let streamVersion = token.pos.streamVersion - let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection token.stream.name streamVersion encodedEvents + member _.TrySync log streamName (Token.Unpack token as streamToken) (events, encodedEvents : EventData array) isCompactionEventType : Async = async { + let streamVersion = token.streamVersion + let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection streamName streamVersion encodedEvents match wr with | EsSyncResult.Conflict actualVersion -> - return GatewaySyncResult.ConflictUnknown (Token.ofNonCompacting token.stream.name actualVersion) + return GatewaySyncResult.ConflictUnknown (Token.ofNonCompacting actualVersion) | EsSyncResult.Written wr -> let version' = wr.NextExpectedVersion let token = match isCompactionEventType with - | None -> Token.ofNonCompacting token.stream.name version' + | None -> Token.ofNonCompacting version' | Some isCompactionEvent -> match events |> Array.ofList |> Array.tryFindIndexBack isCompactionEvent with | None -> Token.ofPreviousTokenAndEventsLength streamToken encodedEvents.Length batching.BatchSize version' @@ -388,20 +385,20 @@ type Context(conn : Connection, batching : BatchingPolicy) = Token.ofPreviousStreamVersionAndCompactionEventDataIndex streamToken compactionEventIndex encodedEvents.Length batching.BatchSize version' return GatewaySyncResult.Written token } - member __.Sync(log, streamName, streamVersion, events: FsCodec.IEventData[]) : Async = async { + member _.Sync(log, streamName, streamVersion, events : FsCodec.IEventData[]) : Async = async { let encodedEvents : EventData[] = events |> Array.map UnionEncoderAdapters.eventDataOfEncodedEvent let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection streamName streamVersion encodedEvents match wr with | EsSyncResult.Conflict actualVersion -> - return GatewaySyncResult.ConflictUnknown (Token.ofNonCompacting streamName actualVersion) + return GatewaySyncResult.ConflictUnknown (Token.ofNonCompacting actualVersion) | EsSyncResult.Written wr -> let version' = wr.NextExpectedVersion - let token = Token.ofNonCompacting streamName version' + let token = Token.ofNonCompacting version' return GatewaySyncResult.Written token } [] -type AccessStrategy<'event,'state> = - /// Load only the single most recent event defined in 'event` and trust that doing a fold from any such event +type AccessStrategy<'event, 'state> = + /// Load only the single most recent event defined in 'event and trust that doing a fold from any such event /// will yield a correct and complete state /// In other words, the fold function should not need to consider either the preceding 'state or 'events. | LatestKnownEvent @@ -413,9 +410,9 @@ type AccessStrategy<'event,'state> = type private CompactionContext(eventsLen : int, capacityBeforeCompaction : int) = /// Determines whether writing a Compaction event is warranted (based on the existing state and the current accumulated changes) - member __.IsCompactionDue = eventsLen > capacityBeforeCompaction + member _.IsCompactionDue = eventsLen > capacityBeforeCompaction -type private Category<'event, 'state, 'context>(context : Context, codec : FsCodec.IEventCodec<_, _, 'context>, ?access : AccessStrategy<'event, 'state>) = +type private Category<'event, 'state, 'context>(context : EventStoreContext, codec : FsCodec.IEventCodec<_, _, 'context>, ?access : AccessStrategy<'event, 'state>) = let tryDecode (e : ResolvedEvent) = e |> UnionEncoderAdapters.encodedEventOfResolvedEvent |> codec.TryDecode let compactionPredicate = @@ -430,7 +427,7 @@ type private Category<'event, 'state, 'context>(context : Context, codec : FsCod | Some (AccessStrategy.RollingSnapshots (isValid, _)) -> isValid let loadAlgorithm load streamName initial log = - let batched = load initial (context.LoadBatched streamName log (tryDecode,None)) + let batched = load initial (context.LoadBatched streamName log (tryDecode, None)) let compacted = load initial (context.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin)) match access with | None -> batched @@ -441,34 +438,34 @@ type private Category<'event, 'state, 'context>(context : Context, codec : FsCod let! token, events = f return token, fold initial events } - member __.Load (fold : 'state -> 'event seq -> 'state) (initial : 'state) (streamName : string) (log : ILogger) : Async = + member _.Load (fold : 'state -> 'event seq -> 'state) (initial : 'state) (streamName : string) (log : ILogger) : Async = loadAlgorithm (load fold) streamName initial log - member __.LoadFromToken (fold : 'state -> 'event seq -> 'state) (state : 'state) (streamName : string) token (log : ILogger) : Async = + member _.LoadFromToken (fold : 'state -> 'event seq -> 'state) (state : 'state) (streamName : string) token (log : ILogger) : Async = (load fold) state (context.LoadFromToken false streamName log token (tryDecode, compactionPredicate)) - member __.TrySync<'context> - ( log : ILogger, fold: 'state -> 'event seq -> 'state, - (Token.StreamPos (stream, pos) as streamToken), state : 'state, events : 'event list, ctx : 'context option) : Async> = async { + member _.TrySync<'context> + ( log : ILogger, fold : 'state -> 'event seq -> 'state, + streamName, (Token.Unpack token as streamToken), state : 'state, events : 'event list, ctx : 'context option) : Async> = async { let encode e = codec.Encode(ctx, e) let events = match access with | None | Some AccessStrategy.LatestKnownEvent -> events | Some (AccessStrategy.RollingSnapshots (_, compact)) -> - let cc = CompactionContext(List.length events, pos.batchCapacityLimit.Value) + let cc = CompactionContext(List.length events, token.batchCapacityLimit.Value) if cc.IsCompactionDue then events @ [fold state events |> compact] else events let encodedEvents : EventData[] = events |> Seq.map (encode >> UnionEncoderAdapters.eventDataOfEncodedEvent) |> Array.ofSeq - let! syncRes = context.TrySync log streamToken (events, encodedEvents) compactionPredicate + let! syncRes = context.TrySync log streamName streamToken (events, encodedEvents) compactionPredicate match syncRes with | GatewaySyncResult.ConflictUnknown _ -> - return SyncResult.Conflict (load fold state (context.LoadFromToken true stream.name log streamToken (tryDecode, compactionPredicate))) + return SyncResult.Conflict (load fold state (context.LoadFromToken true streamName log streamToken (tryDecode, compactionPredicate))) | GatewaySyncResult.Written token' -> return SyncResult.Written (token', fold state (Seq.ofList events)) } module Caching = /// Forwards all state changes in all streams of an ICategory to a `tee` function - type CategoryTee<'event, 'state, 'context>(inner: ICategory<'event, 'state, string, 'context>, tee : string -> StreamToken * 'state -> Async) = + type CategoryTee<'event, 'state, 'context>(inner : ICategory<'event, 'state, string, 'context>, tee : string -> StreamToken * 'state -> Async) = let intercept streamName tokenAndState = async { let! _ = tee streamName tokenAndState return tokenAndState } @@ -478,15 +475,15 @@ module Caching = return! intercept streamName tokenAndState } interface ICategory<'event, 'state, string, 'context> with - member __.Load(log, streamName : string, opt) : Async = + member _.Load(log, streamName : string, opt) : Async = loadAndIntercept (inner.Load(log, streamName, opt)) streamName - member __.TrySync(log : ILogger, (Token.StreamPos (stream,_) as token), state, events : 'event list, context) : Async> = async { - let! syncRes = inner.TrySync(log, token, state, events, context) + member _.TrySync(log : ILogger, stream, token, state, events : 'event list, context) : Async> = async { + let! syncRes = inner.TrySync(log, stream, token, state, events, context) match syncRes with - | SyncResult.Conflict resync -> return SyncResult.Conflict (loadAndIntercept resync stream.name) + | SyncResult.Conflict resync -> return SyncResult.Conflict (loadAndIntercept resync stream) | SyncResult.Written (token', state') -> - let! intercepted = intercept stream.name (token', state') + let! intercepted = intercept stream (token', state') return SyncResult.Written intercepted } let applyCacheUpdatesWithSlidingExpiration @@ -500,32 +497,56 @@ module Caching = let addOrUpdateSlidingExpirationCacheEntry streamName value = cache.UpdateIfNewer(prefix + streamName, options, mkCacheEntry value) CategoryTee<'event, 'state, 'context>(category, addOrUpdateSlidingExpirationCacheEntry) :> _ -type private Folder<'event, 'state, 'context>(category : Category<'event, 'state, 'context>, fold: 'state -> 'event seq -> 'state, initial: 'state, ?readCache) = + let applyCacheUpdatesWithFixedTimeSpan + (cache : ICache) + (prefix : string) + (period : TimeSpan) + (category : ICategory<'event, 'state, string, 'context>) + : ICategory<'event, 'state, string, 'context> = + let mkCacheEntry (initialToken : StreamToken, initialState : 'state) = CacheEntry<'state>(initialToken, initialState, Token.supersedes) + let addOrUpdateFixedLifetimeCacheEntry streamName value = + let expirationPoint = let creationDate = DateTimeOffset.UtcNow in creationDate.Add period + let options = CacheItemOptions.AbsoluteExpiration expirationPoint + cache.UpdateIfNewer(prefix + streamName, options, mkCacheEntry value) + CategoryTee<'event, 'state, 'context>(category, addOrUpdateFixedLifetimeCacheEntry) :> _ + +type private Folder<'event, 'state, 'context>(category : Category<'event, 'state, 'context>, fold : 'state -> 'event seq -> 'state, initial : 'state, ?readCache) = let batched log streamName = category.Load fold initial streamName log interface ICategory<'event, 'state, string, 'context> with - member __.Load(log, streamName, opt) : Async = + member _.Load(log, streamName, allowStale) : Async = match readCache with | None -> batched log streamName | Some (cache : ICache, prefix : string) -> async { match! cache.TryGet(prefix + streamName) with | None -> return! batched log streamName - | Some tokenAndState when opt = Some AllowStale -> return tokenAndState + | Some tokenAndState when allowStale -> return tokenAndState | Some (token, state) -> return! category.LoadFromToken fold state streamName token log } - member __.TrySync(log : ILogger, token, initialState, events : 'event list, context) : Async> = async { - let! syncRes = category.TrySync(log, fold, token, initialState, events, context) + member _.TrySync(log : ILogger, streamName, token, initialState, events : 'event list, context) : Async> = async { + let! syncRes = category.TrySync(log, fold, streamName, token, initialState, events, context) match syncRes with | SyncResult.Conflict resync -> return SyncResult.Conflict resync - | SyncResult.Written (token',state') -> return SyncResult.Written (token',state') } + | SyncResult.Written (token', state') -> return SyncResult.Written (token', state') } +/// For EventStoreDB, caching is less critical than it is for e.g. CosmosDB +/// As such, it can often be omitted, particularly if streams are short or there are snapshots being maintained [] type CachingStrategy = + /// Retain a single 'state per streamName. + /// Each cache hit for a stream renews the retention period for the defined window. + /// Upon expiration of the defined window from the point at which the cache was entry was last used, a full reload is triggered. + /// Unless LoadOption.AllowStale is used, each cache hit still incurs a roundtrip to load any subsequently-added events. | SlidingWindow of ICache * window : TimeSpan - /// Prefix is used to segregate multiple folds per stream when they are stored in the cache + /// Retain a single 'state per streamName. + /// Upon expiration of the defined period, a full reload is triggered. + /// Unless LoadOption.AllowStale is used, each cache hit still incurs a roundtrip to load any subsequently-added events. + | FixedTimeSpan of ICache * period : TimeSpan + /// Prefix is used to segregate multiple folds per stream when they are stored in the cache. + /// Semantics are identical to SlidingWindow. | SlidingWindowPrefixed of ICache * window : TimeSpan * prefix : string -type Resolver<'event, 'state, 'context> - ( context : Context, codec : FsCodec.IEventCodec<_, _, 'context>, fold, initial, +type EventStoreCategory<'event, 'state, 'context> + ( context : EventStoreContext, codec : FsCodec.IEventCodec<_, _, 'context>, fold, initial, /// Caching can be overkill for EventStore esp considering the degree to which its intrinsic caching is a first class feature /// e.g., A key benefit is that reads of streams more than a few pages long get completed in constant time after the initial load [] ?caching, @@ -542,30 +563,25 @@ type Resolver<'event, 'state, 'context> let readCacheOption = match caching with | None -> None - | Some (CachingStrategy.SlidingWindow(cache, _)) -> Some(cache, null) - | Some (CachingStrategy.SlidingWindowPrefixed(cache, _, prefix)) -> Some(cache, prefix) + | Some (CachingStrategy.SlidingWindow (cache, _)) + | Some (CachingStrategy.FixedTimeSpan (cache, _)) -> Some (cache, null) + | Some (CachingStrategy.SlidingWindowPrefixed (cache, _, prefix)) -> Some (cache, prefix) let folder = Folder<'event, 'state, 'context>(inner, fold, initial, ?readCache = readCacheOption) let category : ICategory<_, _, _, 'context> = match caching with | None -> folder :> _ - | Some (CachingStrategy.SlidingWindow(cache, window)) -> + | Some (CachingStrategy.SlidingWindow (cache, window)) -> Caching.applyCacheUpdatesWithSlidingExpiration cache null window folder - | Some (CachingStrategy.SlidingWindowPrefixed(cache, window, prefix)) -> + | Some (CachingStrategy.FixedTimeSpan (cache, period)) -> + Caching.applyCacheUpdatesWithFixedTimeSpan cache null period folder + | Some (CachingStrategy.SlidingWindowPrefixed (cache, window, prefix)) -> Caching.applyCacheUpdatesWithSlidingExpiration cache prefix window folder - - let resolveStream = Stream.create category - let loadEmpty sn = context.LoadEmpty sn, initial - - member __.Resolve(streamName : FsCodec.StreamName, [] ?option, [] ?context) = - match FsCodec.StreamName.toString streamName, option with - | sn, (None|Some AllowStale) -> resolveStream sn option context - | sn, Some AssumeEmpty -> Stream.ofMemento (loadEmpty sn) (resolveStream sn option context) - - /// Resolve from a Memento being used in a Continuation [based on position and state typically from Stream.CreateMemento] - member __.FromMemento(Token.Unpack token as streamToken, state, ?context) = - Stream.ofMemento (streamToken, state) (resolveStream token.stream.name context None) + let resolve streamName = category, FsCodec.StreamName.toString streamName, None + let empty = context.TokenEmpty, initial + let storeCategory = StoreCategory(resolve, empty) + member _.Resolve(streamName : FsCodec.StreamName, [] ?context) = storeCategory.Resolve(streamName, ?context = context) #if PREVIEW3 // - there's no logging implemented in V3 as yet, so disabling for now type private SerilogAdapter(log : ILogger) = From 26508ee9abc2965bc98e876c9cc463154d470ded Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Feb 2022 10:40:32 +0000 Subject: [PATCH 11/29] Complete EventStoreDb impl --- src/Equinox.EventStoreDb/EventStoreDb.fs | 53 +++++++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 57c0fab4d..ee808a12e 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -583,7 +583,15 @@ type EventStoreCategory<'event, 'state, 'context> let storeCategory = StoreCategory(resolve, empty) member _.Resolve(streamName : FsCodec.StreamName, [] ?context) = storeCategory.Resolve(streamName, ?context = context) -#if PREVIEW3 // - there's no logging implemented in V3 as yet, so disabling for now +// see https://github.com/EventStore/EventStore/issues/1652 +[] +type ConnectionStrategy = + /// Pair of master and slave connections, writes direct, often can't read writes, resync without backoff (kind to master, writes+resyncs optimal) + | ClusterTwinPreferSlaveReads + /// Single connection, with resync backoffs appropriate to the NodePreference + | ClusterSingle of NodePreference + +#if false type private SerilogAdapter(log : ILogger) = interface EventStore.ClientAPI.ILogger with member __.Debug(format : string, args : obj []) = log.Debug(format, args) @@ -605,7 +613,6 @@ type Logger = | SerilogNormal logger -> b.UseCustomLogger(SerilogAdapter(logger)) | CustomVerbose logger -> b.EnableVerboseLogging().UseCustomLogger(logger) | CustomNormal logger -> b.UseCustomLogger(logger) -#endif [] type NodePreference = @@ -730,3 +737,45 @@ type Connector let! slave = __.Connect(name + "-TwinR", discovery, NodePreference.PreferSlave) let! master = masterInParallel return Connection(readConnection = slave, writeConnection = master, ?readRetryPolicy = readRetryPolicy, ?writeRetryPolicy = writeRetryPolicy) } +#endif + +[] +type Discovery = + // Allow Uri-based connection definition (esdb://, etc) + | Uri of Uri + +type EventStoreConnector + ( reqTimeout : TimeSpan, reqRetries : int, + ?readRetryPolicy, ?writeRetryPolicy, ?tags, + ?customize : EventStoreClientSettings -> unit) = + + member _.Connect + ( /// Name should be sufficient to uniquely identify this connection within a single app instance's logs + name, discovery : Discovery, ?clusterNodePreference) : EventStoreClient = + let settings = match discovery with Discovery.Uri uri -> EventStoreClientSettings.Create(string uri) + if name = null then nullArg "name" + let name = String.concat ";" <| seq { + name + string clusterNodePreference + match tags with None -> () | Some tags -> for key, value in tags do sprintf "%s=%s" key value } + let sanitizedName = name.Replace('\'','_').Replace(':','_') // ES internally uses `:` and `'` as separators in log messages and ... people regex logs + settings.ConnectionName <- sanitizedName + match clusterNodePreference with None -> () | Some np -> settings.ConnectivitySettings.NodePreference <- np + match customize with None -> () | Some f -> f settings + settings.OperationOptions.TimeoutAfter <- reqTimeout + // TODO implement reqRetries + new EventStoreClient(settings) + + /// Yields a Connection (which may internally be twin connections) configured per the specified strategy + member x.Establish + ( /// Name should be sufficient to uniquely identify this (aggregate) connection within a single app instance's logs + name, + discovery : Discovery, strategy : ConnectionStrategy) : EventStoreConnection = + match strategy with + | ConnectionStrategy.ClusterSingle nodePreference -> + let client = x.Connect(name, discovery, nodePreference) + EventStoreConnection(client, ?readRetryPolicy = readRetryPolicy, ?writeRetryPolicy = writeRetryPolicy) + | ConnectionStrategy.ClusterTwinPreferSlaveReads -> + let leader = x.Connect(name + "-TwinW", discovery, NodePreference.Leader) + let follower = x.Connect(name + "-TwinR", discovery, NodePreference.Follower) + EventStoreConnection(readConnection = follower, writeConnection = leader, ?readRetryPolicy = readRetryPolicy, ?writeRetryPolicy = writeRetryPolicy) From af7712ad8fc55291e49c1309612ed6e967fd0a6e Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Feb 2022 11:20:00 +0000 Subject: [PATCH 12/29] Add Equinox.EventStoreDb.Integration --- Equinox.sln | 6 +++ diagrams/EventStore.puml | 4 +- diagrams/container.puml | 4 +- samples/Tutorial/AsAt.fsx | 2 +- src/Equinox.EventStoreDb/EventStoreDb.fs | 45 +++++++++++-------- .../Equinox.EventStore.Integration.fsproj | 1 + .../EventStoreTokenTests.fs | 4 ++ .../Infrastructure.fs | 4 ++ .../StoreIntegration.fs | 15 ++++++- .../Equinox.EventStoreDb.Integration.fsproj | 36 +++++++++++++++ 10 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj diff --git a/Equinox.sln b/Equinox.sln index c72f971c6..2a91415bf 100644 --- a/Equinox.sln +++ b/Equinox.sln @@ -93,6 +93,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.CosmosStore.Prometh EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.EventStoreDb", "src\Equinox.EventStoreDb\Equinox.EventStoreDb.fsproj", "{C828E360-6FE8-47F9-9415-0A21869D7A93}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.EventStoreDb.Integration", "tests\Equinox.EventStoreDb.Integration\Equinox.EventStoreDb.Integration.fsproj", "{BA63048B-3CA3-448D-A4CD-0C772D57B6F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -203,6 +205,10 @@ Global {C828E360-6FE8-47F9-9415-0A21869D7A93}.Debug|Any CPU.Build.0 = Debug|Any CPU {C828E360-6FE8-47F9-9415-0A21869D7A93}.Release|Any CPU.ActiveCfg = Release|Any CPU {C828E360-6FE8-47F9-9415-0A21869D7A93}.Release|Any CPU.Build.0 = Release|Any CPU + {BA63048B-3CA3-448D-A4CD-0C772D57B6F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA63048B-3CA3-448D-A4CD-0C772D57B6F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA63048B-3CA3-448D-A4CD-0C772D57B6F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA63048B-3CA3-448D-A4CD-0C772D57B6F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/diagrams/EventStore.puml b/diagrams/EventStore.puml index 8583064de..ae743c61e 100644 --- a/diagrams/EventStore.puml +++ b/diagrams/EventStore.puml @@ -21,9 +21,9 @@ rectangle "Application Consistent Processing" <> { interface IStream <> } -rectangle "Equinox.EventStore" <> { +rectangle "Equinox.EventStoreDb" <> { rectangle eqxes <> [ - Equinox.EventStore OR + Equinox.EventStoreDb OR Equinox.SqlStreamStore ] database esstore <> [ diff --git a/diagrams/container.puml b/diagrams/container.puml index b622b184e..761d3c4b6 100644 --- a/diagrams/container.puml +++ b/diagrams/container.puml @@ -28,9 +28,9 @@ frame "Consistent Event Stores" as stores <> { rectangle "Azure.Cosmos" <> as cc } frame "EventStore" <> { - rectangle "Equinox.EventStore" <> as es + rectangle "Equinox.EventStoreDb" <> as es rectangle "Propulsion.EventStore" <> as er - rectangle "EventStore.ClientAPI" <> as esc + rectangle "EventStore.Client.Grpc.Streams" <> as esc } frame "Integration Test Support" <> { rectangle "Equinox.MemoryStore" <> as ms diff --git a/samples/Tutorial/AsAt.fsx b/samples/Tutorial/AsAt.fsx index d43e180ff..4879a4e95 100644 --- a/samples/Tutorial/AsAt.fsx +++ b/samples/Tutorial/AsAt.fsx @@ -41,7 +41,7 @@ #r "nuget:Serilog.Sinks.Console" #r "nuget:Serilog.Sinks.Seq" #r "nuget:Equinox.CosmosStore, *-*" -#r "nuget:Equinox.EventStore, *-*" +#r "nuget:Equinox.EventStoreDb, *-*" #r "nuget:FsCodec.SystemTextJson, *-*" #endif open System diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index ee808a12e..b2b32baa0 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -1,11 +1,23 @@ -namespace Equinox.EventStore +namespace Equinox.EventStoreDb open Equinox.Core open EventStore.Client open Serilog open System +[] +type Direction = Forward | Backward with + override this.ToString() = match this with Forward -> "Forward" | Backward -> "Backward" + member x.op_Implicit = + match x with + | Forward -> EventStore.Client.Direction.Forwards + | Backward -> EventStore.Client.Direction.Backwards + module Log = + + /// Name of Property used for Event in LogEvents. + let [] PropertyTag = "esdbEvt" + [] type Measurement = { stream: string; interval: StopwatchInterval; bytes: int; count: int } @@ -13,7 +25,6 @@ module Log = type Metric = | WriteSuccess of Measurement | WriteConflict of Measurement - | Slice of Direction * Measurement | Batch of Direction * slices: int * Measurement let prop name value (log : ILogger) = log.ForContext(name, value) @@ -61,12 +72,11 @@ module Log = module Stats = let inline (|Stats|) ({ interval = i } : Measurement) = let e = i.Elapsed in int64 e.TotalMilliseconds - let (|Read|Write|Resync|Rollup|) = function - | Slice (_, Stats s) -> Read s + let (|Read|Write|Resync|) = function | WriteSuccess (Stats s) -> Write s | WriteConflict (Stats s) -> Resync s - // slices are rolled up into batches so be sure not to double-count - | Batch (_, _, Stats s) -> Rollup s + // No slices, so no rolling up + | Batch (_, _, Stats s) -> Read s let (|SerilogScalar|_|) : LogEventPropertyValue -> obj option = function | :? ScalarValue as x -> Some x.Value @@ -101,7 +111,6 @@ module Log = | EsMetric (Read stats) -> LogSink.Read.Ingest stats | EsMetric (Write stats) -> LogSink.Write.Ingest stats | EsMetric (Resync stats) -> LogSink.Resync.Ingest stats - | EsMetric (Rollup _) -> () | _ -> () /// Relies on feeding of metrics from Log through to Stats.LogSink @@ -176,7 +185,7 @@ module private Read = let private readAsyncEnum (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) : Async> = async { let! ct = Async.CancellationToken - return conn.ReadStreamAsync(direction, streamName, startPos, int64 batchSize, resolveLinkTos = false, cancellationToken = ct) + return conn.ReadStreamAsync(direction.op_Implicit, streamName, startPos, int64 batchSize, resolveLinkTos = false, cancellationToken = ct) |> AsyncSeq.ofAsyncEnum } let readAsyncEnumVer (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) : Async = async { @@ -184,9 +193,9 @@ module private Read = let! events = res |> AsyncSeq.toArrayAsync let version = match events with - | [||] when direction = Direction.Backwards -> -1L // When reading backwards, the startPos is End, which is not directly convertible + | [||] when direction = Direction.Backward -> -1L // When reading backwards, the startPos is End, which is not directly convertible | [||] -> startPos.ToInt64() // e.g. if reading forward from (verified existing) event 10 and there are none, the version is still 10 - | xs when direction = Direction.Backwards -> let le = xs.[0].Event.EventNumber in le.ToInt64() // the events arrive backwards, so first is the 'version' + | xs when direction = Direction.Backward -> let le = xs.[0].Event.EventNumber in le.ToInt64() // the events arrive backwards, so first is the 'version' | xs -> let le = xs.[xs.Length - 1].Event.EventNumber in le.ToInt64() // In normal case, the last event represents the version of the stream return version, events with :? StreamNotFoundException -> return -1L, [||] } @@ -197,9 +206,9 @@ module private Read = let! t, (version, events) = readAsyncEnumVer conn streamName direction batchSize startPos |> Stopwatch.Time let bytes, count = events |> Array.sumBy (|ResolvedEventLen|), events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} - let evt = Log.Slice (direction, reqMetric) +// let evt = Log.Slice (direction, reqMetric) let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" events - (log |> Log.prop "startPos" startPos |> Log.prop "bytes" bytes |> Log.event evt).Information("Ges{action:l} count={count} version={version}", + (log |> Log.prop "startPos" startPos |> Log.prop "bytes" bytes).Information("Ges{action:l} count={count} version={version}", "Read", count, version) return version, events } @@ -209,7 +218,7 @@ module private Read = let bytes, count = resolvedEventBytes events, events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} let batches = (events.Length - 1) / batchSize + 1 - let action = if direction = Direction.Forwards then "LoadF" else "LoadB" + let action = if direction = Direction.Forward then "LoadF" else "LoadB" let evt = Log.Metric.Batch (direction, batches, reqMetric) (log |> Log.prop "bytes" bytes |> Log.event evt).Information( "Ges{action:l} stream={stream} count={count}/{batches} version={version}", @@ -217,9 +226,9 @@ module private Read = let loadForwardsFrom (log : ILogger) retryPolicy conn batchSize streamName startPosition : Async = async { - let call pos = loggedReadAsyncEnumVer conn streamName Direction.Forwards batchSize pos + let call pos = loggedReadAsyncEnumVer conn streamName Direction.Forward batchSize pos let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) - let direction = Direction.Forwards + let direction = Direction.Forward let log = log |> Log.prop "batchSize" batchSize |> Log.prop "direction" direction |> Log.prop "stream" streamName let! t, (version, events) = retryingLoggingRead startPosition log |> Stopwatch.Time log |> logBatchRead direction streamName t events batchSize version @@ -245,11 +254,11 @@ module private Read = |> Seq.toArray let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy conn batchSize streamName (tryDecode, isOrigin) : Async = async { - let call pos = loggedReadAsyncEnumVer conn streamName Direction.Backwards batchSize pos + let call pos = loggedReadAsyncEnumVer conn streamName Direction.Backward batchSize pos let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) let log = log |> Log.prop "batchSize" batchSize |> Log.prop "stream" streamName let startPosition = StreamPosition.End - let direction = Direction.Backwards + let direction = Direction.Backward let readLog = log |> Log.prop "direction" direction let! t, (version, eventsBackward) = retryingLoggingRead startPosition readLog |> Stopwatch.Time let events = mergeFromCompactionPointOrStartFromBackwardsStream streamName (tryDecode, isOrigin) log eventsBackward @@ -326,7 +335,7 @@ type EventStoreConnection(readConnection, [] ?writeConnection, [ int, [] ?batchCountLimit) = +type BatchingPolicy(getMaxBatchSize : unit -> int, [] ?batchCountLimit) = // TODO batchCountLimit new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) member _.BatchSize = getMaxBatchSize() // member _.MaxBatches = batchCountLimit diff --git a/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj b/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj index 07b4ee52d..f6d92fbd6 100644 --- a/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj +++ b/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj @@ -4,6 +4,7 @@ net6.0 false 5 + $(DefineConstants);STORE_EVENTSTORE_LEGACY diff --git a/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs b/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs index b75a4f47d..e74b6d4a3 100644 --- a/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs +++ b/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs @@ -1,7 +1,11 @@ module Equinox.EventStore.Tests.EventStoreTokenTests open Equinox.Core +#if STORE_EVENTSTORE_LEGACY open Equinox.EventStore +#else +open Equinox.EventStoreDb +#endif open FsCheck.Xunit open Swensen.Unquote.Assertions open Xunit diff --git a/tests/Equinox.EventStore.Integration/Infrastructure.fs b/tests/Equinox.EventStore.Integration/Infrastructure.fs index 75e33fd20..29cc3e49e 100644 --- a/tests/Equinox.EventStore.Integration/Infrastructure.fs +++ b/tests/Equinox.EventStore.Integration/Infrastructure.fs @@ -15,7 +15,11 @@ type FsCheckGenerators = #if STORE_POSTGRES || STORE_MSSQL || STORE_MYSQL open Equinox.SqlStreamStore #else +#if STORE_EVENTSTORE_LEGACY open Equinox.EventStore +#else +open Equinox.EventStoreDb +#endif #endif [] diff --git a/tests/Equinox.EventStore.Integration/StoreIntegration.fs b/tests/Equinox.EventStore.Integration/StoreIntegration.fs index b1abad481..2d2632f10 100644 --- a/tests/Equinox.EventStore.Integration/StoreIntegration.fs +++ b/tests/Equinox.EventStore.Integration/StoreIntegration.fs @@ -46,7 +46,8 @@ let connectToLocalStore (_ : ILogger) = type Context = SqlStreamStoreContext type Category<'event, 'state, 'context> = SqlStreamStoreCategory<'event, 'state, 'context> -#else // STORE_EVENTSTORE +#else +#if STORE_EVENTSTORE_LEGACY open Equinox.EventStore // NOTE: use `docker compose up` to establish the standard 3 node config at ports 1113/2113 @@ -64,6 +65,18 @@ let connectToLocalStore log = type Context = EventStoreContext type Category<'event, 'state, 'context> = EventStoreCategory<'event, 'state, 'context> +#else // STORE_EVENTSTORE_LEGACY +open Equinox.EventStoreDb + +/// Connect directly to a locally running EventStoreDB Node using gRPC, without using Gossip-driven discovery +let connectToLocalStore (_log : ILogger) = async { + let c = EventStoreConnector(reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, (*, log=Logger.SerilogVerbose log,*) tags=["I",Guid.NewGuid() |> string]) + let conn = c.Establish("Equinox-integration", Discovery.Uri(Uri "esdb://localhost:2113?tls=false"), ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) + return conn } + +type Context = EventStoreContext +type Category<'event, 'state, 'context> = EventStoreCategory<'event, 'state, 'context> +#endif #endif #endif #endif diff --git a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj new file mode 100644 index 000000000..1fa837e0f --- /dev/null +++ b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj @@ -0,0 +1,36 @@ + + + + net5.0 + false + 5 + $(DefineConstants);STORE_EVENTSTOREDB + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + From 661cda1eea407586794989c72d4317507baae5a6 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Feb 2022 11:49:26 +0000 Subject: [PATCH 13/29] Update to EventStore.Client.Grpc.Streams 22.0 --- src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj | 2 +- src/Equinox.EventStoreDb/EventStoreDb.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj index 514deb0f2..fc7711aff 100644 --- a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj +++ b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj @@ -23,7 +23,7 @@ - + diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index b2b32baa0..7c26c3618 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -771,7 +771,7 @@ type EventStoreConnector settings.ConnectionName <- sanitizedName match clusterNodePreference with None -> () | Some np -> settings.ConnectivitySettings.NodePreference <- np match customize with None -> () | Some f -> f settings - settings.OperationOptions.TimeoutAfter <- reqTimeout + settings.DefaultDeadline <- reqTimeout // TODO implement reqRetries new EventStoreClient(settings) From dd384f7d1620b6b6cff58e6fd40b76c72aeacb81 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Feb 2022 11:52:49 +0000 Subject: [PATCH 14/29] Flip samples to use .EventStoreDb --- samples/Infrastructure/Infrastructure.fsproj | 2 +- samples/Infrastructure/Services.fs | 4 +-- samples/Infrastructure/Storage.fs | 31 +++++++++---------- samples/Store/Integration/CartIntegration.fs | 6 ++-- .../ContactPreferencesIntegration.fs | 6 ++-- .../Integration/EventStoreIntegration.fs | 9 +++--- .../Store/Integration/FavoritesIntegration.fs | 4 +-- samples/Store/Integration/Integration.fsproj | 2 +- samples/Store/Integration/LogIntegration.fs | 10 +++--- samples/Tutorial/Tutorial.fsproj | 2 +- samples/Tutorial/Upload.fs | 2 +- samples/Web/Program.fs | 2 +- .../Equinox.EventStoreDb.fsproj | 4 +-- src/Equinox.EventStoreDb/EventStoreDb.fs | 20 ++++++------ .../Equinox.EventStoreDb.Integration.fsproj | 2 +- tools/Equinox.Tool/Program.fs | 8 ++--- 16 files changed, 58 insertions(+), 56 deletions(-) diff --git a/samples/Infrastructure/Infrastructure.fsproj b/samples/Infrastructure/Infrastructure.fsproj index 7fa2cb6c6..85f8101d7 100644 --- a/samples/Infrastructure/Infrastructure.fsproj +++ b/samples/Infrastructure/Infrastructure.fsproj @@ -16,7 +16,7 @@ - + diff --git a/samples/Infrastructure/Services.fs b/samples/Infrastructure/Services.fs index 5f02c077f..b0b40ae28 100644 --- a/samples/Infrastructure/Services.fs +++ b/samples/Infrastructure/Services.fs @@ -18,8 +18,8 @@ type StreamResolver(storage) = let accessStrategy = if unfolds then Equinox.CosmosStore.AccessStrategy.Snapshot snapshot else Equinox.CosmosStore.AccessStrategy.Unoptimized Equinox.CosmosStore.CosmosStoreCategory<'event,'state,_>(store, codec.ToJsonElementCodec(), fold, initial, caching, accessStrategy).Resolve | Storage.StorageConfig.Es (context, caching, unfolds) -> - let accessStrategy = if unfolds then Equinox.EventStore.AccessStrategy.RollingSnapshots snapshot |> Some else None - Equinox.EventStore.EventStoreCategory<'event,'state,_>(context, codec, fold, initial, ?caching = caching, ?access = accessStrategy).Resolve + let accessStrategy = if unfolds then Equinox.EventStoreDb.AccessStrategy.RollingSnapshots snapshot |> Some else None + Equinox.EventStoreDb.EventStoreCategory<'event,'state,_>(context, codec, fold, initial, ?caching = caching, ?access = accessStrategy).Resolve | Storage.StorageConfig.Sql (context, caching, unfolds) -> let accessStrategy = if unfolds then Equinox.SqlStreamStore.AccessStrategy.RollingSnapshots snapshot |> Some else None Equinox.SqlStreamStore.SqlStreamStoreCategory<'event,'state,_>(context, codec, fold, initial, ?caching = caching, ?access = accessStrategy).Resolve diff --git a/samples/Infrastructure/Storage.fs b/samples/Infrastructure/Storage.fs index 6e28c5bc7..5db0e26d0 100644 --- a/samples/Infrastructure/Storage.fs +++ b/samples/Infrastructure/Storage.fs @@ -9,7 +9,7 @@ type StorageConfig = // For MemoryStore, we keep the events as UTF8 arrays - we could use FsCodec.Codec.Box to remove the JSON encoding, which would improve perf but can conceal problems | Memory of Equinox.MemoryStore.VolatileStore> | Cosmos of Equinox.CosmosStore.CosmosStoreContext * Equinox.CosmosStore.CachingStrategy * unfolds: bool - | Es of Equinox.EventStore.EventStoreContext * Equinox.EventStore.CachingStrategy option * unfolds: bool + | Es of Equinox.EventStoreDb.EventStoreContext * Equinox.EventStoreDb.CachingStrategy option * unfolds: bool | Sql of Equinox.SqlStreamStore.SqlStreamStoreContext * Equinox.SqlStreamStore.CachingStrategy option * unfolds: bool module MemoryStore = @@ -132,9 +132,8 @@ module EventStore = | [] VerboseStore | [] Timeout of float | [] Retries of int - | [] Host of string - | [] Username of string - | [] Password of string + | [] ConnectionString of string + | [] Credentials of string | [] ConcurrentOperationsLimit of int | [] HeartbeatTimeout of float | [] MaxEvents of int @@ -143,17 +142,17 @@ module EventStore = | VerboseStore -> "include low level Store logging." | Timeout _ -> "specify operation timeout in seconds (default: 5)." | Retries _ -> "specify operation retries (default: 1)." - | Host _ -> "specify a DNS query, using Gossip-driven discovery against all A records returned (default: localhost)." - | Username _ -> "specify a username (default: admin)." - | Password _ -> "specify a Password (default: changeit)." + | ConnectionString _ -> "Portion of connection string that's safe to write to console or log. default: esdb://localhost:2113?tls=false" + | Credentials _ -> "specify a sensitive portion of the connection string that should not be logged. Default: none" | ConcurrentOperationsLimit _ -> "max concurrent operations in flight (default: 5000)." | HeartbeatTimeout _ -> "specify heartbeat timeout in seconds (default: 1.5)." | MaxEvents _ -> "Maximum number of Events to request per batch. Default 500." - open Equinox.EventStore + open Equinox.EventStoreDb type Info(args : ParseResults) = - member _.Host = args.GetResult(Host,"localhost") - member _.Credentials = args.GetResult(Username,"admin"), args.GetResult(Password,"changeit") + member _.Host = args.GetResult(ConnectionString,"esdb://localhost:2113?tls=false") + member _.Credentials = args.GetResult(Credentials, null) + member _.Timeout = args.GetResult(Timeout,5.) |> TimeSpan.FromSeconds member _.Retries = args.GetResult(Retries, 1) member _.HeartbeatTimeout = args.GetResult(HeartbeatTimeout,1.5) |> float |> TimeSpan.FromSeconds @@ -162,12 +161,12 @@ module EventStore = open Serilog - let private connect (log: ILogger) (dnsQuery, heartbeatTimeout, col) (username, password) (operationTimeout, operationRetries) = - EventStoreConnector(username, password, reqTimeout=operationTimeout, reqRetries=operationRetries, - heartbeatTimeout=heartbeatTimeout, concurrentOperationsLimit = col, - log=(if log.IsEnabled(Serilog.Events.LogEventLevel.Debug) then Logger.SerilogVerbose log else Logger.SerilogNormal log), + let private connect (log: ILogger) (connectionString, heartbeatTimeout, col) credentialsString (operationTimeout, operationRetries) = + EventStoreConnector(reqTimeout=operationTimeout, reqRetries=operationRetries, + // TODO heartbeatTimeout=heartbeatTimeout, concurrentOperationsLimit = col, + // TODO log=(if log.IsEnabled(Serilog.Events.LogEventLevel.Debug) then Logger.SerilogVerbose log else Logger.SerilogNormal log), tags=["M", Environment.MachineName; "I", Guid.NewGuid() |> string]) - .Establish(appName, Discovery.GossipDns dnsQuery, ConnectionStrategy.ClusterTwinPreferSlaveReads) + .Establish(appName, Discovery.Uri (String.Join(";", connectionString, credentialsString) |> Uri), ConnectionStrategy.ClusterTwinPreferSlaveReads) let private createContext connection batchSize = EventStoreContext(connection, BatchingPolicy(maxBatchSize = batchSize)) let config (log: ILogger, storeLog) (cache, unfolds) (args : ParseResults) = let a = Info(args) @@ -176,7 +175,7 @@ module EventStore = let concurrentOperationsLimit = a.ConcurrentOperationsLimit log.Information("EventStoreDB {host} heartbeat: {heartbeat}s timeout: {timeout}s concurrent reqs: {concurrency} retries {retries}", a.Host, heartbeatTimeout.TotalSeconds, timeout.TotalSeconds, concurrentOperationsLimit, retries) - let connection = connect storeLog (a.Host, heartbeatTimeout, concurrentOperationsLimit) a.Credentials operationThrottling |> Async.RunSynchronously + let connection = connect storeLog (a.Host, heartbeatTimeout, concurrentOperationsLimit) a.Credentials operationThrottling let cacheStrategy = cache |> Option.map (fun c -> CachingStrategy.SlidingWindow (c, TimeSpan.FromMinutes 20.)) StorageConfig.Es ((createContext connection a.MaxEvents), cacheStrategy, unfolds) diff --git a/samples/Store/Integration/CartIntegration.fs b/samples/Store/Integration/CartIntegration.fs index e81b9bb1f..53236d7db 100644 --- a/samples/Store/Integration/CartIntegration.fs +++ b/samples/Store/Integration/CartIntegration.fs @@ -17,9 +17,9 @@ let codec = Cart.Events.codec let codecJe = Cart.Events.codecJe let resolveGesStreamWithRollingSnapshots context = - EventStore.EventStoreCategory(context, codec, fold, initial, access = EventStore.AccessStrategy.RollingSnapshots snapshot).Resolve + EventStoreDb.EventStoreCategory(context, codec, fold, initial, access = EventStoreDb.AccessStrategy.RollingSnapshots snapshot).Resolve let resolveGesStreamWithoutCustomAccessStrategy context = - EventStore.EventStoreCategory(context, codec, fold, initial).Resolve + EventStoreDb.EventStoreCategory(context, codec, fold, initial).Resolve let resolveCosmosStreamWithSnapshotStrategy context = CosmosStore.CosmosStoreCategory(context, codecJe, fold, initial, CosmosStore.CachingStrategy.NoCaching, CosmosStore.AccessStrategy.Snapshot snapshot).Resolve @@ -52,7 +52,7 @@ type Tests(testOutputHelper) = } let arrangeEs connect choose resolveStream = async { - let! client = connect log + let client = connect log let context = choose client defaultBatchSize return Cart.create log (resolveStream context) } diff --git a/samples/Store/Integration/ContactPreferencesIntegration.fs b/samples/Store/Integration/ContactPreferencesIntegration.fs index 6a1ae27ea..31c1c5762 100644 --- a/samples/Store/Integration/ContactPreferencesIntegration.fs +++ b/samples/Store/Integration/ContactPreferencesIntegration.fs @@ -14,9 +14,9 @@ let createServiceMemory log store = let codec = ContactPreferences.Events.codec let codecJe = ContactPreferences.Events.codecJe let resolveStreamGesWithOptimizedStorageSemantics context = - EventStore.EventStoreCategory(context 1, codec, fold, initial, access = EventStore.AccessStrategy.LatestKnownEvent).Resolve + EventStoreDb.EventStoreCategory(context 1, codec, fold, initial, access = EventStoreDb.AccessStrategy.LatestKnownEvent).Resolve let resolveStreamGesWithoutAccessStrategy context = - EventStore.EventStoreCategory(context defaultBatchSize, codec, fold, initial).Resolve + EventStoreDb.EventStoreCategory(context defaultBatchSize, codec, fold, initial).Resolve let resolveStreamCosmosWithLatestKnownEventSemantics context = CosmosStore.CosmosStoreCategory(context, codecJe, fold, initial, CosmosStore.CachingStrategy.NoCaching, CosmosStore.AccessStrategy.LatestKnownEvent).Resolve @@ -44,7 +44,7 @@ type Tests(testOutputHelper) = } let arrangeEs connect choose resolveStream = async { - let! client = connect log + let client = connect log let context = choose client return ContactPreferences.create log (resolveStream context) } diff --git a/samples/Store/Integration/EventStoreIntegration.fs b/samples/Store/Integration/EventStoreIntegration.fs index 04554792c..27263e9cd 100644 --- a/samples/Store/Integration/EventStoreIntegration.fs +++ b/samples/Store/Integration/EventStoreIntegration.fs @@ -1,14 +1,15 @@ [] module Samples.Store.Integration.EventStoreIntegration -open Equinox.EventStore +open Equinox.EventStoreDb +open EventStore.Client open System let connectToLocalEventStoreNode log = // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER - EventStoreConnector("admin", "changeit", custom = (fun c -> c.DisableServerCertificateValidation()), - reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, log=Logger.SerilogVerbose log, tags=["I",Guid.NewGuid() |> string] + EventStoreConnector(customize = (fun c -> c.ConnectivitySettings.Insecure <- true), + reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, tags=["I",Guid.NewGuid() |> string] // Connect directly to the locally running EventStore Node without using Gossip-driven discovery - ).Establish("Equinox-sample", Discovery.Uri(Uri "tcp://localhost:1113"), ConnectionStrategy.ClusterSingle NodePreference.Master) + ).Establish("Equinox-sample", Discovery.Uri(Uri "tcp://localhost:1113"), ConnectionStrategy.ClusterSingle NodePreference.Leader) let defaultBatchSize = 500 let createContext connection batchSize = EventStoreContext(connection, BatchingPolicy(maxBatchSize = batchSize)) diff --git a/samples/Store/Integration/FavoritesIntegration.fs b/samples/Store/Integration/FavoritesIntegration.fs index e95dbe395..4b1e4dd01 100644 --- a/samples/Store/Integration/FavoritesIntegration.fs +++ b/samples/Store/Integration/FavoritesIntegration.fs @@ -15,7 +15,7 @@ let createServiceMemory log store = let codec = Favorites.Events.codec let codecJe = Favorites.Events.codecJe let createServiceGes log context = - let cat = EventStore.EventStoreCategory(context, codec, fold, initial, access = EventStore.AccessStrategy.RollingSnapshots snapshot) + let cat = EventStoreDb.EventStoreCategory(context, codec, fold, initial, access = EventStoreDb.AccessStrategy.RollingSnapshots snapshot) Favorites.create log cat.Resolve let createServiceCosmosSnapshotsUncached log context = @@ -68,7 +68,7 @@ type Tests(testOutputHelper) = [] let ``Can roundtrip against EventStore, correctly folding the events`` args = Async.RunSynchronously <| async { - let! client = connectToLocalEventStoreNode log + let client = connectToLocalEventStoreNode log let context = createContext client defaultBatchSize let service = createServiceGes log context let! version, items = act service args diff --git a/samples/Store/Integration/Integration.fsproj b/samples/Store/Integration/Integration.fsproj index 6efc9526d..f9d46cb4c 100644 --- a/samples/Store/Integration/Integration.fsproj +++ b/samples/Store/Integration/Integration.fsproj @@ -20,7 +20,7 @@ - + diff --git a/samples/Store/Integration/LogIntegration.fs b/samples/Store/Integration/LogIntegration.fs index 393c097c8..b384380c9 100644 --- a/samples/Store/Integration/LogIntegration.fs +++ b/samples/Store/Integration/LogIntegration.fs @@ -9,7 +9,7 @@ open System open System.Collections.Concurrent module EquinoxEsInterop = - open Equinox.EventStore + open Equinox.EventStoreDb [] type FlatMetric = { action: string; stream : string; interval: StopwatchInterval; bytes: int; count: int; batches: int option } with override x.ToString() = $"%s{x.action}-Stream=%s{x.stream} %s{x.action}-Elapsed={x.interval.Elapsed}" @@ -18,8 +18,8 @@ module EquinoxEsInterop = match evt with | Log.WriteSuccess m -> "AppendToStreamAsync", m, None | Log.WriteConflict m -> "AppendToStreamAsync", m, None - | Log.Slice (Direction.Forward,m) -> "ReadStreamEventsForwardAsync", m, None - | Log.Slice (Direction.Backward,m) -> "ReadStreamEventsBackwardAsync", m, None +// | Log.Slice (Direction.Forward,m) -> "ReadStreamEventsForwardAsync", m, None +// | Log.Slice (Direction.Backward,m) -> "ReadStreamEventsBackwardAsync", m, None | Log.Batch (Direction.Forward,c,m) -> "ReadStreamAsyncF", m, Some c | Log.Batch (Direction.Backward,c,m) -> "ReadStreamAsyncB", m, Some c { action = action; stream = metric.stream; interval = metric.interval; bytes = metric.bytes; count = metric.count; batches = batches } @@ -60,7 +60,7 @@ type SerilogMetricsExtractor(emit : string -> unit) = let (|EsMetric|CosmosMetric|GenericMessage|) (logEvent : Serilog.Events.LogEvent) = logEvent.Properties |> Seq.tryPick (function - | KeyValue (k, SerilogScalar (:? Equinox.EventStore.Log.Metric as m)) -> Some <| Choice1Of3 (k,m) + | KeyValue (k, SerilogScalar (:? Equinox.EventStoreDb.Log.Metric as m)) -> Some <| Choice1Of3 (k,m) | KeyValue (k, SerilogScalar (:? Equinox.CosmosStore.Core.Log.Metric as m)) -> Some <| Choice2Of3 (k,m) | _ -> None) |> Option.defaultValue (Choice3Of3 ()) @@ -106,7 +106,7 @@ type Tests(testOutputHelper) = let batchSize = defaultBatchSize let buffer = ConcurrentQueue() let log = createLoggerWithMetricsExtraction buffer.Enqueue - let! client = connectToLocalEventStoreNode log + let client = connectToLocalEventStoreNode log let context = createContext client batchSize let service = Cart.create log (CartIntegration.resolveGesStreamWithRollingSnapshots context) let itemCount = batchSize / 2 + 1 diff --git a/samples/Tutorial/Tutorial.fsproj b/samples/Tutorial/Tutorial.fsproj index 88d504219..081060c18 100644 --- a/samples/Tutorial/Tutorial.fsproj +++ b/samples/Tutorial/Tutorial.fsproj @@ -22,7 +22,7 @@ - + diff --git a/samples/Tutorial/Upload.fs b/samples/Tutorial/Upload.fs index caab35472..202b44e06 100644 --- a/samples/Tutorial/Upload.fs +++ b/samples/Tutorial/Upload.fs @@ -67,7 +67,7 @@ module Cosmos = create category.Resolve module EventStore = - open Equinox.EventStore + open Equinox.EventStoreDb let create context = let cat = EventStoreCategory(context, Events.codec, Fold.fold, Fold.initial, access=AccessStrategy.LatestKnownEvent) create cat.Resolve diff --git a/samples/Web/Program.fs b/samples/Web/Program.fs index fab9549b7..5a740ded8 100644 --- a/samples/Web/Program.fs +++ b/samples/Web/Program.fs @@ -30,7 +30,7 @@ module Program = .WriteTo.Console() // TOCONSIDER log and reset every minute or something ? .WriteTo.Sink(Equinox.CosmosStore.Core.Log.InternalMetrics.Stats.LogSink()) - .WriteTo.Sink(Equinox.EventStore.Log.InternalMetrics.Stats.LogSink()) + .WriteTo.Sink(Equinox.EventStoreDb.Log.InternalMetrics.Stats.LogSink()) .WriteTo.Sink(Equinox.SqlStreamStore.Log.InternalMetrics.Stats.LogSink()) let c = let maybeSeq = if args.Contains LocalSeq then Some "http://localhost:5341" else None diff --git a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj index fc7711aff..d40095b64 100644 --- a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj +++ b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj @@ -21,10 +21,10 @@ - + - + diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 7c26c3618..b4d9ab1a8 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -5,6 +5,8 @@ open EventStore.Client open Serilog open System +type EventBody = ReadOnlyMemory + [] type Direction = Forward | Backward with override this.ToString() = match this with Forward -> "Forward" | Backward -> "Backward" @@ -15,7 +17,7 @@ type Direction = Forward | Backward with module Log = - /// Name of Property used for Event in LogEvents. + /// Name of Property used for Metric in LogEvents. let [] PropertyTag = "esdbEvt" [] @@ -51,7 +53,7 @@ module Log = /// Attach a property to the log context to hold the metrics // Sidestep Log.ForContext converting to a string; see https://github.com/serilog/serilog/issues/1124 let event (value : Metric) (log : ILogger) = - let enrich (e : LogEvent) = e.AddPropertyIfAbsent(LogEventProperty("esEvt", ScalarValue(value))) + let enrich (e : LogEvent) = e.AddPropertyIfAbsent(LogEventProperty(PropertyTag, ScalarValue(value))) log.ForContext({ new Serilog.Core.ILogEventEnricher with member _.Enrich(evt, _) = enrich evt }) let withLoggedRetries<'t> retryPolicy (contextLabel : string) (f : ILogger -> Async<'t>) log : Async<'t> = @@ -266,20 +268,20 @@ module private Read = return version, events } module UnionEncoderAdapters = - let encodedEventOfResolvedEvent (x : ResolvedEvent) : FsCodec.ITimelineEvent = + let encodedEventOfResolvedEvent (x : ResolvedEvent) : FsCodec.ITimelineEvent = let e = x.Event // Inspecting server code shows both Created and CreatedEpoch are set; taking this as it's less ambiguous than DateTime in the general case let ts = DateTimeOffset(e.Created) // TODO something like let ts = DateTimeOffset.FromUnixTimeMilliseconds(e.CreatedEpoch) // TOCONSIDER wire e.Metadata.["$correlationId"] and .["$causationId"] into correlationId and causationId // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - let n, d, m = e.EventNumber, e.Data, e.Metadata - FsCodec.Core.TimelineEvent.Create(n.ToInt64(), e.EventType, d.ToArray(), m.ToArray(), correlationId = null, causationId = null, timestamp = ts) + let n = e.EventNumber + FsCodec.Core.TimelineEvent.Create(n.ToInt64(), e.EventType, e.Data, e.Metadata, correlationId = null, causationId = null, timestamp = ts) - let eventDataOfEncodedEvent (x : FsCodec.IEventData) = + let eventDataOfEncodedEvent (x : FsCodec.IEventData) = // TOCONSIDER wire x.CorrelationId, x.CausationId into x.Meta.["$correlationId"] and .["$causationId"] // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - EventData(Uuid.NewUuid(), x.EventType, contentType = "application/json", data = ReadOnlyMemory(x.Data), metadata = ReadOnlyMemory(x.Meta)) + EventData(Uuid.NewUuid(), x.EventType, contentType = "application/json", data = x.Data, metadata = x.Meta) type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } @@ -394,7 +396,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = Token.ofPreviousStreamVersionAndCompactionEventDataIndex streamToken compactionEventIndex encodedEvents.Length batching.BatchSize version' return GatewaySyncResult.Written token } - member _.Sync(log, streamName, streamVersion, events : FsCodec.IEventData[]) : Async = async { + member _.Sync(log, streamName, streamVersion, events : FsCodec.IEventData[]) : Async = async { let encodedEvents : EventData[] = events |> Array.map UnionEncoderAdapters.eventDataOfEncodedEvent let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection streamName streamVersion encodedEvents match wr with @@ -759,7 +761,7 @@ type EventStoreConnector ?customize : EventStoreClientSettings -> unit) = member _.Connect - ( /// Name should be sufficient to uniquely identify this connection within a single app instance's logs + ( // Name should be sufficient to uniquely identify this connection within a single app instance's logs name, discovery : Discovery, ?clusterNodePreference) : EventStoreClient = let settings = match discovery with Discovery.Uri uri -> EventStoreClientSettings.Create(string uri) if name = null then nullArg "name" diff --git a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj index 1fa837e0f..f97d4834e 100644 --- a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj +++ b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj @@ -22,7 +22,7 @@ - + diff --git a/tools/Equinox.Tool/Program.fs b/tools/Equinox.Tool/Program.fs index 21824bc10..ec97c082c 100644 --- a/tools/Equinox.Tool/Program.fs +++ b/tools/Equinox.Tool/Program.fs @@ -226,7 +226,7 @@ let createStoreLog verbose verboseConsole maybeSeqEndpoint = let c = LoggerConfiguration().Destructure.FSharpTypes() let c = if verbose then c.MinimumLevel.Debug() else c let c = c.WriteTo.Sink(Equinox.CosmosStore.Core.Log.InternalMetrics.Stats.LogSink()) - let c = c.WriteTo.Sink(Equinox.EventStore.Log.InternalMetrics.Stats.LogSink()) + let c = c.WriteTo.Sink(Equinox.EventStoreDb.Log.InternalMetrics.Stats.LogSink()) let c = c.WriteTo.Sink(Equinox.SqlStreamStore.Log.InternalMetrics.Stats.LogSink()) let level = match verbose, verboseConsole with @@ -243,7 +243,7 @@ let dumpStats storeConfig log = | Some (Storage.StorageConfig.Cosmos _) -> Equinox.CosmosStore.Core.Log.InternalMetrics.dump log | Some (Storage.StorageConfig.Es _) -> - Equinox.EventStore.Log.InternalMetrics.dump log + Equinox.EventStoreDb.Log.InternalMetrics.dump log | Some (Storage.StorageConfig.Sql _) -> Equinox.SqlStreamStore.Log.InternalMetrics.dump log | _ -> () @@ -301,7 +301,7 @@ module LoadTest = test, a.Duration, a.TestsPerSecond, clients.Length, a.ErrorCutoff, a.ReportingIntervals, reportFilename) // Reset the start time based on which the shared global metrics will be computed let _ = Equinox.CosmosStore.Core.Log.InternalMetrics.Stats.LogSink.Restart() - let _ = Equinox.EventStore.Log.InternalMetrics.Stats.LogSink.Restart() + let _ = Equinox.EventStoreDb.Log.InternalMetrics.Stats.LogSink.Restart() let _ = Equinox.SqlStreamStore.Log.InternalMetrics.Stats.LogSink.Restart() let results = runLoadTest log a.TestsPerSecond (duration.Add(TimeSpan.FromSeconds 5.)) a.ErrorCutoff a.ReportingIntervals clients runSingleTest |> Async.RunSynchronously @@ -315,7 +315,7 @@ let createDomainLog verbose verboseConsole maybeSeqEndpoint = let c = LoggerConfiguration().Destructure.FSharpTypes().Enrich.FromLogContext() let c = if verbose then c.MinimumLevel.Debug() else c let c = c.WriteTo.Sink(Equinox.CosmosStore.Core.Log.InternalMetrics.Stats.LogSink()) - let c = c.WriteTo.Sink(Equinox.EventStore.Log.InternalMetrics.Stats.LogSink()) + let c = c.WriteTo.Sink(Equinox.EventStoreDb.Log.InternalMetrics.Stats.LogSink()) let c = c.WriteTo.Sink(Equinox.SqlStreamStore.Log.InternalMetrics.Stats.LogSink()) let outputTemplate = "{Timestamp:T} {Level:u1} {Message:l} {Properties}{NewLine}{Exception}" let c = c.WriteTo.Console((if verboseConsole then LogEventLevel.Debug else LogEventLevel.Information), outputTemplate, theme = Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code) From 76682598202ec6e2ffb19532e371a4db5d2bb3c8 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Feb 2022 12:17:20 +0000 Subject: [PATCH 15/29] Fixes --- DOCUMENTATION.md | 1 - README.md | 2 +- build.proj | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 8ac9cf6c9..bc94dec90 100755 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2230,7 +2230,6 @@ raise Issues first though ;) ). EventStore, and it's Store adapter is the most proven and is pretty feature rich relative to the need of consumers to date. Some things remain though: -- __Get impl in `master` ported to the modern gRPC client, currently parked in [#196](https://github.com/jet/equinox/pull/196). See also [#232](https://github.com/jet/equinox/issues/232)__ - Provide a low level walking events in F# API akin to `Equinox.CosmosStore.Core.Events`; this would allow consumers to jump from direct use of `EventStore.ClientAPI` -> `Equinox.EventStore.Core.Events` -> diff --git a/README.md b/README.md index 9f2552135..1d9e54c29 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ The components within this repository are delivered as multi-targeted Nuget pack - `Equinox.CosmosStore` [![CosmosStore NuGet](https://img.shields.io/nuget/v/Equinox.CosmosStore.svg)](https://www.nuget.org/packages/Equinox.CosmosStore/): Azure CosmosDB Adapter with integrated 'unfolds' feature, facilitating optimal read performance in terms of latency and RU costs, instrumented to meet Jet's production monitoring requirements. ([depends](https://www.fuget.org/packages/Equinox.CosmosStore) on `Equinox.Core`, `Microsoft.Azure.Cosmos >= 3.25`, `FsCodec`, `System.Text.Json`, `FSharp.Control.AsyncSeq >= 2.0.23`) - `Equinox.CosmosStore.Prometheus` [![CosmosStore.Prometheus NuGet](https://img.shields.io/nuget/v/Equinox.CosmosStore.Prometheus.svg)](https://www.nuget.org/packages/Equinox.CosmosStore.Prometheus/): Integration package providing a `Serilog.Core.ILogEventSink` that extracts detailed metrics information attached to the `LogEvent`s and feeds them to the `prometheus-net`'s `Prometheus.Metrics` static instance. ([depends](https://www.fuget.org/packages/Equinox.CosmosStore.Prometheus) on `Equinox.CosmosStore`, `prometheus-net >= 3.6.0`) - `Equinox.EventStore` [![EventStore NuGet](https://img.shields.io/nuget/v/Equinox.EventStore.svg)](https://www.nuget.org/packages/Equinox.EventStore/): [EventStoreDB](https://eventstore.org/) Adapter designed to meet Jet's production monitoring requirements. ([depends](https://www.fuget.org/packages/Equinox.EventStore) on `Equinox.Core`, `EventStore.Client >= 22.0.0-preview`, `FSharp.Control.AsyncSeq >= 2.0.23`), EventStore Server version `21.10` or later) -- `Equinox.EventStoreDb` [![EventStoreDb NuGet](https://img.shields.io/nuget/v/Equinox.EventStoreDb.svg)](https://www.nuget.org/packages/Equinox.EventStoreDb/): Production-strength [EventStoreDB](https://eventstore.org/) Adapter. ([depends](https://www.fuget.org/packages/Equinox.EventStoreDb) on `Equinox.Core`, `EventStore.Client.Grpc.Streams` >= `21.2.0`, `FSharp.Control.AsyncSeq` v `2.0.23`, EventStore Server version `21.10` or later) +- `Equinox.EventStoreDb` [![EventStoreDb NuGet](https://img.shields.io/nuget/v/Equinox.EventStoreDb.svg)](https://www.nuget.org/packages/Equinox.EventStoreDb/): Production-strength [EventStoreDB](https://eventstore.org/) Adapter. ([depends](https://www.fuget.org/packages/Equinox.EventStoreDb) on `Equinox.Core`, `EventStore.Client.Grpc.Streams` >= `22.0.0`, `FSharp.Control.AsyncSeq` v `2.0.23`, EventStore Server version `21.10` or later) - `Equinox.SqlStreamStore` [![SqlStreamStore NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore/): [SqlStreamStore](https://github.com/SQLStreamStore/SQLStreamStore) Adapter derived from `Equinox.EventStore` - provides core facilities (but does not connect to a specific database; see sibling `SqlStreamStore`.* packages). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore) on `Equinox.Core`, `FsCodec`, `SqlStreamStore >= 1.2.0-beta.8`, `FSharp.Control.AsyncSeq`) - `Equinox.SqlStreamStore.MsSql` [![MsSql NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.MsSql.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore.MsSql/): [SqlStreamStore.MsSql](https://sqlstreamstore.readthedocs.io/en/latest/sqlserver) Sql Server `Connector` implementation for `Equinox.SqlStreamStore` package). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore.MsSql) on `Equinox.SqlStreamStore`, `SqlStreamStore.MsSql >= 1.2.0-beta.8`) - `Equinox.SqlStreamStore.MySql` [![MySql NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.MySql.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore.MySql/): `SqlStreamStore.MySql` MySQL `Connector` implementation for `Equinox.SqlStreamStore` package). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore.MySql) on `Equinox.SqlStreamStore`, `SqlStreamStore.MySql >= 1.2.0-beta.8`) diff --git a/build.proj b/build.proj index 7ca2f8064..b08c27936 100644 --- a/build.proj +++ b/build.proj @@ -19,7 +19,7 @@ - + From c0a6efd9e21101d8afbf1b62d16682a6f7e79d22 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 28 Feb 2022 18:22:53 +0000 Subject: [PATCH 16/29] Tidy unported --- src/Equinox.EventStoreDb/EventStoreDb.fs | 118 ++++++----------------- 1 file changed, 28 insertions(+), 90 deletions(-) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index b4d9ab1a8..5a36eaf9c 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -340,7 +340,7 @@ type EventStoreConnection(readConnection, [] ?writeConnection, [ int, [] ?batchCountLimit) = // TODO batchCountLimit new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) member _.BatchSize = getMaxBatchSize() -// member _.MaxBatches = batchCountLimit +//TODO member _.MaxBatches = batchCountLimit [] type GatewaySyncResult = Written of StreamToken | ConflictUnknown of StreamToken @@ -594,23 +594,15 @@ type EventStoreCategory<'event, 'state, 'context> let storeCategory = StoreCategory(resolve, empty) member _.Resolve(streamName : FsCodec.StreamName, [] ?context) = storeCategory.Resolve(streamName, ?context = context) -// see https://github.com/EventStore/EventStore/issues/1652 -[] -type ConnectionStrategy = - /// Pair of master and slave connections, writes direct, often can't read writes, resync without backoff (kind to master, writes+resyncs optimal) - | ClusterTwinPreferSlaveReads - /// Single connection, with resync backoffs appropriate to the NodePreference - | ClusterSingle of NodePreference - -#if false +(* TODO type private SerilogAdapter(log : ILogger) = interface EventStore.ClientAPI.ILogger with - member __.Debug(format : string, args : obj []) = log.Debug(format, args) - member __.Debug(ex : exn, format : string, args : obj []) = log.Debug(ex, format, args) - member __.Info(format : string, args : obj []) = log.Information(format, args) - member __.Info(ex : exn, format : string, args : obj []) = log.Information(ex, format, args) - member __.Error(format : string, args : obj []) = log.Error(format, args) - member __.Error(ex : exn, format : string, args : obj []) = log.Error(ex, format, args) + member _.Debug(format : string, args : obj []) = log.Debug(format, args) + member _.Debug(ex : exn, format : string, args : obj []) = log.Debug(ex, format, args) + member _.Info(format : string, args : obj []) = log.Information(format, args) + member _.Info(ex : exn, format : string, args : obj []) = log.Information(ex, format, args) + member _.Error(format : string, args : obj []) = log.Error(format, args) + member _.Error(ex : exn, format : string, args : obj []) = log.Error(ex, format, args) [] type Logger = @@ -624,22 +616,13 @@ type Logger = | SerilogNormal logger -> b.UseCustomLogger(SerilogAdapter(logger)) | CustomVerbose logger -> b.EnableVerboseLogging().UseCustomLogger(logger) | CustomNormal logger -> b.UseCustomLogger(logger) - -[] -type NodePreference = - /// Track master via gossip, writes direct, reads should immediately reflect writes, resync without backoff (highest load on master, good write perf) - | Master - /// Take the first connection that comes along, ideally a master, but do not track master changes - | PreferMaster - /// Prefer slave node, writes normally need forwarding, often can't read writes, resync requires backoff (kindest to master, writes and resyncs expensive) - | PreferSlave - /// Take random node, writes may need forwarding, sometimes can't read writes, resync requires backoff (balanced load on master, balanced write perf) - | Random +*) [] type Discovery = - // Allow Uri-based connection definition (discovery://, tcp:// or + // Allow Uri-based connection definition (esdb://, etc) | Uri of Uri +(* TODO /// Supply a set of pre-resolved EndPoints instead of letting Gossip resolution derive from the DNS outcome | GossipSeeded of seedManagerEndpoints : System.Net.IPEndPoint [] // Standard Gossip-based discovery based on Dns query and standard manager port @@ -671,6 +654,7 @@ module private Discovery = | (Discovery.GossipSeeded seedEndpoints), np -> DiscoverViaGossip (buildSeeded np (configureSeeded seedEndpoints)) | (Discovery.GossipDns clusterDns), np -> DiscoverViaGossip (buildDns np (configureDns clusterDns None)) | (Discovery.GossipDnsCustomPort (dns, port)), np ->DiscoverViaGossip (buildDns np (configureDns dns (Some port))) +*) // see https://github.com/EventStore/EventStore/issues/1652 [] @@ -680,14 +664,11 @@ type ConnectionStrategy = /// Single connection, with resync backoffs appropriate to the NodePreference | ClusterSingle of NodePreference -type Connector - ( username, password, reqTimeout: TimeSpan, reqRetries: int, - [] ?log : Logger, [] ?heartbeatTimeout: TimeSpan, [] ?concurrentOperationsLimit, - [] ?readRetryPolicy, [] ?writeRetryPolicy, - [] ?gossipTimeout, [] ?clientConnectionTimeout, - /// Additional strings identifying the context of this connection; should provide enough context to disambiguate all potential connections to a cluster - /// NB as this will enter server and client logs, it should not contain sensitive information - [] ?tags : (string*string) seq) = +type EventStoreConnector + ( reqTimeout : TimeSpan, reqRetries : int, + ?readRetryPolicy, ?writeRetryPolicy, ?tags, + ?customize : EventStoreClientSettings -> unit) = +(* TODO port let connSettings node = ConnectionSettings.Create().SetDefaultUserCredentials(SystemData.UserCredentials(username, password)) .KeepReconnecting() // ES default: .LimitReconnectionsTo(10) @@ -709,61 +690,18 @@ type Connector |> fun s -> match clientConnectionTimeout with Some v -> s.WithConnectionTimeoutOf v | None -> s // default: 1000 ms |> fun s -> match log with Some log -> log.Configure s | None -> s |> fun s -> s.Build() - - /// Yields an IEventStoreConnection configured and Connect()ed to a node (or the cluster) per the supplied `discovery` and `clusterNodePrefence` preference - member __.Connect - ( /// Name should be sufficient to uniquely identify this connection within a single app instance's logs - name, - discovery : Discovery, ?clusterNodePreference) : Async = async { - if name = null then nullArg "name" - let clusterNodePreference = defaultArg clusterNodePreference NodePreference.Master - let name = String.concat ";" <| seq { - yield name - yield string clusterNodePreference - match tags with None -> () | Some tags -> for key, value in tags do yield sprintf "%s=%s" key value } - let sanitizedName = name.Replace('\'','_').Replace(':','_') // ES internally uses `:` and `'` as separators in log messages and ... people regex logs - let conn = - match discovery, clusterNodePreference with - | Discovery.DiscoverViaUri uri -> - // This overload picks up the discovery settings via ConnectionSettingsBuilder.PreferSlaveNode/.PreferRandomNode - EventStoreConnection.Create(connSettings clusterNodePreference, uri, sanitizedName) - | Discovery.DiscoverViaGossip clusterSettings -> - // NB This overload's implementation ignores the calls to ConnectionSettingsBuilder.PreferSlaveNode/.PreferRandomNode and - // requires equivalent ones on the GossipSeedClusterSettingsBuilder or ClusterSettingsBuilder - EventStoreConnection.Create(connSettings clusterNodePreference, clusterSettings, sanitizedName) - do! conn.ConnectAsync() |> Async.AwaitTaskCorrect - return conn } - - /// Yields a Connection (which may internally be twin connections) configured per the specified strategy - member __.Establish - ( /// Name should be sufficient to uniquely identify this (aggregate) connection within a single app instance's logs - name, - discovery : Discovery, strategy : ConnectionStrategy) : Async = async { - match strategy with - | ConnectionStrategy.ClusterSingle nodePreference -> - let! conn = __.Connect(name, discovery, nodePreference) - return Connection(conn, ?readRetryPolicy=readRetryPolicy, ?writeRetryPolicy=writeRetryPolicy) - | ConnectionStrategy.ClusterTwinPreferSlaveReads -> - let! masterInParallel = Async.StartChild (__.Connect(name + "-TwinW", discovery, NodePreference.Master)) - let! slave = __.Connect(name + "-TwinR", discovery, NodePreference.PreferSlave) - let! master = masterInParallel - return Connection(readConnection = slave, writeConnection = master, ?readRetryPolicy = readRetryPolicy, ?writeRetryPolicy = writeRetryPolicy) } -#endif - -[] -type Discovery = - // Allow Uri-based connection definition (esdb://, etc) - | Uri of Uri - -type EventStoreConnector - ( reqTimeout : TimeSpan, reqRetries : int, - ?readRetryPolicy, ?writeRetryPolicy, ?tags, - ?customize : EventStoreClientSettings -> unit) = - +*) member _.Connect ( // Name should be sufficient to uniquely identify this connection within a single app instance's logs name, discovery : Discovery, ?clusterNodePreference) : EventStoreClient = - let settings = match discovery with Discovery.Uri uri -> EventStoreClientSettings.Create(string uri) + let settings = + match discovery with + | Discovery.Uri uri -> EventStoreClientSettings.Create(string uri) +(* TODO + | Discovery.DiscoverViaGossip clusterSettings -> + // NB This overload's implementation ignores the calls to ConnectionSettingsBuilder.PreferSlaveNode/.PreferRandomNode and + // requires equivalent ones on the GossipSeedClusterSettingsBuilder or ClusterSettingsBuilder + EventStoreConnection.Create(connSettings clusterNodePreference, clusterSettings, sanitizedName) *) if name = null then nullArg "name" let name = String.concat ";" <| seq { name @@ -775,11 +713,11 @@ type EventStoreConnector match customize with None -> () | Some f -> f settings settings.DefaultDeadline <- reqTimeout // TODO implement reqRetries - new EventStoreClient(settings) + EventStoreClient(settings) /// Yields a Connection (which may internally be twin connections) configured per the specified strategy member x.Establish - ( /// Name should be sufficient to uniquely identify this (aggregate) connection within a single app instance's logs + ( // Name should be sufficient to uniquely identify this (aggregate) connection within a single app instance's logs name, discovery : Discovery, strategy : ConnectionStrategy) : EventStoreConnection = match strategy with From 82bc99dfcebe7b2c4dcc3372d9e5fc3c55c23b1f Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 6 Mar 2022 01:57:06 +0000 Subject: [PATCH 17/29] Fix all the things --- README.md | 2 +- samples/Infrastructure/Storage.fs | 6 ++-- .../Integration/EventStoreIntegration.fs | 8 +++--- samples/Tutorial/AsAt.fsx | 23 ++++++++------- src/Equinox.EventStoreDb/EventStoreDb.fs | 28 +++++++++---------- .../StoreIntegration.fs | 6 ++-- .../Equinox.EventStoreDb.Integration.fsproj | 2 +- 7 files changed, 39 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 1d9e54c29..440b334aa 100644 --- a/README.md +++ b/README.md @@ -538,7 +538,7 @@ The CLI can drive the Store and TodoBackend samples in the `samples/Web` ASP.NET ### Provisioning EventStore (when not using -s or -se) -There's a `docker-compose.yml` file in the root, so installing `docker-compose` and then running `docker-compose up` rigs a local 3-node cluster, which is assumed to be configured for `Equinox.EventStore.Integration` +There's a `docker-compose.yml` file in the root, so installing `docker-compose` and then running `docker-compose up` rigs a local 3-node cluster, which is assumed to be configured for `Equinox.EventStore.Integration` and `Equinox.EventStoreDb.Integration` For more complete instructions, follow https://developers.eventstore.com/server/v21.10/installation.html#use-docker-compose diff --git a/samples/Infrastructure/Storage.fs b/samples/Infrastructure/Storage.fs index 5db0e26d0..42d8eb4f3 100644 --- a/samples/Infrastructure/Storage.fs +++ b/samples/Infrastructure/Storage.fs @@ -142,7 +142,7 @@ module EventStore = | VerboseStore -> "include low level Store logging." | Timeout _ -> "specify operation timeout in seconds (default: 5)." | Retries _ -> "specify operation retries (default: 1)." - | ConnectionString _ -> "Portion of connection string that's safe to write to console or log. default: esdb://localhost:2113?tls=false" + | ConnectionString _ -> "Portion of connection string that's safe to write to console or log. default: esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false" | Credentials _ -> "specify a sensitive portion of the connection string that should not be logged. Default: none" | ConcurrentOperationsLimit _ -> "max concurrent operations in flight (default: 5000)." | HeartbeatTimeout _ -> "specify heartbeat timeout in seconds (default: 1.5)." @@ -150,7 +150,7 @@ module EventStore = open Equinox.EventStoreDb type Info(args : ParseResults) = - member _.Host = args.GetResult(ConnectionString,"esdb://localhost:2113?tls=false") + member _.Host = args.GetResult(ConnectionString, "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false") member _.Credentials = args.GetResult(Credentials, null) member _.Timeout = args.GetResult(Timeout,5.) |> TimeSpan.FromSeconds @@ -166,7 +166,7 @@ module EventStore = // TODO heartbeatTimeout=heartbeatTimeout, concurrentOperationsLimit = col, // TODO log=(if log.IsEnabled(Serilog.Events.LogEventLevel.Debug) then Logger.SerilogVerbose log else Logger.SerilogNormal log), tags=["M", Environment.MachineName; "I", Guid.NewGuid() |> string]) - .Establish(appName, Discovery.Uri (String.Join(";", connectionString, credentialsString) |> Uri), ConnectionStrategy.ClusterTwinPreferSlaveReads) + .Establish(appName, Discovery.ConnectionString (String.Join(";", connectionString, credentialsString)), ConnectionStrategy.ClusterTwinPreferSlaveReads) let private createContext connection batchSize = EventStoreContext(connection, BatchingPolicy(maxBatchSize = batchSize)) let config (log: ILogger, storeLog) (cache, unfolds) (args : ParseResults) = let a = Info(args) diff --git a/samples/Store/Integration/EventStoreIntegration.fs b/samples/Store/Integration/EventStoreIntegration.fs index 27263e9cd..bbf88f977 100644 --- a/samples/Store/Integration/EventStoreIntegration.fs +++ b/samples/Store/Integration/EventStoreIntegration.fs @@ -2,14 +2,14 @@ module Samples.Store.Integration.EventStoreIntegration open Equinox.EventStoreDb -open EventStore.Client open System -let connectToLocalEventStoreNode log = +// NOTE: use `docker compose up` to establish the standard 3 node config at ports 1113/2113 +let connectToLocalEventStoreNode (_log : Serilog.ILogger) = // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER EventStoreConnector(customize = (fun c -> c.ConnectivitySettings.Insecure <- true), reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, tags=["I",Guid.NewGuid() |> string] - // Connect directly to the locally running EventStore Node without using Gossip-driven discovery - ).Establish("Equinox-sample", Discovery.Uri(Uri "tcp://localhost:1113"), ConnectionStrategy.ClusterSingle NodePreference.Leader) + // Connect to the locally running EventStore Node using Gossip-driven discovery + ).Establish("Equinox-sample", Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false", ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) let defaultBatchSize = 500 let createContext connection batchSize = EventStoreContext(connection, BatchingPolicy(maxBatchSize = batchSize)) diff --git a/samples/Tutorial/AsAt.fsx b/samples/Tutorial/AsAt.fsx index 4879a4e95..10f985da2 100644 --- a/samples/Tutorial/AsAt.fsx +++ b/samples/Tutorial/AsAt.fsx @@ -32,8 +32,9 @@ #r "FSharp.Control.AsyncSeq.dll" #r "System.Net.Http" #r "Serilog.Sinks.Seq.dll" -#r "EventStore.ClientAPI.dll" -#r "Equinox.EventStore.dll" +#r "Equinox.EventStoreDb.dll" +#r "EventStore.Client.dll" +#r "EventStore.Client.Streams.dll" #r "Microsoft.Azure.Cosmos.Direct.dll" #r "Microsoft.Azure.Cosmos.Client.dll" #r "Equinox.CosmosStore.dll" @@ -134,28 +135,30 @@ module Log = let log = let c = LoggerConfiguration() let c = if verbose then c.MinimumLevel.Debug() else c - let c = c.WriteTo.Sink(Equinox.EventStore.Log.InternalMetrics.Stats.LogSink()) // to power Log.InternalMetrics.dump + let c = c.WriteTo.Sink(Equinox.EventStoreDb.Log.InternalMetrics.Stats.LogSink()) // to power Log.InternalMetrics.dump let c = c.WriteTo.Sink(Equinox.CosmosStore.Core.Log.InternalMetrics.Stats.LogSink()) // to power Log.InternalMetrics.dump let c = c.WriteTo.Seq("http://localhost:5341") // https://getseq.net let c = c.WriteTo.Console(if verbose then LogEventLevel.Debug else LogEventLevel.Information) c.CreateLogger() let dumpMetrics () = Equinox.CosmosStore.Core.Log.InternalMetrics.dump log - Equinox.EventStore.Log.InternalMetrics.dump log + Equinox.EventStoreDb.Log.InternalMetrics.dump log let [] AppName = "equinox-tutorial" let cache = Equinox.Cache(AppName, 20) module EventStore = - open Equinox.EventStore + open Equinox.EventStoreDb let snapshotWindow = 500 - // see QuickStart for how to run a local instance in a mode that emulates the behavior of a cluster - let host, username, password = "localhost", "admin", "changeit" - let connector = Connector(username,password,TimeSpan.FromSeconds 5., reqRetries=3, log=Logger.SerilogNormal Log.log) - let esc = connector.Connect(AppName, Discovery.GossipDns host) |> Async.RunSynchronously - let log = Logger.SerilogNormal Log.log + // NOTE: use `docker compose up` to establish the standard 3 node config at ports 1113/2113 + let connector = + EventStoreConnector( + // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER + customize = (fun c -> c.ConnectivitySettings.Insecure <- true), + reqTimeout = TimeSpan.FromSeconds 5., reqRetries = 3) + let esc = connector.Connect(AppName, Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false") let connection = EventStoreConnection(esc) let context = EventStoreContext(connection, BatchingPolicy(maxBatchSize=snapshotWindow)) // cache so normal read pattern is to read from whatever we've built in memory diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 5a36eaf9c..25d1c7fe3 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -195,12 +195,12 @@ module private Read = let! events = res |> AsyncSeq.toArrayAsync let version = match events with - | [||] when direction = Direction.Backward -> -1L // When reading backwards, the startPos is End, which is not directly convertible + | [||] when direction = Direction.Backward -> 0L // When reading backwards, the startPos is End, which is not directly convertible | [||] -> startPos.ToInt64() // e.g. if reading forward from (verified existing) event 10 and there are none, the version is still 10 - | xs when direction = Direction.Backward -> let le = xs.[0].Event.EventNumber in le.ToInt64() // the events arrive backwards, so first is the 'version' +// | xs when direction = Direction.Backward -> let le = xs.[0].Event.EventNumber in le.ToInt64() // the events arrive backwards, so first is the 'version' | xs -> let le = xs.[xs.Length - 1].Event.EventNumber in le.ToInt64() // In normal case, the last event represents the version of the stream return version, events - with :? StreamNotFoundException -> return -1L, [||] } + with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> return 0L, [||] } let (|ResolvedEventLen|) (x : ResolvedEvent) = match x.Event.Data, x.Event.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes @@ -359,9 +359,9 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = | None -> return Token.ofUncompactedVersion batching.BatchSize version, Array.choose tryDecode events | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose tryDecode events } - member _.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin) : Async = async { + member _.LoadBackwardsStoppingAtCompactionEvent(streamName, log, limit, (tryDecode, isOrigin)) : Async = async { let! version, events = - Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName (tryDecode, isOrigin) + Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection (defaultArg limit Int32.MaxValue) streamName (tryDecode, isOrigin) match Array.tryHead events |> Option.filter (function _, Some e -> isOrigin e | _ -> false) with | None -> return Token.ofUncompactedVersion batching.BatchSize version, Array.choose snd events | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose snd events } @@ -439,11 +439,11 @@ type private Category<'event, 'state, 'context>(context : EventStoreContext, cod let loadAlgorithm load streamName initial log = let batched = load initial (context.LoadBatched streamName log (tryDecode, None)) - let compacted = load initial (context.LoadBackwardsStoppingAtCompactionEvent streamName log (tryDecode, isOrigin)) + let compacted limit = load initial (context.LoadBackwardsStoppingAtCompactionEvent(streamName, log, limit, (tryDecode, isOrigin))) match access with | None -> batched - | Some AccessStrategy.LatestKnownEvent - | Some (AccessStrategy.RollingSnapshots _) -> compacted + | Some AccessStrategy.LatestKnownEvent -> compacted (Some 1) + | Some (AccessStrategy.RollingSnapshots _) -> compacted None let load (fold : 'state -> 'event seq -> 'state) initial f = async { let! token, events = f @@ -558,14 +558,14 @@ type CachingStrategy = type EventStoreCategory<'event, 'state, 'context> ( context : EventStoreContext, codec : FsCodec.IEventCodec<_, _, 'context>, fold, initial, - /// Caching can be overkill for EventStore esp considering the degree to which its intrinsic caching is a first class feature - /// e.g., A key benefit is that reads of streams more than a few pages long get completed in constant time after the initial load + // Caching can be overkill for EventStore esp considering the degree to which its intrinsic caching is a first class feature + // e.g., A key benefit is that reads of streams more than a few pages long get completed in constant time after the initial load [] ?caching, [] ?access) = do match access with | Some AccessStrategy.LatestKnownEvent when Option.isSome caching -> - "Equinox.EventStore does not support (and it would make things _less_ efficient even if it did)" + "Equinox.EventStoreDb does not support (and it would make things _less_ efficient even if it did)" + "mixing AccessStrategy.LatestKnownEvent with Caching at present." |> invalidOp | _ -> () @@ -621,13 +621,13 @@ type Logger = [] type Discovery = // Allow Uri-based connection definition (esdb://, etc) - | Uri of Uri + | ConnectionString of string (* TODO /// Supply a set of pre-resolved EndPoints instead of letting Gossip resolution derive from the DNS outcome | GossipSeeded of seedManagerEndpoints : System.Net.IPEndPoint [] // Standard Gossip-based discovery based on Dns query and standard manager port | GossipDns of clusterDns : string - // Standard Gossip-based discovery based on Dns query (with manager port overriding default 30778) + // Standard Gossip-based discovery based on Dns query (with manager port overriding default 2113) | GossipDnsCustomPort of clusterDns : string * managerPortOverride : int module private Discovery = @@ -696,7 +696,7 @@ type EventStoreConnector name, discovery : Discovery, ?clusterNodePreference) : EventStoreClient = let settings = match discovery with - | Discovery.Uri uri -> EventStoreClientSettings.Create(string uri) + | Discovery.ConnectionString s -> EventStoreClientSettings.Create(s) (* TODO | Discovery.DiscoverViaGossip clusterSettings -> // NB This overload's implementation ignores the calls to ConnectionSettingsBuilder.PreferSlaveNode/.PreferRandomNode and diff --git a/tests/Equinox.EventStore.Integration/StoreIntegration.fs b/tests/Equinox.EventStore.Integration/StoreIntegration.fs index 2d2632f10..8219dc098 100644 --- a/tests/Equinox.EventStore.Integration/StoreIntegration.fs +++ b/tests/Equinox.EventStore.Integration/StoreIntegration.fs @@ -60,18 +60,18 @@ let connectToLocalStore log = ).Establish("Equinox-integration", Discovery.Uri(Uri "tcp://localhost:1113"), ConnectionStrategy.ClusterSingle NodePreference.Master) #else // Connect directly to the locally running EventStore Node using Gossip-driven discovery - ).Establish("Equinox-integration", Discovery.GossipDns "localhost", ConnectionStrategy.ClusterTwinPreferSlaveReads) + ).Establish("Equinox-integration", Discovery.GossipDns ("localhost"), ConnectionStrategy.ClusterTwinPreferSlaveReads) #endif type Context = EventStoreContext type Category<'event, 'state, 'context> = EventStoreCategory<'event, 'state, 'context> -#else // STORE_EVENTSTORE_LEGACY +#else // !STORE_EVENTSTORE_LEGACY open Equinox.EventStoreDb /// Connect directly to a locally running EventStoreDB Node using gRPC, without using Gossip-driven discovery let connectToLocalStore (_log : ILogger) = async { let c = EventStoreConnector(reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, (*, log=Logger.SerilogVerbose log,*) tags=["I",Guid.NewGuid() |> string]) - let conn = c.Establish("Equinox-integration", Discovery.Uri(Uri "esdb://localhost:2113?tls=false"), ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) + let conn = c.Establish("Equinox-integration", Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false", ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) return conn } type Context = EventStoreContext diff --git a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj index f97d4834e..1b9b0b165 100644 --- a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj +++ b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 false 5 $(DefineConstants);STORE_EVENTSTOREDB From d5dd5cd355eb26565dbe011019cfc8bfaad4b769 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 8 Mar 2022 12:55:00 +0000 Subject: [PATCH 18/29] Reorg test fixtures --- samples/Store/Integration/LogIntegration.fs | 5 +- src/Equinox.EventStoreDb/EventStoreDb.fs | 105 ++++++++++++++------ 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/samples/Store/Integration/LogIntegration.fs b/samples/Store/Integration/LogIntegration.fs index b384380c9..e350caec0 100644 --- a/samples/Store/Integration/LogIntegration.fs +++ b/samples/Store/Integration/LogIntegration.fs @@ -18,8 +18,9 @@ module EquinoxEsInterop = match evt with | Log.WriteSuccess m -> "AppendToStreamAsync", m, None | Log.WriteConflict m -> "AppendToStreamAsync", m, None -// | Log.Slice (Direction.Forward,m) -> "ReadStreamEventsForwardAsync", m, None -// | Log.Slice (Direction.Backward,m) -> "ReadStreamEventsBackwardAsync", m, None +// For the gRPC edition, no slice information is available +// | Log.Slice (Direction.Forward,m) -> "ReadStreamEventsForwardAsync", m, None +// | Log.Slice (Direction.Backward,m) -> "ReadStreamEventsBackwardAsync", m, None | Log.Batch (Direction.Forward,c,m) -> "ReadStreamAsyncF", m, Some c | Log.Batch (Direction.Backward,c,m) -> "ReadStreamAsyncB", m, Some c { action = action; stream = metric.stream; interval = metric.interval; bytes = metric.bytes; count = metric.count; batches = batches } diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 25d1c7fe3..0b51ea892 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -77,7 +77,7 @@ module Log = let (|Read|Write|Resync|) = function | WriteSuccess (Stats s) -> Write s | WriteConflict (Stats s) -> Resync s - // No slices, so no rolling up + // slices are rolled up into batches in other stores, but we don't log slices in this impl | Batch (_, _, Stats s) -> Read s let (|SerilogScalar|_|) : LogEventPropertyValue -> obj option = function @@ -143,16 +143,18 @@ module Log = type EsSyncResult = Written of ConditionalWriteResult | Conflict of actualVersion: int64 module private Write = - /// Yields `EsSyncResult.Written` or `EsSyncResult.Conflict` to signify WrongExpectedVersion - let private writeEventsAsync (log : ILogger) (conn : EventStoreClient) (streamName : string) (version : int64) (events : EventData[]) + + let private writeEventsAsync (log : ILogger) (conn : EventStoreClient) (streamName : string) version (events : EventData[]) : Async = async { - try let! ct = Async.CancellationToken - let! wr = conn.ConditionalAppendToStreamAsync(streamName, StreamRevision.FromInt64 version, events, cancellationToken=ct) |> Async.AwaitTaskCorrect - return EsSyncResult.Written wr - with :? WrongExpectedVersionException as ex -> - log.Information(ex, "Ges TrySync WrongExpectedVersionException writing {EventTypes}, actual {ActualVersion}", - [| for x in events -> x.Type |], ex.ActualVersion) - return EsSyncResult.Conflict (let v = ex.ActualVersion in v.Value) } + let! ct = Async.CancellationToken + let! wr = conn.ConditionalAppendToStreamAsync(streamName, StreamRevision.FromInt64 version, events, cancellationToken = ct) |> Async.AwaitTaskCorrect + if wr.Status = ConditionalWriteStatus.VersionMismatch then + log.Information("Esdb TrySync VersionMismatch writing {EventTypes}, actual {ActualVersion}", + [| for x in events -> x.Type |], wr.NextExpectedVersion) + return EsSyncResult.Conflict wr.NextExpectedVersion + elif wr.Status = ConditionalWriteStatus.StreamDeleted then return failwithf "Unexpected write to deleted stream %s" streamName + elif wr.Status = ConditionalWriteStatus.Succeeded then return EsSyncResult.Written wr + else return failwithf "Unexpected write response code %O" wr.Status } let eventDataBytes events = let eventDataLen (x : EventData) = match x.Data, x.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes @@ -162,8 +164,8 @@ module private Write = : Async = async { let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propEventData "Json" events let bytes, count = eventDataBytes events, events.Length - let log = log |> Log.prop "bytes" bytes - let writeLog = log |> Log.prop "stream" streamName |> Log.prop "expectedVersion" version |> Log.prop "count" count + let log = log |> Log.prop "bytes" bytes |> Log.prop "expectedVersion" version + let writeLog = log |> Log.prop "stream" streamName |> Log.prop "count" count let! t, result = writeEventsAsync writeLog conn streamName version events |> Stopwatch.Time let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} let resultLog, evt = @@ -172,7 +174,7 @@ module private Write = log |> Log.prop "actualVersion" actualVersion, Log.WriteConflict m | EsSyncResult.Written x, m -> log |> Log.prop "nextExpectedVersion" x.NextExpectedVersion |> Log.prop "logPosition" x.LogPosition, Log.WriteSuccess m - (resultLog |> Log.event evt).Information("Ges{action:l} count={count} conflict={conflict}", + (resultLog |> Log.event evt).Information("Esdb{action:l} count={count} conflict={conflict}", "Write", events.Length, match evt with Log.WriteConflict _ -> true | _ -> false) return result } @@ -183,7 +185,7 @@ module private Write = module private Read = open FSharp.Control - +(* let private readAsyncEnum (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) : Async> = async { let! ct = Async.CancellationToken @@ -201,31 +203,30 @@ module private Read = | xs -> let le = xs.[xs.Length - 1].Event.EventNumber in le.ToInt64() // In normal case, the last event represents the version of the stream return version, events with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> return 0L, [||] } - - let (|ResolvedEventLen|) (x : ResolvedEvent) = match x.Event.Data, x.Event.Metadata with Log.BlobLen bytes, Log.BlobLen metaBytes -> bytes + metaBytes - +*) + let resolvedEventBytes (x : ResolvedEvent) = let Log.BlobLen bytes, Log.BlobLen metaBytes = x.Event.Data, x.Event.Metadata in bytes + metaBytes + let resolvedEventsBytes events = events |> Array.sumBy resolvedEventBytes +(* let private loggedReadAsyncEnumVer conn streamName direction batchSize startPos (log : ILogger) : Async = async { let! t, (version, events) = readAsyncEnumVer conn streamName direction batchSize startPos |> Stopwatch.Time - let bytes, count = events |> Array.sumBy (|ResolvedEventLen|), events.Length + let bytes, count = resolvedEventsBytes events, events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} // let evt = Log.Slice (direction, reqMetric) let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" events (log |> Log.prop "startPos" startPos |> Log.prop "bytes" bytes).Information("Ges{action:l} count={count} version={version}", "Read", count, version) return version, events } - - let resolvedEventBytes events = events |> Array.sumBy (|ResolvedEventLen|) - +*) let logBatchRead direction streamName t events batchSize version (log : ILogger) = - let bytes, count = resolvedEventBytes events, events.Length + let bytes, count = resolvedEventsBytes events, events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} - let batches = (events.Length - 1) / batchSize + 1 + let batches = match batchSize with Some batchSize -> (events.Length - 1) / batchSize + 1 | None -> -1 let action = if direction = Direction.Forward then "LoadF" else "LoadB" let evt = Log.Metric.Batch (direction, batches, reqMetric) (log |> Log.prop "bytes" bytes |> Log.event evt).Information( - "Ges{action:l} stream={stream} count={count}/{batches} version={version}", + "Esdb{action:l} stream={stream} count={count}/{batches} version={version}", action, streamName, count, batches, version) - +(* let loadForwardsFrom (log : ILogger) retryPolicy conn batchSize streamName startPosition : Async = async { let call pos = loggedReadAsyncEnumVer conn streamName Direction.Forward batchSize pos @@ -254,7 +255,7 @@ module private Read = | _ -> true) // continue the search |> Seq.rev |> Seq.toArray - let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy conn batchSize streamName (tryDecode, isOrigin) + let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy (conn : EventStoreClient) batchSize streamName (tryDecode, isOrigin) : Async = async { let call pos = loggedReadAsyncEnumVer conn streamName Direction.Backward batchSize pos let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) @@ -266,6 +267,50 @@ module private Read = let events = mergeFromCompactionPointOrStartFromBackwardsStream streamName (tryDecode, isOrigin) log eventsBackward log |> logBatchRead direction streamName t (Array.map fst events) batchSize version return version, events } +*) + let loadBackwardsUntilOrigin (log : ILogger) (conn : EventStoreClient) batchSize streamName (tryDecode, isOrigin) + : Async = async { + let! ct = Async.CancellationToken + let res = conn.ReadStreamAsync(Direction.Backwards, streamName, StreamPosition.End, int64 batchSize, resolveLinkTos = false, cancellationToken = ct) + try let! events = + AsyncSeq.ofAsyncEnum res + |> AsyncSeq.map (fun x -> x, tryDecode x) + |> AsyncSeq.takeWhileInclusive (function + | x, Some e when isOrigin e -> + log.Information("EsdbStop stream={stream} at={eventNumber}", streamName, let en = x.Event.EventNumber in en.ToInt64()) + false + | _ -> true) + |> AsyncSeq.toArrayAsync + let v = match Seq.tryHead events with Some (r, _) -> let en = r.Event.EventNumber in en.ToInt64() | None -> -1 + Array.Reverse events + return v, events + with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> + return -1L, [||] } + + let loadBackwards (log : ILogger) (conn : EventStoreClient) batchSize streamName (tryDecode, isOrigin) + : Async = async { + let! t, (version, events) = loadBackwardsUntilOrigin log conn batchSize streamName (tryDecode, isOrigin) |> Stopwatch.Time + let log = log |> Log.prop "batchSize" batchSize |> Log.prop "stream" streamName + log |> logBatchRead Direction.Backward streamName t (Array.map fst events) (Some batchSize) version + return version, events } + + let loadForward (conn : EventStoreClient) streamName startPosition + : Async = async { + let! ct = Async.CancellationToken + let res = conn.ReadStreamAsync(Direction.Forwards, streamName, startPosition, Int64.MaxValue, resolveLinkTos = false, cancellationToken = ct) + try let! events = AsyncSeq.ofAsyncEnum res |> AsyncSeq.toArrayAsync + let v = match Seq.tryLast events with Some r -> let en = r.Event.EventNumber in en.ToInt64() | None -> startPosition.ToInt64() - 1L + return v, events + with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> + return -1L, [||] } + + let loadForwards log conn streamName startPosition + : Async = async { + let direction = Direction.Forward + let! t, (version, events) = loadForward conn streamName startPosition |> Stopwatch.Time + let log = log |> Log.prop "startPos" startPosition |> Log.prop "direction" direction |> Log.prop "stream" streamName + log |> logBatchRead direction streamName t events None version + return version, events } module UnionEncoderAdapters = let encodedEventOfResolvedEvent (x : ResolvedEvent) : FsCodec.ITimelineEvent = @@ -283,7 +328,6 @@ module UnionEncoderAdapters = // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata EventData(Uuid.NewUuid(), x.EventType, contentType = "application/json", data = x.Data, metadata = x.Meta) - type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } type Token = { pos : Position } @@ -351,7 +395,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = member _.TokenEmpty = Token.ofUncompactedVersion batching.BatchSize -1L member _.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { - let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy conn.ReadConnection batching.BatchSize streamName StreamPosition.Start + let! version, events = Read.loadForwards log conn.ReadConnection streamName StreamPosition.Start match tryIsResolvedEventEventType isCompactionEventType with | None -> return Token.ofNonCompacting version, Array.choose tryDecode events | Some isCompactionEvent -> @@ -360,8 +404,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose tryDecode events } member _.LoadBackwardsStoppingAtCompactionEvent(streamName, log, limit, (tryDecode, isOrigin)) : Async = async { - let! version, events = - Read.loadBackwardsUntilCompactionOrStart log conn.ReadRetryPolicy conn.ReadConnection (defaultArg limit Int32.MaxValue) streamName (tryDecode, isOrigin) + let! version, events = Read.loadBackwards log conn.ReadConnection (defaultArg limit Int32.MaxValue) streamName (tryDecode, isOrigin) match Array.tryHead events |> Option.filter (function _, Some e -> isOrigin e | _ -> false) with | None -> return Token.ofUncompactedVersion batching.BatchSize version, Array.choose snd events | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose snd events } @@ -370,7 +413,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = : Async = async { let streamPosition = StreamPosition.FromInt64(token.streamVersion + 1L) let connToUse = if useWriteConn then conn.WriteConnection else conn.ReadConnection - let! version, events = Read.loadForwardsFrom log conn.ReadRetryPolicy connToUse batching.BatchSize streamName streamPosition + let! version, events = Read.loadForwards log connToUse streamName streamPosition match isCompactionEventType with | None -> return Token.ofNonCompacting version, Array.choose tryDecode events | Some isCompactionEvent -> From 93b57db670f6b1ae6e3299898831a51b2d1edbed Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 8 Mar 2022 15:19:48 +0000 Subject: [PATCH 19/29] Tidy --- .../EventStoreTokenTests.fs | 6 ++--- .../Infrastructure.fs | 6 ++--- .../StoreIntegration.fs | 24 ++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs b/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs index e74b6d4a3..e77b4637c 100644 --- a/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs +++ b/tests/Equinox.EventStore.Integration/EventStoreTokenTests.fs @@ -1,10 +1,10 @@ module Equinox.EventStore.Tests.EventStoreTokenTests open Equinox.Core -#if STORE_EVENTSTORE_LEGACY -open Equinox.EventStore -#else +#if !STORE_EVENTSTORE_LEGACY open Equinox.EventStoreDb +#else +open Equinox.EventStore #endif open FsCheck.Xunit open Swensen.Unquote.Assertions diff --git a/tests/Equinox.EventStore.Integration/Infrastructure.fs b/tests/Equinox.EventStore.Integration/Infrastructure.fs index 29cc3e49e..06da36049 100644 --- a/tests/Equinox.EventStore.Integration/Infrastructure.fs +++ b/tests/Equinox.EventStore.Integration/Infrastructure.fs @@ -15,10 +15,10 @@ type FsCheckGenerators = #if STORE_POSTGRES || STORE_MSSQL || STORE_MYSQL open Equinox.SqlStreamStore #else -#if STORE_EVENTSTORE_LEGACY -open Equinox.EventStore -#else +#if !STORE_EVENTSTORE_LEGACY open Equinox.EventStoreDb +#else +open Equinox.EventStore #endif #endif diff --git a/tests/Equinox.EventStore.Integration/StoreIntegration.fs b/tests/Equinox.EventStore.Integration/StoreIntegration.fs index 8219dc098..68c71c92b 100644 --- a/tests/Equinox.EventStore.Integration/StoreIntegration.fs +++ b/tests/Equinox.EventStore.Integration/StoreIntegration.fs @@ -47,7 +47,15 @@ let connectToLocalStore (_ : ILogger) = type Context = SqlStreamStoreContext type Category<'event, 'state, 'context> = SqlStreamStoreCategory<'event, 'state, 'context> #else -#if STORE_EVENTSTORE_LEGACY +#if !STORE_EVENTSTORE_LEGACY +open Equinox.EventStoreDb + +/// Connect directly to a locally running EventStoreDB Node using gRPC, without using Gossip-driven discovery +let connectToLocalStore (_log : ILogger) = async { + let c = EventStoreConnector(reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, (*, log=Logger.SerilogVerbose log,*) tags=["I",Guid.NewGuid() |> string]) + let conn = c.Establish("Equinox-integration", Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false", ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) + return conn } +#else // STORE_EVENTSTORE_LEGACY open Equinox.EventStore // NOTE: use `docker compose up` to establish the standard 3 node config at ports 1113/2113 @@ -62,24 +70,12 @@ let connectToLocalStore log = // Connect directly to the locally running EventStore Node using Gossip-driven discovery ).Establish("Equinox-integration", Discovery.GossipDns ("localhost"), ConnectionStrategy.ClusterTwinPreferSlaveReads) #endif - -type Context = EventStoreContext -type Category<'event, 'state, 'context> = EventStoreCategory<'event, 'state, 'context> -#else // !STORE_EVENTSTORE_LEGACY -open Equinox.EventStoreDb - -/// Connect directly to a locally running EventStoreDB Node using gRPC, without using Gossip-driven discovery -let connectToLocalStore (_log : ILogger) = async { - let c = EventStoreConnector(reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, (*, log=Logger.SerilogVerbose log,*) tags=["I",Guid.NewGuid() |> string]) - let conn = c.Establish("Equinox-integration", Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false", ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) - return conn } - +#endif type Context = EventStoreContext type Category<'event, 'state, 'context> = EventStoreCategory<'event, 'state, 'context> #endif #endif #endif -#endif let createContext connection batchSize = Context(connection, BatchingPolicy(maxBatchSize = batchSize)) From 354c98bb8cea4c443f7c5041a84e7d784967cbe3 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 8 Mar 2022 15:42:24 +0000 Subject: [PATCH 20/29] Cleanup --- .../Integration/EventStoreIntegration.fs | 6 +- samples/Tutorial/AsAt.fsx | 6 +- src/Equinox.EventStoreDb/EventStoreDb.fs | 80 +------------------ .../StoreIntegration.fs | 12 ++- 4 files changed, 15 insertions(+), 89 deletions(-) diff --git a/samples/Store/Integration/EventStoreIntegration.fs b/samples/Store/Integration/EventStoreIntegration.fs index bbf88f977..394976da6 100644 --- a/samples/Store/Integration/EventStoreIntegration.fs +++ b/samples/Store/Integration/EventStoreIntegration.fs @@ -6,10 +6,8 @@ open System // NOTE: use `docker compose up` to establish the standard 3 node config at ports 1113/2113 let connectToLocalEventStoreNode (_log : Serilog.ILogger) = - // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER - EventStoreConnector(customize = (fun c -> c.ConnectivitySettings.Insecure <- true), - reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, tags=["I",Guid.NewGuid() |> string] + let c = EventStoreConnector(reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, tags=["I",Guid.NewGuid() |> string]) // Connect to the locally running EventStore Node using Gossip-driven discovery - ).Establish("Equinox-sample", Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false", ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) + c.Establish("Equinox-sample", Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false", ConnectionStrategy.ClusterSingle EventStore.Client.NodePreference.Leader) let defaultBatchSize = 500 let createContext connection batchSize = EventStoreContext(connection, BatchingPolicy(maxBatchSize = batchSize)) diff --git a/samples/Tutorial/AsAt.fsx b/samples/Tutorial/AsAt.fsx index 10f985da2..112170908 100644 --- a/samples/Tutorial/AsAt.fsx +++ b/samples/Tutorial/AsAt.fsx @@ -153,11 +153,7 @@ module EventStore = let snapshotWindow = 500 // NOTE: use `docker compose up` to establish the standard 3 node config at ports 1113/2113 - let connector = - EventStoreConnector( - // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER - customize = (fun c -> c.ConnectivitySettings.Insecure <- true), - reqTimeout = TimeSpan.FromSeconds 5., reqRetries = 3) + let connector = EventStoreConnector(reqTimeout = TimeSpan.FromSeconds 5., reqRetries = 3) let esc = connector.Connect(AppName, Discovery.ConnectionString "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false") let connection = EventStoreConnection(esc) let context = EventStoreContext(connection, BatchingPolicy(maxBatchSize=snapshotWindow)) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 0b51ea892..885501135 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -10,10 +10,6 @@ type EventBody = ReadOnlyMemory [] type Direction = Forward | Backward with override this.ToString() = match this with Forward -> "Forward" | Backward -> "Backward" - member x.op_Implicit = - match x with - | Forward -> EventStore.Client.Direction.Forwards - | Backward -> EventStore.Client.Direction.Backwards module Log = @@ -185,89 +181,18 @@ module private Write = module private Read = open FSharp.Control -(* - let private readAsyncEnum (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) - : Async> = async { - let! ct = Async.CancellationToken - return conn.ReadStreamAsync(direction.op_Implicit, streamName, startPos, int64 batchSize, resolveLinkTos = false, cancellationToken = ct) - |> AsyncSeq.ofAsyncEnum } - - let readAsyncEnumVer (conn : EventStoreClient) (streamName : string) (direction : Direction) (batchSize : int) (startPos : StreamPosition) : Async = async { - try let! res = readAsyncEnum conn streamName direction batchSize startPos - let! events = res |> AsyncSeq.toArrayAsync - let version = - match events with - | [||] when direction = Direction.Backward -> 0L // When reading backwards, the startPos is End, which is not directly convertible - | [||] -> startPos.ToInt64() // e.g. if reading forward from (verified existing) event 10 and there are none, the version is still 10 -// | xs when direction = Direction.Backward -> let le = xs.[0].Event.EventNumber in le.ToInt64() // the events arrive backwards, so first is the 'version' - | xs -> let le = xs.[xs.Length - 1].Event.EventNumber in le.ToInt64() // In normal case, the last event represents the version of the stream - return version, events - with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> return 0L, [||] } -*) let resolvedEventBytes (x : ResolvedEvent) = let Log.BlobLen bytes, Log.BlobLen metaBytes = x.Event.Data, x.Event.Metadata in bytes + metaBytes let resolvedEventsBytes events = events |> Array.sumBy resolvedEventBytes -(* - let private loggedReadAsyncEnumVer conn streamName direction batchSize startPos (log : ILogger) : Async = async { - let! t, (version, events) = readAsyncEnumVer conn streamName direction batchSize startPos |> Stopwatch.Time - let bytes, count = resolvedEventsBytes events, events.Length - let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} -// let evt = Log.Slice (direction, reqMetric) - let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" events - (log |> Log.prop "startPos" startPos |> Log.prop "bytes" bytes).Information("Ges{action:l} count={count} version={version}", - "Read", count, version) - return version, events } -*) let logBatchRead direction streamName t events batchSize version (log : ILogger) = let bytes, count = resolvedEventsBytes events, events.Length let reqMetric : Log.Measurement = { stream = streamName; interval = t; bytes = bytes; count = count} let batches = match batchSize with Some batchSize -> (events.Length - 1) / batchSize + 1 | None -> -1 let action = if direction = Direction.Forward then "LoadF" else "LoadB" + let log = if (not << log.IsEnabled) Events.LogEventLevel.Debug then log else log |> Log.propResolvedEvents "Json" events let evt = Log.Metric.Batch (direction, batches, reqMetric) (log |> Log.prop "bytes" bytes |> Log.event evt).Information( "Esdb{action:l} stream={stream} count={count}/{batches} version={version}", action, streamName, count, batches, version) -(* - let loadForwardsFrom (log : ILogger) retryPolicy conn batchSize streamName startPosition - : Async = async { - let call pos = loggedReadAsyncEnumVer conn streamName Direction.Forward batchSize pos - let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) - let direction = Direction.Forward - let log = log |> Log.prop "batchSize" batchSize |> Log.prop "direction" direction |> Log.prop "stream" streamName - let! t, (version, events) = retryingLoggingRead startPosition log |> Stopwatch.Time - log |> logBatchRead direction streamName t events batchSize version - return version, events } - - let takeWhileInclusive (predicate: 'T -> bool) (source: 'T seq) = seq { - let enumerator = source.GetEnumerator() - let mutable fin = false - while not fin && enumerator.MoveNext() do - yield enumerator.Current - if not (predicate enumerator.Current) then - fin <- true } - let mergeFromCompactionPointOrStartFromBackwardsStream streamName (tryDecode, isOrigin) (log : ILogger) (itemsBackward : ResolvedEvent[]) - : (ResolvedEvent * 'event option)[] = - itemsBackward - |> Seq.map (fun x -> x, tryDecode x) - |> takeWhileInclusive (function - | x, Some e when isOrigin e -> - log.Information("GesStop stream={stream} at={eventNumber}", streamName, x.Event.EventNumber) - false - | _ -> true) // continue the search - |> Seq.rev - |> Seq.toArray - let loadBackwardsUntilCompactionOrStart (log : ILogger) retryPolicy (conn : EventStoreClient) batchSize streamName (tryDecode, isOrigin) - : Async = async { - let call pos = loggedReadAsyncEnumVer conn streamName Direction.Backward batchSize pos - let retryingLoggingRead pos = Log.withLoggedRetries retryPolicy "readAttempt" (call pos) - let log = log |> Log.prop "batchSize" batchSize |> Log.prop "stream" streamName - let startPosition = StreamPosition.End - let direction = Direction.Backward - let readLog = log |> Log.prop "direction" direction - let! t, (version, eventsBackward) = retryingLoggingRead startPosition readLog |> Stopwatch.Time - let events = mergeFromCompactionPointOrStartFromBackwardsStream streamName (tryDecode, isOrigin) log eventsBackward - log |> logBatchRead direction streamName t (Array.map fst events) batchSize version - return version, events } -*) let loadBackwardsUntilOrigin (log : ILogger) (conn : EventStoreClient) batchSize streamName (tryDecode, isOrigin) : Async = async { let! ct = Async.CancellationToken @@ -286,7 +211,6 @@ module private Read = return v, events with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> return -1L, [||] } - let loadBackwards (log : ILogger) (conn : EventStoreClient) batchSize streamName (tryDecode, isOrigin) : Async = async { let! t, (version, events) = loadBackwardsUntilOrigin log conn batchSize streamName (tryDecode, isOrigin) |> Stopwatch.Time @@ -303,7 +227,6 @@ module private Read = return v, events with :? AggregateException as e when (e.InnerExceptions.Count = 1 && e.InnerExceptions[0] :? StreamNotFoundException) -> return -1L, [||] } - let loadForwards log conn streamName startPosition : Async = async { let direction = Direction.Forward @@ -383,6 +306,7 @@ type EventStoreConnection(readConnection, [] ?writeConnection, [ int, [] ?batchCountLimit) = // TODO batchCountLimit new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) +// TOCONSIDER remove if Client does not start to expose it member _.BatchSize = getMaxBatchSize() //TODO member _.MaxBatches = batchCountLimit diff --git a/tests/Equinox.EventStore.Integration/StoreIntegration.fs b/tests/Equinox.EventStore.Integration/StoreIntegration.fs index 68c71c92b..cd5a10ad4 100644 --- a/tests/Equinox.EventStore.Integration/StoreIntegration.fs +++ b/tests/Equinox.EventStore.Integration/StoreIntegration.fs @@ -63,12 +63,12 @@ let connectToLocalStore log = // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER EventStoreConnector("admin", "changeit", custom = (fun c -> c.DisableServerCertificateValidation()), reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, log=Logger.SerilogVerbose log, tags=["I",Guid.NewGuid() |> string] -#if EVENTSTORE_NO_CLUSTER +#if !EVENTSTORE_NO_CLUSTER // Connect directly to the locally running EventStore Node without using Gossip-driven discovery ).Establish("Equinox-integration", Discovery.Uri(Uri "tcp://localhost:1113"), ConnectionStrategy.ClusterSingle NodePreference.Master) #else // Connect directly to the locally running EventStore Node using Gossip-driven discovery - ).Establish("Equinox-integration", Discovery.GossipDns ("localhost"), ConnectionStrategy.ClusterTwinPreferSlaveReads) + ).Establish("Equinox-integration", Discovery.GossipDns "localhost", ConnectionStrategy.ClusterTwinPreferSlaveReads) #endif #endif type Context = EventStoreContext @@ -123,7 +123,11 @@ type Tests(testOutputHelper) = let addAndThenRemoveItemsOptimisticManyTimesExceptTheLastOne context cartId skuId service count = addAndThenRemoveItems true true context cartId skuId service count +#if STORE_EVENTSTOREDB // gRPC does not expose slice metrics + let sliceForward = [] +#else let sliceForward = [EsAct.SliceForward] +#endif let singleBatchForward = sliceForward @ [EsAct.BatchForward] let batchForwardAndAppend = singleBatchForward @ [EsAct.Append] @@ -232,7 +236,11 @@ type Tests(testOutputHelper) = test <@ [1; 1] = [for c in [capture1; capture2] -> c.ChooseCalls hadConflict |> List.length] @> } +#if STORE_EVENTSTOREDB // gRPC does not expose slice metrics + let sliceBackward = [] +#else let sliceBackward = [EsAct.SliceBackward] +#endif let singleBatchBackwards = sliceBackward @ [EsAct.BatchBackward] let batchBackwardsAndAppend = singleBatchBackwards @ [EsAct.Append] From e12e84c9bbe2c2897dd12b3b75bc64b11231cd23 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 8 Mar 2022 16:37:25 +0000 Subject: [PATCH 21/29] Flip to cluster --- tests/Equinox.EventStore.Integration/StoreIntegration.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Equinox.EventStore.Integration/StoreIntegration.fs b/tests/Equinox.EventStore.Integration/StoreIntegration.fs index cd5a10ad4..6b814d6a9 100644 --- a/tests/Equinox.EventStore.Integration/StoreIntegration.fs +++ b/tests/Equinox.EventStore.Integration/StoreIntegration.fs @@ -63,7 +63,7 @@ let connectToLocalStore log = // NOTE: disable cert validation for this test suite. ABSOLUTELY DO NOT DO THIS FOR ANY CODE THAT WILL EVER HIT A STAGING OR PROD SERVER EventStoreConnector("admin", "changeit", custom = (fun c -> c.DisableServerCertificateValidation()), reqTimeout=TimeSpan.FromSeconds 3., reqRetries=3, log=Logger.SerilogVerbose log, tags=["I",Guid.NewGuid() |> string] -#if !EVENTSTORE_NO_CLUSTER +#if EVENTSTORE_NO_CLUSTER // Connect directly to the locally running EventStore Node without using Gossip-driven discovery ).Establish("Equinox-integration", Discovery.Uri(Uri "tcp://localhost:1113"), ConnectionStrategy.ClusterSingle NodePreference.Master) #else From 4ddc2a9e219a38baceb37926d10d131ec6bea9ee Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 9 Mar 2022 01:03:57 +0000 Subject: [PATCH 22/29] Formatting --- src/Equinox.EventStoreDb/EventStoreDb.fs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 885501135..9d3ecd4fa 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -318,7 +318,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = let tryIsResolvedEventEventType predicateOption = predicateOption |> Option.map isResolvedEventEventType member _.TokenEmpty = Token.ofUncompactedVersion batching.BatchSize -1L - member _.LoadBatched streamName log (tryDecode, isCompactionEventType) : Async = async { + member _.LoadBatched(streamName, log, tryDecode, isCompactionEventType) : Async = async { let! version, events = Read.loadForwards log conn.ReadConnection streamName StreamPosition.Start match tryIsResolvedEventEventType isCompactionEventType with | None -> return Token.ofNonCompacting version, Array.choose tryDecode events @@ -333,7 +333,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = | None -> return Token.ofUncompactedVersion batching.BatchSize version, Array.choose snd events | Some (resolvedEvent, _) -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose snd events } - member _.LoadFromToken useWriteConn streamName log (Token.Unpack token as streamToken) (tryDecode, isCompactionEventType) + member _.LoadFromToken(useWriteConn, streamName, log, (Token.Unpack token as streamToken), tryDecode, isCompactionEventType) : Async = async { let streamPosition = StreamPosition.FromInt64(token.streamVersion + 1L) let connToUse = if useWriteConn then conn.WriteConnection else conn.ReadConnection @@ -345,7 +345,7 @@ type EventStoreContext(conn : EventStoreConnection, batching : BatchingPolicy) = | None -> return Token.ofPreviousTokenAndEventsLength streamToken events.Length batching.BatchSize version, Array.choose tryDecode events | Some resolvedEvent -> return Token.ofCompactionResolvedEventAndVersion resolvedEvent batching.BatchSize version, Array.choose tryDecode events } - member _.TrySync log streamName (Token.Unpack token as streamToken) (events, encodedEvents : EventData array) isCompactionEventType : Async = async { + member _.TrySync(log, streamName, (Token.Unpack token as streamToken), events, encodedEvents : EventData array, isCompactionEventType): Async = async { let streamVersion = token.streamVersion let! wr = Write.writeEvents log conn.WriteRetryPolicy conn.WriteConnection streamName streamVersion encodedEvents match wr with @@ -405,7 +405,7 @@ type private Category<'event, 'state, 'context>(context : EventStoreContext, cod | Some (AccessStrategy.RollingSnapshots (isValid, _)) -> isValid let loadAlgorithm load streamName initial log = - let batched = load initial (context.LoadBatched streamName log (tryDecode, None)) + let batched = load initial (context.LoadBatched(streamName, log, tryDecode, None)) let compacted limit = load initial (context.LoadBackwardsStoppingAtCompactionEvent(streamName, log, limit, (tryDecode, isOrigin))) match access with | None -> batched @@ -416,11 +416,11 @@ type private Category<'event, 'state, 'context>(context : EventStoreContext, cod let! token, events = f return token, fold initial events } - member _.Load (fold : 'state -> 'event seq -> 'state) (initial : 'state) (streamName : string) (log : ILogger) : Async = + member _.Load(fold : 'state -> 'event seq -> 'state, initial : 'state, streamName : string, log : ILogger) : Async = loadAlgorithm (load fold) streamName initial log - member _.LoadFromToken (fold : 'state -> 'event seq -> 'state) (state : 'state) (streamName : string) token (log : ILogger) : Async = - (load fold) state (context.LoadFromToken false streamName log token (tryDecode, compactionPredicate)) + member _.LoadFromToken(fold : 'state -> 'event seq -> 'state, state : 'state, streamName : string, token, log : ILogger) : Async = + (load fold) state (context.LoadFromToken(false, streamName, log, token, tryDecode, compactionPredicate)) member _.TrySync<'context> ( log : ILogger, fold : 'state -> 'event seq -> 'state, @@ -434,10 +434,10 @@ type private Category<'event, 'state, 'context>(context : EventStoreContext, cod if cc.IsCompactionDue then events @ [fold state events |> compact] else events let encodedEvents : EventData[] = events |> Seq.map (encode >> UnionEncoderAdapters.eventDataOfEncodedEvent) |> Array.ofSeq - let! syncRes = context.TrySync log streamName streamToken (events, encodedEvents) compactionPredicate + let! syncRes = context.TrySync(log, streamName, streamToken, events, encodedEvents, compactionPredicate) match syncRes with | GatewaySyncResult.ConflictUnknown _ -> - return SyncResult.Conflict (load fold state (context.LoadFromToken true streamName log streamToken (tryDecode, compactionPredicate))) + return SyncResult.Conflict (load fold state (context.LoadFromToken(true, streamName, log, streamToken, tryDecode, compactionPredicate))) | GatewaySyncResult.Written token' -> return SyncResult.Written (token', fold state (Seq.ofList events)) } @@ -489,7 +489,7 @@ module Caching = CategoryTee<'event, 'state, 'context>(category, addOrUpdateFixedLifetimeCacheEntry) :> _ type private Folder<'event, 'state, 'context>(category : Category<'event, 'state, 'context>, fold : 'state -> 'event seq -> 'state, initial : 'state, ?readCache) = - let batched log streamName = category.Load fold initial streamName log + let batched log streamName = category.Load(fold, initial, streamName, log) interface ICategory<'event, 'state, string, 'context> with member _.Load(log, streamName, allowStale) : Async = match readCache with @@ -498,7 +498,7 @@ type private Folder<'event, 'state, 'context>(category : Category<'event, 'state match! cache.TryGet(prefix + streamName) with | None -> return! batched log streamName | Some tokenAndState when allowStale -> return tokenAndState - | Some (token, state) -> return! category.LoadFromToken fold state streamName token log } + | Some (token, state) -> return! category.LoadFromToken(fold, state, streamName, token, log) } member _.TrySync(log : ILogger, streamName, token, initialState, events : 'event list, context) : Async> = async { let! syncRes = category.TrySync(log, fold, streamName, token, initialState, events, context) From 66b5ef780fbd1d0b718313f292362e10ddec43b8 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 10 Mar 2022 09:55:03 +0000 Subject: [PATCH 23/29] tmp --- samples/Tutorial/AsAt.fsx | 20 +++++++++---------- samples/Tutorial/FulfilmentCenter.fsx | 3 ++- .../Equinox.EventStoreDb.fsproj | 4 +--- src/Equinox.EventStoreDb/EventStoreDb.fs | 2 +- .../Equinox.EventStore.Integration.fsproj | 2 -- .../Equinox.EventStoreDb.Integration.fsproj | 2 -- 6 files changed, 13 insertions(+), 20 deletions(-) diff --git a/samples/Tutorial/AsAt.fsx b/samples/Tutorial/AsAt.fsx index 112170908..bd54bca92 100644 --- a/samples/Tutorial/AsAt.fsx +++ b/samples/Tutorial/AsAt.fsx @@ -11,7 +11,7 @@ // - the same general point applies to over-using querying of streams for read purposes as we do here; // applying CQRS principles can often lead to a better model regardless of raw necessity -#if LOCAL +#if !LOCAL // Compile Tutorial.fsproj by either a) right-clicking or b) typing // dotnet build samples/Tutorial before attempting to send this to FSI with Alt-Enter #if VISUALSTUDIO @@ -24,19 +24,17 @@ #r "System.Configuration.ConfigurationManager.dll" #r "Equinox.Core.dll" #r "Newtonsoft.Json.dll" -#r "FSharp.UMX.dll" +//#r "FSharp.UMX.dll" #r "FsCodec.dll" #r "Equinox.dll" #r "TypeShape.dll" #r "FsCodec.SystemTextJson.dll" -#r "FSharp.Control.AsyncSeq.dll" -#r "System.Net.Http" -#r "Serilog.Sinks.Seq.dll" +//#r "FSharp.Control.AsyncSeq.dll" +//#r "System.Net.Http" +//#r "EventStore.Client.dll" +//#r "EventStore.Client.Streams.dll" #r "Equinox.EventStoreDb.dll" -#r "EventStore.Client.dll" -#r "EventStore.Client.Streams.dll" -#r "Microsoft.Azure.Cosmos.Direct.dll" -#r "Microsoft.Azure.Cosmos.Client.dll" +//#r "Microsoft.Azure.Cosmos.Client.dll" #r "Equinox.CosmosStore.dll" #else #r "nuget:Serilog.Sinks.Console" @@ -178,8 +176,8 @@ module Cosmos = let category = CosmosStoreCategory(context, Events.codecJe, Fold.fold, Fold.initial, cacheStrategy, accessStrategy) let resolve id = Equinox.Decider(Log.log, category.Resolve(streamName id), maxAttempts = 3) -//let serviceES = Service(EventStore.resolve) -let service= Service(Cosmos.resolve) +let service = Service(EventStore.resolve) +//let service= Service(Cosmos.resolve) let client = "ClientA" service.Add(client, 1) |> Async.RunSynchronously diff --git a/samples/Tutorial/FulfilmentCenter.fsx b/samples/Tutorial/FulfilmentCenter.fsx index ea9f6a566..17d832dba 100644 --- a/samples/Tutorial/FulfilmentCenter.fsx +++ b/samples/Tutorial/FulfilmentCenter.fsx @@ -1,4 +1,4 @@ -#if LOCAL +#if !LOCAL #I "bin/Debug/net6.0/" #r "Serilog.dll" #r "Serilog.Sinks.Console.dll" @@ -142,6 +142,7 @@ module Store = open FulfilmentCenter +open FsCodec.SystemTextJson let category = CosmosStoreCategory(Store.context, Events.codec, Fold.fold, Fold.initial, Store.cacheStrategy, AccessStrategy.Unoptimized) let resolve id = Equinox.Decider(Log.log, category.Resolve(streamName id), maxAttempts = 3) let service = Service(resolve) diff --git a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj index d40095b64..a1c85d45c 100644 --- a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj +++ b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj @@ -1,9 +1,7 @@  - netcoreapp3.1 - 5 - false + net6.0 true true diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 9d3ecd4fa..274276e6d 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -680,7 +680,7 @@ type EventStoreConnector match customize with None -> () | Some f -> f settings settings.DefaultDeadline <- reqTimeout // TODO implement reqRetries - EventStoreClient(settings) + new EventStoreClient(settings) /// Yields a Connection (which may internally be twin connections) configured per the specified strategy member x.Establish diff --git a/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj b/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj index f6d92fbd6..be82aaf12 100644 --- a/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj +++ b/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj @@ -2,8 +2,6 @@ net6.0 - false - 5 $(DefineConstants);STORE_EVENTSTORE_LEGACY diff --git a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj index 1b9b0b165..d64eca76c 100644 --- a/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj +++ b/tests/Equinox.EventStoreDb.Integration/Equinox.EventStoreDb.Integration.fsproj @@ -2,8 +2,6 @@ net6.0 - false - 5 $(DefineConstants);STORE_EVENTSTOREDB From 87950decaedda97f753ce46959b6363893ddf903 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 10 Mar 2022 12:24:05 +0000 Subject: [PATCH 24/29] Share Cache with other ES-like stores --- src/Equinox.Core/Equinox.Core.fsproj | 1 - .../Equinox.EventStore.fsproj | 1 + .../Caching.fs | 0 .../Equinox.EventStoreDb.fsproj | 1 + src/Equinox.EventStoreDb/EventStoreDb.fs | 56 +------------------ .../Equinox.SqlStreamStore.fsproj | 1 + 6 files changed, 6 insertions(+), 54 deletions(-) rename src/{Equinox.Core => Equinox.EventStoreDb}/Caching.fs (100%) diff --git a/src/Equinox.Core/Equinox.Core.fsproj b/src/Equinox.Core/Equinox.Core.fsproj index ea822571d..c5aad3c35 100644 --- a/src/Equinox.Core/Equinox.Core.fsproj +++ b/src/Equinox.Core/Equinox.Core.fsproj @@ -14,7 +14,6 @@ - diff --git a/src/Equinox.EventStore/Equinox.EventStore.fsproj b/src/Equinox.EventStore/Equinox.EventStore.fsproj index 16ce1fbc7..e32cb83f9 100644 --- a/src/Equinox.EventStore/Equinox.EventStore.fsproj +++ b/src/Equinox.EventStore/Equinox.EventStore.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Equinox.Core/Caching.fs b/src/Equinox.EventStoreDb/Caching.fs similarity index 100% rename from src/Equinox.Core/Caching.fs rename to src/Equinox.EventStoreDb/Caching.fs diff --git a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj index a1c85d45c..48347e3ad 100644 --- a/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj +++ b/src/Equinox.EventStoreDb/Equinox.EventStoreDb.fsproj @@ -8,6 +8,7 @@ + diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index 274276e6d..c43b17656 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -441,53 +441,6 @@ type private Category<'event, 'state, 'context>(context : EventStoreContext, cod | GatewaySyncResult.Written token' -> return SyncResult.Written (token', fold state (Seq.ofList events)) } -module Caching = - /// Forwards all state changes in all streams of an ICategory to a `tee` function - type CategoryTee<'event, 'state, 'context>(inner : ICategory<'event, 'state, string, 'context>, tee : string -> StreamToken * 'state -> Async) = - let intercept streamName tokenAndState = async { - let! _ = tee streamName tokenAndState - return tokenAndState } - - let loadAndIntercept load streamName = async { - let! tokenAndState = load - return! intercept streamName tokenAndState } - - interface ICategory<'event, 'state, string, 'context> with - member _.Load(log, streamName : string, opt) : Async = - loadAndIntercept (inner.Load(log, streamName, opt)) streamName - - member _.TrySync(log : ILogger, stream, token, state, events : 'event list, context) : Async> = async { - let! syncRes = inner.TrySync(log, stream, token, state, events, context) - match syncRes with - | SyncResult.Conflict resync -> return SyncResult.Conflict (loadAndIntercept resync stream) - | SyncResult.Written (token', state') -> - let! intercepted = intercept stream (token', state') - return SyncResult.Written intercepted } - - let applyCacheUpdatesWithSlidingExpiration - (cache : ICache) - (prefix : string) - (slidingExpiration : TimeSpan) - (category : ICategory<'event, 'state, string, 'context>) - : ICategory<'event, 'state, string, 'context> = - let mkCacheEntry (initialToken : StreamToken, initialState : 'state) = new CacheEntry<'state>(initialToken, initialState, Token.supersedes) - let options = CacheItemOptions.RelativeExpiration slidingExpiration - let addOrUpdateSlidingExpirationCacheEntry streamName value = cache.UpdateIfNewer(prefix + streamName, options, mkCacheEntry value) - CategoryTee<'event, 'state, 'context>(category, addOrUpdateSlidingExpirationCacheEntry) :> _ - - let applyCacheUpdatesWithFixedTimeSpan - (cache : ICache) - (prefix : string) - (period : TimeSpan) - (category : ICategory<'event, 'state, string, 'context>) - : ICategory<'event, 'state, string, 'context> = - let mkCacheEntry (initialToken : StreamToken, initialState : 'state) = CacheEntry<'state>(initialToken, initialState, Token.supersedes) - let addOrUpdateFixedLifetimeCacheEntry streamName value = - let expirationPoint = let creationDate = DateTimeOffset.UtcNow in creationDate.Add period - let options = CacheItemOptions.AbsoluteExpiration expirationPoint - cache.UpdateIfNewer(prefix + streamName, options, mkCacheEntry value) - CategoryTee<'event, 'state, 'context>(category, addOrUpdateFixedLifetimeCacheEntry) :> _ - type private Folder<'event, 'state, 'context>(category : Category<'event, 'state, 'context>, fold : 'state -> 'event seq -> 'state, initial : 'state, ?readCache) = let batched log streamName = category.Load(fold, initial, streamName, log) interface ICategory<'event, 'state, string, 'context> with @@ -536,7 +489,6 @@ type EventStoreCategory<'event, 'state, 'context> + "mixing AccessStrategy.LatestKnownEvent with Caching at present." |> invalidOp | _ -> () - let inner = Category<'event, 'state, 'context>(context, codec, ?access = access) let readCacheOption = match caching with @@ -544,18 +496,16 @@ type EventStoreCategory<'event, 'state, 'context> | Some (CachingStrategy.SlidingWindow (cache, _)) | Some (CachingStrategy.FixedTimeSpan (cache, _)) -> Some (cache, null) | Some (CachingStrategy.SlidingWindowPrefixed (cache, _, prefix)) -> Some (cache, prefix) - let folder = Folder<'event, 'state, 'context>(inner, fold, initial, ?readCache = readCacheOption) - let category : ICategory<_, _, _, 'context> = match caching with | None -> folder :> _ | Some (CachingStrategy.SlidingWindow (cache, window)) -> - Caching.applyCacheUpdatesWithSlidingExpiration cache null window folder + Caching.applyCacheUpdatesWithSlidingExpiration cache null window folder Token.supersedes | Some (CachingStrategy.FixedTimeSpan (cache, period)) -> - Caching.applyCacheUpdatesWithFixedTimeSpan cache null period folder + Caching.applyCacheUpdatesWithFixedTimeSpan cache null period folder Token.supersedes | Some (CachingStrategy.SlidingWindowPrefixed (cache, window, prefix)) -> - Caching.applyCacheUpdatesWithSlidingExpiration cache prefix window folder + Caching.applyCacheUpdatesWithSlidingExpiration cache prefix window folder Token.supersedes let resolve streamName = category, FsCodec.StreamName.toString streamName, None let empty = context.TokenEmpty, initial let storeCategory = StoreCategory(resolve, empty) diff --git a/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj b/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj index a85dadf82..48631f220 100644 --- a/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj +++ b/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj @@ -8,6 +8,7 @@ + From 30d51aa947bda86f0fb95ba6aec92c3ce5b7790b Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 20 Mar 2022 14:02:32 +0000 Subject: [PATCH 25/29] Cleanup ResolvedEvent mapping --- src/Equinox.EventStoreDb/EventStoreDb.fs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index c43b17656..ff7e41549 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -238,18 +238,15 @@ module private Read = module UnionEncoderAdapters = let encodedEventOfResolvedEvent (x : ResolvedEvent) : FsCodec.ITimelineEvent = let e = x.Event - // Inspecting server code shows both Created and CreatedEpoch are set; taking this as it's less ambiguous than DateTime in the general case - let ts = DateTimeOffset(e.Created) - // TODO something like let ts = DateTimeOffset.FromUnixTimeMilliseconds(e.CreatedEpoch) - // TOCONSIDER wire e.Metadata.["$correlationId"] and .["$causationId"] into correlationId and causationId + // TOCONSIDER wire e.Metadata["$correlationId"] and ["$causationId"] into correlationId and causationId // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - let n = e.EventNumber - FsCodec.Core.TimelineEvent.Create(n.ToInt64(), e.EventType, e.Data, e.Metadata, correlationId = null, causationId = null, timestamp = ts) + let n, eu, ts = e.EventNumber, e.EventId, DateTimeOffset e.Created + FsCodec.Core.TimelineEvent.Create(n.ToInt64(), e.EventType, e.Data, e.Metadata, eu.ToGuid(), correlationId = null, causationId = null, timestamp = ts) let eventDataOfEncodedEvent (x : FsCodec.IEventData) = // TOCONSIDER wire x.CorrelationId, x.CausationId into x.Meta.["$correlationId"] and .["$causationId"] // https://eventstore.org/docs/server/metadata-and-reserved-names/index.html#event-metadata - EventData(Uuid.NewUuid(), x.EventType, contentType = "application/json", data = x.Data, metadata = x.Meta) + EventData(Uuid.FromGuid x.EventId, x.EventType, contentType = "application/json", data = x.Data, metadata = x.Meta) type Position = { streamVersion : int64; compactionEventNumber : int64 option; batchCapacityLimit : int option } type Token = { pos : Position } From 6cd963947aea8af8c19a5f68f74970cde608feb6 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 17 Mar 2022 23:20:49 +0000 Subject: [PATCH 26/29] Relax netstandard requirements --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd48b16b..ac2f2bcb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ The `Unreleased` section name is replaced by the expected version of next releas ### Removed -- Remove explicit `net461` handling; minimum target now `netstandard 2.1` / `net6.0` / `FSharp.Core` v `4.5.4` [#310](https://github.com/jet/equinox/pull/310) [#323](https://github.com/jet/equinox/pull/323) +- Remove explicit `net461` handling; minimum target now `netstandard 2.0`/`1` / `net6.0` / `FSharp.Core` v `4.5.4` [#310](https://github.com/jet/equinox/pull/310) [#323](https://github.com/jet/equinox/pull/323) ### Fixed From bc7379c362f2ecff938a1ca06919429cb3afd403 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 13 Apr 2022 18:44:50 +0100 Subject: [PATCH 27/29] Tidy --- CHANGELOG.md | 2 +- README.md | 6 +++--- samples/Infrastructure/Storage.fs | 13 ++++++------- samples/Tutorial/AsAt.fsx | 2 +- samples/Tutorial/FulfilmentCenter.fsx | 1 - 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2f2bcb2..fbd48b16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ The `Unreleased` section name is replaced by the expected version of next releas ### Removed -- Remove explicit `net461` handling; minimum target now `netstandard 2.0`/`1` / `net6.0` / `FSharp.Core` v `4.5.4` [#310](https://github.com/jet/equinox/pull/310) [#323](https://github.com/jet/equinox/pull/323) +- Remove explicit `net461` handling; minimum target now `netstandard 2.1` / `net6.0` / `FSharp.Core` v `4.5.4` [#310](https://github.com/jet/equinox/pull/310) [#323](https://github.com/jet/equinox/pull/323) ### Fixed diff --git a/README.md b/README.md index 440b334aa..221db657a 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ _If you're looking to learn more about and/or discuss Event Sourcing and it's my # Currently Supported Data Stores - [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db): contains some fragments of code dating back to 2016, however [the storage model](DOCUMENTATION.md#Cosmos-Storage-Model) was arrived at based on intensive benchmarking (squash-merged in [#42](https://github.com/jet/equinox/pull/42)). The V2 and V3 release lines are being used in production systems. (The V3 release provides support for significantly more efficient packing of events ([storing events in the 'Tip'](https://github.com/jet/equinox/pull/251))). -- [EventStoreDB](https://eventstore.org/): this codebase itself has been in production since 2017 (see commit history), with key elements dating back to approx 2016. +- [EventStoreDB](https://eventstore.org/): this codebase itself has been in production since 2017 (see commit history), with key elements dating back to approx 2016. Current versions require EventStoreDB Server editions `21.10` or later, and communicate over the modern gRPC interface. - [SqlStreamStore](https://github.com/SQLStreamStore/SQLStreamStore): bindings for the powerful and widely used SQL-backed Event Storage system, derived from the EventStoreDB adapter. [See SqlStreamStore docs](https://sqlstreamstore.readthedocs.io/en/latest/#introduction). :pray: [@rajivhost](https://github.com/rajivhost) - `MemoryStore`: In-memory store (volatile, for unit or integration test purposes). Fulfils the full contract Equinox imposes on a store, but without I/O costs [(it's ~100 LOC wrapping a `ConcurrentDictionary`)](https://github.com/jet/equinox/blob/master/src/Equinox.MemoryStore/MemoryStore.fs). Also enables [take serialization/deserialization out of the picture](https://github.com/jet/FsCodec#boxcodec) in tests. @@ -137,7 +137,7 @@ The components within this repository are delivered as multi-targeted Nuget pack - `Equinox.CosmosStore` [![CosmosStore NuGet](https://img.shields.io/nuget/v/Equinox.CosmosStore.svg)](https://www.nuget.org/packages/Equinox.CosmosStore/): Azure CosmosDB Adapter with integrated 'unfolds' feature, facilitating optimal read performance in terms of latency and RU costs, instrumented to meet Jet's production monitoring requirements. ([depends](https://www.fuget.org/packages/Equinox.CosmosStore) on `Equinox.Core`, `Microsoft.Azure.Cosmos >= 3.25`, `FsCodec`, `System.Text.Json`, `FSharp.Control.AsyncSeq >= 2.0.23`) - `Equinox.CosmosStore.Prometheus` [![CosmosStore.Prometheus NuGet](https://img.shields.io/nuget/v/Equinox.CosmosStore.Prometheus.svg)](https://www.nuget.org/packages/Equinox.CosmosStore.Prometheus/): Integration package providing a `Serilog.Core.ILogEventSink` that extracts detailed metrics information attached to the `LogEvent`s and feeds them to the `prometheus-net`'s `Prometheus.Metrics` static instance. ([depends](https://www.fuget.org/packages/Equinox.CosmosStore.Prometheus) on `Equinox.CosmosStore`, `prometheus-net >= 3.6.0`) - `Equinox.EventStore` [![EventStore NuGet](https://img.shields.io/nuget/v/Equinox.EventStore.svg)](https://www.nuget.org/packages/Equinox.EventStore/): [EventStoreDB](https://eventstore.org/) Adapter designed to meet Jet's production monitoring requirements. ([depends](https://www.fuget.org/packages/Equinox.EventStore) on `Equinox.Core`, `EventStore.Client >= 22.0.0-preview`, `FSharp.Control.AsyncSeq >= 2.0.23`), EventStore Server version `21.10` or later) -- `Equinox.EventStoreDb` [![EventStoreDb NuGet](https://img.shields.io/nuget/v/Equinox.EventStoreDb.svg)](https://www.nuget.org/packages/Equinox.EventStoreDb/): Production-strength [EventStoreDB](https://eventstore.org/) Adapter. ([depends](https://www.fuget.org/packages/Equinox.EventStoreDb) on `Equinox.Core`, `EventStore.Client.Grpc.Streams` >= `22.0.0`, `FSharp.Control.AsyncSeq` v `2.0.23`, EventStore Server version `21.10` or later) +- `Equinox.EventStoreDb` [![EventStoreDb NuGet](https://img.shields.io/nuget/v/Equinox.EventStoreDb.svg)](https://www.nuget.org/packages/Equinox.EventStoreDb/): Production-strength [EventStoreDB](https://eventstore.org/) Adapter. ([depends](https://www.fuget.org/packages/Equinox.EventStoreDb) on `Equinox.Core`, `EventStore.Client.Grpc.Streams` >= `22.0.0, `FSharp.Control.AsyncSeq` v `2.0.23`, EventStore Server version `21.10` or later) - `Equinox.SqlStreamStore` [![SqlStreamStore NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore/): [SqlStreamStore](https://github.com/SQLStreamStore/SQLStreamStore) Adapter derived from `Equinox.EventStore` - provides core facilities (but does not connect to a specific database; see sibling `SqlStreamStore`.* packages). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore) on `Equinox.Core`, `FsCodec`, `SqlStreamStore >= 1.2.0-beta.8`, `FSharp.Control.AsyncSeq`) - `Equinox.SqlStreamStore.MsSql` [![MsSql NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.MsSql.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore.MsSql/): [SqlStreamStore.MsSql](https://sqlstreamstore.readthedocs.io/en/latest/sqlserver) Sql Server `Connector` implementation for `Equinox.SqlStreamStore` package). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore.MsSql) on `Equinox.SqlStreamStore`, `SqlStreamStore.MsSql >= 1.2.0-beta.8`) - `Equinox.SqlStreamStore.MySql` [![MySql NuGet](https://img.shields.io/nuget/v/Equinox.SqlStreamStore.MySql.svg)](https://www.nuget.org/packages/Equinox.SqlStreamStore.MySql/): `SqlStreamStore.MySql` MySQL `Connector` implementation for `Equinox.SqlStreamStore` package). ([depends](https://www.fuget.org/packages/Equinox.SqlStreamStore.MySql) on `Equinox.SqlStreamStore`, `SqlStreamStore.MySql >= 1.2.0-beta.8`) @@ -152,6 +152,7 @@ Equinox does not focus on projection logic - each store brings its own strengths - `Propulsion.Cosmos` [![Propulsion.Cosmos NuGet](https://img.shields.io/nuget/v/Propulsion.Cosmos.svg)](https://www.nuget.org/packages/Propulsion.Cosmos/): Wraps the [Microsoft .NET `ChangeFeedProcessor` library](https://github.com/Azure/azure-documentdb-changefeedprocessor-dotnet) providing a [processor loop](DOCUMENTATION.md#change-feed-processors) that maintains a continuous query loop per CosmosDB Physical Partition (Range) yielding new or updated documents (optionally unrolling events written by `Equinox.CosmosStore` for processing or forwarding). ([depends](https://www.fuget.org/packages/Propulsion.Cosmos) on `Equinox.Cosmos`, `Microsoft.Azure.DocumentDb.ChangeFeedProcessor >= 2.2.5`) - `Propulsion.CosmosStore` [![Propulsion.CosmosStore NuGet](https://img.shields.io/nuget/v/Propulsion.CosmosStore.svg)](https://www.nuget.org/packages/Propulsion.CosmosStore/): Wraps the CosmosDB V3 SDK's Change Feed API, providing a [processor loop](DOCUMENTATION.md#change-feed-processors) that maintains a continuous query loop per CosmosDB Physical Partition (Range) yielding new or updated documents (optionally unrolling events written by `Equinox.CosmosStore` for processing or forwarding). Used in the [`propulsion project stats cosmos`](dotnet-tool-provisioning--benchmarking-tool) tool command; see [`dotnet new proProjector` to generate a sample app](#quickstart) using it. ([depends](https://www.fuget.org/packages/Propulsion.CosmosStore) on `Equinox.CosmosStore`) - `Propulsion.EventStore` [![Propulsion.EventStore NuGet](https://img.shields.io/nuget/v/Propulsion.EventStore.svg)](https://www.nuget.org/packages/Propulsion.EventStore/) Used in the [`propulsion project es`](dotnet-tool-provisioning--benchmarking-tool) tool command; see [`dotnet new proSync` to generate a sample app](#quickstart) using it. ([depends](https://www.fuget.org/packages/Propulsion.EventStore) on `Equinox.EventStore`) +- `Propulsion.EventStoreDb` [![Propulsion.EventStoreDb NuGet](https://img.shields.io/nuget/v/Propulsion.EventStoreDb.svg)](https://www.nuget.org/packages/Propulsion.EventStoreDb/) Consumes from `EventStoreDB` v `21.10` or later using the gRPC interface. ([depends](https://www.fuget.org/packages/Propulsion.EventStoreDb) on `Equinox.EventStoreDb`) - `Propulsion.Kafka` [![Propulsion.Kafka NuGet](https://img.shields.io/nuget/v/Propulsion.Kafka.svg)](https://www.nuget.org/packages/Propulsion.Kafka/): Provides a canonical `RenderedSpan` that can be used as a default format when projecting events via e.g. the Producer/Consumer pair in `dotnet new proProjector -k; dotnet new proConsumer`. ([depends](https://www.fuget.org/packages/Propulsion.Kafka) on `Newtonsoft.Json >= 11.0.2`, `Propulsion`, `FsKafka`) ## `dotnet tool` provisioning / benchmarking tool @@ -598,7 +599,6 @@ All non-alpha releases derive from tagged commits on `master`. The tag defines t - :cry: the Azure Pipelines script does not run the integration tests, so these need to be run manually via the following steps: - [Provision](#provisioning): - - Start Local EventStore running in simulated cluster mode - Set environment variables x 4 for a CosmosDB database and container (you might need to `eqx init`) - Add a `EQUINOX_COSMOS_CONTAINER_ARCHIVE` environment variable referencing a separate (`eqx init` initialized) CosmosDB Container that will be used to house fallback events in the [Fallback mechanism's tests](https://github.com/jet/equinox/pull/247) - `docker-compose up` to start diff --git a/samples/Infrastructure/Storage.fs b/samples/Infrastructure/Storage.fs index 42d8eb4f3..7d88135c1 100644 --- a/samples/Infrastructure/Storage.fs +++ b/samples/Infrastructure/Storage.fs @@ -128,6 +128,9 @@ module Cosmos = /// To establish a local node to run the tests against, follow https://developers.eventstore.com/server/v21.10/installation.html#use-docker-compose /// and/or do `docker compose up` in github.com/jet/equinox module EventStore = + + open Equinox.EventStoreDb + type [] Arguments = | [] VerboseStore | [] Timeout of float @@ -147,11 +150,9 @@ module EventStore = | ConcurrentOperationsLimit _ -> "max concurrent operations in flight (default: 5000)." | HeartbeatTimeout _ -> "specify heartbeat timeout in seconds (default: 1.5)." | MaxEvents _ -> "Maximum number of Events to request per batch. Default 500." - open Equinox.EventStoreDb - type Info(args : ParseResults) = - member _.Host = args.GetResult(ConnectionString, "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false") - member _.Credentials = args.GetResult(Credentials, null) + member _.Host = args.GetResult(ConnectionString, "esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false") + member _.Credentials = args.GetResult(Credentials, null) member _.Timeout = args.GetResult(Timeout,5.) |> TimeSpan.FromSeconds member _.Retries = args.GetResult(Retries, 1) @@ -159,12 +160,10 @@ module EventStore = member _.ConcurrentOperationsLimit = args.GetResult(ConcurrentOperationsLimit,5000) member _.MaxEvents = args.GetResult(MaxEvents, 500) - open Serilog - let private connect (log: ILogger) (connectionString, heartbeatTimeout, col) credentialsString (operationTimeout, operationRetries) = EventStoreConnector(reqTimeout=operationTimeout, reqRetries=operationRetries, // TODO heartbeatTimeout=heartbeatTimeout, concurrentOperationsLimit = col, - // TODO log=(if log.IsEnabled(Serilog.Events.LogEventLevel.Debug) then Logger.SerilogVerbose log else Logger.SerilogNormal log), + // TODO log=(if log.IsEnabled(Events.LogEventLevel.Debug) then Logger.SerilogVerbose log else Logger.SerilogNormal log), tags=["M", Environment.MachineName; "I", Guid.NewGuid() |> string]) .Establish(appName, Discovery.ConnectionString (String.Join(";", connectionString, credentialsString)), ConnectionStrategy.ClusterTwinPreferSlaveReads) let private createContext connection batchSize = EventStoreContext(connection, BatchingPolicy(maxBatchSize = batchSize)) diff --git a/samples/Tutorial/AsAt.fsx b/samples/Tutorial/AsAt.fsx index bd54bca92..3aa9c33c2 100644 --- a/samples/Tutorial/AsAt.fsx +++ b/samples/Tutorial/AsAt.fsx @@ -24,7 +24,7 @@ #r "System.Configuration.ConfigurationManager.dll" #r "Equinox.Core.dll" #r "Newtonsoft.Json.dll" -//#r "FSharp.UMX.dll" +#r "FSharp.UMX.dll" #r "FsCodec.dll" #r "Equinox.dll" #r "TypeShape.dll" diff --git a/samples/Tutorial/FulfilmentCenter.fsx b/samples/Tutorial/FulfilmentCenter.fsx index 17d832dba..556ec4c5a 100644 --- a/samples/Tutorial/FulfilmentCenter.fsx +++ b/samples/Tutorial/FulfilmentCenter.fsx @@ -142,7 +142,6 @@ module Store = open FulfilmentCenter -open FsCodec.SystemTextJson let category = CosmosStoreCategory(Store.context, Events.codec, Fold.fold, Fold.initial, Store.cacheStrategy, AccessStrategy.Unoptimized) let resolve id = Equinox.Decider(Log.log, category.Resolve(streamName id), maxAttempts = 3) let service = Service(resolve) From 1f495592e8b5d2751dc69fc2c60785146c0eb220 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 13 Apr 2022 20:54:36 +0100 Subject: [PATCH 28/29] Changelog --- CHANGELOG.md | 2 ++ DOCUMENTATION.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd48b16b..8e2418faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The `Unreleased` section name is replaced by the expected version of next releas - `eqx dump`/`Equinox.Tool`: Show payload statistics [#323](https://github.com/jet/equinox/pull/323) - `eqx dump`/`Equinox.Tool`: Add `-B` option to prevent assuming UTF-8 bodies [#323](https://github.com/jet/equinox/pull/323) - `Equinox`: `Decider.Transact(interpret : 'state -> Async<'event list>)` [#314](https://github.com/jet/equinox/pull/314) +- `EventStoreDb`: As per `EventStore` module, but using the modern `EventStore.Client.Grpc.Streams` client [#196](https://github.com/jet/equinox/pull/196) ### Changed @@ -26,6 +27,7 @@ The `Unreleased` section name is replaced by the expected version of next releas - `CosmosStore`: Switch to natively using `System.Text.Json` for serialization of all `Microsoft.Azure.Cosmos` round-trips [#305](https://github.com/jet/equinox/pull/305) :pray: [@ylibrach](https://github.com/ylibrach) - `CosmosStore`: Only log `bytes` when log level is `Debug` [#305](https://github.com/jet/equinox/pull/305) - `EventStore`: Target `EventStore.Client` v `22.0.0-preview`; rename `Connector` -> `EventStoreConnector` [#317](https://github.com/jet/equinox/pull/317) +- `Equinox.Tool`/`samples/`: switched to use `Equinox.EventStoreDb` [#196](https://github.com/jet/equinox/pull/196) - Update all non-Client dependencies except `FSharp.Core`, `FSharp.Control.AsyncSeq` [#310](https://github.com/jet/equinox/pull/310) - Update all Stores to use `FsCodec` v `3.0.0`, with [`EventBody` types switching from `byte[]` to `ReadOnlyMemory`, see FsCodec#75](https://github.com/jet/FsCodec/pull/75) [#323](https://github.com/jet/equinox/pull/323) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index bc94dec90..7d847163a 100755 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2232,7 +2232,7 @@ rich relative to the need of consumers to date. Some things remain though: - Provide a low level walking events in F# API akin to `Equinox.CosmosStore.Core.Events`; this would allow consumers to jump from direct - use of `EventStore.ClientAPI` -> `Equinox.EventStore.Core.Events` -> + use of `EventStore.Client` -> `Equinox.EventStore.Core.Events` -> `Equinox.Decider` (with the potential to swap stores once one gets to using `Equinox.Decider`) - Get conflict handling as efficient and predictable as for `Equinox.CosmosStore` From d9ba265fdb155e194cb3d1213602d247ad34ab10 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 12 May 2022 12:11:23 +0100 Subject: [PATCH 29/29] Remove dead parameter --- src/Equinox.EventStoreDb/EventStoreDb.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Equinox.EventStoreDb/EventStoreDb.fs b/src/Equinox.EventStoreDb/EventStoreDb.fs index ff7e41549..00156fd7c 100755 --- a/src/Equinox.EventStoreDb/EventStoreDb.fs +++ b/src/Equinox.EventStoreDb/EventStoreDb.fs @@ -301,11 +301,10 @@ type EventStoreConnection(readConnection, [] ?writeConnection, [ int, [] ?batchCountLimit) = // TODO batchCountLimit +type BatchingPolicy(getMaxBatchSize : unit -> int) = new (maxBatchSize) = BatchingPolicy(fun () -> maxBatchSize) -// TOCONSIDER remove if Client does not start to expose it + // TOCONSIDER remove if Client does not start to expose it member _.BatchSize = getMaxBatchSize() -//TODO member _.MaxBatches = batchCountLimit [] type GatewaySyncResult = Written of StreamToken | ConflictUnknown of StreamToken