< Summary

Information
Class: dotnet_etcd.Watcher
Assembly: dotnet-etcd
File(s): /home/runner/work/dotnet-etcd/dotnet-etcd/dotnet-etcd/watchclient/WatchStream.cs
Line coverage
89%
Covered lines: 50
Uncovered lines: 6
Coverable lines: 56
Total lines: 128
Line coverage: 89.2%
Branch coverage
66%
Covered branches: 8
Total branches: 12
Branch coverage: 66.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
CreateWatchAsync()100%11100%
CancelWatchAsync()100%11100%
ProcessWatchResponses()60%111081.25%
Dispose()100%11100%

File(s)

/home/runner/work/dotnet-etcd/dotnet-etcd/dotnet-etcd/watchclient/WatchStream.cs

#LineLine coverage
 1#nullable enable
 2using System;
 3using System.Collections.Concurrent;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using dotnet_etcd.interfaces;
 7using Etcdserverpb;
 8using Grpc.Core;
 9
 10namespace dotnet_etcd;
 11
 12/// <summary>
 13///     Manages a bidirectional streaming connection to the etcd watch API
 14/// </summary>
 15public class Watcher : IWatcher
 16{
 6117    private readonly ConcurrentDictionary<long, Action<WatchResponse>> _callbacks = new();
 6118    private readonly CancellationTokenSource _cts = new();
 19
 20    private readonly IAsyncDuplexStreamingCall<WatchRequest, WatchResponse> _streamingCall;
 21    private readonly Action? _onConnectionFailure;
 22
 23
 24    /// <summary>
 25    ///     Creates a new Watcher
 26    /// </summary>
 27    /// <param name="streamingCall">The streaming call to use</param>
 28    /// <param name="onConnectionFailure">Action to invoke when connection fails</param>
 6129    public Watcher(IAsyncDuplexStreamingCall<WatchRequest, WatchResponse> streamingCall, Action? onConnectionFailure = n
 6130    {
 6131        _streamingCall = streamingCall ?? throw new ArgumentNullException(nameof(streamingCall));
 6032        _onConnectionFailure = onConnectionFailure;
 6033        _ = ProcessWatchResponses();
 6034    }
 35
 36
 37    /// <summary>
 38    ///     Creates a watch for the specified request
 39    /// </summary>
 40    /// <param name="request">The watch request</param>
 41    /// <param name="callback">The callback to invoke when a watch event is received</param>
 42    /// <returns>A task that completes when the watch is created</returns>
 43    public async Task CreateWatchAsync(WatchRequest request, Action<WatchResponse> callback)
 7144    {
 7145        ArgumentNullException.ThrowIfNull(request);
 46
 7047        ArgumentNullException.ThrowIfNull(callback);
 48
 6949        _callbacks[request.CreateRequest.WatchId] = callback;
 50
 51        // Send the watch request
 6952        await _streamingCall.RequestStream.WriteAsync(request);
 6953    }
 54
 55    /// <summary>
 56    ///     Cancels a watch with the specified ID
 57    /// </summary>
 58    /// <param name="watchId">The ID of the watch to cancel</param>
 59    /// <returns>A task that completes when the watch is canceled</returns>
 60    public async Task CancelWatchAsync(long watchId)
 261    {
 62        // Send a cancel request
 263        WatchRequest request = new() { CancelRequest = new WatchCancelRequest { WatchId = watchId } };
 64
 265        await _streamingCall.RequestStream.WriteAsync(request);
 66
 67        // Remove the callback
 268        _callbacks.TryRemove(watchId, out _);
 269    }
 70
 71    private async Task ProcessWatchResponses()
 6072    {
 73        try
 6074        {
 14075            while (await _streamingCall.ResponseStream.MoveNext(_cts.Token))
 8076            {
 8077                WatchResponse response = _streamingCall.ResponseStream.Current;
 8078                if (!_callbacks.TryGetValue(response.WatchId, out Action<WatchResponse>? cb))
 379                {
 380                    continue;
 81                }
 82
 7783                cb(response);
 84
 85                // If the watch was canceled, remove the callback after invoking it
 7786                if (response.Canceled)
 287                {
 288                    _callbacks.TryRemove(response.WatchId, out _);
 289                }
 7790            }
 091        }
 692        catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
 193        {
 94            // This is expected when the stream is canceled
 195        }
 2896        catch (OperationCanceledException)
 2897        {
 98            // This is expected when the token is canceled
 2899        }
 5100        catch (RpcException ex)
 5101        {
 102            // Log a simplified message for expected connection failures
 5103            Console.WriteLine($"Watch stream connection lost: {ex.StatusCode} - {ex.Message}");
 5104            _onConnectionFailure?.Invoke();
 5105        }
 0106        catch (Exception ex)
 0107        {
 108            // Log the exception
 0109            await Console.Error.WriteAsync($"Error processing watch responses: {ex}");
 0110            _onConnectionFailure?.Invoke();
 111#if DEBUG
 112            // Only re-throw in debug mode to help with debugging
 0113            throw;
 114#endif
 115        }
 34116    }
 117
 118    /// <summary>
 119    ///     Disposes the watch stream
 120    /// </summary>
 121    public void Dispose()
 30122    {
 30123        _cts.Cancel();
 30124        _streamingCall.Dispose();
 125
 30126        GC.SuppressFinalize(this);
 30127    }
 128}