Skip to content

Commit f2d75ba

Browse files
committed
MessageDb: propulsion checkpoint
1 parent 993ffd0 commit f2d75ba

File tree

11 files changed

+168
-160
lines changed

11 files changed

+168
-160
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
2020
- `Propulsion.EventStoreDb`: Ported `EventStore` to target `Equinox.EventStore` >= `4.0.0` (using the gRPC interface) [#139](https://github.com/jet/propulsion/pull/139)
2121
- `Propulsion.CosmosStore3`: Special cased version of `Propulsion.CosmosStore` to target `Equinox.CosmosStore` v `[3.0.7`-`3.99.0]` **Deprecated; Please migrate to `Propulsion.CosmosStore` by updating `Equinox.CosmosStore` dependencies to `4.0.0`** [#139](https://github.com/jet/propulsion/pull/139)
2222
- `Propulsion.DynamoStore`: `Equinox.CosmosStore`-equivalent functionality for `Equinox.DynamoStore`. Combines elements of `CosmosStore`, `SqlStreamStore`, `Feed` [#140](https://github.com/jet/propulsion/pull/143) [#140](https://github.com/jet/propulsion/pull/143) [#177](https://github.com/jet/propulsion/pull/177)
23-
- `Propulsion.MessageDb`: `FeedSource` for [MessageDb](http://docs.eventide-project.org/user-guide/message-db/) [#181](https://github.com/jet/propulsion/pull/181) :pray: [@nordfjord](https://github.com/nordfjord)
23+
- `Propulsion.MessageDb`: `FeedSource` and `CheckpointStore` for [MessageDb](http://docs.eventide-project.org/user-guide/message-db/) [#181](https://github.com/jet/propulsion/pull/181) :pray: [@nordfjord](https://github.com/nordfjord)
2424
- `Propulsion.MemoryStore`: `MemoryStoreSource` to align with other sources for integration testing. Includes *deterministic* `AwaitCompletion` as per `Propulsion.Feed`-based Sources [#165](https://github.com/jet/propulsion/pull/165)
2525
- `Propulsion.SqlStreamStore`: Added `startFromTail` [#173](https://github.com/jet/propulsion/pull/173)
2626
- `Propulsion.Tool`: `checkpoint` commandline option; enables viewing or overriding checkpoints [#141](https://github.com/jet/propulsion/pull/141)

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,11 @@ The ubiquitous `Serilog` dependency is solely on the core module, not any sinks.
108108

109109
- `Propulsion.Tool` [![Tool NuGet](https://img.shields.io/nuget/v/Propulsion.Tool.svg)](https://www.nuget.org/packages/Propulsion.Tool/): Tool used to initialize a Change Feed Processor `aux` container for `Propulsion.Cosmos` and demonstrate basic projection, including to Kafka. See [quickstart](#quickstart).
110110

111-
- CosmosDB: Initialize `-aux` Container for ChangeFeedProcessor
112-
- CosmosDB/DynamoStore/EventStoreDB/Feed/SqlStreamStore: adjust checkpoints
113-
- CosmosDB/DynamoStore/EventStoreDB/MessageDb: walk change feeds/indexes and/or project to Kafka
114-
- DynamoStore: validate and/or reindex DynamoStore Index
115-
- MessageDb: Initialize a checkpoints table in a Postgres Database
111+
- `init` CosmosDB: Initialize an `-aux` Container for ChangeFeedProcessor
112+
- `initpg` : MessageDb: Initialize a checkpoints table in a Postgres Database
113+
- `index`: DynamoStore: validate and/or reindex DynamoStore Index
114+
- `checkpoint`: CosmosDB/DynamoStore/EventStoreDB/Feed/SqlStreamStore: adjust checkpoints in DynamoStore/CosmosStore/Postgres
115+
- `project`: CosmosDB/DynamoStore/EventStoreDB/MessageDb: walk change feeds/indexes and/or project to Kafka
116116

117117
## Deprecated components
118118

src/Propulsion.MessageDb/Internal.fs

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/Propulsion.MessageDb/MessageDbSource.fs

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
namespace Propulsion.MessageDb
22

3-
open FSharp.Control
43
open FsCodec
5-
open FsCodec.Core
4+
open FSharp.Control
65
open NpgsqlTypes
7-
open Propulsion.Feed
8-
open Propulsion.Feed.Core
96
open Propulsion.Internal
107
open System
118
open System.Data.Common
12-
open System.Diagnostics
139

10+
module internal Npgsql =
11+
12+
let connect connectionString ct = task {
13+
let conn = new Npgsql.NpgsqlConnection(connectionString)
14+
do! conn.OpenAsync(ct)
15+
return conn }
16+
17+
module Internal =
18+
19+
open Propulsion.Feed
20+
open System.Threading.Tasks
21+
open Propulsion.Infrastructure // AwaitTaskCorrect
1422

15-
module Core =
1623
type MessageDbCategoryClient(connectionString) =
1724
let connect = Npgsql.connect connectionString
1825
let parseRow (reader: DbDataReader) =
1926
let readNullableString idx = if reader.IsDBNull(idx) then None else Some (reader.GetString idx)
2027
let streamName = reader.GetString(8)
21-
let event = TimelineEvent.Create(
28+
let event = FsCodec.Core.TimelineEvent.Create(
2229
index = reader.GetInt64(0),
2330
eventType = reader.GetString(1),
2431
data = ReadOnlyMemory(Text.Encoding.UTF8.GetBytes(reader.GetString 2)),
@@ -28,9 +35,9 @@ module Core =
2835
?causationId = readNullableString 6,
2936
context = reader.GetInt64(9),
3037
timestamp = DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc)))
31-
3238
struct(StreamName.parse streamName, event)
33-
member _.ReadCategoryMessages(category: TrancheId, fromPositionInclusive: int64, batchSize: int, ct) = task {
39+
40+
member _.ReadCategoryMessages(category: TrancheId, fromPositionInclusive: int64, batchSize: int, ct) : Task<Propulsion.Feed.Core.Batch<_>> = task {
3441
use! conn = connect ct
3542
let command = conn.CreateCommand(CommandText = "select position, type, data, metadata, id::uuid,
3643
(metadata::jsonb->>'$correlationId')::text,
@@ -44,66 +51,65 @@ module Core =
4451
let mutable checkpoint = fromPositionInclusive
4552

4653
use! reader = command.ExecuteReaderAsync(ct)
47-
let events = [| while reader.Read() do yield parseRow reader |]
54+
let events = [| while reader.Read() do parseRow reader |]
4855

4956
checkpoint <- match Array.tryLast events with Some (_, ev) -> unbox<int64> ev.Context | None -> checkpoint
5057

51-
return { checkpoint = Position.parse checkpoint; items = events; isTail = events.Length = 0 } }
52-
member _.ReadCategoryLastVersion(category: TrancheId, ct) = task {
58+
return ({ checkpoint = Position.parse checkpoint; items = events; isTail = events.Length = 0 } : Propulsion.Feed.Core.Batch<_>) }
59+
60+
member _.ReadCategoryLastVersion(category: TrancheId, ct) : Task<int64> = task {
5361
use! conn = connect ct
5462
let command = conn.CreateCommand(CommandText = "select max(global_position) from messages where category(stream_name) = @Category;")
5563
command.Parameters.AddWithValue("Category", NpgsqlDbType.Text, TrancheId.toString category) |> ignore
5664

5765
use! reader = command.ExecuteReaderAsync(ct)
5866
return if reader.Read() then reader.GetInt64(0) else 0L }
5967

60-
module private Impl =
61-
open Core
62-
open Propulsion.Infrastructure // AwaitTaskCorrect
63-
64-
let readBatch batchSize (store : MessageDbCategoryClient) (category, pos) : Async<Propulsion.Feed.Core.Batch<_>> = async {
68+
let internal readBatch batchSize (store : MessageDbCategoryClient) (category, pos) : Async<Propulsion.Feed.Core.Batch<_>> = async {
6569
let! ct = Async.CancellationToken
6670
let positionInclusive = Position.toInt64 pos
67-
let! x = store.ReadCategoryMessages(category, positionInclusive, batchSize, ct) |> Async.AwaitTaskCorrect
68-
return x }
71+
return! store.ReadCategoryMessages(category, positionInclusive, batchSize, ct) |> Async.AwaitTaskCorrect }
6972

70-
let readTailPositionForTranche (store : MessageDbCategoryClient) trancheId : Async<Propulsion.Feed.Position> = async {
73+
let internal readTailPositionForTranche (store : MessageDbCategoryClient) trancheId : Async<Propulsion.Feed.Position> = async {
7174
let! ct = Async.CancellationToken
7275
let! lastEventPos = store.ReadCategoryLastVersion(trancheId, ct) |> Async.AwaitTaskCorrect
7376
return Position.parse lastEventPos }
7477

75-
type MessageDbSource
78+
type MessageDbSource internal
7679
( log : Serilog.ILogger, statsInterval,
77-
client: Core.MessageDbCategoryClient, batchSize, tailSleepInterval,
80+
client: Internal.MessageDbCategoryClient, batchSize, tailSleepInterval,
7881
checkpoints : Propulsion.Feed.IFeedCheckpointStore, sink : Propulsion.Streams.Default.Sink,
79-
categories,
80-
// Override default start position to be at the tail of the index. Default: Replay all events.
81-
?startFromTail,
82-
?sourceId) =
82+
tranches, ?startFromTail, ?sourceId) =
8383
inherit Propulsion.Feed.Core.TailingFeedSource
8484
( log, statsInterval, defaultArg sourceId FeedSourceId.wellKnownId, tailSleepInterval, checkpoints,
8585
( if startFromTail <> Some true then None
86-
else Some (Impl.readTailPositionForTranche client)),
86+
else Some (Internal.readTailPositionForTranche client)),
8787
sink,
8888
(fun req -> asyncSeq {
89-
let sw = Stopwatch.StartNew()
90-
let! b = Impl.readBatch batchSize client req
89+
let sw = Stopwatch.start ()
90+
let! b = Internal.readBatch batchSize client req
9191
yield sw.Elapsed, b }),
9292
string)
93-
new (log, statsInterval, connectionString, batchSize, tailSleepInterval, checkpoints, sink, trancheIds, ?startFromTail, ?sourceId) =
94-
MessageDbSource(log, statsInterval, Core.MessageDbCategoryClient(connectionString),
95-
batchSize, tailSleepInterval, checkpoints, sink, trancheIds, ?startFromTail=startFromTail, ?sourceId=sourceId)
93+
new( log, statsInterval,
94+
connectionString, batchSize, tailSleepInterval,
95+
checkpoints, sink,
96+
categories,
97+
// Override default start position to be at the tail of the index. Default: Replay all events.
98+
?startFromTail, ?sourceId) =
99+
MessageDbSource(log, statsInterval, Internal.MessageDbCategoryClient(connectionString),
100+
batchSize, tailSleepInterval, checkpoints, sink,
101+
categories |> Array.map Propulsion.Feed.TrancheId.parse,
102+
?startFromTail=startFromTail, ?sourceId=sourceId)
96103

97104
abstract member ListTranches : unit -> Async<Propulsion.Feed.TrancheId array>
98-
default _.ListTranches() = async { return categories |> Array.map TrancheId.parse }
105+
default _.ListTranches() = async { return tranches }
99106

100107
abstract member Pump : unit -> Async<unit>
101108
default x.Pump() = base.Pump(x.ListTranches)
102109

103110
abstract member Start : unit -> Propulsion.SourcePipeline<Propulsion.Feed.Core.FeedMonitor>
104111
default x.Start() = base.Start(x.Pump())
105112

106-
107113
/// Pumps to the Sink until either the specified timeout has been reached, or all items in the Source have been fully consumed
108114
member x.RunUntilCaughtUp(timeout : TimeSpan, statsInterval : IntervalTimer) = task {
109115
let sw = Stopwatch.start ()

src/Propulsion.MessageDb/Propulsion.MessageDb.fsproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,24 @@
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
5-
<GenerateDocumentationFile>true</GenerateDocumentationFile>
65
</PropertyGroup>
76

87
<ItemGroup>
98
<Compile Include="..\Propulsion\Infrastructure.fs">
109
<Link>Infrastructure.fs</Link>
1110
</Compile>
12-
<Compile Include="Internal.fs" />
11+
<Compile Include="Types.fs" />
1312
<Compile Include="MessageDbSource.fs" />
1413
<Compile Include="ReaderCheckpoint.fs" />
1514
<Content Include="Readme.md" />
1615
</ItemGroup>
1716

1817
<ItemGroup>
1918
<PackageReference Include="MinVer" Version="4.2.0" PrivateAssets="All" />
19+
2020
<PackageReference Include="Npgsql" Version="6.0.7" />
2121
</ItemGroup>
2222

23-
2423
<ItemGroup>
2524
<ProjectReference Include="..\Propulsion.Feed\Propulsion.Feed.fsproj" />
2625
</ItemGroup>

src/Propulsion.MessageDb/ReaderCheckpoint.fs

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,69 +5,73 @@ open NpgsqlTypes
55
open Propulsion.Feed
66
open Propulsion.Infrastructure
77

8+
let [<Literal>] TableName = "propulsion_checkpoint"
89

9-
let table = "propulsion_checkpoint"
10+
module internal Impl =
1011

11-
let createIfNotExists (conn : NpgsqlConnection, schema: string) =
12-
let cmd = conn.CreateCommand(CommandText = $"create table if not exists {schema}.{table} (
13-
source text not null,
14-
tranche text not null,
15-
consumer_group text not null,
16-
position bigint not null,
17-
primary key (source, tranche, consumer_group));")
18-
cmd.ExecuteNonQueryAsync() |> Async.AwaitTaskCorrect |> Async.Ignore<int>
12+
open System.Threading
13+
open System.Threading.Tasks
1914

20-
let commitPosition (conn : NpgsqlConnection, schema: string) source tranche (consumerGroup : string) (position : int64)
21-
= async {
22-
let cmd = conn.CreateCommand(CommandText = $"insert into {schema}.{table}(source, tranche, consumer_group, position)
23-
values (@Source, @Tranche, @ConsumerGroup, @Position)
24-
on conflict (source, tranche, consumer_group)
25-
do update set position = @Position;")
26-
cmd.Parameters.AddWithValue("Source", NpgsqlDbType.Text, SourceId.toString source) |> ignore
27-
cmd.Parameters.AddWithValue("Tranche", NpgsqlDbType.Text, TrancheId.toString tranche) |> ignore
28-
cmd.Parameters.AddWithValue("ConsumerGroup", NpgsqlDbType.Text, consumerGroup) |> ignore
29-
cmd.Parameters.AddWithValue("Position", NpgsqlDbType.Bigint, position) |> ignore
15+
let createIfNotExists (conn : NpgsqlConnection, schema: string) ct = task {
16+
let cmd = conn.CreateCommand(CommandText = $"create table if not exists {schema}.{TableName} (
17+
source text not null,
18+
tranche text not null,
19+
consumer_group text not null,
20+
position bigint not null,
21+
primary key (source, tranche, consumer_group));")
22+
do! cmd.ExecuteNonQueryAsync(ct) : Task }
3023

31-
let! ct = Async.CancellationToken
32-
do! cmd.ExecuteNonQueryAsync(ct) |> Async.AwaitTaskCorrect |> Async.Ignore<int> }
24+
let commitPosition (conn : NpgsqlConnection, schema: string) source tranche (consumerGroup : string) (position : int64) ct = task {
25+
let cmd = conn.CreateCommand(CommandText = $"insert into {schema}.{TableName}(source, tranche, consumer_group, position)
26+
values (@Source, @Tranche, @ConsumerGroup, @Position)
27+
on conflict (source, tranche, consumer_group)
28+
do update set position = @Position;")
29+
cmd.Parameters.AddWithValue("Source", NpgsqlDbType.Text, SourceId.toString source) |> ignore
30+
cmd.Parameters.AddWithValue("Tranche", NpgsqlDbType.Text, TrancheId.toString tranche) |> ignore
31+
cmd.Parameters.AddWithValue("ConsumerGroup", NpgsqlDbType.Text, consumerGroup) |> ignore
32+
cmd.Parameters.AddWithValue("Position", NpgsqlDbType.Bigint, position) |> ignore
33+
do! cmd.ExecuteNonQueryAsync(ct) :> Task }
3334

34-
let tryGetPosition (conn : NpgsqlConnection, schema : string) source tranche (consumerGroup : string) = async {
35-
let cmd = conn.CreateCommand(CommandText = $"select position from {schema}.{table}
36-
where source = @Source
37-
and tranche = @Tranche
38-
and consumer_group = @ConsumerGroup")
39-
cmd.Parameters.AddWithValue("Source", NpgsqlDbType.Text, SourceId.toString source) |> ignore
40-
cmd.Parameters.AddWithValue("Tranche", NpgsqlDbType.Text, TrancheId.toString tranche) |> ignore
41-
cmd.Parameters.AddWithValue("ConsumerGroup", NpgsqlDbType.Text, consumerGroup) |> ignore
35+
let tryGetPosition (conn : NpgsqlConnection, schema : string) source tranche (consumerGroup : string) (ct : CancellationToken) = task {
36+
let cmd = conn.CreateCommand(CommandText = $"select position from {schema}.{TableName}
37+
where source = @Source
38+
and tranche = @Tranche
39+
and consumer_group = @ConsumerGroup")
40+
cmd.Parameters.AddWithValue("Source", NpgsqlDbType.Text, SourceId.toString source) |> ignore
41+
cmd.Parameters.AddWithValue("Tranche", NpgsqlDbType.Text, TrancheId.toString tranche) |> ignore
42+
cmd.Parameters.AddWithValue("ConsumerGroup", NpgsqlDbType.Text, consumerGroup) |> ignore
43+
use! reader = cmd.ExecuteReaderAsync(ct)
44+
return if reader.Read() then ValueSome (reader.GetInt64 0) else ValueNone }
4245

43-
let! ct = Async.CancellationToken
44-
use! reader = cmd.ExecuteReaderAsync(ct) |> Async.AwaitTaskCorrect
45-
return if reader.Read() then ValueSome (reader.GetInt64 0) else ValueNone }
46-
47-
type CheckpointStore(connString : string, schema: string, consumerGroupName, defaultCheckpointFrequency) =
48-
let connect = Npgsql.connect connString
49-
50-
member _.CreateSchemaIfNotExists() = async {
46+
let exec connString f= async {
5147
let! ct = Async.CancellationToken
52-
use! conn = connect ct |> Async.AwaitTaskCorrect
53-
return! createIfNotExists (conn, schema) }
48+
use! conn = connect connString ct |> Async.AwaitTaskCorrect
49+
return! f conn ct |> Async.AwaitTaskCorrect }
5450

55-
interface IFeedCheckpointStore with
51+
type CheckpointStore(connString : string, schema : string, consumerGroupName, defaultCheckpointFrequency : System.TimeSpan) =
52+
let exec f = Impl.exec connString f
53+
let setPos source tranche pos =
54+
let commit conn = Impl.commitPosition (conn, schema) source tranche consumerGroupName (Position.toInt64 pos)
55+
exec commit
5656

57-
member _.Start(source, tranche, ?establishOrigin) = async {
58-
let! ct = Async.CancellationToken
59-
use! conn = connect ct |> Async.AwaitTaskCorrect
60-
let! maybePos = tryGetPosition (conn, schema) source tranche consumerGroupName
61-
let! pos =
62-
match maybePos, establishOrigin with
63-
| ValueSome pos, _ -> async { return Position.parse pos }
64-
| ValueNone, Some f -> f
65-
| ValueNone, None -> async { return Position.initial }
66-
return defaultCheckpointFrequency, pos }
57+
member _.CreateSchemaIfNotExists() : Async<unit> =
58+
let creat conn = Impl.createIfNotExists (conn, schema)
59+
exec creat
6760

68-
member _.Commit(source, tranche, pos) = async {
69-
let! ct = Async.CancellationToken
70-
use! conn = connect ct |> Async.AwaitTaskCorrect
71-
return! commitPosition (conn, schema) source tranche consumerGroupName (Position.toInt64 pos) }
61+
member _.Override(source, tranche, pos : Position) : Async<unit> =
62+
setPos source tranche pos
7263

64+
interface IFeedCheckpointStore with
7365

66+
member _.Start(source, tranche, ?establishOrigin) =
67+
let start conn ct = task {
68+
let! maybePos = Impl.tryGetPosition (conn, schema) source tranche consumerGroupName ct |> Async.AwaitTaskCorrect
69+
let! pos =
70+
match maybePos, establishOrigin with
71+
| ValueSome pos, _ -> async { return Position.parse pos }
72+
| ValueNone, Some f -> f
73+
| ValueNone, None -> async { return Position.initial }
74+
return struct (defaultCheckpointFrequency, pos) }
75+
exec start
76+
member _.Commit(source, tranche, pos) : Async<unit> =
77+
setPos source tranche pos

0 commit comments

Comments
 (0)