Skip to content

Commit 4dbb834

Browse files
feat: impl disposing Writer.cs (#279)
**Breaking Change**: `IWriter` now implements `IAsyncDisposable` instead of `IDisposable`. This change requires updates to code that disposes `IWriter` instances. Use `await using` instead of `using`.
1 parent e12f15a commit 4dbb834

File tree

11 files changed

+191
-43
lines changed

11 files changed

+191
-43
lines changed

.github/workflows/slo-topic.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
cd slo/src/TopicService
2929
dotnet run create grpc://localhost:2135 /Root/testdb
3030
- name: Run SLO Tests
31+
continue-on-error: true
3132
run: |
3233
cd slo/src/TopicService
3334
dotnet run run grpc://localhost:2135 /Root/testdb \

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
- Topic Reader & Writer: update auth token in bidirectional stream.
1+
- **Breaking Change**: `IWriter` now implements `IAsyncDisposable` instead of `IDisposable`.
2+
This change requires updates to code that disposes `IWriter` instances. Use `await using` instead of `using`.
3+
- Topic `Reader` & `Writer`: update auth token in bidirectional stream.
24

35
## v0.14.1
46
- Fixed bug: public key presented not for certificate signature.

slo/src/TopicService/SloTopicContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public async Task Run(RunConfig config)
7373
{
7474
try
7575
{
76-
using var writer = new WriterBuilder<string>(driver, PathTopic)
76+
await using var writer = new WriterBuilder<string>(driver, PathTopic)
7777
{
7878
BufferMaxSize = 8 * 1024 * 1024,
7979
ProducerId = "producer-" + partitionId,

src/Ydb.Sdk/src/IDriver.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public IBidirectionalStream<TRequest, TResponse> BidirectionalStreamCall<TReques
3030
ILoggerFactory LoggerFactory { get; }
3131
}
3232

33-
public interface IBidirectionalStream<in TRequest, out TResponse> : IDisposable
33+
public interface IBidirectionalStream<in TRequest, out TResponse> : IAsyncDisposable
3434
{
3535
public Task Write(TRequest request);
3636

@@ -225,6 +225,7 @@ internal class BidirectionalStream<TRequest, TResponse> : IBidirectionalStream<T
225225
private readonly AsyncDuplexStreamingCall<TRequest, TResponse> _stream;
226226
private readonly Action<RpcException> _rpcErrorAction;
227227
private readonly ICredentialsProvider _credentialsProvider;
228+
private readonly TaskCompletionSource _closedResponseStreamTcs = new();
228229

229230
internal BidirectionalStream(
230231
AsyncDuplexStreamingCall<TRequest, TResponse> stream,
@@ -258,6 +259,7 @@ public async ValueTask<bool> MoveNextAsync()
258259
}
259260
catch (RpcException e)
260261
{
262+
_closedResponseStreamTcs.SetResult();
261263
_rpcErrorAction(e);
262264

263265
throw new Driver.TransportException(e);
@@ -268,8 +270,17 @@ public async ValueTask<bool> MoveNextAsync()
268270

269271
public string? AuthToken => _credentialsProvider.GetAuthInfo();
270272

271-
public void Dispose()
273+
public async ValueTask DisposeAsync()
272274
{
275+
try
276+
{
277+
await _stream.RequestStream.CompleteAsync();
278+
}
279+
catch (RpcException e)
280+
{
281+
_rpcErrorAction(e);
282+
}
283+
273284
_stream.Dispose();
274285
}
275286
}

src/Ydb.Sdk/src/Services/Topic/IWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Ydb.Sdk.Services.Topic;
44

5-
public interface IWriter<TValue> : IDisposable
5+
public interface IWriter<TValue> : IAsyncDisposable
66
{
77
/// <summary>
88
/// Asynchronously send a data to a YDB Topic.

src/Ydb.Sdk/src/Services/Topic/TopicSession.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Ydb.Sdk.Services.Topic;
44

5-
internal abstract class TopicSession<TFromClient, TFromServer> : IDisposable
5+
internal abstract class TopicSession<TFromClient, TFromServer> : IAsyncDisposable
66
{
77
private readonly Func<Task> _initialize;
88

@@ -58,10 +58,12 @@ protected async Task SendMessage(TFromClient fromClient)
5858
await Stream.Write(fromClient);
5959
}
6060

61-
public void Dispose()
61+
protected abstract TFromClient GetSendUpdateTokenRequest(string token);
62+
63+
public ValueTask DisposeAsync()
6264
{
63-
Stream.Dispose();
64-
}
65+
Logger.LogInformation("TopicSession[{SessionId}] is being deleted", SessionId);
6566

66-
protected abstract TFromClient GetSendUpdateTokenRequest(string token);
67+
return Stream.DisposeAsync();
68+
}
6769
}

src/Ydb.Sdk/src/Services/Topic/Writer/Writer.cs

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ internal class Writer<TValue> : IWriter<TValue>
2121
private readonly WriterConfig _config;
2222
private readonly ILogger<Writer<TValue>> _logger;
2323
private readonly ISerializer<TValue> _serializer;
24-
private readonly GrpcRequestSettings _writerGrpcRequestSettings;
24+
private readonly GrpcRequestSettings _writerGrpcRequestSettings = new();
2525
private readonly ConcurrentQueue<MessageSending> _toSendBuffer = new();
2626
private readonly ConcurrentQueue<MessageSending> _inFlightMessages = new();
2727
private readonly CancellationTokenSource _disposeCts = new();
28-
private readonly SemaphoreSlim _clearInFlightMessagesSemaphoreSlim = new(1);
28+
private readonly SemaphoreSlim _sendInFlightMessagesSemaphoreSlim = new(1);
2929

3030
private volatile TaskCompletionSource _tcsWakeUp = new();
3131
private volatile TaskCompletionSource _tcsBufferAvailableEvent = new();
@@ -39,7 +39,6 @@ internal Writer(IDriver driver, WriterConfig config, ISerializer<TValue> seriali
3939
_logger = driver.LoggerFactory.CreateLogger<Writer<TValue>>();
4040
_serializer = serializer;
4141
_limitBufferMaxSize = config.BufferMaxSize;
42-
_writerGrpcRequestSettings = new GrpcRequestSettings { CancellationToken = _disposeCts.Token };
4342

4443
StartWriteWorker();
4544
}
@@ -95,7 +94,9 @@ public async Task<WriteResult> WriteAsync(Message<TValue> message, CancellationT
9594
if (Interlocked.CompareExchange(ref _limitBufferMaxSize,
9695
curLimitBufferSize - data.Length, curLimitBufferSize) == curLimitBufferSize)
9796
{
98-
_toSendBuffer.Enqueue(new MessageSending(messageData, tcs));
97+
_toSendBuffer.Enqueue(
98+
new MessageSending(messageData, tcs, writerDisposedCancellationTokenRegistration)
99+
);
99100
WakeUpWorker();
100101

101102
break;
@@ -162,7 +163,7 @@ private async void StartWriteWorker()
162163
continue;
163164
}
164165

165-
await _clearInFlightMessagesSemaphoreSlim.WaitAsync(_disposeCts.Token);
166+
await _sendInFlightMessagesSemaphoreSlim.WaitAsync(_disposeCts.Token);
166167
try
167168
{
168169
if (_session.IsActive)
@@ -172,7 +173,7 @@ private async void StartWriteWorker()
172173
}
173174
finally
174175
{
175-
_clearInFlightMessagesSemaphoreSlim.Release();
176+
_sendInFlightMessagesSemaphoreSlim.Release();
176177
}
177178
}
178179
}
@@ -193,7 +194,7 @@ private async Task Initialize()
193194

194195
try
195196
{
196-
if (_disposeCts.IsCancellationRequested)
197+
if (_disposeCts.IsCancellationRequested && _inFlightMessages.IsEmpty)
197198
{
198199
_logger.LogWarning("Initialize writer is canceled because it has been disposed");
199200

@@ -267,7 +268,7 @@ private async Task Initialize()
267268
return;
268269
}
269270

270-
await _clearInFlightMessagesSemaphoreSlim.WaitAsync(_disposeCts.Token);
271+
await _sendInFlightMessagesSemaphoreSlim.WaitAsync();
271272
try
272273
{
273274
var copyInFlightMessages = new ConcurrentQueue<MessageSending>();
@@ -315,7 +316,7 @@ private async Task Initialize()
315316
}
316317
finally
317318
{
318-
_clearInFlightMessagesSemaphoreSlim.Release();
319+
_sendInFlightMessagesSemaphoreSlim.Release();
319320
}
320321
}
321322
catch (Driver.TransportException e)
@@ -330,17 +331,55 @@ private async Task Initialize()
330331
}
331332
}
332333

333-
public void Dispose()
334+
public async ValueTask DisposeAsync()
334335
{
335-
_disposeCts.Cancel();
336+
if (_disposeCts.IsCancellationRequested)
337+
{
338+
return;
339+
}
340+
341+
_logger.LogInformation("Starting Writer[{WriterConfig}] disposal process", _config);
342+
343+
await _sendInFlightMessagesSemaphoreSlim.WaitAsync();
344+
try
345+
{
346+
_logger.LogDebug("Signaling cancellation token to stop writing new messages");
347+
348+
_disposeCts.Cancel();
349+
}
350+
finally
351+
{
352+
_sendInFlightMessagesSemaphoreSlim.Release();
353+
}
354+
355+
_logger.LogDebug("Writer[{WriterConfig}] is waiting for all in-flight messages to complete...", _config);
356+
357+
foreach (var inFlightMessage in _inFlightMessages)
358+
{
359+
try
360+
{
361+
await inFlightMessage.Tcs.Task;
362+
}
363+
catch (Exception e)
364+
{
365+
_logger.LogError(e, "Error occurred while waiting for in-flight message SeqNo: {SeqNo}",
366+
inFlightMessage.MessageData.SeqNo);
367+
}
368+
}
369+
370+
await _session.DisposeAsync();
336371

337-
_session = new NotStartedWriterSession("Writer is disposed");
372+
_logger.LogInformation("Writer[{WriterConfig}] is disposed", _config);
338373
}
339374
}
340375

341-
internal record MessageSending(MessageData MessageData, TaskCompletionSource<WriteResult> Tcs);
376+
internal record MessageSending(
377+
MessageData MessageData,
378+
TaskCompletionSource<WriteResult> Tcs,
379+
CancellationTokenRegistration DisposedCtr
380+
);
342381

343-
internal interface IWriteSession
382+
internal interface IWriteSession : IAsyncDisposable
344383
{
345384
Task Write(ConcurrentQueue<MessageSending> toSendBuffer);
346385

@@ -372,6 +411,11 @@ public Task Write(ConcurrentQueue<MessageSending> toSendBuffer)
372411
}
373412

374413
public bool IsActive => true;
414+
415+
public ValueTask DisposeAsync()
416+
{
417+
return ValueTask.CompletedTask;
418+
}
375419
}
376420

377421
internal class DummyWriterSession : IWriteSession
@@ -388,6 +432,11 @@ public Task Write(ConcurrentQueue<MessageSending> toSendBuffer)
388432
}
389433

390434
public bool IsActive => false;
435+
436+
public ValueTask DisposeAsync()
437+
{
438+
return ValueTask.CompletedTask;
439+
}
391440
}
392441

393442
internal class WriterSession : TopicSession<MessageFromClient, MessageFromServer>, IWriteSession
@@ -437,6 +486,8 @@ public async Task Write(ConcurrentQueue<MessageSending> toSendBuffer)
437486
continue;
438487
}
439488

489+
sendData.DisposedCtr.Unregister();
490+
440491
var messageData = sendData.MessageData;
441492

442493
if (messageData.SeqNo == default)

src/Ydb.Sdk/src/Services/Topic/Writer/WriterConfig.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public override string ToString()
3737
toString.Append(", ProducerId: ").Append(ProducerId);
3838
}
3939

40+
if (PartitionId != null)
41+
{
42+
toString.Append(", PartitionId: ").Append(PartitionId);
43+
}
44+
4045
return toString.Append(", Codec: ").Append(Codec).ToString();
4146
}
4247
}

src/Ydb.Sdk/tests/Topic/ReaderIntegrationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public async Task StressTest_WhenReadingThenCommiting_ReturnMessages()
2525
topicSettings.Consumers.Add(new Consumer("Consumer"));
2626
await topicClient.CreateTopic(topicSettings);
2727

28-
using var writer = new WriterBuilder<string>(_driver, _topicName)
28+
await using var writer = new WriterBuilder<string>(_driver, _topicName)
2929
{ ProducerId = "producerId" }.Build();
3030
var reader = new ReaderBuilder<string>(_driver)
3131
{

src/Ydb.Sdk/tests/Topic/WriterIntegrationTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task WriteAsync_WhenOneMessage_ReturnWritten()
3030
};
3131
await topicClient.CreateTopic(topicSettings);
3232

33-
using var writer = new WriterBuilder<string>(_driver, _topicName) { ProducerId = "producerId" }.Build();
33+
await using var writer = new WriterBuilder<string>(_driver, _topicName) { ProducerId = "producerId" }.Build();
3434

3535
var result = await writer.WriteAsync("abacaba");
3636

@@ -42,7 +42,7 @@ public async Task WriteAsync_WhenOneMessage_ReturnWritten()
4242
[Fact]
4343
public async Task WriteAsync_WhenTopicNotFound_ReturnNotFoundException()
4444
{
45-
using var writer = new WriterBuilder<string>(_driver, _topicName + "_not_found")
45+
await using var writer = new WriterBuilder<string>(_driver, _topicName + "_not_found")
4646
{ ProducerId = "producerId" }.Build();
4747

4848
Assert.Contains(
@@ -61,7 +61,7 @@ public async Task WriteAsync_When1000Messages_ReturnWriteResultIsPersisted()
6161
topicSettings.Consumers.Add(new Consumer("Consumer"));
6262
await topicClient.CreateTopic(topicSettings);
6363

64-
using var writer = new WriterBuilder<int>(_driver, topicName)
64+
await using var writer = new WriterBuilder<int>(_driver, topicName)
6565
{ ProducerId = "producerId" }.Build();
6666

6767
var tasks = new List<Task>();

0 commit comments

Comments
 (0)