Commit cb75b78
authored
Make message delivery push-only (#180)
* Make MariaDB batch restart claims precise
RestartExecutions claimed via UPDATE..WHERE owner IS NULL and then
SELECTed all requested rows, filtering by owner = replica. A flow
claimed by an earlier call from the same replica was therefore returned
to a concurrent caller as if it had just been claimed, letting two
claimers (e.g. two watchdogs) restart the same flow concurrently -
surfacing as UnexpectedStateException: concurrent modification.
Run a locking SELECT..FOR UPDATE and the claiming UPDATE inside one
transaction so the caller receives exactly the rows its own call
claimed, mirroring the already-precise UPDATE..RETURNING (PostgreSQL)
and UPDATE..OUTPUT (SqlServer) implementations.
* Only claim postponed/suspended flows in batch restarts
The batch RestartExecutions/RestartExecutionsWithoutMessages claimed any
ownerless flow, so a caller targeting a flow that had meanwhile
completed would resurrect it - e.g. a message arriving after its target
succeeded, once message-driven restarts land. Guard the claim with
status IN (Postponed, Suspended) in all stores. The singular
RestartExecution (manual restart) intentionally keeps claiming
completed flows.
* Deliver restart in-hand messages via QueueManager.Push
PrepareForReInvocation received the messages loaded by the restart
claim but discarded them, so a restarted flow had to re-fetch its own
messages from the store. Push them straight into the queue manager's
delivery pipeline instead. Push now initializes the queue manager
first so the idempotency-key state is loaded before processing.
Initialize additionally seeds the fetched-positions set from the
persisted delivered positions: the in-hand messages are loaded at
claim time - before Initialize clears the delivered positions from
the store - so without the seed a message the previous incarnation
already delivered (but had not yet cleared) would be delivered again.
* Push pending messages immediately on flow initialization
Extract the MessageWatchdog's fetch-and-push cycle into PushOnce and
invoke it when a queue manager initializes through CreateQueueClient,
so a freshly started/resumed flow receives its pending messages
immediately instead of waiting for the next watchdog poll.
The pending-push is skipped when initialization is triggered by an
incoming Push: the pusher already holds an in-flight batch and a
nested fetch would stage newer messages ahead of it. Staged messages
are additionally kept position-sorted so delivery order stays append
order even when concurrent pushers stage batches out of order.
* Hand over crashed replicas' messages to a live replica
GetMessagesForReplica only returns messages assigned to the fetching
replica, so messages published by a crashed replica to ownerless flows
were never fetched again once the replica died. The ReplicaWatchdog now
reassigns a crashed replica's message rows to itself alongside
rescheduling its crashed functions - before deleting the crashed
replica from the replica store, so an interrupted handover is retried
on a later iteration.
* Rework hand-rolled QueueManager tests to production paths
Three tests constructed their own QueueManager inside the flow, which
exercised the store-pull directly and duplicated runtime wiring:
- QueueManagerFailsOnMessageDeserializationError now registers the
exception-throwing serializer on the registry and uses
workflow.Message
- RegisteredTimeoutIsRemovedWhenPullingMessage now asserts through the
observable postpone: a delivered message's timeout must not linger
and shorten a subsequent delay's postpone
- PullEnvelopeReturnsEnvelopeWithReceiverAndSender uses the flow's own
queue manager via a new internal Workflow.QueueManager accessor
The tests now stay valid when the queue manager's store-pull is
removed and message delivery becomes push-only.
* Reopen pushed positions that cannot be delivered
A push whose target flow was suspended, not yet queue-manager-attached,
or absent from the manager's dictionary was silently dropped while its
positions stayed in the clearer's ignore-set. Later fetches then only
returned higher positions, so a later-positioned duplicate consumed the
idempotency key before the original message - observed on CI as
MultipleIterationsWithDuplicateIdempotencyKeysProcessCorrectly
delivering the second-copy values.
The queue manager now attaches to its flow state at construction, and
undeliverable pushes reopen their positions so they are re-fetched
instead of stranded.
* Reset EffectContext at the start of every flow invocation
The invocation body runs on a Task that captures the scheduler's
execution context. When a flow is restarted from a watchdog chain that
has itself touched the ambient EffectContext, all invocations spawned
from that chain inherit - and mutate - the same context instance, so
implicit effect ids shift between incarnations and replay diverges:
a restarted flow re-subscribes under a different effect id, filters on
the wrong message and suspends, restarting endlessly.
Give every invocation a fresh context at start so implicit ids are
deterministic regardless of the scheduling context.
* Run MessageWatchdog at the messages-pull frequency
MessagesPullFrequency (default 250ms) lost its consumer when the
per-flow fetch loop was removed. The MessageWatchdog is the message-
delivery loop, so it should run at that frequency - the slower
watchdog check frequency made every push-restarted message exchange
poll-bound.
* Restart not-live flows from FlowsManager.Push with in-hand messages
Messages pushed to a flow that is not live are now delivered by
claiming and restarting the flow (RestartExecutionsWithoutMessages)
with the pushed messages handed to the new invocation - the message
itself becomes the wake-up signal instead of relying on the interrupt
flag. A flow-state entry whose flow has suspended counts as not live:
a suspended flow's parked invocation never reaches RemoveFlow by
design, and treating the lingering entry as live swallowed the wake-up.
Claim failures distinguish retryable targets (reopened for the next
poll) from completed/deleted flows, whose positions stay ignored so
their messages are not refetched forever. A failed claim call reopens
everything.
Positions that reach a dying queue manager are reopened rather than
dropped: pushes hitting a disposed instance, staged-but-undelivered
messages at dispose, and messages for unregistered flow types. A
stranded position otherwise loses the flow's wake-up once the message
watchdog has marked it as pushed.
The tests' thread pool is pre-sized since every test's registry now
runs its message poll at 250ms, which starves the default pool growth
on small CI runners.
* Make QueueManager push-only: remove its message-store pull
Delete FetchAndNotify and the queue manager's message-store dependency.
Messages now arrive exclusively via Push - the MessageWatchdog poll,
the immediate fetch at initialization and the restart hand-over - so
Subscribe, Interrupt and FetchMessagesOnce only re-evaluate the already
staged messages against the current subscriptions.
* Stop interrupting target flows when appending messages
Message appends no longer set the interrupted flag: the message row
itself is the durable wake-up signal - the MessageWatchdog fetches it
and either pushes to the live flow or claims and restarts the parked
one with the message in hand. The public Interrupt API and the
suspend-time interrupted-guard remain.
A claim failure for a flow that does not exist (yet) now reopens the
positions instead of stranding them: messages may legally precede
their flow's creation, and the delivery must be retried once the flow
is scheduled.
* Remove the InterruptedWatchdog
Message delivery to live flows is handled by the MessageWatchdog's
push, and parked flows are woken by push-restarts - the interrupt
polling loop no longer had a purpose. Remove it together with its
supporting plumbing: FlowsManagers/FlowsManager Interrupt and
FilterOwned, FlowExecutionState.Interrupt, QueueManager.Interrupt and
the GetInterruptedFunctions/ResetInterrupted store operations. The
public Interrupt API and the suspend-time interrupted-guard remain.
* Remove unused FlowsManager.Schedule
Message-driven flow wake-up is push-restart based; the interrupt-only
scheduling helper has no callers left.
* Give at-least-once tests a load-tolerant suspension delay
The tests rely on workflow.Delay suspending the flow so the captured
work is re-executed on restart. A 10ms delay can fully elapse before
the suspension check on a loaded machine (an effect-persist roundtrip
sits in between), letting the capture complete inline on the first
attempt. Use 500ms so the suspension reliably happens.
* Remove the immediate fetch-and-push at queue manager initialization
The init-time PushOnce ran on the initializing flow's own async branch,
which made restart chains executable from inside another flow's
execution context - the trigger for the EffectContext id-shift bug -
and coupled queue manager construction to the MessageWatchdog. Message
delivery is now driven solely by the watchdog poll and the restart
in-hand hand-over, simplifying the wiring (no push delegate threaded
through FunctionsRegistry/InvocationHelper/QueueManager).
The trade-off is latency: a flow awaiting an already-present message
suspends once and is restarted by the next poll instead of receiving it
inline. Tests encoding the stronger inline guarantee are adjusted:
initial-state flows are awaited tolerantly via Schedule+Completion,
AwaitMessageAfterAppendShouldNotCauseSuspension is removed, and
WorkflowMessageIsIdempotentAcrossRestarts counts deliveries instead of
delivery-timing-dependent invocations.
* Wake the MessageWatchdog on message appends
Add MessageWatchdog.Notify: appending a message completes a wake signal
that cuts the watchdog's inter-poll sleep short, so locally appended
messages are delivered immediately instead of waiting out the poll
interval. The signal is re-armed before each fetch, so a notify arriving
mid-push is never lost - it simply makes the next wait return at once.
Notify only completes the signal: the fetch-and-push always executes on
the watchdog's own loop, so - unlike the removed initialization-time
push - no flow execution context is ever captured by another flow's
restart. Wired into MessageWriter/MessageWriters appends and the
child-to-parent completion message; reopened positions deliberately do
not notify, as a persistently unclaimable flow would otherwise spin the
poll loop.
The faster delivery exposed a stranding hazard: a push whose message
fails deserialization poisons the queue manager without staging the
positions, and when that races the flow's suspension the message stayed
marked forever and the flow never failed. A poisoned queue manager now
reopens the batch's unstaged positions so a restarted incarnation
receives the message and surfaces the failure.
* Pass MessageWatchdog to InvocationHelper instead of an Action
* Pass IMessageClearer to FlowExecutionState instead of an Action
* wip
* Pass MessageWatchdog to MessageWriter and MessageWriters instead of an Action1 parent e18b535 commit cb75b78
49 files changed
Lines changed: 728 additions & 964 deletions
File tree
- Core
- Cleipnir.ResilientFunctions.Tests
- InMemoryTests
- RFunctionTests
- WatchDogsTests
- Messaging/TestTemplates
- TestTemplates
- FunctionTests
- WatchDogsTests
- Cleipnir.ResilientFunctions
- CoreRuntime
- Invocation
- Watchdogs
- Domain
- Messaging
- Queuing
- Storage
- Samples/Sample.ConsoleApp/Utils
- Stores
- MariaDB
- Cleipnir.ResilientFunctions.MariaDB.Tests
- RFunctionTests
- WatchDogsTests
- Cleipnir.ResilientFunctions.MariaDB
- PostgreSQL
- Cleipnir.ResilientFunctions.PostgreSQL.Tests
- RFunctionTests
- WatchDogsTests
- Cleipnir.ResilientFunctions.PostgreSQL
- SqlServer
- Cleipnir.ResilientFunctions.SqlServer.Tests
- RFunctionTests
- WatchDogsTests
- Cleipnir.ResilientFunctions.SqlServer
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 0 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | 91 | | |
96 | 92 | | |
97 | 93 | | |
| |||
Lines changed: 14 additions & 22 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | 101 | | |
106 | 102 | | |
107 | 103 | | |
| |||
239 | 235 | | |
240 | 236 | | |
241 | 237 | | |
242 | | - | |
243 | | - | |
244 | | - | |
245 | | - | |
246 | | - | |
247 | | - | |
| 238 | + | |
| 239 | + | |
248 | 240 | | |
249 | 241 | | |
250 | | - | |
251 | | - | |
| 242 | + | |
| 243 | + | |
252 | 244 | | |
253 | 245 | | |
254 | | - | |
255 | | - | |
| 246 | + | |
| 247 | + | |
256 | 248 | | |
257 | 249 | | |
258 | | - | |
259 | | - | |
| 250 | + | |
| 251 | + | |
260 | 252 | | |
261 | 253 | | |
262 | | - | |
263 | | - | |
| 254 | + | |
| 255 | + | |
264 | 256 | | |
265 | 257 | | |
266 | | - | |
267 | | - | |
| 258 | + | |
| 259 | + | |
268 | 260 | | |
269 | 261 | | |
270 | | - | |
271 | | - | |
| 262 | + | |
| 263 | + | |
272 | 264 | | |
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
56 | 60 | | |
Lines changed: 10 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
637 | 637 | | |
638 | 638 | | |
639 | 639 | | |
| 640 | + | |
640 | 641 | | |
641 | | - | |
642 | | - | |
643 | | - | |
| 642 | + | |
| 643 | + | |
644 | 644 | | |
645 | | - | |
646 | | - | |
| 645 | + | |
647 | 646 | | |
648 | 647 | | |
649 | 648 | | |
| |||
676 | 675 | | |
677 | 676 | | |
678 | 677 | | |
| 678 | + | |
679 | 679 | | |
680 | | - | |
681 | | - | |
| 680 | + | |
682 | 681 | | |
683 | 682 | | |
684 | 683 | | |
| |||
739 | 738 | | |
740 | 739 | | |
741 | 740 | | |
| 741 | + | |
742 | 742 | | |
743 | | - | |
| 743 | + | |
744 | 744 | | |
745 | 745 | | |
746 | | - | |
| 746 | + | |
747 | 747 | | |
748 | 748 | | |
749 | 749 | | |
| |||
761 | 761 | | |
762 | 762 | | |
763 | 763 | | |
764 | | - | |
765 | | - | |
| 764 | + | |
766 | 765 | | |
767 | 766 | | |
768 | 767 | | |
| |||
Lines changed: 18 additions & 98 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | 21 | | |
33 | 22 | | |
34 | 23 | | |
| |||
558 | 547 | | |
559 | 548 | | |
560 | 549 | | |
561 | | - | |
562 | 550 | | |
563 | 551 | | |
564 | 552 | | |
565 | | - | |
| 553 | + | |
566 | 554 | | |
567 | 555 | | |
568 | 556 | | |
569 | 557 | | |
570 | 558 | | |
571 | 559 | | |
572 | | - | |
573 | | - | |
574 | | - | |
575 | | - | |
576 | | - | |
577 | | - | |
578 | | - | |
579 | | - | |
580 | | - | |
581 | | - | |
582 | | - | |
583 | | - | |
584 | | - | |
585 | | - | |
586 | | - | |
587 | | - | |
588 | | - | |
589 | | - | |
590 | | - | |
591 | | - | |
592 | | - | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
| 560 | + | |
597 | 561 | | |
598 | 562 | | |
599 | 563 | | |
| |||
622 | 586 | | |
623 | 587 | | |
624 | 588 | | |
625 | | - | |
626 | 589 | | |
627 | 590 | | |
628 | 591 | | |
629 | 592 | | |
630 | 593 | | |
631 | | - | |
632 | 594 | | |
633 | 595 | | |
634 | 596 | | |
635 | 597 | | |
636 | | - | |
637 | | - | |
638 | | - | |
639 | | - | |
640 | | - | |
641 | | - | |
642 | | - | |
643 | | - | |
644 | | - | |
645 | | - | |
646 | | - | |
647 | | - | |
648 | | - | |
649 | | - | |
650 | | - | |
651 | | - | |
652 | | - | |
653 | | - | |
654 | | - | |
655 | | - | |
656 | | - | |
657 | | - | |
658 | | - | |
659 | | - | |
660 | | - | |
661 | | - | |
662 | | - | |
663 | | - | |
664 | | - | |
665 | | - | |
666 | | - | |
667 | | - | |
668 | | - | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
669 | 604 | | |
670 | 605 | | |
671 | 606 | | |
672 | 607 | | |
673 | | - | |
| 608 | + | |
674 | 609 | | |
675 | 610 | | |
676 | 611 | | |
677 | | - | |
678 | | - | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
679 | 617 | | |
| 618 | + | |
680 | 619 | | |
681 | 620 | | |
682 | 621 | | |
| |||
685 | 624 | | |
686 | 625 | | |
687 | 626 | | |
688 | | - | |
689 | 627 | | |
690 | 628 | | |
691 | 629 | | |
| |||
695 | 633 | | |
696 | 634 | | |
697 | 635 | | |
698 | | - | |
699 | | - | |
700 | | - | |
701 | | - | |
702 | | - | |
703 | | - | |
704 | | - | |
705 | | - | |
706 | | - | |
707 | | - | |
708 | | - | |
709 | | - | |
710 | | - | |
711 | | - | |
712 | | - | |
713 | | - | |
714 | | - | |
715 | | - | |
716 | | - | |
717 | | - | |
718 | | - | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
719 | 639 | | |
720 | 640 | | |
721 | 641 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
1 | 3 | | |
2 | 4 | | |
3 | 5 | | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
| 67 | + | |
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| |||
0 commit comments