Skip to content

Commit 86b9d4c

Browse files
authored
Add super stream exists API (#358)
- Add documentation - Add configuration to BestPracticesClient client Signed-off-by: Gabriele Santomaggio <[email protected]>
1 parent ac23f56 commit 86b9d4c

File tree

17 files changed

+238
-48
lines changed

17 files changed

+238
-48
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@ Please refer to the [documentation](https://rabbitmq.github.io/rabbitmq-stream-d
3030
The library requires .NET 6 or .NET 7.
3131

3232
### Documentation
33-
34-
3533
- [HTML documentation](https://rabbitmq.github.io/rabbitmq-stream-dotnet-client/stable/htmlsingle/index.html)
3634
- [PDF documentation](https://rabbitmq.github.io/rabbitmq-stream-dotnet-client/stable/dotnet-stream-client.pdf)
37-
3835
- [A Simple Getting started](https://github.com/rabbitmq/rabbitmq-stream-dotnet-client/blob/main/docs/Documentation/)
36+
- [Best practices to write a reliable client](https://github.com/rabbitmq/rabbitmq-stream-dotnet-client/tree/main/docs/ReliableClient/)
3937
- [Super Stream example](https://github.com/rabbitmq/rabbitmq-stream-dotnet-client/blob/main/docs/SuperStream)
4038
- [Stream Performance Test](https://github.com/rabbitmq/rabbitmq-stream-dotnet-client/tree/main/RabbitMQ.Stream.Client.PerfTest)
4139

RabbitMQ.Stream.Client/Client.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,19 @@ public async ValueTask<MetaDataResponse> QueryMetadata(string[] streams)
864864
.ConfigureAwait(false);
865865
}
866866

867+
public async Task<bool> SuperStreamExists(string stream)
868+
{
869+
var response = await QueryPartition(stream).ConfigureAwait(false);
870+
if (response is { Streams.Length: >= 0 } &&
871+
response.ResponseCode == ResponseCode.StreamNotAvailable)
872+
{
873+
ClientExceptions.MaybeThrowException(ResponseCode.StreamNotAvailable, stream);
874+
}
875+
876+
return response is { Streams.Length: >= 0 } &&
877+
response.ResponseCode == ResponseCode.Ok;
878+
}
879+
867880
public async Task<bool> StreamExists(string stream)
868881
{
869882
var streams = new[] { stream };

RabbitMQ.Stream.Client/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ RabbitMQ.Stream.Client.Client.Publishers.get -> System.Collections.Generic.IDict
4747
RabbitMQ.Stream.Client.Client.QueryRoute(string superStream, string routingKey) -> System.Threading.Tasks.Task<RabbitMQ.Stream.Client.RouteQueryResponse>
4848
RabbitMQ.Stream.Client.Client.StreamStats(string stream) -> System.Threading.Tasks.ValueTask<RabbitMQ.Stream.Client.StreamStatsResponse>
4949
RabbitMQ.Stream.Client.Client.Subscribe(string stream, RabbitMQ.Stream.Client.IOffsetType offsetType, ushort initialCredit, System.Collections.Generic.Dictionary<string, string> properties, System.Func<RabbitMQ.Stream.Client.Deliver, System.Threading.Tasks.Task> deliverHandler, System.Func<bool, System.Threading.Tasks.Task<RabbitMQ.Stream.Client.IOffsetType>> consumerUpdateHandler = null, RabbitMQ.Stream.Client.ConnectionsPool pool = null) -> System.Threading.Tasks.Task<(byte, RabbitMQ.Stream.Client.SubscribeResponse)>
50+
RabbitMQ.Stream.Client.Client.SuperStreamExists(string stream) -> System.Threading.Tasks.Task<bool>
5051
RabbitMQ.Stream.Client.Client.Unsubscribe(byte subscriptionId, bool ignoreIfAlreadyRemoved = false) -> System.Threading.Tasks.Task<RabbitMQ.Stream.Client.UnsubscribeResponse>
5152
RabbitMQ.Stream.Client.Client.UpdateSecret(string newSecret) -> System.Threading.Tasks.Task
5253
RabbitMQ.Stream.Client.ClientParameters.AuthMechanism.get -> RabbitMQ.Stream.Client.AuthMechanism
@@ -182,6 +183,7 @@ RabbitMQ.Stream.Client.OffsetTypeTimestamp.OffsetTypeTimestamp(System.DateTime d
182183
RabbitMQ.Stream.Client.OffsetTypeTimestamp.OffsetTypeTimestamp(System.DateTimeOffset dateTimeOffset) -> void
183184
RabbitMQ.Stream.Client.PartitionsSuperStreamSpec
184185
RabbitMQ.Stream.Client.PartitionsSuperStreamSpec.Partitions.get -> int
186+
RabbitMQ.Stream.Client.PartitionsSuperStreamSpec.PartitionsSuperStreamSpec(string Name) -> void
185187
RabbitMQ.Stream.Client.PartitionsSuperStreamSpec.PartitionsSuperStreamSpec(string Name, int partitions) -> void
186188
RabbitMQ.Stream.Client.ProducerFilter
187189
RabbitMQ.Stream.Client.ProducerFilter.FilterValue.get -> System.Func<RabbitMQ.Stream.Client.Message, string>
@@ -309,6 +311,7 @@ RabbitMQ.Stream.Client.StreamSystem.CreateSuperStreamConsumer(RabbitMQ.Stream.Cl
309311
RabbitMQ.Stream.Client.StreamSystem.DeleteSuperStream(string superStream) -> System.Threading.Tasks.Task
310312
RabbitMQ.Stream.Client.StreamSystem.StreamInfo(string streamName) -> System.Threading.Tasks.Task<RabbitMQ.Stream.Client.StreamInfo>
311313
RabbitMQ.Stream.Client.StreamSystem.StreamStats(string stream) -> System.Threading.Tasks.Task<RabbitMQ.Stream.Client.StreamStats>
314+
RabbitMQ.Stream.Client.StreamSystem.SuperStreamExists(string superStream) -> System.Threading.Tasks.Task<bool>
312315
RabbitMQ.Stream.Client.StreamSystem.UpdateSecret(string newSecret) -> System.Threading.Tasks.Task
313316
RabbitMQ.Stream.Client.StreamSystemConfig.AuthMechanism.get -> RabbitMQ.Stream.Client.AuthMechanism
314317
RabbitMQ.Stream.Client.StreamSystemConfig.AuthMechanism.set -> void

RabbitMQ.Stream.Client/StreamSpec.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public int MaxSegmentSizeBytes
3838
public IDictionary<string, string> Args => args;
3939
}
4040

41+
/// <summary>
42+
/// Abstract class for SuperStreamSpec
43+
/// </summary>
44+
/// <param name="Name"> Super Stream Name</param>
4145
public abstract record SuperStreamSpec(string Name)
4246
{
4347
internal virtual void Validate()
@@ -79,9 +83,22 @@ public int MaxSegmentSizeBytes
7983
public IDictionary<string, string> Args => args;
8084
}
8185

86+
/// <summary>
87+
/// Create a super stream based on the number of partitions.
88+
/// So there will be N partitions and N binding keys.
89+
/// The stream names is the super stream name with a partition number appended.
90+
/// The routing key is the partition number.
91+
/// Producer should use HASH strategy to route the message to the correct partition.
92+
/// Partitions should be at least 1.
93+
/// </summary>
8294
public record PartitionsSuperStreamSpec : SuperStreamSpec
8395
{
8496

97+
public PartitionsSuperStreamSpec(string Name) : base(Name)
98+
{
99+
Partitions = 3;
100+
}
101+
85102
public PartitionsSuperStreamSpec(string Name, int partitions) : base(Name)
86103
{
87104
Partitions = partitions;
@@ -121,6 +138,13 @@ internal override List<string> GetBindingKeys()
121138

122139
}
123140

141+
/// <summary>
142+
/// Create a super stream based on the number of binding keys.
143+
/// So there will be N partitions and N binding keys.
144+
/// The stream names is the super stream name with a binding key appended.
145+
/// Producer should use KEY strategy to route the message to the correct partition.
146+
/// The binding keys should be unique duplicates are not allowed.
147+
/// </summary>
124148
public record BindingsSuperStreamSpec : SuperStreamSpec
125149
{
126150
public BindingsSuperStreamSpec(string Name, string[] bindingKeys) : base(Name)

RabbitMQ.Stream.Client/StreamSystem.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,20 @@ public async Task<bool> StreamExists(string stream)
395395
}
396396
}
397397

398+
public async Task<bool> SuperStreamExists(string superStream)
399+
{
400+
await MayBeReconnectLocator().ConfigureAwait(false);
401+
await _semClientProvidedName.WaitAsync().ConfigureAwait(false);
402+
try
403+
{
404+
return await _client.SuperStreamExists(superStream).ConfigureAwait(false);
405+
}
406+
finally
407+
{
408+
_semClientProvidedName.Release();
409+
}
410+
}
411+
398412
private static void MaybeThrowQueryException(string reference, string stream)
399413
{
400414
if (string.IsNullOrWhiteSpace(reference) || string.IsNullOrWhiteSpace(stream))

Tests/SystemTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,9 @@ public async void CreateDeleteSuperStream()
375375
Assert.Equal("1000", spec.Args["stream-max-segment-size-bytes"]);
376376
Assert.Equal("20000", spec.Args["max-length-bytes"]);
377377
await system.CreateSuperStream(spec);
378+
Assert.True(await system.SuperStreamExists(SuperStream));
378379
await system.DeleteSuperStream(SuperStream);
380+
Assert.False(await system.SuperStreamExists(SuperStream));
379381
await system.Close();
380382
}
381383

Tests/Utils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ public static async Task ResetSuperStreams()
440440
}
441441

442442
Wait();
443-
var spec = new PartitionsSuperStreamSpec(InvoicesExchange, 3);
443+
var spec = new PartitionsSuperStreamSpec(InvoicesExchange);
444444
await system.CreateSuperStream(spec);
445445
await system.Close();
446446
}

docs/ReliableClient/RClient.cs renamed to docs/ReliableClient/BestPracticesClient.cs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414

1515
namespace ReliableClient;
1616

17-
public class RClient
17+
public class BestPracticesClient
1818
{
1919
public record Config
2020
{
21-
public string Host { get; set; } = "localhost";
21+
public string? Host { get; set; } = "localhost";
2222
public int Port { get; set; } = 5552;
23-
public string Username { get; set; } = "guest";
24-
public string Password { get; set; } = "guest";
23+
public string? Username { get; set; } = "guest";
24+
public string? Password { get; set; } = "guest";
25+
26+
public string? StreamName { get; set; } = "DotNetClientTest";
2527
public bool LoadBalancer { get; set; } = false;
2628
public bool SuperStream { get; set; } = false;
2729
public int Streams { get; set; } = 1;
@@ -30,6 +32,10 @@ public record Config
3032
public int MessagesPerProducer { get; set; } = 5_000_000;
3133
public int Consumers { get; set; } = 9;
3234
public byte ConsumersPerConnection { get; set; } = 8;
35+
36+
public int DelayDuringSendMs { get; set; } = 0;
37+
38+
3339
}
3440

3541
public static async Task Start(Config config)
@@ -61,12 +67,18 @@ public static async Task Start(Config config)
6167
switch (Uri.CheckHostName(config.Host))
6268
{
6369
case UriHostNameType.IPv4:
64-
ep = new IPEndPoint(IPAddress.Parse(config.Host), config.Port);
70+
if (config.Host != null) ep = new IPEndPoint(IPAddress.Parse(config.Host), config.Port);
6571
break;
6672
case UriHostNameType.Dns:
67-
var addresses = await Dns.GetHostAddressesAsync(config.Host).ConfigureAwait(false);
68-
ep = new IPEndPoint(addresses[0], config.Port);
73+
if (config.Host != null)
74+
{
75+
var addresses = await Dns.GetHostAddressesAsync(config.Host).ConfigureAwait(false);
76+
ep = new IPEndPoint(addresses[0], config.Port);
77+
}
78+
6979
break;
80+
default:
81+
throw new ArgumentOutOfRangeException();
7082
}
7183
}
7284

@@ -105,13 +117,13 @@ public static async Task Start(Config config)
105117
var streamsList = new List<string>();
106118
if (config.SuperStream)
107119
{
108-
streamsList.Add("invoices");
120+
if (config.StreamName != null) streamsList.Add(config.StreamName);
109121
}
110122
else
111123
{
112124
for (var i = 0; i < config.Streams; i++)
113125
{
114-
streamsList.Add($"invoices-{i}");
126+
streamsList.Add($"{config.StreamName}-{i}");
115127
}
116128
}
117129

@@ -141,6 +153,18 @@ public static async Task Start(Config config)
141153
List<Consumer> consumersList = new();
142154
List<Producer> producersList = new();
143155
var obj = new object();
156+
if (config.SuperStream)
157+
{
158+
if (await system.SuperStreamExists(streamsList[0]).ConfigureAwait(false))
159+
{
160+
await system.DeleteSuperStream(streamsList[0]).ConfigureAwait(false);
161+
}
162+
163+
164+
await system.CreateSuperStream(new PartitionsSuperStreamSpec(streamsList[0], config.Streams)).ConfigureAwait(false);
165+
}
166+
167+
144168
foreach (var stream in streamsList)
145169
{
146170
if (!config.SuperStream)
@@ -153,7 +177,7 @@ public static async Task Start(Config config)
153177
await system.CreateStream(new StreamSpec(stream) {MaxLengthBytes = 30_000_000_000,})
154178
.ConfigureAwait(false);
155179
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
156-
}
180+
}
157181

158182
for (var z = 0; z < config.Consumers; z++)
159183
{
@@ -162,15 +186,24 @@ await system.CreateStream(new StreamSpec(stream) {MaxLengthBytes = 30_000_000_00
162186
OffsetSpec = new OffsetTypeLast(),
163187
IsSuperStream = config.SuperStream,
164188
IsSingleActiveConsumer = config.SuperStream,
165-
Reference = "myApp",
166-
Identifier = $"my_c_{z}",
189+
Reference = "myApp",// needed for the Single Active Consumer or fot the store offset
190+
// can help to identify the consumer on the logs and RabbitMQ Management
191+
Identifier = $"my_consumer_{z}",
167192
InitialCredits = 10,
168-
MessageHandler = (source, ctx, _, _) =>
193+
MessageHandler = async (source, consumer, ctx, _) =>
169194
{
195+
if (totalConsumed % 10_000 == 0)
196+
{
197+
// don't store the offset every time, it could be a performance issue
198+
// store the offset every 1_000/5_000/10_000 messages
199+
await consumer.StoreOffset(ctx.Offset).ConfigureAwait(false);
200+
}
170201
Interlocked.Increment(ref totalConsumed);
171-
return Task.CompletedTask;
172202
},
173203
};
204+
205+
// This is the callback that will be called when the consumer status changes
206+
// DON'T PUT ANY BLOCKING CODE HERE
174207
conf.StatusChanged += (status) =>
175208
{
176209
var streamInfo = status.Partition is not null
@@ -190,27 +223,33 @@ async Task MaybeSend(Producer producer, Message message, ManualResetEvent publis
190223
await producer.Send(message).ConfigureAwait(false);
191224
}
192225

226+
// this example is meant to show how to use the producer and consumer
227+
// Create too many tasks for the producers and consumers is not a good idea
193228
for (var z = 0; z < config.Producers; z++)
194229
{
195230
var z1 = z;
196231
_ = Task.Run(async () =>
197232
{
233+
// the list of unconfirmed messages in case of error or disconnection
234+
// This example is only for the example, in a real scenario you should handle the unconfirmed messages
235+
// since the list could grow event the publishEvent should avoid it.
198236
var unconfirmedMessages = new ConcurrentBag<Message>();
237+
// the event to wait for the producer to be ready to send
238+
// in case of disconnection the event will be reset
199239
var publishEvent = new ManualResetEvent(false);
200-
201240
var producerConfig = new ProducerConfig(system, stream)
202241
{
203-
Identifier = $"my_super_{z1}",
242+
Identifier = $"my_producer_{z1}",
204243
SuperStreamConfig = new SuperStreamConfig()
205244
{
206245
Enabled = config.SuperStream, Routing = msg => msg.Properties.MessageId.ToString(),
207246
},
208247
ConfirmationHandler = confirmation =>
209248
{
249+
// Add the unconfirmed messages to the list in case of error
210250
if (confirmation.Status != ConfirmationStatus.Confirmed)
211251
{
212252
confirmation.Messages.ForEach(m => { unconfirmedMessages.Add(m); });
213-
214253
Interlocked.Add(ref totalError, confirmation.Messages.Count);
215254
return Task.CompletedTask;
216255
}
@@ -219,15 +258,21 @@ async Task MaybeSend(Producer producer, Message message, ManualResetEvent publis
219258
return Task.CompletedTask;
220259
},
221260
};
261+
262+
// Like the consumer don't put any blocking code here
222263
producerConfig.StatusChanged += (status) =>
223264
{
224265
var streamInfo = status.Partition is not null
225266
? $" Partition {status.Partition} of super stream: {status.Stream}"
226267
: $"Stream: {status.Stream}";
227268

269+
// just log the status change
228270
lp.LogInformation("Consumer: {Id} - status changed from: {From} to: {To} reason: {Reason} {Info}",
229271
status.Identifier, status.From, status.To,status.Reason, streamInfo);
230272

273+
// in case of disconnection the event will be reset
274+
// in case of reconnection the event will be set so the producer can send messages
275+
// It is important to use the ManualReset to avoid to send messages before the producer is ready
231276
if (status.To == ReliableEntityStatus.Open)
232277
{
233278
publishEvent.Set();
@@ -247,6 +292,7 @@ async Task MaybeSend(Producer producer, Message message, ManualResetEvent publis
247292
{
248293
if (!unconfirmedMessages.IsEmpty)
249294
{
295+
// checks if there are unconfirmed messages and send them
250296
var msgs = unconfirmedMessages.ToArray();
251297
unconfirmedMessages.Clear();
252298
foreach (var msg in msgs)
@@ -261,7 +307,8 @@ async Task MaybeSend(Producer producer, Message message, ManualResetEvent publis
261307
Properties = new Properties() {MessageId = $"hello{i}"}
262308
};
263309
await MaybeSend(producer, message, publishEvent).ConfigureAwait(false);
264-
// await Task.Delay(1).ConfigureAwait(false);
310+
// You don't need this it is only for the example
311+
await Task.Delay(config.DelayDuringSendMs).ConfigureAwait(false);
265312
Interlocked.Increment(ref totalSent);
266313
}
267314
});

0 commit comments

Comments
 (0)