Skip to content

Commit 5750c4c

Browse files
committed
- TupleId internalized
- Shutdown timeout
1 parent 988145c commit 5750c4c

21 files changed

Lines changed: 107 additions & 88 deletions

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
#### 5.1.0 - Apr 2026
2+
* Breaking: hide TupleId as internal
3+
* introduce SUPERVISOR_WORKER_SHUTDOWN_SLEEP_SECS that defaults to TOPOLOGY_MESSAGE_TIMEOUT_SECS
4+
15
#### 5.0.0 - Mar 2026
26
* split Multilang support into a separate package
37
* .net 10 and Disruptor 6
8+
* Timeout setting now affects the tuple expiry
49
* self-hosting: executor abstraction and standard .net telemetry
510

611
#### 4.1.2 - Sep 2023

docs/content/architecture.fsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ stateDiagram-v2
150150
151151
1. **System** — stop timers
152152
2. **Spouts** — stop generating messages (deactivate)
153-
3. **Sleep(timeout)** — allow in-flight tuples to drain through the bolt DAG
153+
3. **Sleep(timeout)** — allow in-flight tuples to drain through the bolt DAG (`SUPERVISOR_WORKER_SHUTDOWN_SLEEP_SECS`, defaults to `TOPOLOGY_MESSAGE_TIMEOUT_SECS`)
154154
4. **Bolts** — stop processing (deactivate)
155155
5. **Ackers** — stop tracking (last, so late acks can still be processed)
156156

docs/content/guaranteed.fsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ let source =
7474
return! loop <|
7575
match cmd, nacked with
7676
| Get rc, [] ->
77-
let tupleId,number = Named(string(nextId())), rnd.Next(0, 100)
77+
let tupleId,number = TupleId.OfString(string(nextId())), rnd.Next(0, 100)
7878
pending.Add(tupleId,number)
7979
rc.Reply(tupleId,number)
8080
[]

docs/content/self-hosting.fsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ Configuration
150150
| `TOPOLOGY_ACKER_TASKS` | 4 | Number of acker task instances (logical units with independent state) |
151151
| `TOPOLOGY_ACKER_EXECUTORS` | 2 | Number of acker executor threads (tasks are distributed round-robin) |
152152
| `TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE` | 256 | Base Disruptor ring buffer size (scaled by tasks per executor) |
153-
| `TOPOLOGY_MESSAGE_TIMEOUT_SECS` | 30 | Timeout for tuple completion; controls acker bucket rotation interval and drain window during shutdown |
153+
| `TOPOLOGY_MESSAGE_TIMEOUT_SECS` | 30 | Timeout for tuple completion; controls acker bucket rotation interval |
154+
| `SUPERVISOR_WORKER_SHUTDOWN_SLEEP_SECS` | = `TOPOLOGY_MESSAGE_TIMEOUT_SECS` | Drain window during shutdown (time between stopping spouts and stopping bolts) |
154155
| `TOPOLOGY_SLEEP_SPOUT_WAIT_STRATEGY_TIME_MS` | 100 | Spout executor timeout: how often the spout wakes to poll for new tuples when idle |
155156
| `TOPOLOGY_DEBUG` | false | Enable trace-level logging with timing |
156157
| `TOPOLOGY_TICK_TUPLE_FREQ_SECS` | (none) | Per-bolt tick tuple interval in seconds |
@@ -198,13 +199,14 @@ The ring buffer must also be large enough to hold `TOPOLOGY_MAX_SPOUT_PENDING` i
198199
199200
**Message timeout and shutdown:**
200201
201-
`TOPOLOGY_MESSAGE_TIMEOUT_SECS` serves three roles:
202+
`TOPOLOGY_MESSAGE_TIMEOUT_SECS` serves two roles:
202203
203204
1. **Acker bucket rotation** — tuples older than this are expired
204-
2. **Shutdown drain window** — the system sleeps this long between stopping spouts and stopping bolts
205-
3. **Restart backoff ceiling** — indirectly affects how long restarts take
205+
2. **Restart backoff ceiling** — indirectly affects how long restarts take
206206
207-
Lower values speed up shutdown and failure detection, but risk false-expiring tuples that are merely slow to process.
207+
`SUPERVISOR_WORKER_SHUTDOWN_SLEEP_SECS` controls the **shutdown drain window** — the sleep between stopping spouts and stopping bolts. It defaults to `TOPOLOGY_MESSAGE_TIMEOUT_SECS` when not set, so existing configurations behave identically. Set it independently when you need a long message timeout (for slow processing) but a fast shutdown.
208+
209+
Lower `TOPOLOGY_MESSAGE_TIMEOUT_SECS` speeds up failure detection but risks false-expiring tuples that are merely slow to process.
208210
209211
**Acker capacity:**
210212
@@ -220,6 +222,7 @@ Acker tasks are stateless relative to each other (each tracks a disjoint set of
220222
| Reduce thread count | Set `withExecutors` lower than `withParallelism` (share threads) |
221223
| Handle slow consumers | Increase `TOPOLOGY_MAX_SPOUT_PENDING` and `TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE` proportionally |
222224
| Faster failure detection | Lower `TOPOLOGY_MESSAGE_TIMEOUT_SECS` (but ensure it exceeds your slowest bolt processing time) |
225+
| Faster shutdown | Set `SUPERVISOR_WORKER_SHUTDOWN_SLEEP_SECS` independently of message timeout |
223226
| Reduce idle CPU | Increase `TOPOLOGY_SLEEP_SPOUT_WAIT_STRATEGY_TIME_MS` (default 100ms is usually fine) |
224227
| Handle high acker load | Increase `TOPOLOGY_ACKER_TASKS` and/or `TOPOLOGY_ACKER_EXECUTORS` |
225228

samples/Guaranteed/Topology.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ let source =
5151
let! cmd = inbox.Receive()
5252
return! loop <| match cmd, nacked with
5353
| Get rc, [] ->
54-
let tupleId,number = Named(string(nextId())), rnd.Next(0, 100)
54+
let tupleId,number = TupleId.ofString(string(nextId())), rnd.Next(0, 100)
5555
pending.Add(tupleId,number)
5656
rc.Reply(tupleId,number)
5757
[]

samples/WordCount/Topology.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let sentences source =
1818
// so that it could be replayed in case of downstream failure
1919
let reliableSentences source =
2020
let sentence = source()
21-
Some(Named sentence, Sentence sentence) // we'll just pretend we've generated a unique Id
21+
Some(TupleId.ofString sentence, Sentence sentence) // we'll just pretend we've generated a unique Id
2222

2323
// split bolt - consumes sentences and emits words
2424
let splitIntoWords (input, emit) =

src/FsShelter.Multilang/IO/JsonIO.fs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ let private toEmit streamRW t (tid:TupleId option,anchors:TupleId list,stream:st
3131
if not (List.isEmpty anchors) then
3232
w.WritePropertyName("anchors")
3333
w.WriteStartArray()
34-
anchors |> List.iter (fun a -> w.WriteValue(string a))
34+
anchors |> List.iter (fun a -> w.WriteValue(TupleId.toString a))
3535
w.WriteEndArray()
3636

3737
let writeId (w:JsonTextWriter) =
3838
match tid with
3939
| Some tid ->
4040
w.WritePropertyName("id")
41-
w.WriteValue(string tid)
41+
w.WriteValue(TupleId.toString tid)
4242
| _ -> ()
4343

4444
let writeTask (w:JsonTextWriter) =
@@ -117,8 +117,8 @@ let private (|Control|_|) (o:JObject) =
117117
| true, Some p ->
118118
match p.ToObject() with
119119
| "next" -> Some (Next)
120-
| "ack" -> Some (Ack (Named (o.["id"].ToObject())))
121-
| "fail" -> Some (Nack (Named (o.["id"].ToObject())))
120+
| "ack" -> Some (Ack (TupleId.ofString (o.["id"].ToObject())))
121+
| "fail" -> Some (Nack (TupleId.ofString (o.["id"].ToObject())))
122122
| "activate" -> Some(Activate)
123123
| "deactivate" -> Some(Deactivate)
124124
| _ -> None
@@ -133,7 +133,7 @@ let private (|Stream|_|) findConstructor (o:JObject) =
133133
let xs = o.["tuple"].Children().GetEnumerator()
134134
let constr = findConstructor streamId <| fun t -> xs.MoveNext() |> ignore; xs.Current.ToObject(t)
135135
let comp = if isNull o.["comp"] then "" else o.["comp"].ToObject()
136-
Some (InCommand.Tuple(constr(), Named (o.["id"].ToObject()), comp, streamId, o.["task"].ToObject()))
136+
Some (InCommand.Tuple(constr(), TupleId.ofString (o.["id"].ToObject()), comp, streamId, o.["task"].ToObject()))
137137
| _ -> None
138138

139139
let private toCommand (findConstructor:string->FieldReader->unit->'t) str : InCommand<'t> =
@@ -159,8 +159,8 @@ let startWith (stdin:TextReader,stdout:TextWriter) syncOut (log:Log) :Topology.I
159159
match cmd with
160160
| Sync -> """{"command":"sync"}"""
161161
| Pid pid -> sprintf """{"pid":%d}""" pid
162-
| Fail tid -> sprintf """{"command":"fail","id":"%s"}""" (string tid)
163-
| Ok tid -> sprintf """{"command":"ack","id":"%s"}""" (string tid)
162+
| Fail tid -> sprintf """{"command":"fail","id":"%s"}""" (TupleId.toString tid)
163+
| Ok tid -> sprintf """{"command":"ack","id":"%s"}""" (TupleId.toString tid)
164164
| Log (msg,lvl) -> toLog msg (int lvl)
165165
| Error (msg,ex) -> toLog (sprintf "%s: %s" msg (Exception.toString ex)) (int LogLevel.Error)
166166
| Emit (t,tid,anchors,stream, task, needTaskIds) -> toEmit streamRW t (tid, anchors, stream, task, needTaskIds)

src/FsShelter.Multilang/IO/ProtoIO.fs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@ let private toCommand log (findConstructor:string->FieldReader->unit->'t) (msg:M
140140
let toConf conf = conf |> Seq.map (fun (x:Collections.Generic.KeyValuePair<string,VL>) -> x.Key, ofValue x.Value) |> Map
141141
let toContext (ctx:Messages.Context) = { ComponentId = ctx.ComponentId; TaskId = ctx.TaskId; Components = ctx.TaskComponents |> Seq.map (|KeyValue|) |> Map }
142142
match msg.MsgCase with
143-
| Messages.StormMsg.MsgOneofCase.AckCmd -> Ack (Named msg.AckCmd.Id)
143+
| Messages.StormMsg.MsgOneofCase.AckCmd -> Ack (TupleId.ofString msg.AckCmd.Id)
144144
| Messages.StormMsg.MsgOneofCase.Handshake -> Handshake (toConf msg.Handshake.Config, msg.Handshake.PidDir, (toContext msg.Handshake.Context))
145145
| Messages.StormMsg.MsgOneofCase.Heartbeat -> Heartbeat
146-
| Messages.StormMsg.MsgOneofCase.NackCmd -> Nack (Named msg.NackCmd.Id)
146+
| Messages.StormMsg.MsgOneofCase.NackCmd -> Nack (TupleId.ofString msg.NackCmd.Id)
147147
| Messages.StormMsg.MsgOneofCase.NextCmd -> Next
148148
| Messages.StormMsg.MsgOneofCase.TaskIds -> TaskIds (msg.TaskIds.TaskIds |> List.ofSeq)
149149
| Messages.StormMsg.MsgOneofCase.StreamIn ->
150150
let constr = findConstructor msg.StreamIn.Stream
151-
InCommand.Tuple ((ofFields constr msg.StreamIn.Tuple)(), Named msg.StreamIn.Id, msg.StreamIn.Comp, msg.StreamIn.Stream, msg.StreamIn.Task)
151+
InCommand.Tuple ((ofFields constr msg.StreamIn.Tuple)(), TupleId.ofString msg.StreamIn.Id, msg.StreamIn.Comp, msg.StreamIn.Stream, msg.StreamIn.Task)
152152
| Messages.StormMsg.MsgOneofCase.ActivateCmd -> Activate
153153
| Messages.StormMsg.MsgOneofCase.DeactivateCmd -> Deactivate
154154
| _ -> failwithf "Unexpected command: %A" msg
@@ -165,17 +165,17 @@ let startWith (stdin:#Stream,stdout:#Stream) syncOut (log:Log) :Topology.IO<'t>
165165
match cmd with
166166
| Sync -> Messages.ShellMsg(Sync = Messages.SyncReply())
167167
| Pid pid -> Messages.ShellMsg(Pid = Messages.PidReply(Pid = pid))
168-
| Fail tid -> Messages.ShellMsg(Fail = Messages.FailReply(Id = string tid))
169-
| Ok tid -> Messages.ShellMsg(Ok = Messages.OkReply(Id = string tid))
168+
| Fail tid -> Messages.ShellMsg(Fail = Messages.FailReply(Id = TupleId.toString tid))
169+
| Ok tid -> Messages.ShellMsg(Ok = Messages.OkReply(Id = TupleId.toString tid))
170170
| Log (msg,lvl) -> Messages.ShellMsg(Log = Messages.LogCommand(Text = msg, Level = enum (int lvl)))
171171
| Error (msg,ex) -> Messages.ShellMsg(Log = Messages.LogCommand(Text = (sprintf "%s: %s" msg (Exception.toString ex)), Level = Messages.LogCommand.Types.LogLevel.Error))
172172
| Emit (t,tid,anchors,stream,task,needTaskIds) ->
173173
let (_,d) = streamRW |> Map.find stream
174174
let cmd = Messages.EmitCommand(Stream = stream)
175175
cmd.Tuple.Add (toFields d t)
176-
if Option.isSome tid then cmd.Id <- string tid.Value
176+
if Option.isSome tid then cmd.Id <- TupleId.toString tid.Value
177177
if Option.isSome needTaskIds && needTaskIds.Value then cmd.NeedTaskIds <- needTaskIds.Value
178-
if not (List.isEmpty anchors) then cmd.Anchors.Add (anchors |> List.map string)
178+
if not (List.isEmpty anchors) then cmd.Anchors.Add (anchors |> List.map TupleId.toString)
179179
Messages.ShellMsg(Emit = cmd)
180180
|> write
181181

src/FsShelter.Tests/AckerTests.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type Tracker =
3030
let mkTrackedTopology (tracker: Tracker) maxPending timeout =
3131
let numbers (t: Tracker) =
3232
Interlocked.Increment &t.emitted.contents |> ignore
33-
Some(Named(string t.emitted.Value), Original { x = 1 })
33+
Some(TupleId.ofString(string t.emitted.Value), Original { x = 1 })
3434

3535
let t = topology "acker-test" {
3636
let s1 = numbers
@@ -107,7 +107,7 @@ let ``Acker nacks on bolt failure`` () =
107107

108108
let failNumbers (t: Tracker) =
109109
Interlocked.Increment &t.emitted.contents |> ignore
110-
Some(Named(string t.emitted.Value), Original { x = 1 })
110+
Some(TupleId.ofString(string t.emitted.Value), Original { x = 1 })
111111

112112
let failingBolt (input: Schema, emit: Schema -> unit) =
113113
Interlocked.Increment failCount |> ignore
@@ -157,7 +157,7 @@ let ``Acker expires timed-out tuples`` () =
157157
if not emittedOnce.Value then
158158
emittedOnce := true
159159
Interlocked.Increment &t.emitted.contents |> ignore
160-
Some(Named(string t.emitted.Value), Original { x = 42 })
160+
Some(TupleId.ofString(string t.emitted.Value), Original { x = 42 })
161161
else
162162
None
163163

@@ -210,7 +210,7 @@ let ``Acker capacity overflow nacks tuples`` () =
210210

211211
let fastNumbers (t: Tracker) =
212212
Interlocked.Increment &t.emitted.contents |> ignore
213-
Some(Named(string t.emitted.Value), Original { x = 1 })
213+
Some(TupleId.ofString(string t.emitted.Value), Original { x = 1 })
214214

215215
// bot that never acks - will cause acker inFlight to grow
216216
let blackHole (input: Schema, _: Schema -> unit) = ()

src/FsShelter.Tests/BoltAndGroupingTests.fs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ let ``Shuffle grouping distributes across instances`` () =
2424
let acked = ref 0L
2525

2626
let numbers (_: unit) =
27-
Some(Named(string (Guid.NewGuid())), Original { x = 1 })
27+
Some(TupleId.ofString(string (Guid.NewGuid())), Original { x = 1 })
2828

2929
let countingBolt (input: Schema, _: Schema -> unit) =
3030
let tid = Thread.CurrentThread.ManagedThreadId
@@ -72,7 +72,7 @@ let ``Fields grouping routes same key to same instance`` () =
7272
let numbersWithKeys (_: unit) =
7373
let x = int (Interlocked.Increment emitted)
7474
let key = x % 5 // 5 distinct keys
75-
Some(Named(string x), Original { x = key })
75+
Some(TupleId.ofString(string x), Original { x = key })
7676

7777
let split' (input, emit) =
7878
match input with
@@ -135,7 +135,7 @@ let ``All grouping broadcasts to all instances`` () =
135135

136136
let oneNumber (_: unit) =
137137
if not (Interlocked.Exchange(emittedOnce, true)) then
138-
Some(Named "1", Original { x = 42 })
138+
Some(TupleId.ofString "1", Original { x = 42 })
139139
else
140140
None
141141

@@ -194,10 +194,10 @@ let ``Auto-ack bolt sends Ok on success`` () =
194194
Map.empty
195195
out
196196

197-
dispatch (InCommand.Tuple(Original {x = 1}, Named "tuple-1", "s1", "Original", 0))
197+
dispatch (InCommand.Tuple(Original {x = 1}, TupleId.ofString "tuple-1", "s1", "Original", 0))
198198

199199
let oks = outMsgs |> Seq.choose (function OutCommand.Ok id -> Some id | _ -> None) |> Seq.toList
200-
test <@ oks = [Named "tuple-1"] @>
200+
test <@ oks = [TupleId.ofString "tuple-1"] @>
201201

202202
// ---------------------------------------------------------------------------
203203
// Storm BoltTest: testBoltFailOnException
@@ -221,10 +221,10 @@ let ``Auto-ack bolt sends Fail on exception`` () =
221221
Map.empty
222222
out
223223

224-
dispatch (InCommand.Tuple(Original {x = 1}, Named "tuple-1", "s1", "Original", 0))
224+
dispatch (InCommand.Tuple(Original {x = 1}, TupleId.ofString "tuple-1", "s1", "Original", 0))
225225

226226
let fails = outMsgs |> Seq.choose (function OutCommand.Fail id -> Some id | _ -> None) |> Seq.toList
227-
test <@ fails = [Named "tuple-1"] @>
227+
test <@ fails = [TupleId.ofString "tuple-1"] @>
228228

229229
// ---------------------------------------------------------------------------
230230
// Storm BoltTest: testBoltAnchoredEmit
@@ -237,7 +237,7 @@ let ``Anchored emit preserves tuple lineage`` () =
237237

238238
let numbers (t: AckerTests.Tracker) =
239239
Interlocked.Increment &t.emitted.contents |> ignore
240-
Some(Named(string t.emitted.Value), Original { x = 1 })
240+
Some(TupleId.ofString(string t.emitted.Value), Original { x = 1 })
241241

242242
let anchoredPassthrough (input: Schema, emit: Schema -> unit) =
243243
match input with

0 commit comments

Comments
 (0)