diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b9c6f314..6a4aa284 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,6 +13,17 @@ updates: versions: [ "[6.0,)" ] - dependency-name: "org.junit.platform:*" versions: [ "[6.0,)" ] + - package-ecosystem: "maven" + directory: "/java-amqp" + schedule: + interval: "daily" + open-pull-requests-limit: 20 + target-branch: "main" + ignore: + - dependency-name: "org.junit.jupiter:*" + versions: [ "[6.0,)" ] + - dependency-name: "org.junit.platform:*" + versions: [ "[6.0,)" ] - package-ecosystem: "maven" directory: "/java-stream-mvn" schedule: diff --git a/.gitignore b/.gitignore index 42ae1774..35e5b18c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ target/ .classpath .project .settings + +/.cursor/plans diff --git a/dotnet-amqp/.gitattributes b/dotnet-amqp/.gitattributes new file mode 100644 index 00000000..27ab416b --- /dev/null +++ b/dotnet-amqp/.gitattributes @@ -0,0 +1,13 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Auto detect text files and perform LF normalization +*.cs text=auto eol=lf +*.txt text=auto + +# Declare files that will always have CRLF line endings on checkout. +*.sln text eol=crlf +*.csproj text eol=crlf + +# Custom for Visual Studio +*.cs diff=csharp diff --git a/dotnet-amqp/.gitignore b/dotnet-amqp/.gitignore new file mode 100644 index 00000000..4cbb9f26 --- /dev/null +++ b/dotnet-amqp/.gitignore @@ -0,0 +1,6 @@ +*.dll +*.exe +*.lock.json +packages/ +bin/ +obj/ diff --git a/dotnet-amqp/EmitLog/EmitLog.csproj b/dotnet-amqp/EmitLog/EmitLog.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/EmitLog/EmitLog.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/EmitLog/Program.cs b/dotnet-amqp/EmitLog/Program.cs new file mode 100644 index 00000000..78e95436 --- /dev/null +++ b/dotnet-amqp/EmitLog/Program.cs @@ -0,0 +1,58 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string exchangeName = "logs"; + +string message = args.Length < 1 ? "info: Hello World!" : string.Join(" ", args); + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-emitlog") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IExchangeSpecification exchangeSpec = management.Exchange(exchangeName).Type("fanout"); + await exchangeSpec.DeclareAsync(); + + IPublisher publisher = await connection.PublisherBuilder().Exchange(exchangeName).BuildAsync(); + try + { + var amqpMessage = new AmqpMessage(Encoding.UTF8.GetBytes(message)); + PublishResult pr = await publisher.PublishAsync(amqpMessage); + switch (pr.Outcome.State) + { + case OutcomeState.Accepted: + break; + case OutcomeState.Released: + Console.Error.WriteLine($"Released message: {pr.Message.BodyAsString()}"); + Environment.Exit(1); + break; + case OutcomeState.Rejected: + Console.Error.WriteLine($"[Publisher] Message: {pr.Message.BodyAsString()} rejected with error: {pr.Outcome.Error}"); + Environment.Exit(1); + break; + default: + Console.Error.WriteLine($"Unexpected publish outcome: {pr.Outcome.State}"); + Environment.Exit(1); + break; + } + + Console.WriteLine($" [x] Sent '{message}'"); + } + finally + { + await publisher.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/EmitLogDirect/EmitLogDirect.csproj b/dotnet-amqp/EmitLogDirect/EmitLogDirect.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/EmitLogDirect/EmitLogDirect.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/EmitLogDirect/Program.cs b/dotnet-amqp/EmitLogDirect/Program.cs new file mode 100644 index 00000000..004036d0 --- /dev/null +++ b/dotnet-amqp/EmitLogDirect/Program.cs @@ -0,0 +1,71 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string exchangeName = "logs_direct"; + +string severity = GetSeverity(args); +string message = GetMessage(args); + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-emitlogdirect") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IExchangeSpecification exchangeSpec = management.Exchange(exchangeName).Type("direct"); + await exchangeSpec.DeclareAsync(); + + IPublisher publisher = await connection.PublisherBuilder().Exchange(exchangeName).Key(severity).BuildAsync(); + try + { + var amqpMessage = new AmqpMessage(Encoding.UTF8.GetBytes(message)); + PublishResult pr = await publisher.PublishAsync(amqpMessage); + switch (pr.Outcome.State) + { + case OutcomeState.Accepted: + break; + case OutcomeState.Released: + Console.Error.WriteLine($"Released message: {pr.Message.BodyAsString()}"); + Environment.Exit(1); + break; + case OutcomeState.Rejected: + Console.Error.WriteLine($"[Publisher] Message: {pr.Message.BodyAsString()} rejected with error: {pr.Outcome.Error}"); + Environment.Exit(1); + break; + default: + Console.Error.WriteLine($"Unexpected publish outcome: {pr.Outcome.State}"); + Environment.Exit(1); + break; + } + + Console.WriteLine($" [x] Sent '{severity}':'{message}'"); + } + finally + { + await publisher.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} + +static string GetSeverity(string[] strings) => strings.Length < 1 ? "info" : strings[0]; + +static string GetMessage(string[] strings) +{ + if (strings.Length < 2) + { + return "Hello World!"; + } + + return string.Join(" ", strings.Skip(1)); +} diff --git a/dotnet-amqp/EmitLogTopic/EmitLogTopic.csproj b/dotnet-amqp/EmitLogTopic/EmitLogTopic.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/EmitLogTopic/EmitLogTopic.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/EmitLogTopic/Program.cs b/dotnet-amqp/EmitLogTopic/Program.cs new file mode 100644 index 00000000..388717e9 --- /dev/null +++ b/dotnet-amqp/EmitLogTopic/Program.cs @@ -0,0 +1,71 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string exchangeName = "logs_topic"; + +string routingKey = GetRouting(args); +string message = GetMessage(args); + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-emitlogtopic") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IExchangeSpecification exchangeSpec = management.Exchange(exchangeName).Type("topic"); + await exchangeSpec.DeclareAsync(); + + IPublisher publisher = await connection.PublisherBuilder().Exchange(exchangeName).Key(routingKey).BuildAsync(); + try + { + var amqpMessage = new AmqpMessage(Encoding.UTF8.GetBytes(message)); + PublishResult pr = await publisher.PublishAsync(amqpMessage); + switch (pr.Outcome.State) + { + case OutcomeState.Accepted: + break; + case OutcomeState.Released: + Console.Error.WriteLine($"Released message: {pr.Message.BodyAsString()}"); + Environment.Exit(1); + break; + case OutcomeState.Rejected: + Console.Error.WriteLine($"[Publisher] Message: {pr.Message.BodyAsString()} rejected with error: {pr.Outcome.Error}"); + Environment.Exit(1); + break; + default: + Console.Error.WriteLine($"Unexpected publish outcome: {pr.Outcome.State}"); + Environment.Exit(1); + break; + } + + Console.WriteLine($" [x] Sent '{routingKey}':'{message}'"); + } + finally + { + await publisher.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} + +static string GetRouting(string[] strings) => strings.Length < 1 ? "anonymous.info" : strings[0]; + +static string GetMessage(string[] strings) +{ + if (strings.Length < 2) + { + return "Hello World!"; + } + + return string.Join(" ", strings.Skip(1)); +} diff --git a/dotnet-amqp/NewTask/NewTask.csproj b/dotnet-amqp/NewTask/NewTask.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/NewTask/NewTask.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/NewTask/Program.cs b/dotnet-amqp/NewTask/Program.cs new file mode 100644 index 00000000..62b110a5 --- /dev/null +++ b/dotnet-amqp/NewTask/Program.cs @@ -0,0 +1,58 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string taskQueueName = "task_queue"; + +string message = args.Length > 0 ? string.Join(" ", args) : "Hello World!"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-newtask") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IQueueSpecification queueSpec = management.Queue(taskQueueName).Type(QueueType.QUORUM); + await queueSpec.DeclareAsync(); + + IPublisher publisher = await connection.PublisherBuilder().Queue(taskQueueName).BuildAsync(); + try + { + var amqpMessage = new AmqpMessage(Encoding.UTF8.GetBytes(message)); + PublishResult pr = await publisher.PublishAsync(amqpMessage); + switch (pr.Outcome.State) + { + case OutcomeState.Accepted: + break; + case OutcomeState.Released: + Console.Error.WriteLine($"Released message: {pr.Message.BodyAsString()}"); + Environment.Exit(1); + break; + case OutcomeState.Rejected: + Console.Error.WriteLine($"[Publisher] Message: {pr.Message.BodyAsString()} rejected with error: {pr.Outcome.Error}"); + Environment.Exit(1); + break; + default: + Console.Error.WriteLine($"Unexpected publish outcome: {pr.Outcome.State}"); + Environment.Exit(1); + break; + } + + Console.WriteLine($" [x] Sent '{message}'"); + } + finally + { + await publisher.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/PublisherConfirms/Program.cs b/dotnet-amqp/PublisherConfirms/Program.cs new file mode 100644 index 00000000..b7d1aa57 --- /dev/null +++ b/dotnet-amqp/PublisherConfirms/Program.cs @@ -0,0 +1,76 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-publisherconfirms") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + string queueName = Guid.NewGuid().ToString(); + IQueueSpecification queueSpec = management.Queue(queueName).Exclusive(true).AutoDelete(true); + await queueSpec.DeclareAsync(); + + IConsumer consumer = await connection.ConsumerBuilder() + .Queue(queueName) + .MessageHandler((ctx, message) => + { + Console.WriteLine($"Received a message: {message.BodyAsString()}"); + ctx.Accept(); + return Task.CompletedTask; + }) + .BuildAndStartAsync(); + + try + { + await Task.Delay(300); + + IPublisher publisher = await connection.PublisherBuilder().Queue(queueName).BuildAsync(); + try + { + const string text = "hello"; + PublishResult pr = await publisher.PublishAsync(new AmqpMessage(Encoding.UTF8.GetBytes(text))); + switch (pr.Outcome.State) + { + case OutcomeState.Accepted: + Console.WriteLine($" Accepted Message: {pr.Message.BodyAsString()} confirmed"); + break; + case OutcomeState.Released: + Console.Error.WriteLine($"Released message: {pr.Message.BodyAsString()}"); + Environment.Exit(1); + break; + case OutcomeState.Rejected: + Console.Error.WriteLine($"[Publisher] Message: {pr.Message.BodyAsString()} rejected with error: {pr.Outcome.Error}"); + Environment.Exit(1); + break; + default: + Console.Error.WriteLine($"Unexpected publish outcome: {pr.Outcome.State}"); + Environment.Exit(1); + break; + } + + await Task.Delay(500); + } + finally + { + await publisher.CloseAsync(); + } + } + finally + { + await consumer.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/PublisherConfirms/PublisherConfirms.csproj b/dotnet-amqp/PublisherConfirms/PublisherConfirms.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/PublisherConfirms/PublisherConfirms.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/README.md b/dotnet-amqp/README.md new file mode 100644 index 00000000..f4aa527e --- /dev/null +++ b/dotnet-amqp/README.md @@ -0,0 +1,96 @@ +# Dotnet C# code for RabbitMQ tutorials (AMQP 1.0) + +Here you can find C# examples from the [RabbitMQ tutorials](https://www.rabbitmq.com/getstarted.html), using the [RabbitMQ AMQP 1.0 .NET client](https://github.com/rabbitmq/rabbitmq-amqp-dotnet-client) (`RabbitMQ.AMQP.Client` on NuGet) for RabbitMQ 4.x. + +You need a RabbitMQ node running locally. The client requires **RabbitMQ 4.0 or newer** with AMQP 1.0 on port **5672**. The examples use the default `guest` user. See the [AMQP 1.0 client libraries overview](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) and [AMQP in RabbitMQ](https://www.rabbitmq.com/docs/amqp). + +There is a solution file for Visual Studio 2022 (`dotnet-amqp.sln`). From this directory, use `dotnet run -f net8.0 --project` as below. + +End-to-end smoke tests (broker on `localhost`, management plugin used to reset `hello` / `task_queue` when needed): + +```bash +./test-tutorials.sh +``` + +## Requirements + +- [.NET 8 or .NET 10 SDK](https://dotnet.microsoft.com/download) (projects target `net8.0` and `net10.0`; use `-f net10.0` instead of `-f net8.0` in the commands below if you prefer) +- NuGet package: `RabbitMQ.AMQP.Client` (see each `.csproj`; restore runs automatically with `dotnet run` or `dotnet build`) + +## Code + +Run each example from the `dotnet-amqp` directory. + +#### Tutorial one: "Hello World!" + +```bash +dotnet run -f net8.0 --project Receive/Receive.csproj +dotnet run -f net8.0 --project Send/Send.csproj +``` + +#### Tutorial two: Work Queues + +```bash +dotnet run -f net8.0 --project Worker/Worker.csproj +dotnet run -f net8.0 --project Worker/Worker.csproj +dotnet run -f net8.0 --project NewTask/NewTask.csproj "First Message" +dotnet run -f net8.0 --project NewTask/NewTask.csproj "Second Message" +dotnet run -f net8.0 --project NewTask/NewTask.csproj "Third Message" +dotnet run -f net8.0 --project NewTask/NewTask.csproj "Fourth Message" +dotnet run -f net8.0 --project NewTask/NewTask.csproj "Fifth Message" +``` + +#### Tutorial three: Publish/Subscribe + +```bash +dotnet run -f net8.0 --project ReceiveLogs/ReceiveLogs.csproj +dotnet run -f net8.0 --project ReceiveLogs/ReceiveLogs.csproj +dotnet run -f net8.0 --project EmitLog/EmitLog.csproj +``` + +#### Tutorial four: Routing + +```bash +dotnet run -f net8.0 --project ReceiveLogsDirect/ReceiveLogsDirect.csproj warning error +dotnet run -f net8.0 --project ReceiveLogsDirect/ReceiveLogsDirect.csproj info warning error +dotnet run -f net8.0 --project EmitLogDirect/EmitLogDirect.csproj info "Run. Run. Or it will explode." +dotnet run -f net8.0 --project EmitLogDirect/EmitLogDirect.csproj warning "Run. Run. Or it will explode." +dotnet run -f net8.0 --project EmitLogDirect/EmitLogDirect.csproj error "Run. Run. Or it will explode." +``` + +#### Tutorial five: Topics + +```bash +dotnet run -f net8.0 --project ReceiveLogsTopic/ReceiveLogsTopic.csproj "#" +dotnet run -f net8.0 --project ReceiveLogsTopic/ReceiveLogsTopic.csproj "kern.*" +dotnet run -f net8.0 --project ReceiveLogsTopic/ReceiveLogsTopic.csproj "*.critical" +dotnet run -f net8.0 --project ReceiveLogsTopic/ReceiveLogsTopic.csproj "kern.*" "*.critical" +dotnet run -f net8.0 --project EmitLogTopic/EmitLogTopic.csproj kern.critical "A critical kernel error" +``` + +#### Tutorial six: RPC + +```bash +dotnet run -f net8.0 --project RPCServer/RPCServer.csproj +dotnet run -f net8.0 --project RPCClient/RPCClient.csproj +``` + +The client requests Fibonacci numbers for `0` through `31`, matching the Java AMQP 1.0 tutorial client. + +#### Publisher confirms (AMQP 1.0 publish outcomes) + +```bash +dotnet run -f net8.0 --project PublisherConfirms/PublisherConfirms.csproj +``` + +#### AMQP 1.0 Direct Reply-To RPC + +On RabbitMQ **4.2 and newer**, the .NET `Requester` uses [Direct Reply-To](https://www.rabbitmq.com/docs/direct-reply-to) when no explicit reply queue is set (see the client’s requester implementation). Older brokers fall back to a temporary reply queue. + +```bash +dotnet run -f net8.0 --project RpcAmqp10/RpcAmqp10.csproj +``` + +This sample runs a responder and a requester in one process; press CTRL+C to exit. + +To learn more, see the [API documentation](https://rabbitmq.github.io/rabbitmq-amqp-dotnet-client/api/RabbitMQ.AMQP.Client.html). diff --git a/dotnet-amqp/RPCClient/Program.cs b/dotnet-amqp/RPCClient/Program.cs new file mode 100644 index 00000000..0825cdde --- /dev/null +++ b/dotnet-amqp/RPCClient/Program.cs @@ -0,0 +1,44 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-rpcclient") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IRequester requester = await connection.RequesterBuilder() + .RequestAddress() + .Queue("rpc_queue") + .Requester() + .BuildAsync(); + + try + { + for (int i = 0; i < 32; i++) + { + string iStr = i.ToString(); + Console.WriteLine($" [x] Requesting fib({iStr})"); + IMessage request = new AmqpMessage(Encoding.UTF8.GetBytes(iStr)); + IMessage reply = await requester.PublishAsync(request); + string response = reply.BodyAsString(); + Console.WriteLine($" [.] Got '{response}'"); + } + } + finally + { + await requester.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/RPCClient/RPCClient.csproj b/dotnet-amqp/RPCClient/RPCClient.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/RPCClient/RPCClient.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/RPCServer/Program.cs b/dotnet-amqp/RPCServer/Program.cs new file mode 100644 index 00000000..bac2b135 --- /dev/null +++ b/dotnet-amqp/RPCServer/Program.cs @@ -0,0 +1,83 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string rpcQueueName = "rpc_queue"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-rpcserver") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IQueueSpecification queueSpec = management.Queue(rpcQueueName).Type(QueueType.QUORUM); + await queueSpec.DeclareAsync(); + await queueSpec.PurgeAsync(); + + Console.WriteLine(" [x] Awaiting RPC requests"); + + IResponder responder = await connection.ResponderBuilder() + .RequestQueue(rpcQueueName) + .Handler((ctx, request) => + { + string response = ""; + try + { + string message = request.BodyAsString(); + int n = int.Parse(message); + Console.WriteLine($" [.] fib({message})"); + response += Fib(n); + } + catch (Exception e) + { + Console.WriteLine($" [.] {e.Message}"); + } + + return Task.FromResult(ctx.Message(Encoding.UTF8.GetBytes(response))); + }) + .BuildAsync(); + + try + { + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) + { + } + finally + { + await responder.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} + +static int Fib(int n) +{ + if (n == 0) + { + return 0; + } + + if (n == 1) + { + return 1; + } + + return Fib(n - 1) + Fib(n - 2); +} diff --git a/dotnet-amqp/RPCServer/RPCServer.csproj b/dotnet-amqp/RPCServer/RPCServer.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/RPCServer/RPCServer.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/Receive/Program.cs b/dotnet-amqp/Receive/Program.cs new file mode 100644 index 00000000..c0012dd0 --- /dev/null +++ b/dotnet-amqp/Receive/Program.cs @@ -0,0 +1,53 @@ +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-receive") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IQueueSpecification queueSpec = management.Queue("hello").Type(QueueType.QUORUM); + await queueSpec.DeclareAsync(); + + IConsumer consumer = await connection.ConsumerBuilder() + .Queue("hello") + .MessageHandler((ctx, message) => + { + Console.WriteLine($"Received a message: {message.BodyAsString()}"); + ctx.Accept(); + return Task.CompletedTask; + }) + .BuildAndStartAsync(); + + try + { + Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C"); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) + { + } + finally + { + await consumer.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/Receive/Receive.csproj b/dotnet-amqp/Receive/Receive.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/Receive/Receive.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/ReceiveLogs/Program.cs b/dotnet-amqp/ReceiveLogs/Program.cs new file mode 100644 index 00000000..60d1f0b2 --- /dev/null +++ b/dotnet-amqp/ReceiveLogs/Program.cs @@ -0,0 +1,65 @@ +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string exchangeName = "logs"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-receivelogs") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IExchangeSpecification exchangeSpec = management.Exchange(exchangeName).Type("fanout"); + await exchangeSpec.DeclareAsync(); + + IQueueSpecification tempQueue = management.Queue().Exclusive(true).AutoDelete(true); + IQueueInfo queueInfo = await tempQueue.DeclareAsync(); + string queueName = queueInfo.Name(); + + IBindingSpecification binding = management.Binding() + .SourceExchange(exchangeSpec) + .DestinationQueue(queueName) + .Key(string.Empty); + await binding.BindAsync(); + + IConsumer consumer = await connection.ConsumerBuilder() + .Queue(queueName) + .MessageHandler((ctx, message) => + { + string body = message.BodyAsString(); + Console.WriteLine($" [x] Received '{body}'"); + ctx.Accept(); + return Task.CompletedTask; + }) + .BuildAndStartAsync(); + + try + { + Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C"); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) + { + } + finally + { + await consumer.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/ReceiveLogs/ReceiveLogs.csproj b/dotnet-amqp/ReceiveLogs/ReceiveLogs.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/ReceiveLogs/ReceiveLogs.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/ReceiveLogsDirect/Program.cs b/dotnet-amqp/ReceiveLogsDirect/Program.cs new file mode 100644 index 00000000..fbafc3cb --- /dev/null +++ b/dotnet-amqp/ReceiveLogsDirect/Program.cs @@ -0,0 +1,86 @@ +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string exchangeName = "logs_direct"; + +if (args.Length < 1) +{ + Console.Error.WriteLine("Usage: ReceiveLogsDirect [info] [warning] [error]"); + Environment.Exit(1); +} + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-receivelogsdirect") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IExchangeSpecification exchangeSpec = management.Exchange(exchangeName).Type("direct"); + await exchangeSpec.DeclareAsync(); + + IQueueSpecification tempQueue = management.Queue().Exclusive(true).AutoDelete(true); + IQueueInfo queueInfo = await tempQueue.DeclareAsync(); + string queueName = queueInfo.Name(); + + foreach (string severity in args) + { + IBindingSpecification binding = management.Binding() + .SourceExchange(exchangeSpec) + .DestinationQueue(queueName) + .Key(severity); + await binding.BindAsync(); + } + + IConsumer consumer = await connection.ConsumerBuilder() + .Queue(queueName) + .MessageHandler((ctx, message) => + { + string body = message.BodyAsString(); + string routingKey = RoutingKey(message); + Console.WriteLine($" [x] Received '{routingKey}':'{body}'"); + ctx.Accept(); + return Task.CompletedTask; + }) + .BuildAndStartAsync(); + + try + { + Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C"); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) + { + } + finally + { + await consumer.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} + +static string RoutingKey(IMessage message) +{ + object? rk = message.Annotation("x-routing-key"); + if (rk != null) + { + return rk.ToString() ?? ""; + } + + return message.Subject() ?? ""; +} diff --git a/dotnet-amqp/ReceiveLogsDirect/ReceiveLogsDirect.csproj b/dotnet-amqp/ReceiveLogsDirect/ReceiveLogsDirect.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/ReceiveLogsDirect/ReceiveLogsDirect.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/ReceiveLogsTopic/Program.cs b/dotnet-amqp/ReceiveLogsTopic/Program.cs new file mode 100644 index 00000000..7a033a4b --- /dev/null +++ b/dotnet-amqp/ReceiveLogsTopic/Program.cs @@ -0,0 +1,86 @@ +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string exchangeName = "logs_topic"; + +if (args.Length < 1) +{ + Console.Error.WriteLine("Usage: ReceiveLogsTopic [binding_key]..."); + Environment.Exit(1); +} + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-receivelogstopic") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IExchangeSpecification exchangeSpec = management.Exchange(exchangeName).Type("topic"); + await exchangeSpec.DeclareAsync(); + + IQueueSpecification tempQueue = management.Queue().Exclusive(true).AutoDelete(true); + IQueueInfo queueInfo = await tempQueue.DeclareAsync(); + string queueName = queueInfo.Name(); + + foreach (string bindingKey in args) + { + IBindingSpecification binding = management.Binding() + .SourceExchange(exchangeSpec) + .DestinationQueue(queueName) + .Key(bindingKey); + await binding.BindAsync(); + } + + IConsumer consumer = await connection.ConsumerBuilder() + .Queue(queueName) + .MessageHandler((ctx, message) => + { + string body = message.BodyAsString(); + string routingKey = RoutingKey(message); + Console.WriteLine($" [x] Received '{routingKey}':'{body}'"); + ctx.Accept(); + return Task.CompletedTask; + }) + .BuildAndStartAsync(); + + try + { + Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C"); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) + { + } + finally + { + await consumer.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} + +static string RoutingKey(IMessage message) +{ + object? rk = message.Annotation("x-routing-key"); + if (rk != null) + { + return rk.ToString() ?? ""; + } + + return message.Subject() ?? ""; +} diff --git a/dotnet-amqp/ReceiveLogsTopic/ReceiveLogsTopic.csproj b/dotnet-amqp/ReceiveLogsTopic/ReceiveLogsTopic.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/ReceiveLogsTopic/ReceiveLogsTopic.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/RpcAmqp10/Program.cs b/dotnet-amqp/RpcAmqp10/Program.cs new file mode 100644 index 00000000..48fc7b21 --- /dev/null +++ b/dotnet-amqp/RpcAmqp10/Program.cs @@ -0,0 +1,162 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string requestQueue = "rpc-requests"; + +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, e) => +{ + e.Cancel = true; + cts.Cancel(); +}; + +Console.WriteLine("Starting Direct Reply-To RPC example (RabbitMQ 4.2+ uses Direct Reply-To for the requester when no reply queue is set)."); +Console.WriteLine("Ensure RabbitMQ is running on localhost:5672."); + +Task serverTask = RunServerAsync(cts.Token); +await Task.Delay(500, CancellationToken.None); + +Task clientTask = RunClientAsync(cts.Token); + +try +{ + await Task.Delay(Timeout.Infinite, cts.Token); +} +catch (OperationCanceledException) +{ +} + +Console.WriteLine("Application shutting down..."); +await Task.WhenAll( + AwaitCancelled(serverTask), + AwaitCancelled(clientTask)); + +static async Task AwaitCancelled(Task task) +{ + try + { + await task; + } + catch (OperationCanceledException) + { + } +} + +async Task RunServerAsync(CancellationToken cancellationToken) +{ + ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("rpc-amqp10-server") + .Build(); + + IEnvironment environment = AmqpEnvironment.Create(settings); + IConnection connection = await environment.CreateConnectionAsync(); + + try + { + IManagement management = connection.Management(); + IQueueSpecification queueSpec = management.Queue(requestQueue).Type(QueueType.QUORUM); + await queueSpec.DeclareAsync(); + + IResponder responder = await connection.ResponderBuilder() + .RequestQueue(requestQueue) + .Handler((ctx, request) => + { + string payload = request.BodyAsString(); + object? mid = request.MessageId(); + if (mid != null) + { + Console.WriteLine($"RPC Server: Received {payload} request (ID: {mid})"); + } + else + { + Console.WriteLine($"RPC Server: Received {payload} request"); + } + + return Task.FromResult(ctx.Message(Encoding.UTF8.GetBytes("pong"))); + }) + .BuildAsync(); + + try + { + Console.WriteLine("RPC Server: Started and listening for requests..."); + await Task.Delay(Timeout.Infinite, cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine("RPC Server: Shutting down..."); + } + finally + { + await responder.CloseAsync(); + } + } + finally + { + await connection.CloseAsync(); + await environment.CloseAsync(); + } +} + +async Task RunClientAsync(CancellationToken cancellationToken) +{ + ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("rpc-amqp10-client") + .Build(); + + IEnvironment environment = AmqpEnvironment.Create(settings); + IConnection connection = await environment.CreateConnectionAsync(); + + try + { + IRequester requester = await connection.RequesterBuilder() + .RequestAddress() + .Queue(requestQueue) + .Requester() + .BuildAsync(); + + try + { + Console.WriteLine($"RPC Client: Reply address: {requester.GetReplyToQueue()}"); + Console.WriteLine("RPC Client: Started. Sending ping every second (CTRL+C to exit)."); + + int requestId = 0; + using var ticker = new PeriodicTimer(TimeSpan.FromSeconds(1)); + while (await ticker.WaitForNextTickAsync(cancellationToken)) + { + requestId++; + IMessage reply; + try + { + reply = await requester.PublishAsync(new AmqpMessage(Encoding.UTF8.GetBytes("ping")), + cancellationToken); + } + catch (Exception ex) + { + Console.WriteLine($"RPC Client: Error sending request: {ex.Message}"); + continue; + } + + string payload = reply.BodyAsString(); + Console.WriteLine($"RPC Client: Sent ping request ({requestId})"); + Console.WriteLine($"RPC Client: Received reply - {payload}"); + } + } + catch (OperationCanceledException) + { + Console.WriteLine("RPC Client: Shutting down..."); + } + finally + { + await requester.CloseAsync(); + } + } + finally + { + await connection.CloseAsync(); + await environment.CloseAsync(); + } +} diff --git a/dotnet-amqp/RpcAmqp10/RpcAmqp10.csproj b/dotnet-amqp/RpcAmqp10/RpcAmqp10.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/RpcAmqp10/RpcAmqp10.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/Send/Program.cs b/dotnet-amqp/Send/Program.cs new file mode 100644 index 00000000..0a00594e --- /dev/null +++ b/dotnet-amqp/Send/Program.cs @@ -0,0 +1,56 @@ +using System.Text; +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-send") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IQueueSpecification queueSpec = management.Queue("hello").Type(QueueType.QUORUM); + await queueSpec.DeclareAsync(); + + IPublisher publisher = await connection.PublisherBuilder().Queue("hello").BuildAsync(); + try + { + const string body = "Hello World!"; + var message = new AmqpMessage(Encoding.UTF8.GetBytes(body)); + PublishResult pr = await publisher.PublishAsync(message); + switch (pr.Outcome.State) + { + case OutcomeState.Accepted: + break; + case OutcomeState.Released: + Console.Error.WriteLine($"Released message: {pr.Message.BodyAsString()}"); + Environment.Exit(1); + break; + case OutcomeState.Rejected: + Console.Error.WriteLine($"[Publisher] Message: {pr.Message.BodyAsString()} rejected with error: {pr.Outcome.Error}"); + Environment.Exit(1); + break; + default: + Console.Error.WriteLine($"Unexpected publish outcome: {pr.Outcome.State}"); + Environment.Exit(1); + break; + } + + Console.WriteLine($" [x] Sent {body}"); + } + finally + { + await publisher.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} diff --git a/dotnet-amqp/Send/Send.csproj b/dotnet-amqp/Send/Send.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/Send/Send.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/Worker/Program.cs b/dotnet-amqp/Worker/Program.cs new file mode 100644 index 00000000..cbae24cd --- /dev/null +++ b/dotnet-amqp/Worker/Program.cs @@ -0,0 +1,71 @@ +using RabbitMQ.AMQP.Client; +using RabbitMQ.AMQP.Client.Impl; + +const string brokerUri = "amqp://guest:guest@localhost:5672/%2f"; +const string taskQueueName = "task_queue"; + +ConnectionSettings settings = ConnectionSettingsBuilder.Create() + .Uri(new Uri(brokerUri)) + .ContainerId("tutorial-worker") + .Build(); + +IEnvironment environment = AmqpEnvironment.Create(settings); +IConnection connection = await environment.CreateConnectionAsync(); + +try +{ + IManagement management = connection.Management(); + IQueueSpecification queueSpec = management.Queue(taskQueueName).Type(QueueType.QUORUM); + await queueSpec.DeclareAsync(); + + IConsumer consumer = await connection.ConsumerBuilder() + .Queue(taskQueueName) + .InitialCredits(1) + .MessageHandler((ctx, message) => + { + string body = message.BodyAsString(); + Console.WriteLine($" [x] Received '{body}'"); + try + { + DoWork(body); + } + finally + { + Console.WriteLine(" [x] Done"); + ctx.Accept(); + } + + return Task.CompletedTask; + }) + .BuildAndStartAsync(); + + try + { + Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C"); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) + { + } + finally + { + await consumer.CloseAsync(); + } +} +finally +{ + await connection.CloseAsync(); + await environment.CloseAsync(); +} + +static void DoWork(string task) +{ + int dots = task.ToCharArray().Count(ch => ch == '.'); + Thread.Sleep(dots * 1000); +} diff --git a/dotnet-amqp/Worker/Worker.csproj b/dotnet-amqp/Worker/Worker.csproj new file mode 100644 index 00000000..a0a7e254 --- /dev/null +++ b/dotnet-amqp/Worker/Worker.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0;net10.0 + enable + enable + + + + + + + diff --git a/dotnet-amqp/dotnet-amqp.sln b/dotnet-amqp/dotnet-amqp.sln new file mode 100644 index 00000000..c04dc1c8 --- /dev/null +++ b/dotnet-amqp/dotnet-amqp.sln @@ -0,0 +1,223 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Send", "Send\Send.csproj", "{43E0129B-2E72-4F72-81E3-7C330EF2CF47}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Receive", "Receive\Receive.csproj", "{E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NewTask", "NewTask\NewTask.csproj", "{5B2CCD62-8809-4323-8285-362865708A74}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker", "Worker\Worker.csproj", "{26A2EC9E-6A41-481F-B062-AEC85A271EA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmitLog", "EmitLog\EmitLog.csproj", "{CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReceiveLogs", "ReceiveLogs\ReceiveLogs.csproj", "{8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmitLogDirect", "EmitLogDirect\EmitLogDirect.csproj", "{256AE298-12EF-41E6-AC16-B92AE1038729}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReceiveLogsDirect", "ReceiveLogsDirect\ReceiveLogsDirect.csproj", "{DB1CD74C-6816-4FB3-8E89-F481AFC272F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmitLogTopic", "EmitLogTopic\EmitLogTopic.csproj", "{1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReceiveLogsTopic", "ReceiveLogsTopic\ReceiveLogsTopic.csproj", "{B2F3320F-FB70-41F0-8EB7-F1298E6AE652}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPCServer", "RPCServer\RPCServer.csproj", "{C0C23D16-8819-4795-9CDA-B33E70A7D431}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPCClient", "RPCClient\RPCClient.csproj", "{3B406A89-7605-4FCB-BF3A-BC097378FF73}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublisherConfirms", "PublisherConfirms\PublisherConfirms.csproj", "{7DFF910C-284B-42F1-BDA2-00B4CC61719E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RpcAmqp10", "RpcAmqp10\RpcAmqp10.csproj", "{FB9E3563-8EF8-4723-A9E0-798B1C1A4250}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8F2A9C10-4E5B-4D7A-9C3E-1A2B3C4D5E6F}" + ProjectSection(SolutionItems) = preProject + .gitattributes = .gitattributes + .gitignore = .gitignore + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Debug|x64.ActiveCfg = Debug|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Debug|x64.Build.0 = Debug|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Debug|x86.ActiveCfg = Debug|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Debug|x86.Build.0 = Debug|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Release|Any CPU.Build.0 = Release|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Release|x64.ActiveCfg = Release|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Release|x64.Build.0 = Release|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Release|x86.ActiveCfg = Release|Any CPU + {43E0129B-2E72-4F72-81E3-7C330EF2CF47}.Release|x86.Build.0 = Release|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Debug|x64.ActiveCfg = Debug|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Debug|x64.Build.0 = Debug|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Debug|x86.ActiveCfg = Debug|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Debug|x86.Build.0 = Debug|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Release|Any CPU.Build.0 = Release|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Release|x64.ActiveCfg = Release|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Release|x64.Build.0 = Release|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Release|x86.ActiveCfg = Release|Any CPU + {E2EBD25D-57C5-41B8-8061-6ECF5E8179E4}.Release|x86.Build.0 = Release|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Debug|x64.Build.0 = Debug|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Debug|x86.Build.0 = Debug|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Release|Any CPU.Build.0 = Release|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Release|x64.ActiveCfg = Release|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Release|x64.Build.0 = Release|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Release|x86.ActiveCfg = Release|Any CPU + {5B2CCD62-8809-4323-8285-362865708A74}.Release|x86.Build.0 = Release|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Debug|x64.ActiveCfg = Debug|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Debug|x64.Build.0 = Debug|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Debug|x86.ActiveCfg = Debug|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Debug|x86.Build.0 = Debug|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Release|Any CPU.Build.0 = Release|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Release|x64.ActiveCfg = Release|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Release|x64.Build.0 = Release|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Release|x86.ActiveCfg = Release|Any CPU + {26A2EC9E-6A41-481F-B062-AEC85A271EA3}.Release|x86.Build.0 = Release|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Debug|x64.Build.0 = Debug|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Debug|x86.Build.0 = Debug|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Release|Any CPU.Build.0 = Release|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Release|x64.ActiveCfg = Release|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Release|x64.Build.0 = Release|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Release|x86.ActiveCfg = Release|Any CPU + {CFA7D080-D6ED-4DAF-B3B0-E4894D0568CE}.Release|x86.Build.0 = Release|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Debug|x64.Build.0 = Debug|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Debug|x86.Build.0 = Debug|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Release|Any CPU.Build.0 = Release|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Release|x64.ActiveCfg = Release|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Release|x64.Build.0 = Release|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Release|x86.ActiveCfg = Release|Any CPU + {8A6F8803-3BF3-42A5-B9A0-E2B6205C39B0}.Release|x86.Build.0 = Release|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Debug|Any CPU.Build.0 = Debug|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Debug|x64.ActiveCfg = Debug|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Debug|x64.Build.0 = Debug|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Debug|x86.ActiveCfg = Debug|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Debug|x86.Build.0 = Debug|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Release|Any CPU.ActiveCfg = Release|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Release|Any CPU.Build.0 = Release|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Release|x64.ActiveCfg = Release|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Release|x64.Build.0 = Release|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Release|x86.ActiveCfg = Release|Any CPU + {256AE298-12EF-41E6-AC16-B92AE1038729}.Release|x86.Build.0 = Release|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Debug|x64.Build.0 = Debug|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Debug|x86.Build.0 = Debug|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Release|Any CPU.Build.0 = Release|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Release|x64.ActiveCfg = Release|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Release|x64.Build.0 = Release|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Release|x86.ActiveCfg = Release|Any CPU + {DB1CD74C-6816-4FB3-8E89-F481AFC272F9}.Release|x86.Build.0 = Release|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Debug|x64.Build.0 = Debug|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Debug|x86.Build.0 = Debug|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Release|Any CPU.Build.0 = Release|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Release|x64.ActiveCfg = Release|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Release|x64.Build.0 = Release|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Release|x86.ActiveCfg = Release|Any CPU + {1E7B25AD-4D61-4B66-8F4C-2B1556D5CFB9}.Release|x86.Build.0 = Release|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Debug|x64.Build.0 = Debug|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Debug|x86.Build.0 = Debug|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Release|Any CPU.Build.0 = Release|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Release|x64.ActiveCfg = Release|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Release|x64.Build.0 = Release|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Release|x86.ActiveCfg = Release|Any CPU + {B2F3320F-FB70-41F0-8EB7-F1298E6AE652}.Release|x86.Build.0 = Release|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Debug|x64.Build.0 = Debug|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Debug|x86.Build.0 = Debug|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Release|Any CPU.Build.0 = Release|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Release|x64.ActiveCfg = Release|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Release|x64.Build.0 = Release|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Release|x86.ActiveCfg = Release|Any CPU + {C0C23D16-8819-4795-9CDA-B33E70A7D431}.Release|x86.Build.0 = Release|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Debug|x64.ActiveCfg = Debug|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Debug|x64.Build.0 = Debug|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Debug|x86.ActiveCfg = Debug|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Debug|x86.Build.0 = Debug|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Release|Any CPU.Build.0 = Release|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Release|x64.ActiveCfg = Release|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Release|x64.Build.0 = Release|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Release|x86.ActiveCfg = Release|Any CPU + {3B406A89-7605-4FCB-BF3A-BC097378FF73}.Release|x86.Build.0 = Release|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Debug|x64.Build.0 = Debug|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Debug|x86.Build.0 = Debug|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Release|Any CPU.Build.0 = Release|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Release|x64.ActiveCfg = Release|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Release|x64.Build.0 = Release|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Release|x86.ActiveCfg = Release|Any CPU + {7DFF910C-284B-42F1-BDA2-00B4CC61719E}.Release|x86.Build.0 = Release|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Debug|x64.Build.0 = Debug|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Debug|x86.Build.0 = Debug|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Release|Any CPU.Build.0 = Release|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Release|x64.ActiveCfg = Release|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Release|x64.Build.0 = Release|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Release|x86.ActiveCfg = Release|Any CPU + {FB9E3563-8EF8-4723-A9E0-798B1C1A4250}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/dotnet-amqp/test-tutorials.sh b/dotnet-amqp/test-tutorials.sh new file mode 100755 index 00000000..8dde91ba --- /dev/null +++ b/dotnet-amqp/test-tutorials.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# End-to-end smoke tests for dotnet-amqp tutorials. +# Requires: bash, dotnet 8+, RabbitMQ 4.x on localhost:5672 (guest/guest). +# Does not start RabbitMQ; ensure the broker is already running. +# +# Usage: +# ./dotnet-amqp/test-tutorials.sh +# cd dotnet-amqp && ./test-tutorials.sh +# +# Environment: +# SKIP_BROKER_CHECK=1 Skip TCP check on port 5672 (tests still need a broker) +# SKIP_MGMT_DELETE=1 Do not DELETE tutorial queues via management HTTP API before T1/T2 +# +# If tutorial queues (hello, task_queue) already have consumers from other processes, T1/T2 can +# fail until those queues are deleted or the other consumers stop. By default this script tries +# DELETE http://localhost:15672/api/queues/%2F/ (requires the management plugin). + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +PASS=0 +FAIL=0 + +pass() { + printf 'ok\t%s\n' "$1" + PASS=$((PASS + 1)) +} + +fail() { + printf 'FAIL\t%s\n' "$1" >&2 + FAIL=$((FAIL + 1)) +} + +require_dotnet() { + command -v dotnet >/dev/null 2>&1 || { + echo "dotnet is not on PATH" >&2 + exit 1 + } +} + +check_broker() { + if command -v nc >/dev/null 2>&1; then + nc -z localhost 5672 2>/dev/null + return + fi + { echo >/dev/tcp/127.0.0.1/5672; } 2>/dev/null +} + +require_broker() { + if [[ "${SKIP_BROKER_CHECK:-}" == "1" ]]; then + return 0 + fi + if ! check_broker; then + echo "RabbitMQ does not appear to be reachable on localhost:5672" >&2 + echo "Start the broker or set SKIP_BROKER_CHECK=1 to skip this check." >&2 + exit 1 + fi +} + +build_once() { + dotnet build "$ROOT/dotnet-amqp.sln" -c Release -v minimal >/dev/null +} + +# Wait until log file contains a line (consumer ready, etc.). Handles slow `dotnet run` cold start. +wait_for_log() { + local file=$1 + local needle=$2 + local max_attempts=${3:-120} + local i=0 + while [[ $i -lt $max_attempts ]]; do + if [[ -f "$file" ]] && grep -qF -- "$needle" "$file" 2>/dev/null; then + return 0 + fi + sleep 0.25 + i=$((i + 1)) + done + return 1 +} + +# Remove a queue so stale consumers from other tutorial runs cannot steal messages (needs management plugin). +delete_queue_mgmt() { + local name=$1 + if [[ "${SKIP_MGMT_DELETE:-}" == "1" ]]; then + return 0 + fi + if ! command -v curl >/dev/null 2>&1; then + return 0 + fi + curl -sf -o /dev/null -u guest:guest -X DELETE "http://localhost:15672/api/queues/%2F/${name}" 2>/dev/null || true +} + +# dotnet run --project Dir/Dir.csproj -- args... +# Use line-buffered stdout when not attached to a TTY so logs capture "Received..." lines before kill. +runproj() { + local proj=$1 + shift + if command -v stdbuf >/dev/null 2>&1; then + stdbuf -oL -eL dotnet run --no-build -c Release -f net8.0 --project "$ROOT/$proj/$proj.csproj" -- "$@" + else + dotnet run --no-build -c Release -f net8.0 --project "$ROOT/$proj/$proj.csproj" -- "$@" + fi +} + +assert_file_contains() { + local file=$1 + local needle=$2 + local msg=$3 + if [[ -f "$file" ]] && grep -qF -- "$needle" "$file"; then + pass "$msg" + else + fail "$msg (log should contain: $needle)" + if [[ -f "$file" ]]; then + echo "--- $file (first 40 lines) ---" >&2 + head -n 40 "$file" >&2 + fi + fi +} + +# Run command in background; sleep $secs; kill. Captures stdout/stderr to $log. +run_timed() { + local log=$1 + local secs=$2 + shift 2 + : >"$log" + "$@" >"$log" 2>&1 & + local pid=$! + sleep "$secs" + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true +} + +test_t1_hello() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + delete_queue_mgmt hello + runproj Receive >"$recv_log" 2>&1 & + local rpid=$! + wait_for_log "$recv_log" "Waiting for messages" || true + sleep 1 + runproj Send >"$log" 2>&1 + sleep 3 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T1 send publishes" + assert_file_contains "$recv_log" "Hello World" "T1 receive gets message" + rm -f "$log" "$recv_log" +} + +test_t2_work_queues() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + delete_queue_mgmt task_queue + runproj Worker >"$recv_log" 2>&1 & + local rpid=$! + wait_for_log "$recv_log" "Waiting for messages" || true + sleep 1 + runproj NewTask "e2e.task.test" >"$log" 2>&1 + sleep 4 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T2 NewTask publishes" + assert_file_contains "$recv_log" "e2e.task.test" "T2 Worker receives task" + rm -f "$log" "$recv_log" +} + +test_t3_pubsub() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + runproj ReceiveLogs >"$recv_log" 2>&1 & + local rpid=$! + wait_for_log "$recv_log" "Waiting for messages" || true + sleep 1 + runproj EmitLog "e2e" "pubsub" "hello" >"$log" 2>&1 + sleep 3 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T3 EmitLog publishes" + assert_file_contains "$recv_log" "e2e pubsub hello" "T3 ReceiveLogs receives broadcast" + rm -f "$log" "$recv_log" +} + +test_t4_routing() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + runproj ReceiveLogsDirect warn >"$recv_log" 2>&1 & + local rpid=$! + wait_for_log "$recv_log" "Waiting for messages" || true + sleep 1 + runproj EmitLogDirect warn "e2e routing" >"$log" 2>&1 + sleep 3 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T4 EmitLogDirect publishes" + assert_file_contains "$recv_log" "e2e routing" "T4 ReceiveLogsDirect gets routed message" + rm -f "$log" "$recv_log" +} + +test_t5_topics() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + runproj ReceiveLogsTopic "kern.*" >"$recv_log" 2>&1 & + local rpid=$! + wait_for_log "$recv_log" "Waiting for messages" || true + sleep 1 + runproj EmitLogTopic kern.info "e2e topic" >"$log" 2>&1 + sleep 3 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T5 EmitLogTopic publishes" + assert_file_contains "$recv_log" "e2e topic" "T5 ReceiveLogsTopic matches binding" + rm -f "$log" "$recv_log" +} + +test_t6_rpc() { + local log srv_log + log=$(mktemp) + srv_log=$(mktemp) + runproj RPCServer >"$srv_log" 2>&1 & + local spid=$! + sleep 3 + if ! runproj RPCClient >"$log" 2>&1; then + kill "$spid" 2>/dev/null || true + wait "$spid" 2>/dev/null || true + fail "T6 RPCClient process failed" + rm -f "$log" "$srv_log" + return + fi + kill "$spid" 2>/dev/null || true + wait "$spid" 2>/dev/null || true + assert_file_contains "$log" "Requesting fib(10)" "T6 RPCClient sends request" + assert_file_contains "$log" "Got '55'" "T6 RPCClient gets fib(10)==55" + rm -f "$log" "$srv_log" +} + +test_publisher_confirms() { + local log + log=$(mktemp) + run_timed "$log" 6 runproj PublisherConfirms + assert_file_contains "$log" "Accepted Message" "PublisherConfirms sees accepted publish" + assert_file_contains "$log" "Received a message" "PublisherConfirms consumer sees message" + rm -f "$log" +} + +test_rpc_amqp10() { + local log + log=$(mktemp) + run_timed "$log" 8 runproj RpcAmqp10 + assert_file_contains "$log" "pong" "RpcAmqp10 ping/pong (Direct Reply-To)" + rm -f "$log" +} + +main() { + require_dotnet + require_broker + echo "Building dotnet-amqp (Release)..." + build_once + echo "Running dotnet-amqp tutorial smoke tests from $ROOT" + test_t1_hello + test_t2_work_queues + test_t3_pubsub + test_t4_routing + test_t5_topics + test_t6_rpc + test_publisher_confirms + test_rpc_amqp10 + echo "----" + echo "Passed: $PASS Failed: $FAIL" + if [[ "$FAIL" -gt 0 ]]; then + exit 1 + fi +} + +main "$@" diff --git a/go-amqp/.gitignore b/go-amqp/.gitignore new file mode 100644 index 00000000..8af6f160 --- /dev/null +++ b/go-amqp/.gitignore @@ -0,0 +1,2 @@ +pkg/* +src/* diff --git a/go-amqp/README.md b/go-amqp/README.md new file mode 100644 index 00000000..76b0f9d1 --- /dev/null +++ b/go-amqp/README.md @@ -0,0 +1,58 @@ +# Go code for RabbitMQ tutorials (AMQP 1.0) + + +Here you can find Go code examples from [RabbitMQ tutorials](https://www.rabbitmq.com/getstarted.html), using the AMQP 1.0 client. + + +## Requirements + +These examples use the [`rabbitmq-amqp-go-client`](https://github.com/rabbitmq/rabbitmq-amqp-go-client) library for RabbitMQ 4.x. Get it with + + go get github.com/rabbitmq/rabbitmq-amqp-go-client + +A RabbitMQ node must be running on `localhost` with the default port (`5672`) and credentials (`guest` / `guest`). + + +## Code + +Run each example from this directory with `go run`: + +[Tutorial one: "Hello World!"](https://www.rabbitmq.com/tutorials/tutorial-one-go.html): + + go run send.go + go run receive.go + +[Tutorial two: Work Queues](https://www.rabbitmq.com/tutorials/tutorial-two-go.html): + + go run new_task.go hello world + go run worker.go + +[Tutorial three: Publish/Subscribe](https://www.rabbitmq.com/tutorials/tutorial-three-go.html) + + go run receive_logs.go + go run emit_log.go hello world + +[Tutorial four: Routing](https://www.rabbitmq.com/tutorials/tutorial-four-go.html) + + go run receive_logs_direct.go info warn + go run emit_log_direct.go warn "a warning" + +[Tutorial five: Topics](https://www.rabbitmq.com/tutorials/tutorial-five-go.html) + + go run receive_logs_topic.go "kern.*" "*.critical" + go run emit_log_topic.go kern.critical "A critical kernel error" + +[Tutorial six: RPC](https://www.rabbitmq.com/tutorials/tutorial-six-go.html) + + go run rpc_server.go + go run rpc_client.go 10 + +[Publisher confirms](https://www.rabbitmq.com/tutorials/tutorial-seven-java.html) (AMQP 1.0 publish outcomes) + + go run publisher_confirms.go + +[AMQP 1.0 Direct Reply-To RPC](https://www.rabbitmq.com/docs/next/direct-reply-to) + + go run rpc_amqp10.go + +To learn more, see the [package documentation](https://pkg.go.dev/github.com/rabbitmq/rabbitmq-amqp-go-client) and [AMQP in RabbitMQ](https://www.rabbitmq.com/docs/amqp). diff --git a/go-amqp/emit_log.go b/go-amqp/emit_log.go new file mode 100644 index 00000000..4beb174b --- /dev/null +++ b/go-amqp/emit_log.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "log" + "os" + "strings" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareExchange(ctx, &rmq.FanOutExchangeSpecification{Name: "logs"}) + if err != nil { + log.Panicf("Failed to declare an exchange: %v", err) + } + + publisher, err := conn.NewPublisher(ctx, &rmq.ExchangeAddress{Exchange: "logs", Key: ""}, nil) + if err != nil { + log.Panicf("Failed to create publisher: %v", err) + } + defer func() { _ = publisher.Close(context.Background()) }() + + body := bodyFrom(os.Args) + res, err := publisher.Publish(ctx, rmq.NewMessage([]byte(body))) + if err != nil { + log.Panicf("Failed to publish a message: %v", err) + } + switch res.Outcome.(type) { + case *rmq.StateAccepted: + case *rmq.StateRejected: + log.Fatalf("Message was rejected: %v", res.Outcome) + case *rmq.StateReleased: + log.Fatalf("Message was released: %v", res.Outcome) + case *rmq.StateModified: + log.Fatalf("Message was modified: %v", res.Outcome) + default: + log.Fatalf("Unexpected publish outcome: %v", res.Outcome) + } + log.Printf(" [x] Sent %s", body) +} + +func bodyFrom(args []string) string { + var s string + if (len(args) < 2) || args[1] == "" { + s = "hello" + } else { + s = strings.Join(args[1:], " ") + } + return s +} diff --git a/go-amqp/emit_log_direct.go b/go-amqp/emit_log_direct.go new file mode 100644 index 00000000..28e54dc9 --- /dev/null +++ b/go-amqp/emit_log_direct.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "log" + "os" + "strings" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareExchange(ctx, &rmq.DirectExchangeSpecification{Name: "logs_direct"}) + if err != nil { + log.Panicf("Failed to declare an exchange: %v", err) + } + + publisher, err := conn.NewPublisher(ctx, &rmq.ExchangeAddress{ + Exchange: "logs_direct", + Key: severityFrom(os.Args), + }, nil) + if err != nil { + log.Panicf("Failed to create publisher: %v", err) + } + defer func() { _ = publisher.Close(context.Background()) }() + + body := bodyFrom(os.Args) + res, err := publisher.Publish(ctx, rmq.NewMessage([]byte(body))) + if err != nil { + log.Panicf("Failed to publish a message: %v", err) + } + switch res.Outcome.(type) { + case *rmq.StateAccepted: + case *rmq.StateRejected: + log.Fatalf("Message was rejected: %v", res.Outcome) + case *rmq.StateReleased: + log.Fatalf("Message was released: %v", res.Outcome) + case *rmq.StateModified: + log.Fatalf("Message was modified: %v", res.Outcome) + default: + log.Fatalf("Unexpected publish outcome: %v", res.Outcome) + } + log.Printf(" [x] Sent %s", body) +} + +func bodyFrom(args []string) string { + var s string + if (len(args) < 3) || args[2] == "" { + s = "hello" + } else { + s = strings.Join(args[2:], " ") + } + return s +} + +func severityFrom(args []string) string { + var s string + if (len(args) < 2) || args[1] == "" { + s = "info" + } else { + s = args[1] + } + return s +} diff --git a/go-amqp/emit_log_topic.go b/go-amqp/emit_log_topic.go new file mode 100644 index 00000000..824c463c --- /dev/null +++ b/go-amqp/emit_log_topic.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "log" + "os" + "strings" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareExchange(ctx, &rmq.TopicExchangeSpecification{Name: "logs_topic"}) + if err != nil { + log.Panicf("Failed to declare an exchange: %v", err) + } + + publisher, err := conn.NewPublisher(ctx, &rmq.ExchangeAddress{ + Exchange: "logs_topic", + Key: severityFrom(os.Args), + }, nil) + if err != nil { + log.Panicf("Failed to create publisher: %v", err) + } + defer func() { _ = publisher.Close(context.Background()) }() + + body := bodyFrom(os.Args) + res, err := publisher.Publish(ctx, rmq.NewMessage([]byte(body))) + if err != nil { + log.Panicf("Failed to publish a message: %v", err) + } + switch res.Outcome.(type) { + case *rmq.StateAccepted: + case *rmq.StateRejected: + log.Fatalf("Message was rejected: %v", res.Outcome) + case *rmq.StateReleased: + log.Fatalf("Message was released: %v", res.Outcome) + case *rmq.StateModified: + log.Fatalf("Message was modified: %v", res.Outcome) + default: + log.Fatalf("Unexpected publish outcome: %v", res.Outcome) + } + log.Printf(" [x] Sent %s", body) +} + +func bodyFrom(args []string) string { + var s string + if (len(args) < 3) || args[2] == "" { + s = "hello" + } else { + s = strings.Join(args[2:], " ") + } + return s +} + +func severityFrom(args []string) string { + var s string + if (len(args) < 2) || args[1] == "" { + s = "anonymous.info" + } else { + s = args[1] + } + return s +} diff --git a/go-amqp/go.mod b/go-amqp/go.mod new file mode 100644 index 00000000..27d18182 --- /dev/null +++ b/go-amqp/go.mod @@ -0,0 +1,16 @@ +module github.com/rabbitmq/rabbitmq-tutorials/go-amqp + +go 1.24.0 + +require ( + github.com/Azure/go-amqp v1.5.1 + github.com/rabbitmq/rabbitmq-amqp-go-client v1.0.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect +) diff --git a/go-amqp/go.sum b/go-amqp/go.sum new file mode 100644 index 00000000..ce0187f1 --- /dev/null +++ b/go-amqp/go.sum @@ -0,0 +1,56 @@ +github.com/Azure/go-amqp v1.5.1 h1:WyiPTz2C3zVvDL7RLAqwWdeoYhMtX62MZzQoP09fzsU= +github.com/Azure/go-amqp v1.5.1/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= +github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/rabbitmq-amqp-go-client v0.7.0 h1:mQnrLwgZbYwf6CdRJ2l2kTf8cbn9j4AyBJM9tXnwebg= +github.com/rabbitmq/rabbitmq-amqp-go-client v0.7.0/go.mod h1:u2HAJ0fUFyayNcIvTGCTlpmdPKHrKO9Xl81FbiEzYO4= +github.com/rabbitmq/rabbitmq-amqp-go-client v1.0.0 h1:VvrVB+Fz7g0hg0ZcxDXf+WHShQsfQ1iI5FbT5ZnJ3lY= +github.com/rabbitmq/rabbitmq-amqp-go-client v1.0.0/go.mod h1:u2HAJ0fUFyayNcIvTGCTlpmdPKHrKO9Xl81FbiEzYO4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-amqp/new_task.go b/go-amqp/new_task.go new file mode 100644 index 00000000..dbf07792 --- /dev/null +++ b/go-amqp/new_task.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "log" + "os" + "strings" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareQueue(ctx, &rmq.QuorumQueueSpecification{Name: "task_queue"}) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + publisher, err := conn.NewPublisher(ctx, &rmq.QueueAddress{Queue: "task_queue"}, nil) + if err != nil { + log.Panicf("Failed to create publisher: %v", err) + } + defer func() { _ = publisher.Close(context.Background()) }() + + body := bodyFrom(os.Args) + res, err := publisher.Publish(ctx, rmq.NewMessage([]byte(body))) + if err != nil { + log.Panicf("Failed to publish a message: %v", err) + } + switch res.Outcome.(type) { + case *rmq.StateAccepted: + case *rmq.StateRejected: + log.Fatalf("Message was rejected: %v", res.Outcome) + case *rmq.StateReleased: + log.Fatalf("Message was released: %v", res.Outcome) + case *rmq.StateModified: + log.Fatalf("Message was modified: %v", res.Outcome) + default: + log.Fatalf("Unexpected publish outcome: %v", res.Outcome) + } + log.Printf(" [x] Sent %s", body) +} + +func bodyFrom(args []string) string { + var s string + if (len(args) < 2) || args[1] == "" { + s = "hello" + } else { + s = strings.Join(args[1:], " ") + } + return s +} diff --git a/go-amqp/publisher_confirms.go b/go-amqp/publisher_confirms.go new file mode 100644 index 00000000..765b0530 --- /dev/null +++ b/go-amqp/publisher_confirms.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "log" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + qInfo, err := conn.Management().DeclareQueue(ctx, &rmq.AutoGeneratedQueueSpecification{ + IsExclusive: false, + IsAutoDelete: false, + }) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + consume(conn, qInfo.Name()) + publish(conn, qInfo.Name(), "hello") + + log.Printf(" [*] Waiting for messages. To exit press CTRL+C") + select {} +} + +func consume(conn *rmq.AmqpConnection, qName string) { + ctx := context.Background() + consumer, err := conn.NewConsumer(ctx, qName, nil) + if err != nil { + log.Panicf("Failed to create consumer: %v", err) + } + + go func() { + for { + delivery, err := consumer.Receive(ctx) + if err != nil { + log.Printf("consumer receive: %v", err) + return + } + msg := delivery.Message() + var body string + if len(msg.Data) > 0 { + body = string(msg.Data[0]) + } + log.Printf("Received a message: %s", body) + _ = delivery.Accept(ctx) + } + }() +} + +func publish(conn *rmq.AmqpConnection, qName, text string) { + ctx := context.Background() + publisher, err := conn.NewPublisher(ctx, &rmq.QueueAddress{Queue: qName}, nil) + if err != nil { + log.Panicf("Failed to create publisher: %v", err) + } + defer func() { _ = publisher.Close(context.Background()) }() + + res, err := publisher.Publish(ctx, rmq.NewMessage([]byte(text))) + if err != nil { + log.Panicf("Failed to publish a message: %v", err) + } + switch res.Outcome.(type) { + case *rmq.StateAccepted: + log.Printf("Confirmed") + case *rmq.StateRejected: + log.Fatalf("Message was rejected: %v", res.Outcome) + case *rmq.StateReleased: + log.Fatalf("Message was released: %v", res.Outcome) + case *rmq.StateModified: + log.Fatalf("Message was modified: %v", res.Outcome) + default: + log.Fatalf("Unexpected publish outcome: %v", res.Outcome) + } +} diff --git a/go-amqp/receive.go b/go-amqp/receive.go new file mode 100644 index 00000000..40b32344 --- /dev/null +++ b/go-amqp/receive.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "errors" + "log" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareQueue(ctx, &rmq.QuorumQueueSpecification{Name: "hello"}) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + consumer, err := conn.NewConsumer(ctx, "hello", nil) + if err != nil { + log.Panicf("Failed to create consumer: %v", err) + } + defer func() { _ = consumer.Close(context.Background()) }() + + log.Printf(" [*] Waiting for messages. To exit press CTRL+C") + for { + delivery, err := consumer.Receive(ctx) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Panicf("Failed to receive a message: %v", err) + } + msg := delivery.Message() + var body string + if len(msg.Data) > 0 { + body = string(msg.Data[0]) + } + log.Printf("Received a message: %s", body) + err = delivery.Accept(ctx) + if err != nil { + log.Panicf("Failed to accept message: %v", err) + } + } +} diff --git a/go-amqp/receive_logs.go b/go-amqp/receive_logs.go new file mode 100644 index 00000000..1842c341 --- /dev/null +++ b/go-amqp/receive_logs.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "errors" + "log" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareExchange(ctx, &rmq.FanOutExchangeSpecification{Name: "logs"}) + if err != nil { + log.Panicf("Failed to declare an exchange: %v", err) + } + + qInfo, err := conn.Management().DeclareQueue(ctx, &rmq.AutoGeneratedQueueSpecification{ + IsExclusive: true, + IsAutoDelete: true, + }) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + _, err = conn.Management().Bind(ctx, &rmq.ExchangeToQueueBindingSpecification{ + SourceExchange: "logs", + DestinationQueue: qInfo.Name(), + BindingKey: "", + }) + if err != nil { + log.Panicf("Failed to bind a queue: %v", err) + } + + consumer, err := conn.NewConsumer(ctx, qInfo.Name(), nil) + if err != nil { + log.Panicf("Failed to create consumer: %v", err) + } + defer func() { _ = consumer.Close(context.Background()) }() + + log.Printf(" [*] Waiting for logs. To exit press CTRL+C") + for { + delivery, err := consumer.Receive(ctx) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Panicf("Failed to receive a message: %v", err) + } + msg := delivery.Message() + var body string + if len(msg.Data) > 0 { + body = string(msg.Data[0]) + } + log.Printf(" [x] %s", body) + err = delivery.Accept(ctx) + if err != nil { + log.Panicf("Failed to accept message: %v", err) + } + } +} diff --git a/go-amqp/receive_logs_direct.go b/go-amqp/receive_logs_direct.go new file mode 100644 index 00000000..eaaf6b72 --- /dev/null +++ b/go-amqp/receive_logs_direct.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "errors" + "log" + "os" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareExchange(ctx, &rmq.DirectExchangeSpecification{Name: "logs_direct"}) + if err != nil { + log.Panicf("Failed to declare an exchange: %v", err) + } + + qInfo, err := conn.Management().DeclareQueue(ctx, &rmq.AutoGeneratedQueueSpecification{ + IsExclusive: true, + IsAutoDelete: true, + }) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + if len(os.Args) < 2 { + log.Printf("Usage: %s [info] [warning] [error]", os.Args[0]) + os.Exit(0) + } + for _, s := range os.Args[1:] { + log.Printf("Binding queue %s to exchange %s with routing key %s", qInfo.Name(), "logs_direct", s) + _, err = conn.Management().Bind(ctx, &rmq.ExchangeToQueueBindingSpecification{ + SourceExchange: "logs_direct", + DestinationQueue: qInfo.Name(), + BindingKey: s, + }) + if err != nil { + log.Panicf("Failed to bind a queue: %v", err) + } + } + + consumer, err := conn.NewConsumer(ctx, qInfo.Name(), nil) + if err != nil { + log.Panicf("Failed to create consumer: %v", err) + } + defer func() { _ = consumer.Close(context.Background()) }() + + log.Printf(" [*] Waiting for logs. To exit press CTRL+C") + for { + delivery, err := consumer.Receive(ctx) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Panicf("Failed to receive a message: %v", err) + } + msg := delivery.Message() + var body string + if len(msg.Data) > 0 { + body = string(msg.Data[0]) + } + log.Printf(" [x] %s", body) + err = delivery.Accept(ctx) + if err != nil { + log.Panicf("Failed to accept message: %v", err) + } + } +} diff --git a/go-amqp/receive_logs_topic.go b/go-amqp/receive_logs_topic.go new file mode 100644 index 00000000..ea33b5ec --- /dev/null +++ b/go-amqp/receive_logs_topic.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "errors" + "log" + "os" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareExchange(ctx, &rmq.TopicExchangeSpecification{Name: "logs_topic"}) + if err != nil { + log.Panicf("Failed to declare an exchange: %v", err) + } + + qInfo, err := conn.Management().DeclareQueue(ctx, &rmq.AutoGeneratedQueueSpecification{ + IsExclusive: true, + IsAutoDelete: true, + }) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + if len(os.Args) < 2 { + log.Printf("Usage: %s [binding_key]...", os.Args[0]) + os.Exit(0) + } + for _, s := range os.Args[1:] { + log.Printf("Binding queue %s to exchange %s with routing key %s", qInfo.Name(), "logs_topic", s) + _, err = conn.Management().Bind(ctx, &rmq.ExchangeToQueueBindingSpecification{ + SourceExchange: "logs_topic", + DestinationQueue: qInfo.Name(), + BindingKey: s, + }) + if err != nil { + log.Panicf("Failed to bind a queue: %v", err) + } + } + + consumer, err := conn.NewConsumer(ctx, qInfo.Name(), nil) + if err != nil { + log.Panicf("Failed to create consumer: %v", err) + } + defer func() { _ = consumer.Close(context.Background()) }() + + log.Printf(" [*] Waiting for logs. To exit press CTRL+C") + for { + delivery, err := consumer.Receive(ctx) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Panicf("Failed to receive a message: %v", err) + } + msg := delivery.Message() + var body string + if len(msg.Data) > 0 { + body = string(msg.Data[0]) + } + log.Printf(" [x] %s", body) + err = delivery.Accept(ctx) + if err != nil { + log.Panicf("Failed to accept message: %v", err) + } + } +} diff --git a/go-amqp/rpc_amqp10.go b/go-amqp/rpc_amqp10.go new file mode 100644 index 00000000..e586edc6 --- /dev/null +++ b/go-amqp/rpc_amqp10.go @@ -0,0 +1,151 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + amqp "github.com/Azure/go-amqp" + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const ( + amqpURL = "amqp://guest:guest@localhost:5672/" + requestQueue = "rpc-requests" +) + +func rpcServer(ctx context.Context) error { + env := rmq.NewEnvironment(amqpURL, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + return fmt.Errorf("failed to connect to RabbitMQ: %w", err) + } + defer func() { _ = env.CloseConnections(context.Background()) }() + + _, err = conn.Management().DeclareQueue(ctx, &rmq.QuorumQueueSpecification{Name: requestQueue}) + if err != nil { + return fmt.Errorf("failed to declare queue: %w", err) + } + + responder, err := conn.NewResponder(ctx, rmq.ResponderOptions{ + RequestQueue: requestQueue, + Handler: func(_ context.Context, request *amqp.Message) (*amqp.Message, error) { + var payload string + if len(request.Data) > 0 { + payload = string(request.Data[0]) + } + if request.Properties != nil && request.Properties.MessageID != nil { + log.Printf("RPC Server: Received %s request (ID: %v)", payload, request.Properties.MessageID) + } else { + log.Printf("RPC Server: Received %s request", payload) + } + return amqp.NewMessage([]byte("pong")), nil + }, + }) + if err != nil { + return fmt.Errorf("failed to create responder: %w", err) + } + defer func() { _ = responder.Close(context.Background()) }() + + log.Println("RPC Server: Started and listening for requests...") + + <-ctx.Done() + log.Println("RPC Server: Shutting down...") + return nil +} + +func rpcClient(ctx context.Context) error { + env := rmq.NewEnvironment(amqpURL, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + return fmt.Errorf("failed to connect to RabbitMQ: %w", err) + } + defer func() { _ = env.CloseConnections(context.Background()) }() + + requester, err := conn.NewRequester(ctx, &rmq.RequesterOptions{ + RequestQueueName: requestQueue, + SettleStrategy: rmq.DirectReplyTo, + }) + if err != nil { + return fmt.Errorf("failed to create requester: %w", err) + } + defer func() { _ = requester.Close(context.Background()) }() + + replyAddr, err := requester.GetReplyQueue() + if err != nil { + return err + } + log.Printf("RPC Client: Reply address: %s", replyAddr) + + log.Println("RPC Client: Started and ready to send requests...") + + requestID := 0 + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Println("RPC Client: Shutting down...") + return nil + case <-ticker.C: + requestID++ + replyCh, err := requester.Publish(ctx, requester.Message([]byte("ping"))) + if err != nil { + log.Printf("RPC Client: Error sending request: %v", err) + continue + } + log.Printf("RPC Client: Sent ping request (%d)", requestID) + + reply := <-replyCh + if reply == nil { + log.Printf("RPC Client: No reply for request %d", requestID) + continue + } + var payload string + if len(reply.Data) > 0 { + payload = string(reply.Data[0]) + } + log.Printf("RPC Client: Received reply - %s", payload) + } + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + log.Println("Starting Direct Reply-To RPC example...") + log.Println("Make sure RabbitMQ is running on localhost:5672") + + go func() { + if err := rpcServer(ctx); err != nil { + log.Printf("RPC Server error: %v", err) + cancel() + } + }() + + time.Sleep(500 * time.Millisecond) + + go func() { + if err := rpcClient(ctx); err != nil { + log.Printf("RPC Client error: %v", err) + cancel() + } + }() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigs + log.Println("\nReceived interrupt, shutting down...") + cancel() + }() + + <-ctx.Done() + log.Println("Application shutting down...") +} diff --git a/go-amqp/rpc_client.go b/go-amqp/rpc_client.go new file mode 100644 index 00000000..c901a40f --- /dev/null +++ b/go-amqp/rpc_client.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "errors" + "log" + "os" + "strconv" + "strings" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func fibonacciRPC(n int) (res int, err error) { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + return 0, err + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + requester, err := conn.NewRequester(ctx, &rmq.RequesterOptions{ + RequestQueueName: "rpc_queue", + }) + if err != nil { + return 0, err + } + defer func() { _ = requester.Close(context.Background()) }() + + replyCh, err := requester.Publish(ctx, requester.Message([]byte(strconv.Itoa(n)))) + if err != nil { + return 0, err + } + reply := <-replyCh + if reply == nil { + return 0, errors.New("no reply received") + } + var payload []byte + if len(reply.Data) > 0 { + payload = reply.Data[0] + } + return strconv.Atoi(string(payload)) +} + +func main() { + n := bodyFrom(os.Args) + + log.Printf(" [x] Requesting fib(%d)", n) + res, err := fibonacciRPC(n) + if err != nil { + log.Panicf("Failed to handle RPC request: %v", err) + } + + log.Printf(" [.] Got %d", res) +} + +func bodyFrom(args []string) int { + var s string + if (len(args) < 2) || args[1] == "" { + s = "30" + } else { + s = strings.Join(args[1:], " ") + } + n, err := strconv.Atoi(s) + if err != nil { + log.Panicf("Failed to convert arg to integer: %v", err) + } + return n +} diff --git a/go-amqp/rpc_server.go b/go-amqp/rpc_server.go new file mode 100644 index 00000000..199cbd75 --- /dev/null +++ b/go-amqp/rpc_server.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "log" + "strconv" + + amqp "github.com/Azure/go-amqp" + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func fib(n int) int { + if n == 0 { + return 0 + } else if n == 1 { + return 1 + } + return fib(n-1) + fib(n-2) +} + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareQueue(ctx, &rmq.QuorumQueueSpecification{Name: "rpc_queue"}) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + _, err = conn.Management().PurgeQueue(ctx, "rpc_queue") + if err != nil { + log.Printf("purge: %v", err) + } + + responder, err := conn.NewResponder(ctx, rmq.ResponderOptions{ + RequestQueue: "rpc_queue", + Handler: func(_ context.Context, request *amqp.Message) (*amqp.Message, error) { + var payload []byte + if len(request.Data) > 0 { + payload = request.Data[0] + } + n, err := strconv.Atoi(string(payload)) + if err != nil { + return nil, err + } + log.Printf(" [.] fib(%d)", n) + response := fib(n) + return amqp.NewMessage([]byte(strconv.Itoa(response))), nil + }, + }) + if err != nil { + log.Panicf("Failed to create responder: %v", err) + } + defer func() { _ = responder.Close(context.Background()) }() + + log.Printf(" [*] Awaiting RPC requests") + select {} +} diff --git a/go-amqp/send.go b/go-amqp/send.go new file mode 100644 index 00000000..5948c9b2 --- /dev/null +++ b/go-amqp/send.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "log" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareQueue(ctx, &rmq.QuorumQueueSpecification{Name: "hello"}) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + publisher, err := conn.NewPublisher(ctx, &rmq.QueueAddress{Queue: "hello"}, nil) + if err != nil { + log.Panicf("Failed to create publisher: %v", err) + } + defer func() { _ = publisher.Close(context.Background()) }() + + body := "Hello World!" + res, err := publisher.Publish(ctx, rmq.NewMessage([]byte(body))) + if err != nil { + log.Panicf("Failed to publish a message: %v", err) + } + switch res.Outcome.(type) { + case *rmq.StateAccepted: + case *rmq.StateRejected: + log.Fatalf("Message was rejected: %v", res.Outcome) + case *rmq.StateReleased: + log.Fatalf("Message was released: %v", res.Outcome) + case *rmq.StateModified: + log.Fatalf("Message was modified: %v", res.Outcome) + default: + log.Fatalf("Unexpected publish outcome: %v", res.Outcome) + } + log.Printf(" [x] Sent %s\n", body) +} diff --git a/go-amqp/test-tutorials.sh b/go-amqp/test-tutorials.sh new file mode 100755 index 00000000..25f3d057 --- /dev/null +++ b/go-amqp/test-tutorials.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# End-to-end smoke tests for go-amqp tutorials. +# Requires: bash, go, RabbitMQ 4.x on localhost:5672 (guest/guest). +# +# Usage: +# ./go-amqp/test-tutorials.sh +# cd go-amqp && ./test-tutorials.sh +# +# Environment: +# SKIP_BROKER_CHECK=1 Skip TCP check on port 5672 (tests still need a broker). + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +PASS=0 +FAIL=0 + +pass() { + printf 'ok\t%s\n' "$1" + PASS=$((PASS + 1)) +} + +fail() { + printf 'FAIL\t%s\n' "$1" >&2 + FAIL=$((FAIL + 1)) +} + +require_go() { + command -v go >/dev/null 2>&1 || { + echo "go is not on PATH" >&2 + exit 1 + } +} + +check_broker() { + if command -v nc >/dev/null 2>&1; then + nc -z localhost 5672 2>/dev/null + return + fi + { echo >/dev/tcp/127.0.0.1/5672; } 2>/dev/null +} + +require_broker() { + if [[ "${SKIP_BROKER_CHECK:-}" == "1" ]]; then + return 0 + fi + if ! check_broker; then + echo "RabbitMQ does not appear to be reachable on localhost:5672" >&2 + echo "Start the broker or set SKIP_BROKER_CHECK=1 to skip this check." >&2 + exit 1 + fi +} + +assert_file_contains() { + local file=$1 + local needle=$2 + local msg=$3 + if [[ -f "$file" ]] && grep -qF -- "$needle" "$file"; then + pass "$msg" + else + fail "$msg (log should contain: $needle)" + if [[ -f "$file" ]]; then + echo "--- $file (first 40 lines) ---" >&2 + head -n 40 "$file" >&2 + fi + fi +} + +# Run command in background; sleep $secs; kill. Captures stdout/stderr to $log. +run_timed() { + local log=$1 + local secs=$2 + shift 2 + : >"$log" + "$@" >"$log" 2>&1 & + local pid=$! + sleep "$secs" + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true +} + +test_t1_hello() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + go run receive.go >"$recv_log" 2>&1 & + local rpid=$! + sleep 2 + go run send.go >"$log" 2>&1 + sleep 1 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T1 send publishes" + assert_file_contains "$recv_log" "Hello World" "T1 receive gets message" + rm -f "$log" "$recv_log" +} + +test_t2_work_queues() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + go run worker.go >"$recv_log" 2>&1 & + local rpid=$! + sleep 2 + go run new_task.go e2e.task.test >"$log" 2>&1 + sleep 3 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T2 new_task publishes" + assert_file_contains "$recv_log" "e2e.task.test" "T2 worker receives task" + rm -f "$log" "$recv_log" +} + +test_t3_pubsub() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + go run receive_logs.go >"$recv_log" 2>&1 & + local rpid=$! + sleep 2 + go run emit_log.go e2e pubsub hello >"$log" 2>&1 + sleep 2 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T3 emit_log publishes" + assert_file_contains "$recv_log" "e2e pubsub hello" "T3 receive_logs receives broadcast" + rm -f "$log" "$recv_log" +} + +test_t4_routing() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + go run receive_logs_direct.go warn >"$recv_log" 2>&1 & + local rpid=$! + sleep 2 + go run emit_log_direct.go warn "e2e routing" >"$log" 2>&1 + sleep 2 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T4 emit_log_direct publishes" + assert_file_contains "$recv_log" "e2e routing" "T4 receive_logs_direct gets routed message" + rm -f "$log" "$recv_log" +} + +test_t5_topics() { + local log recv_log + log=$(mktemp) + recv_log=$(mktemp) + go run receive_logs_topic.go "kern.*" >"$recv_log" 2>&1 & + local rpid=$! + sleep 2 + go run emit_log_topic.go kern.info "e2e topic" >"$log" 2>&1 + sleep 2 + kill "$rpid" 2>/dev/null || true + wait "$rpid" 2>/dev/null || true + assert_file_contains "$log" "Sent" "T5 emit_log_topic publishes" + assert_file_contains "$recv_log" "e2e topic" "T5 receive_logs_topic matches binding" + rm -f "$log" "$recv_log" +} + +test_t6_rpc() { + local log srv_log + log=$(mktemp) + srv_log=$(mktemp) + go run rpc_server.go >"$srv_log" 2>&1 & + local spid=$! + sleep 2 + if ! go run rpc_client.go 10 >"$log" 2>&1; then + kill "$spid" 2>/dev/null || true + wait "$spid" 2>/dev/null || true + fail "T6 rpc_client process failed" + rm -f "$log" "$srv_log" + return + fi + kill "$spid" 2>/dev/null || true + wait "$spid" 2>/dev/null || true + assert_file_contains "$log" "Requesting fib(10)" "T6 rpc_client sends request" + assert_file_contains "$log" "Got 55" "T6 rpc_client gets fib(10)==55" + rm -f "$log" "$srv_log" +} + +test_publisher_confirms() { + local log + log=$(mktemp) + run_timed "$log" 4 go run publisher_confirms.go + assert_file_contains "$log" "Confirmed" "publisher_confirms sees accepted publish" + assert_file_contains "$log" "Received a message" "publisher_confirms consumer sees message" + rm -f "$log" +} + +test_rpc_amqp10() { + local log + log=$(mktemp) + run_timed "$log" 5 go run rpc_amqp10.go + assert_file_contains "$log" "pong" "rpc_amqp10 ping/pong (Direct Reply-To)" + rm -f "$log" +} + +main() { + require_go + require_broker + echo "Running go-amqp tutorial smoke tests from $ROOT" + test_t1_hello + test_t2_work_queues + test_t3_pubsub + test_t4_routing + test_t5_topics + test_t6_rpc + test_publisher_confirms + test_rpc_amqp10 + echo "----" + echo "Passed: $PASS Failed: $FAIL" + if [[ "$FAIL" -gt 0 ]]; then + exit 1 + fi +} + +main "$@" diff --git a/go-amqp/worker.go b/go-amqp/worker.go new file mode 100644 index 00000000..f3f52531 --- /dev/null +++ b/go-amqp/worker.go @@ -0,0 +1,61 @@ +package main + +import ( + "bytes" + "context" + "errors" + "log" + "time" + + rmq "github.com/rabbitmq/rabbitmq-amqp-go-client/pkg/rabbitmqamqp" +) + +const brokerURI = "amqp://guest:guest@localhost:5672/" + +func main() { + ctx := context.Background() + env := rmq.NewEnvironment(brokerURI, nil) + conn, err := env.NewConnection(ctx) + if err != nil { + log.Panicf("Failed to connect to RabbitMQ: %v", err) + } + defer func() { + _ = env.CloseConnections(context.Background()) + }() + + _, err = conn.Management().DeclareQueue(ctx, &rmq.QuorumQueueSpecification{Name: "task_queue"}) + if err != nil { + log.Panicf("Failed to declare a queue: %v", err) + } + + consumer, err := conn.NewConsumer(ctx, "task_queue", &rmq.ConsumerOptions{InitialCredits: 1}) + if err != nil { + log.Panicf("Failed to create consumer: %v", err) + } + defer func() { _ = consumer.Close(context.Background()) }() + + log.Printf(" [*] Waiting for messages. To exit press CTRL+C") + for { + delivery, err := consumer.Receive(ctx) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Panicf("Failed to receive a message: %v", err) + } + msg := delivery.Message() + var payload []byte + if len(msg.Data) > 0 { + payload = msg.Data[0] + } + log.Printf("Received a message: %s", payload) + dotCount := bytes.Count(payload, []byte(".")) + t := time.Duration(dotCount) + time.Sleep(t * time.Second) + log.Printf("Done") + err = delivery.Accept(ctx) + if err != nil { + log.Panicf("Failed to accept message: %v", err) + } + } +} diff --git a/java-amqp/.mvn/wrapper/maven-wrapper.jar b/java-amqp/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..cb28b0e3 Binary files /dev/null and b/java-amqp/.mvn/wrapper/maven-wrapper.jar differ diff --git a/java-amqp/.mvn/wrapper/maven-wrapper.properties b/java-amqp/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..346d645f --- /dev/null +++ b/java-amqp/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/java-amqp/README.md b/java-amqp/README.md new file mode 100644 index 00000000..99a3c0e7 --- /dev/null +++ b/java-amqp/README.md @@ -0,0 +1,103 @@ +# Java code for RabbitMQ tutorials (AMQP 1.0 client) + +Here you can find Java examples for the [RabbitMQ tutorials](https://www.rabbitmq.com/getstarted.html) using the [RabbitMQ AMQP 1.0 Java client](https://rabbitmq.github.io/rabbitmq-amqp-java-client/stable/htmlsingle/). + +You need a RabbitMQ node running locally. The client requires **RabbitMQ 4.0 or newer** with AMQP 1.0 (enabled by default on port **5672**). The examples use the default `guest` user. See also the [AMQP 1.0 client libraries overview](https://www.rabbitmq.com/client-libraries/amqp-client-libraries). + +This directory is a self-contained [Maven](https://maven.apache.org/) project. Use `./mvnw` on Unix-like systems or `mvnw.cmd` on Windows. + +Run integration tests with `./mvnw test` (they expect a broker on `localhost`). + +## Code + +#### Tutorial one: "Hello World!" + +```shell +# terminal tab 1 +./mvnw compile exec:java -Dexec.mainClass=Recv + +# terminal tab 2 +./mvnw compile exec:java -Dexec.mainClass=Send +``` + +#### Tutorial two: Work Queues + +```shell +# terminal tab 1 +./mvnw compile exec:java -Dexec.mainClass=Worker + +# terminal tab 2 +./mvnw compile exec:java -Dexec.mainClass=Worker + +# terminal tab 3 +./mvnw compile exec:java -Dexec.mainClass=NewTask -Dexec.args="First Message" +./mvnw compile exec:java -Dexec.mainClass=NewTask -Dexec.args="Second Message" +./mvnw compile exec:java -Dexec.mainClass=NewTask -Dexec.args="Third Message" +./mvnw compile exec:java -Dexec.mainClass=NewTask -Dexec.args="Fourth Message" +./mvnw compile exec:java -Dexec.mainClass=NewTask -Dexec.args="Fifth Message" +``` + +#### Tutorial three: Publish/Subscribe + +```shell +# terminal tab 1 +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogs + +# terminal tab 2 +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogs + +# terminal tab 3 +./mvnw compile exec:java -Dexec.mainClass=EmitLog +``` + +#### Tutorial four: Routing + +```shell +# terminal tab 1 +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogsDirect -Dexec.args="warning error" + +# terminal tab 2 +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogsDirect -Dexec.args="info warning error" + +# terminal tab 3 +./mvnw compile exec:java -Dexec.mainClass=EmitLogDirect -Dexec.args="info Run. Run. Or it will explode." +./mvnw compile exec:java -Dexec.mainClass=EmitLogDirect -Dexec.args="warning Run. Run. Or it will explode." +./mvnw compile exec:java -Dexec.mainClass=EmitLogDirect -Dexec.args="error Run. Run. Or it will explode." +``` + +#### Tutorial five: Topics + +```shell +# terminal tab 1 +# To receive all the logs: +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogsTopic -Dexec.args="#" + +# To receive all logs from the facility "kern": +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogsTopic -Dexec.args="kern.*" + +# Or if you want to hear only about "critical" logs: +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogsTopic -Dexec.args="*.critical" + +# You can create multiple bindings: +./mvnw compile exec:java -Dexec.mainClass=ReceiveLogsTopic -Dexec.args="kern.* *.critical" + +# terminal tab 2 +# And to emit a log with a routing key "kern.critical" type: +./mvnw compile exec:java -Dexec.mainClass=EmitLogTopic -Dexec.args="kern.critical A critical kernel error" +``` + +#### Tutorial six: RPC + +```shell +# terminal tab 1 +./mvnw compile exec:java -Dexec.mainClass=RPCServer + +# terminal tab 2 +./mvnw compile exec:java -Dexec.mainClass=RPCClient +``` + +#### Tutorial seven: Publisher Confirms + +```shell +./mvnw compile exec:java -Dexec.mainClass=PublisherConfirms +``` diff --git a/java-amqp/mvnw b/java-amqp/mvnw new file mode 100755 index 00000000..8d937f4c --- /dev/null +++ b/java-amqp/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/java-amqp/mvnw.cmd b/java-amqp/mvnw.cmd new file mode 100644 index 00000000..c4586b56 --- /dev/null +++ b/java-amqp/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/java-amqp/pom.xml b/java-amqp/pom.xml new file mode 100644 index 00000000..c1826d5a --- /dev/null +++ b/java-amqp/pom.xml @@ -0,0 +1,110 @@ + + 4.0.0 + + com.rabbitmq + rabbitmq-tutorial-amqp + 1.0-SNAPSHOT + jar + + rabbitmq-tutorial-amqp + https://github.com/rabbitmq/rabbitmq-tutorials + + + UTF-8 + 11 + 11 + + + + + com.rabbitmq.client + amqp-client + 0.10.0 + + + + org.slf4j + slf4j-api + 2.0.17 + + + + org.slf4j + slf4j-simple + 2.0.17 + + + + org.junit.jupiter + junit-jupiter + 5.14.3 + test + + + + org.assertj + assertj-core + 3.27.7 + test + + + + + + + + maven-compiler-plugin + 3.15.0 + + 11 + + + + maven-surefire-plugin + 3.5.5 + + ${test-arguments} + + + + org.apache.maven.plugins + maven-clean-plugin + 3.5.0 + + + + org.apache.maven.plugins + maven-resources-plugin + 3.5.0 + + ${project.build.sourceEncoding} + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.2 + + false + + + + + + + + + jvm-test-arguments-java-21-and-more + + [21,) + + + -XX:+EnableDynamicAgentLoading + + + + + + diff --git a/java-amqp/src/main/java/EmitLog.java b/java-amqp/src/main/java/EmitLog.java new file mode 100644 index 00000000..f7c28ff5 --- /dev/null +++ b/java-amqp/src/main/java/EmitLog.java @@ -0,0 +1,39 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EmitLog { + + private static final String EXCHANGE_NAME = "logs"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + try (Management management = connection.management()) { + management.exchange().name(EXCHANGE_NAME).type(Management.ExchangeType.FANOUT).declare(); + } + try (Publisher publisher = connection.publisherBuilder().exchange(EXCHANGE_NAME).build()) { + String message = argv.length < 1 ? "info: Hello World!" : String.join(" ", argv); + Message msg = publisher.message(message.getBytes(StandardCharsets.UTF_8)); + CountDownLatch confirmed = new CountDownLatch(1); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + confirmed.countDown(); + } + }); + if (!confirmed.await(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Publish was not confirmed in time"); + } + System.out.println(" [x] Sent '" + message + "'"); + } + } finally { + environment.close(); + } + } +} diff --git a/java-amqp/src/main/java/EmitLogDirect.java b/java-amqp/src/main/java/EmitLogDirect.java new file mode 100644 index 00000000..1e053ea9 --- /dev/null +++ b/java-amqp/src/main/java/EmitLogDirect.java @@ -0,0 +1,69 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EmitLogDirect { + + private static final String EXCHANGE_NAME = "direct_logs"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + try (Management management = connection.management()) { + management.exchange().name(EXCHANGE_NAME).type(Management.ExchangeType.DIRECT).declare(); + } + String severity = getSeverity(argv); + try (Publisher publisher = connection.publisherBuilder().exchange(EXCHANGE_NAME).key(severity).build()) { + String message = getMessage(argv); + Message msg = publisher.message(message.getBytes(StandardCharsets.UTF_8)); + CountDownLatch confirmed = new CountDownLatch(1); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + confirmed.countDown(); + } + }); + if (!confirmed.await(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Publish was not confirmed in time"); + } + System.out.println(" [x] Sent '" + severity + "':'" + message + "'"); + } + } finally { + environment.close(); + } + } + + private static String getSeverity(String[] strings) { + if (strings.length < 1) { + return "info"; + } + return strings[0]; + } + + private static String getMessage(String[] strings) { + if (strings.length < 2) { + return "Hello World!"; + } + return joinStrings(strings, " ", 1); + } + + private static String joinStrings(String[] strings, String delimiter, int startIndex) { + int length = strings.length; + if (length == 0) { + return ""; + } + if (length <= startIndex) { + return ""; + } + StringBuilder words = new StringBuilder(strings[startIndex]); + for (int i = startIndex + 1; i < length; i++) { + words.append(delimiter).append(strings[i]); + } + return words.toString(); + } +} diff --git a/java-amqp/src/main/java/EmitLogTopic.java b/java-amqp/src/main/java/EmitLogTopic.java new file mode 100644 index 00000000..c8001017 --- /dev/null +++ b/java-amqp/src/main/java/EmitLogTopic.java @@ -0,0 +1,69 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EmitLogTopic { + + private static final String EXCHANGE_NAME = "topic_logs"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + try (Management management = connection.management()) { + management.exchange().name(EXCHANGE_NAME).type(Management.ExchangeType.TOPIC).declare(); + } + String routingKey = getRouting(argv); + try (Publisher publisher = connection.publisherBuilder().exchange(EXCHANGE_NAME).key(routingKey).build()) { + String message = getMessage(argv); + Message msg = publisher.message(message.getBytes(StandardCharsets.UTF_8)); + CountDownLatch confirmed = new CountDownLatch(1); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + confirmed.countDown(); + } + }); + if (!confirmed.await(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Publish was not confirmed in time"); + } + System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'"); + } + } finally { + environment.close(); + } + } + + private static String getRouting(String[] strings) { + if (strings.length < 1) { + return "anonymous.info"; + } + return strings[0]; + } + + private static String getMessage(String[] strings) { + if (strings.length < 2) { + return "Hello World!"; + } + return joinStrings(strings, " ", 1); + } + + private static String joinStrings(String[] strings, String delimiter, int startIndex) { + int length = strings.length; + if (length == 0) { + return ""; + } + if (length < startIndex) { + return ""; + } + StringBuilder words = new StringBuilder(strings[startIndex]); + for (int i = startIndex + 1; i < length; i++) { + words.append(delimiter).append(strings[i]); + } + return words.toString(); + } +} diff --git a/java-amqp/src/main/java/NewTask.java b/java-amqp/src/main/java/NewTask.java new file mode 100644 index 00000000..80aee9fb --- /dev/null +++ b/java-amqp/src/main/java/NewTask.java @@ -0,0 +1,42 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class NewTask { + + private static final String TASK_QUEUE_NAME = "task_queue"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + try (Management management = connection.management()) { + management.queue(TASK_QUEUE_NAME).quorum().queue().declare(); + // Un-comment the following line to declare a Classic Queue + // Comment the previous queue declaration, if you un-comment below + // management.queue(TASK_QUEUE_NAME).classic().queue().declare(); + } + try (Publisher publisher = connection.publisherBuilder().queue(TASK_QUEUE_NAME).build()) { + String message = String.join(" ", argv); + Message msg = publisher.message(message.getBytes(StandardCharsets.UTF_8)).durable(true); + CountDownLatch confirmed = new CountDownLatch(1); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + confirmed.countDown(); + } + }); + if (!confirmed.await(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Publish was not confirmed in time"); + } + System.out.println(" [x] Sent '" + message + "'"); + } + } finally { + environment.close(); + } + } +} diff --git a/java-amqp/src/main/java/PublisherConfirms.java b/java-amqp/src/main/java/PublisherConfirms.java new file mode 100644 index 00000000..ca707c1b --- /dev/null +++ b/java-amqp/src/main/java/PublisherConfirms.java @@ -0,0 +1,132 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * AMQP 1.0 clients publish with broker feedback in the {@link Publisher.Callback} (accepted / rejected / released). + * This is the counterpart to AMQP 0.9.1 publisher confirms. + */ +public class PublisherConfirms { + + static final int MESSAGE_COUNT = 50_000; + + public static void main(String[] args) throws Exception { + publishMessagesIndividually(); + publishMessagesInBatch(); + publishMessagesWithOutstandingTracking(); + } + + static void publishMessagesIndividually() throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue = UUID.randomUUID().toString(); + try (Management management = connection.management()) { + management.queue().name(queue).exclusive(true).autoDelete(true).declare(); + } + try (Publisher publisher = connection.publisherBuilder().queue(queue).build()) { + long start = System.nanoTime(); + for (int i = 0; i < MESSAGE_COUNT; i++) { + String body = String.valueOf(i); + Message msg = publisher.message(body.getBytes()); + CountDownLatch latch = new CountDownLatch(1); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + latch.countDown(); + } + }); + if (!latch.await(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Confirm not received for message " + i); + } + } + long end = System.nanoTime(); + System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT, + Duration.ofNanos(end - start).toMillis()); + } + } finally { + environment.close(); + } + } + + static void publishMessagesInBatch() throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue = UUID.randomUUID().toString(); + try (Management management = connection.management()) { + management.queue().name(queue).exclusive(true).autoDelete(true).declare(); + } + try (Publisher publisher = connection.publisherBuilder().queue(queue).build()) { + int batchSize = 100; + long start = System.nanoTime(); + int i = 0; + while (i < MESSAGE_COUNT) { + int n = Math.min(batchSize, MESSAGE_COUNT - i); + CountDownLatch batchLatch = new CountDownLatch(n); + for (int j = 0; j < n; j++) { + String body = String.valueOf(i + j); + Message msg = publisher.message(body.getBytes()); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + batchLatch.countDown(); + } + }); + } + if (!batchLatch.await(60, TimeUnit.SECONDS)) { + throw new IllegalStateException("Batch confirm incomplete at offset " + i); + } + i += n; + } + long end = System.nanoTime(); + System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT, + Duration.ofNanos(end - start).toMillis()); + } + } finally { + environment.close(); + } + } + + static void publishMessagesWithOutstandingTracking() throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue = UUID.randomUUID().toString(); + try (Management management = connection.management()) { + management.queue().name(queue).exclusive(true).autoDelete(true).declare(); + } + final int maxOutstanding = 1000; + try (Publisher publisher = connection.publisherBuilder().queue(queue).build()) { + AtomicInteger outstanding = new AtomicInteger(0); + CountDownLatch allDone = new CountDownLatch(MESSAGE_COUNT); + long start = System.nanoTime(); + for (int i = 0; i < MESSAGE_COUNT; i++) { + while (outstanding.get() >= maxOutstanding) { + Thread.sleep(1); + } + outstanding.incrementAndGet(); + String body = String.valueOf(i); + Message msg = publisher.message(body.getBytes()); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + outstanding.decrementAndGet(); + allDone.countDown(); + } + }); + } + if (!allDone.await(120, TimeUnit.SECONDS)) { + throw new IllegalStateException("Not all messages confirmed"); + } + long end = System.nanoTime(); + System.out.format("Published %,d messages with outstanding window in %,d ms%n", MESSAGE_COUNT, + Duration.ofNanos(end - start).toMillis()); + } + } finally { + environment.close(); + } + } +} diff --git a/java-amqp/src/main/java/RPCClient.java b/java-amqp/src/main/java/RPCClient.java new file mode 100644 index 00000000..c1093404 --- /dev/null +++ b/java-amqp/src/main/java/RPCClient.java @@ -0,0 +1,30 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Requester; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +public class RPCClient { + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + Requester requester = connection.requesterBuilder() + .requestAddress().queue("rpc_queue") + .requester() + .build()) { + for (int i = 0; i < 32; i++) { + String iStr = Integer.toString(i); + System.out.println(" [x] Requesting fib(" + iStr + ")"); + Message request = requester.message(iStr.getBytes(StandardCharsets.UTF_8)); + Message reply = requester.publish(request).get(30, TimeUnit.SECONDS); + String response = new String(reply.body(), StandardCharsets.UTF_8); + System.out.println(" [.] Got '" + response + "'"); + } + } finally { + environment.close(); + } + } +} diff --git a/java-amqp/src/main/java/RPCServer.java b/java-amqp/src/main/java/RPCServer.java new file mode 100644 index 00000000..718ba625 --- /dev/null +++ b/java-amqp/src/main/java/RPCServer.java @@ -0,0 +1,56 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Responder; + +import java.nio.charset.StandardCharsets; + +public class RPCServer { + + private static final String RPC_QUEUE_NAME = "rpc_queue"; + + private static int fib(int n) { + if (n == 0) { + return 0; + } + if (n == 1) { + return 1; + } + return fib(n - 1) + fib(n - 2); + } + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + try (Management management = connection.management()) { + management.queue().name(RPC_QUEUE_NAME).quorum().queue().declare(); + management.queuePurge(RPC_QUEUE_NAME); + } + + System.out.println(" [x] Awaiting RPC requests"); + + Responder responder = connection.responderBuilder() + .requestQueue(RPC_QUEUE_NAME) + .handler((ctx, req) -> { + String response = ""; + try { + String message = new String(req.body(), StandardCharsets.UTF_8); + int n = Integer.parseInt(message); + System.out.println(" [.] fib(" + message + ")"); + response += fib(n); + } catch (RuntimeException e) { + System.out.println(" [.] " + e); + } + return ctx.message(response.getBytes(StandardCharsets.UTF_8)); + }) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + responder.close(); + connection.close(); + environment.close(); + })); + + Thread.sleep(Long.MAX_VALUE); + } +} diff --git a/java-amqp/src/main/java/ReceiveLogs.java b/java-amqp/src/main/java/ReceiveLogs.java new file mode 100644 index 00000000..6c596427 --- /dev/null +++ b/java-amqp/src/main/java/ReceiveLogs.java @@ -0,0 +1,48 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Management.QueueInfo; + +import java.nio.charset.StandardCharsets; + +public class ReceiveLogs { + + private static final String EXCHANGE_NAME = "logs"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + + String queueName; + try (Management management = connection.management()) { + management.exchange().name(EXCHANGE_NAME).type(Management.ExchangeType.FANOUT).declare(); + QueueInfo q = management.queue().exclusive(true).autoDelete(true).declare(); + queueName = q.name(); + management.binding() + .sourceExchange(EXCHANGE_NAME) + .destinationQueue(queueName) + .key("") + .bind(); + } + + System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); + + Consumer consumer = connection.consumerBuilder() + .queue(queueName) + .messageHandler((ctx, message) -> { + String body = new String(message.body(), StandardCharsets.UTF_8); + System.out.println(" [x] Received '" + body + "'"); + ctx.accept(); + }) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.close(); + connection.close(); + environment.close(); + })); + + Thread.sleep(Long.MAX_VALUE); + } +} diff --git a/java-amqp/src/main/java/ReceiveLogsDirect.java b/java-amqp/src/main/java/ReceiveLogsDirect.java new file mode 100644 index 00000000..5591baea --- /dev/null +++ b/java-amqp/src/main/java/ReceiveLogsDirect.java @@ -0,0 +1,66 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Management.QueueInfo; +import com.rabbitmq.client.amqp.Message; + +import java.nio.charset.StandardCharsets; + +public class ReceiveLogsDirect { + + private static final String EXCHANGE_NAME = "direct_logs"; + + public static void main(String[] argv) throws Exception { + if (argv.length < 1) { + System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]"); + System.exit(1); + } + + Environment environment = TutorialSupport.newEnvironment(); + Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + + String queueName; + try (Management management = connection.management()) { + management.exchange().name(EXCHANGE_NAME).type(Management.ExchangeType.DIRECT).declare(); + QueueInfo q = management.queue().exclusive(true).autoDelete(true).declare(); + queueName = q.name(); + for (String severity : argv) { + management.binding() + .sourceExchange(EXCHANGE_NAME) + .destinationQueue(queueName) + .key(severity) + .bind(); + } + } + + System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); + + Consumer consumer = connection.consumerBuilder() + .queue(queueName) + .messageHandler((ctx, message) -> { + String body = new String(message.body(), StandardCharsets.UTF_8); + String routingKey = routingKey(message); + System.out.println(" [x] Received '" + routingKey + "':'" + body + "'"); + ctx.accept(); + }) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.close(); + connection.close(); + environment.close(); + })); + + Thread.sleep(Long.MAX_VALUE); + } + + private static String routingKey(Message message) { + Object rk = message.annotation("x-routing-key"); + if (rk != null) { + return rk.toString(); + } + String subject = message.subject(); + return subject != null ? subject : ""; + } +} diff --git a/java-amqp/src/main/java/ReceiveLogsTopic.java b/java-amqp/src/main/java/ReceiveLogsTopic.java new file mode 100644 index 00000000..306baabd --- /dev/null +++ b/java-amqp/src/main/java/ReceiveLogsTopic.java @@ -0,0 +1,66 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Management.QueueInfo; +import com.rabbitmq.client.amqp.Message; + +import java.nio.charset.StandardCharsets; + +public class ReceiveLogsTopic { + + private static final String EXCHANGE_NAME = "topic_logs"; + + public static void main(String[] argv) throws Exception { + if (argv.length < 1) { + System.err.println("Usage: ReceiveLogsTopic [binding_key]..."); + System.exit(1); + } + + Environment environment = TutorialSupport.newEnvironment(); + Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + + String queueName; + try (Management management = connection.management()) { + management.exchange().name(EXCHANGE_NAME).type(Management.ExchangeType.TOPIC).declare(); + QueueInfo q = management.queue().exclusive(true).autoDelete(true).declare(); + queueName = q.name(); + for (String bindingKey : argv) { + management.binding() + .sourceExchange(EXCHANGE_NAME) + .destinationQueue(queueName) + .key(bindingKey) + .bind(); + } + } + + System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); + + Consumer consumer = connection.consumerBuilder() + .queue(queueName) + .messageHandler((ctx, message) -> { + String body = new String(message.body(), StandardCharsets.UTF_8); + String routingKey = routingKey(message); + System.out.println(" [x] Received '" + routingKey + "':'" + body + "'"); + ctx.accept(); + }) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.close(); + connection.close(); + environment.close(); + })); + + Thread.sleep(Long.MAX_VALUE); + } + + private static String routingKey(Message message) { + Object rk = message.annotation("x-routing-key"); + if (rk != null) { + return rk.toString(); + } + String subject = message.subject(); + return subject != null ? subject : ""; + } +} diff --git a/java-amqp/src/main/java/Recv.java b/java-amqp/src/main/java/Recv.java new file mode 100644 index 00000000..7bea54a2 --- /dev/null +++ b/java-amqp/src/main/java/Recv.java @@ -0,0 +1,38 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; + +import java.nio.charset.StandardCharsets; + +public class Recv { + + private static final String QUEUE_NAME = "hello"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + try (Management management = connection.management()) { + management.queue().name(QUEUE_NAME).quorum().queue().declare(); + } + + System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); + + Consumer consumer = connection.consumerBuilder() + .queue(QUEUE_NAME) + .messageHandler((ctx, message) -> { + String body = new String(message.body(), StandardCharsets.UTF_8); + System.out.println(" [x] Received '" + body + "'"); + ctx.accept(); + }) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.close(); + connection.close(); + environment.close(); + })); + + Thread.sleep(Long.MAX_VALUE); + } +} diff --git a/java-amqp/src/main/java/Send.java b/java-amqp/src/main/java/Send.java new file mode 100644 index 00000000..3a3410ca --- /dev/null +++ b/java-amqp/src/main/java/Send.java @@ -0,0 +1,39 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class Send { + + private static final String QUEUE_NAME = "hello"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + try (Management management = connection.management()) { + management.queue().name(QUEUE_NAME).quorum().queue().declare(); + } + try (Publisher publisher = connection.publisherBuilder().queue(QUEUE_NAME).build()) { + String message = "Hello World!"; + Message msg = publisher.message(message.getBytes(StandardCharsets.UTF_8)); + CountDownLatch confirmed = new CountDownLatch(1); + publisher.publish(msg, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + confirmed.countDown(); + } + }); + if (!confirmed.await(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Publish was not confirmed in time"); + } + System.out.println(" [x] Sent '" + message + "'"); + } + } finally { + environment.close(); + } + } +} diff --git a/java-amqp/src/main/java/TutorialSupport.java b/java-amqp/src/main/java/TutorialSupport.java new file mode 100644 index 00000000..5eb5fcd8 --- /dev/null +++ b/java-amqp/src/main/java/TutorialSupport.java @@ -0,0 +1,17 @@ +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; + +/** + * Shared connection defaults for the AMQP 1.0 tutorials (guest on localhost, default port). + */ +final class TutorialSupport { + + static final String BROKER_URI = "amqp://guest:guest@localhost:5672/%2f"; + + private TutorialSupport() { + } + + static Environment newEnvironment() { + return new AmqpEnvironmentBuilder().build(); + } +} diff --git a/java-amqp/src/main/java/Worker.java b/java-amqp/src/main/java/Worker.java new file mode 100644 index 00000000..164d7d4f --- /dev/null +++ b/java-amqp/src/main/java/Worker.java @@ -0,0 +1,56 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; + +import java.nio.charset.StandardCharsets; + +public class Worker { + + private static final String TASK_QUEUE_NAME = "task_queue"; + + public static void main(String[] argv) throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build(); + try (Management management = connection.management()) { + management.queue().name(TASK_QUEUE_NAME).quorum().queue().declare(); + } + + System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); + + Consumer consumer = connection.consumerBuilder() + .queue(TASK_QUEUE_NAME) + .initialCredits(1) + .messageHandler((ctx, message) -> { + String body = new String(message.body(), StandardCharsets.UTF_8); + System.out.println(" [x] Received '" + body + "'"); + try { + doWork(body); + } finally { + System.out.println(" [x] Done"); + ctx.accept(); + } + }) + .build(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.close(); + connection.close(); + environment.close(); + })); + + Thread.sleep(Long.MAX_VALUE); + } + + private static void doWork(String task) { + for (char ch : task.toCharArray()) { + if (ch == '.') { + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + } + } +} diff --git a/java-amqp/src/test/java/Amqp10TutorialIntegrationTest.java b/java-amqp/src/test/java/Amqp10TutorialIntegrationTest.java new file mode 100644 index 00000000..a1b3c461 --- /dev/null +++ b/java-amqp/src/test/java/Amqp10TutorialIntegrationTest.java @@ -0,0 +1,306 @@ +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Management; +import com.rabbitmq.client.amqp.Management.QueueInfo; +import com.rabbitmq.client.amqp.Message; +import com.rabbitmq.client.amqp.Publisher; +import com.rabbitmq.client.amqp.Requester; +import com.rabbitmq.client.amqp.Responder; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests against a RabbitMQ node on localhost (same requirement as running the tutorials). + */ +class Amqp10TutorialIntegrationTest { + + @Test + @Timeout(60) + void tutorial1_helloWorld() throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue = "hello_it_" + UUID.randomUUID(); + try (Management management = connection.management()) { + management.queue().name(queue).quorum().queue().declare(); + } + BlockingQueue received = new LinkedBlockingQueue<>(); + Consumer consumer = connection.consumerBuilder() + .queue(queue) + .messageHandler((ctx, msg) -> { + received.offer(new String(msg.body(), StandardCharsets.UTF_8)); + ctx.accept(); + }) + .build(); + try (Publisher publisher = connection.publisherBuilder().queue(queue).build()) { + CountDownLatch ok = new CountDownLatch(1); + Message m = publisher.message("Hello World!".getBytes(StandardCharsets.UTF_8)); + publisher.publish(m, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + ok.countDown(); + } + }); + assertThat(ok.await(30, TimeUnit.SECONDS)).isTrue(); + } + assertThat(received.poll(30, TimeUnit.SECONDS)).isEqualTo("Hello World!"); + consumer.close(); + try (Management management = connection.management()) { + management.queueDelete(queue); + } + } finally { + environment.close(); + } + } + + @Test + @Timeout(120) + void tutorial2_workQueueFairDispatch() throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue = "task_it_" + UUID.randomUUID(); + try (Management management = connection.management()) { + management.queue().name(queue).quorum().queue().declare(); + } + CountDownLatch processed = new CountDownLatch(3); + Consumer consumer = connection.consumerBuilder() + .queue(queue) + .initialCredits(1) + .messageHandler((ctx, msg) -> { + processed.countDown(); + ctx.accept(); + }) + .build(); + try (Publisher publisher = connection.publisherBuilder().queue(queue).build()) { + for (int k = 0; k < 3; k++) { + CountDownLatch ok = new CountDownLatch(1); + Message m = publisher.message(("m" + k).getBytes(StandardCharsets.UTF_8)); + publisher.publish(m, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + ok.countDown(); + } + }); + assertThat(ok.await(30, TimeUnit.SECONDS)).isTrue(); + } + } + assertThat(processed.await(30, TimeUnit.SECONDS)).isTrue(); + consumer.close(); + try (Management management = connection.management()) { + management.queueDelete(queue); + } + } finally { + environment.close(); + } + } + + @Test + @Timeout(60) + void tutorial3_publishSubscribe() throws Exception { + String ex = "logs_it_" + UUID.randomUUID(); + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue; + try (Management management = connection.management()) { + management.exchange().name(ex).type(Management.ExchangeType.FANOUT).declare(); + QueueInfo q = management.queue().exclusive(true).autoDelete(true).declare(); + queue = q.name(); + management.binding().sourceExchange(ex).destinationQueue(queue).key("").bind(); + } + BlockingQueue received = new LinkedBlockingQueue<>(); + Consumer consumer = connection.consumerBuilder() + .queue(queue) + .messageHandler((ctx, msg) -> { + received.offer(new String(msg.body(), StandardCharsets.UTF_8)); + ctx.accept(); + }) + .build(); + try (Publisher publisher = connection.publisherBuilder().exchange(ex).build()) { + CountDownLatch ok = new CountDownLatch(1); + Message m = publisher.message("broadcast".getBytes(StandardCharsets.UTF_8)); + publisher.publish(m, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + ok.countDown(); + } + }); + assertThat(ok.await(30, TimeUnit.SECONDS)).isTrue(); + } + assertThat(received.poll(30, TimeUnit.SECONDS)).isEqualTo("broadcast"); + consumer.close(); + try (Management management = connection.management()) { + management.exchangeDelete(ex); + } + } finally { + environment.close(); + } + } + + @Test + @Timeout(60) + void tutorial4_routing() throws Exception { + String ex = "direct_it_" + UUID.randomUUID(); + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue; + try (Management management = connection.management()) { + management.exchange().name(ex).type(Management.ExchangeType.DIRECT).declare(); + QueueInfo q = management.queue().exclusive(true).autoDelete(true).declare(); + queue = q.name(); + management.binding().sourceExchange(ex).destinationQueue(queue).key("info").bind(); + } + BlockingQueue keys = new LinkedBlockingQueue<>(); + BlockingQueue bodies = new LinkedBlockingQueue<>(); + Consumer consumer = connection.consumerBuilder() + .queue(queue) + .messageHandler((ctx, msg) -> { + Object ann = msg.annotation("x-routing-key"); + String rk = ann != null ? ann.toString() : (msg.subject() != null ? msg.subject() : ""); + keys.offer(rk); + bodies.offer(new String(msg.body(), StandardCharsets.UTF_8)); + ctx.accept(); + }) + .build(); + try (Publisher publisher = connection.publisherBuilder().exchange(ex).key("info").build()) { + CountDownLatch ok = new CountDownLatch(1); + Message m = publisher.message("Hello".getBytes(StandardCharsets.UTF_8)); + publisher.publish(m, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + ok.countDown(); + } + }); + assertThat(ok.await(30, TimeUnit.SECONDS)).isTrue(); + } + assertThat(keys.poll(30, TimeUnit.SECONDS)).isEqualTo("info"); + assertThat(bodies.poll(30, TimeUnit.SECONDS)).isEqualTo("Hello"); + consumer.close(); + try (Management management = connection.management()) { + management.exchangeDelete(ex); + } + } finally { + environment.close(); + } + } + + @Test + @Timeout(60) + void tutorial5_topics() throws Exception { + String ex = "topic_it_" + UUID.randomUUID(); + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue; + try (Management management = connection.management()) { + management.exchange().name(ex).type(Management.ExchangeType.TOPIC).declare(); + QueueInfo q = management.queue().exclusive(true).autoDelete(true).declare(); + queue = q.name(); + management.binding().sourceExchange(ex).destinationQueue(queue).key("*.critical").bind(); + } + BlockingQueue keys = new LinkedBlockingQueue<>(); + Consumer consumer = connection.consumerBuilder() + .queue(queue) + .messageHandler((ctx, msg) -> { + Object ann = msg.annotation("x-routing-key"); + keys.offer(ann != null ? ann.toString() : (msg.subject() != null ? msg.subject() : "")); + ctx.accept(); + }) + .build(); + try (Publisher publisher = connection.publisherBuilder().exchange(ex).key("kern.critical").build()) { + CountDownLatch ok = new CountDownLatch(1); + Message m = publisher.message("A critical kernel error".getBytes(StandardCharsets.UTF_8)); + publisher.publish(m, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + ok.countDown(); + } + }); + assertThat(ok.await(30, TimeUnit.SECONDS)).isTrue(); + } + assertThat(keys.poll(30, TimeUnit.SECONDS)).isEqualTo("kern.critical"); + consumer.close(); + try (Management management = connection.management()) { + management.exchangeDelete(ex); + } + } finally { + environment.close(); + } + } + + @Test + @Timeout(60) + void tutorial6_rpc() throws Exception { + String queue = "rpc_it_" + UUID.randomUUID(); + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + try (Management management = connection.management()) { + management.queue().name(queue).quorum().queue().declare(); + } + Responder responder = connection.responderBuilder() + .requestQueue(queue) + .handler((ctx, req) -> { + int n = Integer.parseInt(new String(req.body(), StandardCharsets.UTF_8)); + int fib = fib(n); + return ctx.message(String.valueOf(fib).getBytes(StandardCharsets.UTF_8)); + }) + .build(); + try (Requester requester = connection.requesterBuilder() + .requestAddress().queue(queue) + .requester() + .build()) { + Message reply = requester.publish(requester.message("10".getBytes(StandardCharsets.UTF_8))) + .get(30, TimeUnit.SECONDS); + assertThat(new String(reply.body(), StandardCharsets.UTF_8)).isEqualTo("55"); + } finally { + responder.close(); + } + try (Management management = connection.management()) { + management.queueDelete(queue); + } + } finally { + environment.close(); + } + } + + private static int fib(int n) { + if (n == 0) { + return 0; + } + if (n == 1) { + return 1; + } + return fib(n - 1) + fib(n - 2); + } + + @Test + @Timeout(120) + void tutorial7_publisherConfirms() throws Exception { + Environment environment = TutorialSupport.newEnvironment(); + try (Connection connection = environment.connectionBuilder().uri(TutorialSupport.BROKER_URI).build()) { + String queue = UUID.randomUUID().toString(); + try (Management management = connection.management()) { + management.queue().name(queue).exclusive(true).autoDelete(true).declare(); + } + int nMsg = 500; + CountDownLatch confirmed = new CountDownLatch(nMsg); + try (Publisher publisher = connection.publisherBuilder().queue(queue).build()) { + for (int i = 0; i < nMsg; i++) { + String body = String.valueOf(i); + Message m = publisher.message(body.getBytes(StandardCharsets.UTF_8)); + publisher.publish(m, ctx -> { + if (ctx.status() == Publisher.Status.ACCEPTED) { + confirmed.countDown(); + } + }); + } + assertThat(confirmed.await(60, TimeUnit.SECONDS)).isTrue(); + } + } finally { + environment.close(); + } + } +}