Skip to content

Commit b6b263a

Browse files
committed
Implement the timeout command
1 parent f1f39a8 commit b6b263a

31 files changed

Lines changed: 1100 additions & 324 deletions
Lines changed: 77 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,78 @@
1-
using System.Text;
2-
using BuslyCLI.Config;
3-
using BuslyCLI.Infrastructure.Factories;
4-
using NServiceBus.DelayedDelivery;
5-
using NServiceBus.Routing;
6-
using NServiceBus.Transport;
7-
using Spectre.Console.Cli;
8-
9-
namespace BuslyCLI.Commands.NsbTimeout;
10-
11-
public class SendTimeout(IRawEndpointFactory rawEndpointFactory, INServiceBusConfiguration nServiceBusConfiguration) : AsyncCommand<SendTimeoutCommandSettings>
12-
{
13-
protected override async Task<int> ExecuteAsync(CommandContext context, SendTimeoutCommandSettings settings, CancellationToken cancellationToken)
14-
{
15-
var config = await nServiceBusConfiguration.GetValidatedConfigurationAsync(settings.Config.Path);
16-
var rawEndpoint = await rawEndpointFactory.CreateRawSendOnlyEndpoint(Constants.DefaultOriginatingEndpoint, config.CurrentTransportConfig);
17-
// TODO: Validate body is valid json/xml
18-
var headers = new Dictionary<string, string>
19-
{
20-
["NServiceBus.OriginatingEndpoint"] = Constants.DefaultOriginatingEndpoint,
21-
["NServiceBus.OriginatingMachine"] = Environment.MachineName,
22-
["NServiceBus.ConversationId"] = Guid.NewGuid().ToString(),
23-
["NServiceBus.CorrelationId"] = Guid.NewGuid().ToString(),
24-
["NServiceBus.MessageIntent"] = Constants.NServiceBus.CommandMessageIntent,
25-
["NServiceBus.ContentType"] = settings.ContentType,
26-
["NServiceBus.EnclosedMessageTypes"] = settings.EnclosedMessageType
27-
};
28-
var message = new OutgoingMessage(
29-
Guid.NewGuid().ToString(),
30-
headers,
31-
Encoding.ASCII.GetBytes(settings.MessageBody)
32-
);
33-
34-
var dispatchProperties = new DispatchProperties();
35-
36-
if (settings.DoNotDeliverBefore is not null)
37-
{
38-
dispatchProperties.DoNotDeliverBefore = new DoNotDeliverBefore(settings.DoNotDeliverBefore.Value);
39-
}
40-
else if (settings.DelayDeliveryWith is not null)
41-
{
42-
dispatchProperties.DelayDeliveryWith = new DelayDeliveryWith(settings.DelayDeliveryWith.Value);
43-
}
44-
45-
var transportOperation = new TransportOperation(
46-
message,
47-
new UnicastAddressTag(settings.DestinationEndpoint),
48-
dispatchProperties
49-
);
50-
51-
await rawEndpoint.Dispatch(
52-
new TransportOperations(transportOperation),
53-
new TransportTransaction(),
54-
cancellationToken);
55-
56-
await rawEndpoint.ShutDownAndCleanUp();
57-
58-
return 0;
59-
}
1+
using System.Text;
2+
using BuslyCLI.Config;
3+
using BuslyCLI.Config.Transports;
4+
using BuslyCLI.Infrastructure.Factories;
5+
using NServiceBus.DelayedDelivery;
6+
using NServiceBus.Routing;
7+
using NServiceBus.Transport;
8+
using Spectre.Console;
9+
using Spectre.Console.Cli;
10+
11+
namespace BuslyCLI.Commands.NsbTimeout;
12+
13+
public class SendTimeout(IAnsiConsole console, IRawEndpointFactory rawEndpointFactory, INServiceBusConfiguration nServiceBusConfiguration) : AsyncCommand<SendTimeoutCommandSettings>
14+
{
15+
private static readonly HashSet<Type> UnsupportedTransportTypes =
16+
[
17+
typeof(SqlServerTransportConfig),
18+
typeof(PostgreSqlTransportConfig),
19+
typeof(AzureStorageQueuesTransportConfig)
20+
];
21+
22+
protected override async Task<int> ExecuteAsync(CommandContext context, SendTimeoutCommandSettings settings, CancellationToken cancellationToken)
23+
{
24+
var config = await nServiceBusConfiguration.GetValidatedConfigurationAsync(settings.Config.Path);
25+
26+
if (UnsupportedTransportTypes.Contains(config.CurrentTransportConfig.Config.GetType()))
27+
{
28+
console.MarkupLine($"[red]Error:[/] The [bold]{config.CurrentTransportConfig.Config.GetType().Name.Replace("Config", "")}[/] transport does not support sending timeouts.");
29+
console.MarkupLine("This transport relies on an in-process poller to forward deferred messages, which is incompatible with the CLI's fire-and-forget execution model.");
30+
console.MarkupLine("For details see: [link]https://tragiccode.com/busly-cli/docs/cli-reference/timeout/send[/]");
31+
return 1;
32+
}
33+
34+
var rawEndpoint = await rawEndpointFactory.CreateRawSendOnlyEndpoint(Constants.DefaultOriginatingEndpoint, config.CurrentTransportConfig);
35+
// TODO: Validate body is valid json/xml
36+
var headers = new Dictionary<string, string>
37+
{
38+
["NServiceBus.OriginatingEndpoint"] = Constants.DefaultOriginatingEndpoint,
39+
["NServiceBus.OriginatingMachine"] = Environment.MachineName,
40+
["NServiceBus.ConversationId"] = Guid.NewGuid().ToString(),
41+
["NServiceBus.CorrelationId"] = Guid.NewGuid().ToString(),
42+
["NServiceBus.MessageIntent"] = Constants.NServiceBus.CommandMessageIntent,
43+
["NServiceBus.ContentType"] = settings.ContentType,
44+
["NServiceBus.EnclosedMessageTypes"] = settings.EnclosedMessageType
45+
};
46+
var message = new OutgoingMessage(
47+
Guid.NewGuid().ToString(),
48+
headers,
49+
Encoding.ASCII.GetBytes(settings.MessageBody)
50+
);
51+
52+
var dispatchProperties = new DispatchProperties();
53+
54+
if (settings.DoNotDeliverBefore is not null)
55+
{
56+
dispatchProperties.DoNotDeliverBefore = new DoNotDeliverBefore(settings.DoNotDeliverBefore.Value);
57+
}
58+
else if (settings.DelayDeliveryWith is not null)
59+
{
60+
dispatchProperties.DelayDeliveryWith = new DelayDeliveryWith(settings.DelayDeliveryWith.Value);
61+
}
62+
63+
var transportOperation = new TransportOperation(
64+
message,
65+
new UnicastAddressTag(settings.DestinationEndpoint),
66+
dispatchProperties
67+
);
68+
69+
await rawEndpoint.Dispatch(
70+
new TransportOperations(transportOperation),
71+
new TransportTransaction(),
72+
cancellationToken);
73+
74+
await rawEndpoint.ShutDownAndCleanUp();
75+
76+
return 0;
77+
}
6078
}
Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
using System.ComponentModel;
2-
using Spectre.Console;
3-
using Spectre.Console.Cli;
4-
5-
namespace BuslyCLI.Commands.NsbTimeout;
6-
7-
public class SendTimeoutCommandSettings : CommonCommandSettings
8-
{
9-
[CommandOption("--do-not-deliver-before <do-not-deliver-before>")]
10-
[Description("Allows specifying a date before which the delivery should not occur, using ISO-8601 format (YYYY-MM-DDTHH:mm:ssZ)")]
11-
public DateTime? DoNotDeliverBefore { get; init; }
12-
13-
[CommandOption("--delay-delivery-with <delay-delivery-with>")]
14-
// ([days.]hh:mm:ss[.fffffff])
15-
[Description("Specifies the delay before the timeout is delivered, using a TimeSpan format")]
16-
public TimeSpan? DelayDeliveryWith { get; init; }
17-
18-
public override ValidationResult Validate()
19-
{
20-
var baseResult = base.Validate();
21-
if (baseResult.Successful == false) return baseResult;
22-
// Neither provided
23-
if (DelayDeliveryWith is null && DoNotDeliverBefore is null)
24-
{
25-
return ValidationResult.Error(
26-
"You must specify either --do-not-deliver-before or --delay-delivery-with.");
27-
}
28-
29-
// Both provided
30-
if (DelayDeliveryWith is not null && DoNotDeliverBefore is not null)
31-
{
32-
return ValidationResult.Error(
33-
"--do-not-deliver-before and --delay-delivery-with cannot be used together.");
34-
}
35-
36-
return ValidationResult.Success();
37-
}
1+
using System.ComponentModel;
2+
using Spectre.Console;
3+
using Spectre.Console.Cli;
4+
5+
namespace BuslyCLI.Commands.NsbTimeout;
6+
7+
public class SendTimeoutCommandSettings : CommonCommandSettings
8+
{
9+
[CommandOption("--do-not-deliver-before <do-not-deliver-before>")]
10+
[Description("Allows specifying a date before which the delivery should not occur, using ISO-8601 format (YYYY-MM-DDTHH:mm:ssZ)")]
11+
public DateTime? DoNotDeliverBefore { get; init; }
12+
13+
[CommandOption("--delay-delivery-with <delay-delivery-with>")]
14+
// ([days.]hh:mm:ss[.fffffff])
15+
[Description("Specifies the delay before the timeout is delivered, using a TimeSpan format")]
16+
public TimeSpan? DelayDeliveryWith { get; init; }
17+
18+
public override ValidationResult Validate()
19+
{
20+
var baseResult = base.Validate();
21+
if (baseResult.Successful == false) return baseResult;
22+
// Neither provided
23+
if (DelayDeliveryWith is null && DoNotDeliverBefore is null)
24+
{
25+
return ValidationResult.Error(
26+
"You must specify either --do-not-deliver-before or --delay-delivery-with.");
27+
}
28+
29+
// Both provided
30+
if (DelayDeliveryWith is not null && DoNotDeliverBefore is not null)
31+
{
32+
return ValidationResult.Error(
33+
"--do-not-deliver-before and --delay-delivery-with cannot be used together.");
34+
}
35+
36+
return ValidationResult.Success();
37+
}
3838
}

src/BuslyCLI.Console/Infrastructure/Endpoints/RawEndpoint.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,6 @@ public Task<ErrorHandleResult> OnError(ErrorContext errorContext, CancellationTo
7676
return Task.FromResult(ErrorHandleResult.Handled);
7777
}
7878

79-
public IncomingMessage TryReceiveMessageWithTimeout()
80-
{
81-
if (_receivedMessages.TryTake(out var incomingMessage, IncomingMessageTimeout)) return incomingMessage;
82-
throw new TimeoutException($"The message did not arrive within {IncomingMessageTimeout.TotalSeconds} seconds.");
83-
}
84-
8579
public IncomingMessage TryReceiveMessage()
8680
{
8781
if (_receivedMessages.TryTake(out var incomingMessage, IncomingMessageTimeout)) return incomingMessage;

tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@
4848
<ProjectReference Include="..\..\src\BuslyCLI.Console\BuslyCLI.Console.csproj" />
4949
</ItemGroup>
5050

51-
<ItemGroup>
52-
<Folder Include="Commands\Event\" />
53-
</ItemGroup>
54-
5551
<ItemGroup>
5652
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
5753
</ItemGroup>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using BuslyCLI.Console.Tests.TestHelpers;
2+
3+
namespace BuslyCLI.Console.Tests.Commands.NsbTimeout;
4+
5+
public class SendTimeoutTests : CommandTestBase
6+
{
7+
[Test]
8+
public void ShouldOutputAnErrorWhenTransportIsSqlServer()
9+
{
10+
// Arrange
11+
var yamlFile = """
12+
---
13+
current-transport: local-sql-server
14+
transports:
15+
- name: local-sql-server
16+
sql-server-transport-config:
17+
connection-string: Server=localhost;Database=test;
18+
""";
19+
using var configFile = new TestableNServiceBusConfigurationFile(yamlFile);
20+
21+
// Act
22+
var result = Sut.Run(
23+
"timeout", "send",
24+
"--content-type", "application/json",
25+
"--enclosed-message-type", "MessageContracts.Timeouts.OrderTimeout",
26+
"--destination-endpoint", "Sales",
27+
"--message-body", "{}",
28+
"--delay-delivery-with", "00:00:01",
29+
"--config", configFile.FilePath);
30+
31+
// Assert
32+
Assert.That(result.ExitCode, Is.EqualTo(1));
33+
Assert.That(result.Output, Does.Contain("SqlServerTransport"));
34+
Assert.That(result.Output, Does.Contain("does not support sending timeouts"));
35+
Assert.That(result.Output, Does.Contain("https://tragiccode.com/busly-cli/docs/cli-reference/timeout/send"));
36+
}
37+
38+
[Test]
39+
public void ShouldOutputAnErrorWhenTransportIsPostgreSql()
40+
{
41+
// Arrange
42+
var yamlFile = """
43+
---
44+
current-transport: local-postgresql
45+
transports:
46+
- name: local-postgresql
47+
postgre-sql-transport-config:
48+
connection-string: Host=localhost;Database=test;
49+
""";
50+
using var configFile = new TestableNServiceBusConfigurationFile(yamlFile);
51+
52+
// Act
53+
var result = Sut.Run(
54+
"timeout", "send",
55+
"--content-type", "application/json",
56+
"--enclosed-message-type", "MessageContracts.Timeouts.OrderTimeout",
57+
"--destination-endpoint", "Sales",
58+
"--message-body", "{}",
59+
"--delay-delivery-with", "00:00:01",
60+
"--config", configFile.FilePath);
61+
62+
// Assert
63+
Assert.That(result.ExitCode, Is.EqualTo(1));
64+
Assert.That(result.Output, Does.Contain("PostgreSqlTransport"));
65+
Assert.That(result.Output, Does.Contain("does not support sending timeouts"));
66+
Assert.That(result.Output, Does.Contain("https://tragiccode.com/busly-cli/docs/cli-reference/timeout/send"));
67+
}
68+
69+
[Test]
70+
public void ShouldOutputAnErrorWhenTransportIsAzureStorageQueues()
71+
{
72+
// Arrange
73+
var yamlFile = """
74+
---
75+
current-transport: local-azure-storage-queues
76+
transports:
77+
- name: local-azure-storage-queues
78+
azure-storage-queues-transport-config:
79+
connection-string: UseDevelopmentStorage=true
80+
""";
81+
using var configFile = new TestableNServiceBusConfigurationFile(yamlFile);
82+
83+
// Act
84+
var result = Sut.Run(
85+
"timeout", "send",
86+
"--content-type", "application/json",
87+
"--enclosed-message-type", "MessageContracts.Timeouts.OrderTimeout",
88+
"--destination-endpoint", "Sales",
89+
"--message-body", "{}",
90+
"--delay-delivery-with", "00:00:01",
91+
"--config", configFile.FilePath);
92+
93+
// Assert
94+
Assert.That(result.ExitCode, Is.EqualTo(1));
95+
Assert.That(result.Output, Does.Contain("AzureStorageQueuesTransport"));
96+
Assert.That(result.Output, Does.Contain("does not support sending timeouts"));
97+
Assert.That(result.Output, Does.Contain("https://tragiccode.com/busly-cli/docs/cli-reference/timeout/send"));
98+
}
99+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Text.Json;
2+
using BuslyCLI.Console.Tests.TestHelpers;
3+
4+
namespace BuslyCLI.Console.Tests.EndToEnd.AmazonSQS;
5+
6+
[TestFixture]
7+
public class PublishEventCommandAmazonSqsEndToEndTests : AmazonSqsEndToEndTestBase
8+
{
9+
[Test]
10+
public async Task ShouldPublishEvent()
11+
{
12+
// Arrange
13+
await TestEndpoint.Subscribe("MessageContracts.Events.OrderCreated");
14+
var messageBody = new { OrderNumber = Guid.NewGuid() };
15+
16+
var json = JsonSerializer.Serialize(messageBody, _jsonObjectOptions);
17+
var yamlFile = $"""
18+
---
19+
current-transport: local-amazonsqs
20+
transports:
21+
- name: local-amazonsqs
22+
amazonsqs-transport-config:
23+
service-url: {Container.GetConnectionString()}
24+
region-name: us-east-1
25+
""";
26+
using var configFile = new TestableNServiceBusConfigurationFile(yamlFile);
27+
28+
// Act
29+
var result = Sut.Run(
30+
"event",
31+
"publish",
32+
"--content-type", "application/json",
33+
"--enclosed-message-type", "MessageContracts.Events.OrderCreated",
34+
"--message-body", json,
35+
"--config", configFile.FilePath);
36+
37+
// Assert
38+
Assert.That(result.ExitCode, Is.EqualTo(0));
39+
AssertMessageReceived(TestEndpoint.TryReceiveMessage(), "MessageContracts.Events.OrderCreated", json);
40+
}
41+
}

0 commit comments

Comments
 (0)