Skip to content

Commit c8678f8

Browse files
committed
Add Service Bus stream reordering article
1 parent 47f25cf commit c8678f8

4 files changed

Lines changed: 278 additions & 0 deletions
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
---
2+
title: "Reordering an Unordered Azure Service Bus Stream with PowerShell"
3+
description: "Explore a PowerShell approach that combines Azure Service Bus message deferral, sequence numbers, and session state to turn an out-of-order stream into ordered output."
4+
author: Andrey Vernigora
5+
authors:
6+
- Andrey Vernigora
7+
date: "2026-09-14T00:00:00+00:00"
8+
categories:
9+
- PowerShell for Developers
10+
tags:
11+
- powershell
12+
- azure-service-bus
13+
- messaging
14+
- message-ordering
15+
- distributed-systems
16+
---
17+
18+
Azure Service Bus sessions are the natural choice when a consumer must process related messages in order. But what if the messages already arrive through a non-session subscription, while a downstream consumer still expects an ordered, session-aware stream?
19+
20+
This article explores one possible bridge between those two models. The idea is to use message deferral as a broker-backed buffer and session state as a small index that remembers which deferred messages can be released later.
21+
22+
It is deliberately an exploration of the approach, not a production-ready implementation. The interesting part is the state machine and the failure modes it exposes.
23+
24+
## The scenario
25+
26+
Suppose every message carries two pieces of application-level metadata:
27+
28+
- `SessionId` identifies one logical stream.
29+
- `order` is a monotonically increasing integer within that stream.
30+
31+
The input subscription does **not** require sessions. Messages for one logical stream can therefore be observed as:
32+
33+
```text
34+
1, 3, 4, 2
35+
```
36+
37+
The downstream subscription does require sessions and should expose:
38+
39+
```text
40+
1, 2, 3, 4
41+
```
42+
43+
In the sample, the flow looks like this:
44+
45+
```text
46+
NO_SESSION / NO_SESS_SUB
47+
|
48+
v
49+
PowerShell reorderer
50+
| |
51+
| defer | forward
52+
v v
53+
broker storage ORDERED_TOPIC / SESS_SUB
54+
^
55+
|
56+
session state: LastSeen + [(order, sequence number)]
57+
```
58+
59+
The full sample is implemented in [`reorderAndForward2.ps1`](https://github.com/eosfor/pubs/blob/main/scripts/orderingTest/reorderAndForward2.ps1) in the [`pubs`](https://github.com/eosfor/pubs) repository.
60+
61+
## Keep payloads in the broker
62+
63+
An obvious design would keep early messages in a PowerShell collection until the missing message arrives. That creates a fragile in-memory buffer: a process restart loses it, and payloads consume memory while a gap remains open.
64+
65+
Service Bus already has a better place for those payloads. A deferred message stays in the broker and can later be retrieved by its `SequenceNumber`.
66+
67+
The reorderer only persists compact metadata in session state:
68+
69+
```text
70+
LastSeenOrderNum = 1
71+
Deferred = [
72+
{ Order = 3; Seq = 42 },
73+
{ Order = 4; Seq = 43 }
74+
]
75+
```
76+
77+
This gives the script enough information to retrieve deferred messages without copying their bodies into its own state.
78+
79+
## The three decisions
80+
81+
For each message, the script loads state for its logical `SessionId` and calculates:
82+
83+
```powershell
84+
$expected = $state.LastSeenOrderNum + 1
85+
```
86+
87+
It then makes one of three decisions:
88+
89+
| Condition | Action |
90+
| --- | --- |
91+
| `order -eq expected` | Forward the message, complete the input, then drain any contiguous deferred messages. |
92+
| `order -gt expected` | Defer the input and save its `order` and `SequenceNumber`. |
93+
| `order -lt expected` | Treat it as stale and dead-letter it. |
94+
95+
The heart of the approach can be reduced to this pseudocode:
96+
97+
```powershell
98+
if ($order -eq $expected) {
99+
Send-ToOrderedTopic $message
100+
Complete-InputMessage $message
101+
$state.LastSeenOrderNum = $order
102+
103+
while ($state contains ($state.LastSeenOrderNum + 1)) {
104+
$next = Receive-DeferredMessage -SequenceNumber $sequenceNumber
105+
Send-ToOrderedTopic $next
106+
Complete-InputMessage $next
107+
$state.LastSeenOrderNum++
108+
}
109+
}
110+
elseif ($order -gt $expected) {
111+
Defer-InputMessage $message
112+
$state.Deferred.Add(@{
113+
Order = $order
114+
Seq = $message.SequenceNumber
115+
})
116+
}
117+
else {
118+
DeadLetter-InputMessage $message
119+
}
120+
121+
Save-State $state
122+
```
123+
124+
The actual script uses typed `SessionOrderingState` and `OrderSeq` objects, rather than untyped hashtables, and separates these operations into small PowerShell functions.
125+
126+
## Walking through a gap
127+
128+
First, the producer sends `1`, `3`, and `4`. The reorderer forwards `1`, but it cannot forward `3` or `4`: both depend on the missing `2`. Those two messages are deferred and their sequence numbers are saved.
129+
130+
![Terminal output after orders 3 and 4 have been deferred](/images/articles/service-bus-reordering-deferred.png)
131+
132+
The state is now:
133+
134+
```text
135+
LastSeen = 1
136+
Deferred = [3, 4]
137+
```
138+
139+
When `2` arrives, the reorderer forwards it and advances `LastSeen` to `2`. It can now retrieve deferred `3` by sequence number. After forwarding `3`, the same check makes `4` contiguous, so the script retrieves and forwards that message too.
140+
141+
![Terminal output showing the ordered result 1, 2, 3, 4](/images/articles/service-bus-reordering-ordered-output.png)
142+
143+
Here is the complete run:
144+
145+
![Terminal screencast of the reordering example](/images/articles/service-bus-reordering-demo.gif)
146+
147+
## Running the experiment locally
148+
149+
The repository contains a Docker Compose definition for the Azure Service Bus Emulator and SQL Edge. You need Docker Desktop, PowerShell 7, and the .NET 8 or 9 SDK.
150+
151+
Clone the repository, create the `.env` file described in its README, and start the emulator:
152+
153+
```bash
154+
git clone https://github.com/eosfor/pubs.git
155+
cd pubs
156+
docker compose -f docker-compose.sbus.yml up -d
157+
dotnet build src/SBPowerShell/SBPowerShell.csproj -c Release
158+
```
159+
160+
Then open PowerShell and load the module and the reordering functions:
161+
162+
```powershell
163+
Import-Module ./src/SBPowerShell/bin/Release/net8.0/pubs.psd1 -Force
164+
. ./scripts/orderingTest/reorderAndForward2.ps1
165+
166+
$conn = 'Endpoint=sb://localhost;' +
167+
'SharedAccessKeyName=RootManageSharedAccessKey;' +
168+
'SharedAccessKey=LocalEmulatorKey123!;' +
169+
'UseDevelopmentEmulator=true;'
170+
171+
$sessionId = "ordering-demo-$([guid]::NewGuid().ToString('N'))"
172+
```
173+
174+
Send the first three messages:
175+
176+
```powershell
177+
foreach ($order in 1, 3, 4) {
178+
$message = New-SBMessage `
179+
-Body "event-$order" `
180+
-SessionId $sessionId `
181+
-CustomProperties @{ order = [int]$order }
182+
183+
Send-SBMessage `
184+
-Topic 'NO_SESSION' `
185+
-Message $message `
186+
-ServiceBusConnectionString $conn
187+
}
188+
189+
Receive-SBMessage `
190+
-Topic 'NO_SESSION' `
191+
-Subscription 'NO_SESS_SUB' `
192+
-ServiceBusConnectionString $conn `
193+
-NoComplete `
194+
-MaxMessages 3 |
195+
Process-Message -ConnStr $conn -Verbose
196+
```
197+
198+
Now send the missing message and process it:
199+
200+
```powershell
201+
$message = New-SBMessage `
202+
-Body 'event-2' `
203+
-SessionId $sessionId `
204+
-CustomProperties @{ order = [int]2 }
205+
206+
Send-SBMessage `
207+
-Topic 'NO_SESSION' `
208+
-Message $message `
209+
-ServiceBusConnectionString $conn
210+
211+
Receive-SBMessage `
212+
-Topic 'NO_SESSION' `
213+
-Subscription 'NO_SESS_SUB' `
214+
-ServiceBusConnectionString $conn `
215+
-NoComplete `
216+
-MaxMessages 1 |
217+
Process-Message -ConnStr $conn -Verbose
218+
```
219+
220+
Finally, read the session-aware output:
221+
222+
```powershell
223+
Receive-SBMessage `
224+
-Topic 'ORDERED_TOPIC' `
225+
-Subscription 'SESS_SUB' `
226+
-ServiceBusConnectionString $conn `
227+
-MaxMessages 4 |
228+
Select-Object `
229+
@{ Name = 'Order'; Expression = { $_.ApplicationProperties['order'] } },
230+
@{ Name = 'Body'; Expression = { $_.Body.ToString() } }
231+
```
232+
233+
The expected result is:
234+
235+
```text
236+
Order Body
237+
----- ----
238+
1 event-1
239+
2 event-2
240+
3 event-3
241+
4 event-4
242+
```
243+
244+
## Where the approach stops being an implementation
245+
246+
The experiment makes several simplifying assumptions that matter in a real system.
247+
248+
### The first message defines the starting point
249+
250+
The state is initialized from the first message the reorderer receives. If `7` is first, the script accepts `7` as the beginning; messages `1` through `6` arriving later are stale. A production design needs an explicit starting-order contract if that behavior is unacceptable.
251+
252+
### Forward, complete, and save are not atomic
253+
254+
The sample forwards a message, completes its input copy, and then saves state. A crash between these operations can produce duplicates or state that no longer reflects the broker. Downstream processing must be idempotent, or the bridge needs a stronger transactional and recovery design.
255+
256+
### An open gap needs limits
257+
258+
If message `2` never arrives, the list of deferred sequence numbers continues to grow. A real worker needs limits for gap size and age, plus a policy for expiry, dead-lettering, alerting, and recovery.
259+
260+
### One logical stream needs one coordinator
261+
262+
Two workers updating the same logical stream can race unless ownership is coordinated. Service Bus sessions normally provide that coordination through a session lock; this example borrows session state for bookkeeping while consuming from a non-session subscription, so concurrency needs deliberate treatment.
263+
264+
### Ordering does not remove the need for duplicate handling
265+
266+
Retries, redelivery, and failures around settlement still exist. The `order` property helps identify stale messages, but a business-level message identifier and idempotent downstream operations are still valuable.
267+
268+
## Why the pattern is useful
269+
270+
Even with those limitations, this is a useful experiment because it separates three concerns:
271+
272+
1. Service Bus stores deferred payloads.
273+
2. Session state stores the minimum ordering index.
274+
3. PowerShell expresses the state transition in a compact, inspectable form.
275+
276+
That makes it practical for exploring message-ordering behavior locally, testing failure hypotheses, and deciding which guarantees a production implementation would actually need.
277+
278+
The result is not “ordered messaging added to a non-session subscription.” It is a small bridge that demonstrates how deferral, sequence numbers, and session state can cooperate—and where their guarantees end.
55.3 KB
Loading
144 KB
Loading
80.8 KB
Loading

0 commit comments

Comments
 (0)