Skip to content

Commit 6976efc

Browse files
authored
Merge pull request #90 from tgiachi/develop
release: SQS subscription wiring fix
2 parents e22c2c0 + 884eb1c commit 6976efc

5 files changed

Lines changed: 48 additions & 17 deletions

File tree

src/SquidStd.Messaging.Sqs/Services/SqsTopicProvider.cs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,19 @@ Func<ReadOnlyMemory<byte>, CancellationToken, Task> handler
149149
_handler = handler;
150150
}
151151

152+
// SNS only fans a publish out to subscriptions that are already confirmed at publish time
153+
// (unlike a plain SQS queue, it does not buffer for endpoints that subscribe later), so the
154+
// subscribe/queue/policy/SNS-subscribe wiring below must complete before Start() returns.
155+
// This mirrors RabbitMqTopicProvider.Subscription.Start(), which blocks the same way.
152156
public void Start()
153-
=> _loop = Task.Run(() => RunAsync(_cts.Token));
157+
{
158+
var queueUrl = SetupAsync(_cts.Token).GetAwaiter().GetResult();
159+
160+
if (queueUrl is not null)
161+
{
162+
_loop = Task.Run(() => ReceiveLoopAsync(queueUrl, _cts.Token));
163+
}
164+
}
154165

155166
private static string BuildPolicy(string queueArn, string topicArn)
156167
=> JsonSerializer.Serialize(
@@ -171,18 +182,21 @@ private static string BuildPolicy(string queueArn, string topicArn)
171182
}
172183
);
173184

174-
private async Task RunAsync(CancellationToken cancellationToken)
185+
/// <summary>
186+
/// Creates the subscriber's dedicated queue, grants SNS permission to send to it and
187+
/// confirms the SNS subscription. Returns the queue URL once the subscription is fully
188+
/// wired and ready to receive fanned-out messages, or null on failure/cancellation.
189+
/// </summary>
190+
private async Task<string?> SetupAsync(CancellationToken cancellationToken)
175191
{
176-
string queueUrl;
177-
178192
try
179193
{
180194
var topicArn = await _provider.EnsureTopicAsync(_topic, cancellationToken);
181195
var queueName = SqsNames.Sanitize(_topic) + "-sub-" + _index;
182-
queueUrl = (await _provider._sqs!.CreateQueueAsync(
183-
new CreateQueueRequest { QueueName = queueName },
184-
cancellationToken
185-
)).QueueUrl;
196+
var queueUrl = (await _provider._sqs!.CreateQueueAsync(
197+
new CreateQueueRequest { QueueName = queueName },
198+
cancellationToken
199+
)).QueueUrl;
186200
_queueUrl = queueUrl;
187201

188202
var attributes = await _provider._sqs.GetQueueAttributesAsync(
@@ -211,18 +225,23 @@ await _provider._sqs.SetQueueAttributesAsync(
211225
},
212226
cancellationToken
213227
)).SubscriptionArn;
228+
229+
return queueUrl;
214230
}
215231
catch (OperationCanceledException)
216232
{
217-
return;
233+
return null;
218234
}
219235
catch (Exception ex)
220236
{
221237
_provider._logger.Warning(ex, "SQS topic '{Topic}' subscribe setup failed", _topic);
222238

223-
return;
239+
return null;
224240
}
241+
}
225242

243+
private async Task ReceiveLoopAsync(string queueUrl, CancellationToken cancellationToken)
244+
{
226245
while (!cancellationToken.IsCancellationRequested)
227246
{
228247
ReceiveMessageResponse response;

tests/SquidStd.Tests/Messaging/InMemoryQueueProviderTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,10 @@ public async Task TwoSubscribers_ReceiveRoundRobin()
163163
await provider.PublishAsync("q", Bytes("m"));
164164
}
165165

166-
Assert.True(done.Wait(Timeout));
166+
// The single consumer loop is dispatched via Task.Run and dispatches round-robin
167+
// strictly in registration order, so the split is deterministic once all four
168+
// deliveries land; a generous bound just absorbs thread-pool scheduling delay under load.
169+
Assert.True(done.Wait(TimeSpan.FromSeconds(30)));
167170
Assert.Equal(2, aCount);
168171
Assert.Equal(2, bCount);
169172
}

tests/SquidStd.Tests/Messaging/Sqs/SqsTopicProviderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public async Task Publish_FansOutToAllSubscribers()
4242
}
4343
);
4444

45-
// Allow the subscriptions to be wired before publishing.
46-
await Task.Delay(2000);
45+
// Subscribe() now blocks until the SNS subscription is confirmed (see SqsTopicProvider),
46+
// so both subscriptions are already fanned-in by the time it returns; no delay needed.
4747
await provider.PublishAsync(topic, Bytes("hello"));
4848

4949
Assert.Equal("hello", await a.Task.WaitAsync(Timeout));

tests/SquidStd.Tests/Services/Core/MetricsCollectionServiceTests.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,14 @@ [new CountingMetricProvider("events", "published.total", 5)],
8383
);
8484

8585
await service.StartAsync(CancellationToken.None);
86-
await WaitUntilAsync(() => firstListener.LastEvent is not null && secondListener.LastEvent is not null);
86+
87+
// The collection loop's first tick still has to be scheduled via Task.Run, so give it
88+
// more headroom than the default: on a loaded CI runner that scheduling can lag well
89+
// past a couple of seconds even though the collector itself collects immediately.
90+
await WaitUntilAsync(
91+
() => firstListener.LastEvent is not null && secondListener.LastEvent is not null,
92+
TimeSpan.FromSeconds(30)
93+
);
8794
await service.StopAsync(CancellationToken.None);
8895

8996
Assert.NotNull(firstListener.LastEvent);
@@ -161,9 +168,9 @@ public async Task StopAsync_StopsCollectionLoop()
161168
Assert.Equal(countAfterStop, provider.CollectionCount);
162169
}
163170

164-
private static async Task WaitUntilAsync(Func<bool> predicate)
171+
private static async Task WaitUntilAsync(Func<bool> predicate, TimeSpan? timeout = null)
165172
{
166-
var deadline = DateTime.UtcNow.AddSeconds(2);
173+
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(2));
167174

168175
while (DateTime.UtcNow < deadline)
169176
{

tests/SquidStd.Tests/Services/Core/Scheduling/TimerWheelPumpServiceTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ public async Task Pump_AdvancesTheWheel()
2525

2626
await pump.StartAsync();
2727

28-
Assert.True(timer.Pumped.Wait(TimeSpan.FromSeconds(2)));
28+
// The pump loop is dispatched via Task.Run; a generous bound absorbs thread-pool
29+
// scheduling delay on loaded CI runners while still failing fast if it never pumps.
30+
Assert.True(timer.Pumped.Wait(TimeSpan.FromSeconds(30)));
2931

3032
await pump.StopAsync();
3133
pump.Dispose();

0 commit comments

Comments
 (0)