diff --git a/README.md b/README.md index 85a6977..e8071ce 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A wrapper for Serilog sinks that asynchronously emits events in batches, useful when logging to a slow and/or remote target. +> [!IMPORTANT] +> Serilog 4.x and later versions support batching natively. New projects should use Serilog's `IBatchedLogEventSink` and +> `WriteTo.Sink(IBatchedLogEventSink)`, not this package which is now only maintained for compatibility reasons. + ### Getting started Sinks that, for performance reasons, need to emit events in batches, can be implemented using `PeriodicBatchingSink` diff --git a/appveyor.yml b/appveyor.yml index 45a9f4f..d78a295 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,7 @@ build_script: test: off artifacts: - path: artifacts/Serilog.*.nupkg +- path: artifacts/Serilog.*.snupkg deploy: - provider: NuGet api_key: @@ -16,7 +17,9 @@ deploy: - provider: GitHub auth_token: secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifact: /Serilog.*\.nupkg/ + artifact: + /Serilog.*\.nupkg/ + /Serilog.*\.snupkg/ tag: v$(appveyor_build_version) on: branch: main diff --git a/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj b/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj index 2371489..6142513 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj +++ b/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj @@ -2,10 +2,15 @@ Buffer batches of log events to be flushed asynchronously. - 4.1.1 + 5.0.0 Serilog Contributors - net462 - $(TargetFrameworks);netstandard2.0;net6.0 + Serilog Contributors + + net471;net462 + + $(TargetFrameworks);net8.0;net6.0;netstandard2.0 true Serilog serilog;batching;timer @@ -16,18 +21,18 @@ git enable README.md + true + true + snupkg - + $(DefineConstants);FEATURE_ASYNCDISPOSABLE - - - - - + + diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/FailureAwareBatchScheduler.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/FailureAwareBatchScheduler.cs deleted file mode 100644 index 8b6366f..0000000 --- a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/FailureAwareBatchScheduler.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright © Serilog Contributors -// -// Licensed 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. - -namespace Serilog.Sinks.PeriodicBatching; - -/// -/// Manages reconnection period and transient fault response for . -/// During normal operation an object of this type will simply echo the configured batch transmission -/// period. When availability fluctuates, the class tracks the number of failed attempts, each time -/// increasing the interval before reconnection is attempted (up to a set maximum) and at predefined -/// points indicating that either the current batch, or entire waiting queue, should be dropped. This -/// Serves two purposes - first, a loaded receiver may need a temporary reduction in traffic while coming -/// back online. Second, the sender needs to account for both bad batches (the first fault response) and -/// also overproduction (the second, queue-dropping response). In combination these should provide a -/// reasonable delivery effort but ultimately protect the sender from memory exhaustion. -/// -class FailureAwareBatchScheduler -{ - static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5); - static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10); - - const int FailuresBeforeDroppingBatch = 8; - const int FailuresBeforeDroppingQueue = 10; - - readonly TimeSpan _period; - - int _failuresSinceSuccessfulBatch; - - public FailureAwareBatchScheduler(TimeSpan period) - { - if (period < TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(period), "The batching period must be a positive timespan."); - - _period = period; - } - - public void MarkSuccess() - { - _failuresSinceSuccessfulBatch = 0; - } - - public void MarkFailure() - { - ++_failuresSinceSuccessfulBatch; - } - - public TimeSpan NextInterval - { - get - { - // Available, and first failure, just try the batch interval - if (_failuresSinceSuccessfulBatch <= 1) return _period; - - // Second failure, start ramping up the interval - first 2x, then 4x, ... - var backoffFactor = Math.Pow(2, _failuresSinceSuccessfulBatch - 1); - - // If the period is ridiculously short, give it a boost so we get some - // visible backoff. - var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); - - // The "ideal" interval - var backedOff = (long) (backoffPeriod * backoffFactor); - - // Capped to the maximum interval - var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff); - - // Unless that's shorter than the period, in which case we'll just apply the period - var actual = Math.Max(_period.Ticks, cappedBackoff); - - return TimeSpan.FromTicks(actual); - } - } - - public bool ShouldDropBatch => _failuresSinceSuccessfulBatch >= FailuresBeforeDroppingBatch; - - public bool ShouldDropQueue => _failuresSinceSuccessfulBatch >= FailuresBeforeDroppingQueue; -} \ No newline at end of file diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/LegacyBatchedSinkAdapter.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/LegacyBatchedSinkAdapter.cs new file mode 100644 index 0000000..a114fbd --- /dev/null +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/LegacyBatchedSinkAdapter.cs @@ -0,0 +1,53 @@ +using Serilog.Events; + +namespace Serilog.Sinks.PeriodicBatching; + +sealed class LegacyBatchedSinkAdapter: Core.IBatchedLogEventSink, IDisposable +#if FEATURE_ASYNCDISPOSABLE + , IAsyncDisposable +#endif +{ + readonly IBatchedLogEventSink _inner; + readonly bool _dispose; + + public LegacyBatchedSinkAdapter(IBatchedLogEventSink inner, bool dispose) + { + _inner = inner; + _dispose = dispose; + } + + public Task EmitBatchAsync(IReadOnlyCollection batch) + { + return _inner.EmitBatchAsync(batch); + } + + public Task OnEmptyBatchAsync() + { + return _inner.OnEmptyBatchAsync(); + } + + public void Dispose() + { + if (!_dispose) + return; + + (_inner as IDisposable)?.Dispose(); + } + +#if FEATURE_ASYNCDISPOSABLE + public async ValueTask DisposeAsync() + { + if (!_dispose) + return; + + if (_inner is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + Dispose(); + } + } +#endif +} diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs index fc04155..6ef723e 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Threading.Channels; +using Serilog.Configuration; using Serilog.Core; -using Serilog.Debugging; using Serilog.Events; // ReSharper disable UnusedParameter.Global, ConvertIfStatementToConditionalTernaryExpression, MemberCanBePrivate.Global, UnusedMember.Global, VirtualMemberNeverOverridden.Global, ClassWithVirtualMembersNeverInherited.Global, SuspiciousTypeConversion.Global @@ -29,27 +28,8 @@ public class PeriodicBatchingSink : ILogEventSink, IDisposable, IBatchedLogEvent , IAsyncDisposable #endif { - // Buffers events from the write- to the read side. - readonly Channel _queue; - - // These fields are used by the write side to signal shutdown. - // A mutex is required because the queue writer `Complete()` call is not idempotent and will throw if - // called multiple times, e.g. via multiple `Dispose()` calls on this sink. - readonly object _stateLock = new(); - // Needed because the read loop needs to observe shutdown even when the target batched (remote) sink is - // unable to accept events (preventing the queue from being drained and completion being observed). - readonly CancellationTokenSource _shutdownSignal = new(); - // The write side can wait on this to ensure shutdown has completed. - readonly Task _runLoop; - - // Used only by the read side. - readonly IBatchedLogEventSink _targetSink = null!; - readonly int _batchSizeLimit; - readonly bool _eagerlyEmitFirstEvent; - readonly FailureAwareBatchScheduler _batchScheduler; - readonly Queue _currentBatch = new(); - readonly Task _waitForShutdownSignal; - Task? _cachedWaitToRead; + readonly ILogEventSink _targetSink; + readonly bool _inheritanceApi; /// /// Constant used with legacy constructor to indicate that the internal queue shouldn't be limited. @@ -57,6 +37,30 @@ public class PeriodicBatchingSink : ILogEventSink, IDisposable, IBatchedLogEvent [Obsolete("Implement `IBatchedLogEventSink` and use the `PeriodicBatchingSinkOptions` constructor.")] public const int NoQueueLimit = -1; + static ILogEventSink CreateSink( + IBatchedLogEventSink batchedLogEventSink, + bool disposeBatchedSink, + PeriodicBatchingSinkOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (options.BatchSizeLimit <= 0) + throw new ArgumentOutOfRangeException(nameof(options), "The batch size limit must be greater than zero."); + if (options.Period <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(options), "The period must be greater than zero."); + + var adaptedOptions = new BatchingOptions + { + BatchSizeLimit = options.BatchSizeLimit, + QueueLimit = options.QueueLimit, + BufferingTimeLimit = options.Period, + EagerlyEmitFirstEvent = options.EagerlyEmitFirstEvent + }; + + var adapter = new LegacyBatchedSinkAdapter(batchedLogEventSink, disposeBatchedSink); + + return LoggerSinkConfiguration.CreateSink(lc => lc.Sink(adapter, adaptedOptions)); + } + /// /// Construct a . /// @@ -65,9 +69,8 @@ public class PeriodicBatchingSink : ILogEventSink, IDisposable, IBatchedLogEvent /// it will dispose this object if possible. /// Options controlling behavior of the sink. public PeriodicBatchingSink(IBatchedLogEventSink batchedSink, PeriodicBatchingSinkOptions options) - : this(options) { - _targetSink = batchedSink ?? throw new ArgumentNullException(nameof(batchedSink)); + _targetSink = CreateSink(batchedSink, true, options); } /// @@ -80,15 +83,15 @@ public PeriodicBatchingSink(IBatchedLogEventSink batchedSink, PeriodicBatchingSi /// The time to wait between checking for event batches. [Obsolete("Implement `IBatchedLogEventSink` and use the `PeriodicBatchingSinkOptions` constructor.")] protected PeriodicBatchingSink(int batchSizeLimit, TimeSpan period) - : this(new PeriodicBatchingSinkOptions + { + _inheritanceApi = true; + _targetSink = CreateSink(this, false, new PeriodicBatchingSinkOptions { BatchSizeLimit = batchSizeLimit, Period = period, EagerlyEmitFirstEvent = true, QueueLimit = null - }) - { - _targetSink = this; + }); } /// @@ -102,39 +105,15 @@ protected PeriodicBatchingSink(int batchSizeLimit, TimeSpan period) /// Maximum number of events in the queue - use for an unbounded queue. [Obsolete("Implement `IBatchedLogEventSink` and use the `PeriodicBatchingSinkOptions` constructor.")] protected PeriodicBatchingSink(int batchSizeLimit, TimeSpan period, int queueLimit) - : this(new PeriodicBatchingSinkOptions + { + _inheritanceApi = true; + _targetSink = CreateSink(this, false, new PeriodicBatchingSinkOptions { BatchSizeLimit = batchSizeLimit, Period = period, EagerlyEmitFirstEvent = true, QueueLimit = queueLimit == NoQueueLimit ? null : queueLimit - }) - { - _targetSink = this; - } - - PeriodicBatchingSink(PeriodicBatchingSinkOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - if (options.BatchSizeLimit <= 0) - throw new ArgumentOutOfRangeException(nameof(options), "The batch size limit must be greater than zero."); - if (options.Period <= TimeSpan.Zero) - throw new ArgumentOutOfRangeException(nameof(options), "The period must be greater than zero."); - - _batchSizeLimit = options.BatchSizeLimit; - _queue = options.QueueLimit is { } limit - ? Channel.CreateBounded(new BoundedChannelOptions(limit) { SingleReader = true }) - : Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); - _batchScheduler = new FailureAwareBatchScheduler(options.Period); - _eagerlyEmitFirstEvent = options.EagerlyEmitFirstEvent; - _waitForShutdownSignal = Task.Delay(Timeout.InfiniteTimeSpan, _shutdownSignal.Token) - .ContinueWith(e => e.Exception, TaskContinuationOptions.OnlyOnFaulted); - - // The conditional here is no longer required in .NET 8+ (dotnet/runtime#82912) - using (ExecutionContext.IsFlowSuppressed() ? (IDisposable?)null : ExecutionContext.SuppressFlow()) - { - _runLoop = Task.Run(LoopAsync); - } + }); } /// @@ -151,142 +130,8 @@ protected PeriodicBatchingSink(int batchSizeLimit, TimeSpan period, int queueLim public void Emit(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - if (_shutdownSignal.IsCancellationRequested) - return; - - _queue.Writer.TryWrite(logEvent); - } - - async Task LoopAsync() - { - var isEagerBatch = _eagerlyEmitFirstEvent; - do - { - // Code from here through to the `try` block is expected to be infallible. It's structured this way because - // any failure modes within it haven't been accounted for in the rest of the sink design, and would need - // consideration in order for the sink to function robustly (i.e. to avoid hot/infinite looping). - - var fillBatch = Task.Delay(_batchScheduler.NextInterval); - do - { - while (_currentBatch.Count < _batchSizeLimit && - !_shutdownSignal.IsCancellationRequested && - _queue.Reader.TryRead(out var next) && - CanInclude(next)) - { - _currentBatch.Enqueue(next); - } - } while ((_currentBatch.Count < _batchSizeLimit && !isEagerBatch || _currentBatch.Count == 0) && - !_shutdownSignal.IsCancellationRequested && - await TryWaitToReadAsync(_queue.Reader, fillBatch, _shutdownSignal.Token).ConfigureAwait(false)); - - try - { - if (_currentBatch.Count == 0) - { - await _targetSink.OnEmptyBatchAsync().ConfigureAwait(false); - } - else - { - isEagerBatch = false; - - await _targetSink.EmitBatchAsync(_currentBatch).ConfigureAwait(false); - - _currentBatch.Clear(); - _batchScheduler.MarkSuccess(); - } - } - catch (Exception ex) - { - WriteToSelfLog("failed emitting a batch", ex); - _batchScheduler.MarkFailure(); - - if (_batchScheduler.ShouldDropBatch) - { - WriteToSelfLog("dropping the current batch"); - _currentBatch.Clear(); - } - - if (_batchScheduler.ShouldDropQueue) - { - WriteToSelfLog("dropping all queued events"); - - // Not ideal, uses some CPU capacity unnecessarily and doesn't complete in bounded time. The goal is - // to reduce memory pressure on the client if the server is offline for extended periods. May be - // worth reviewing and possibly abandoning this. - while (_queue.Reader.TryRead(out _) && !_shutdownSignal.IsCancellationRequested) { } - } - - // Wait out the remainder of the batch fill time so that we don't overwhelm the server. With each - // successive failure the interval will increase. Needs special handling so that we don't need to - // make `fillBatch` cancellable (and thus fallible). - await Task.WhenAny(fillBatch, _waitForShutdownSignal).ConfigureAwait(false); - } - } - while (!_shutdownSignal.IsCancellationRequested); - - // At this point: - // - The sink is being disposed - // - The queue has been completed - // - The queue may or may not be empty - // - The waiting batch may or may not be empty - // - The target sink may or may not be accepting events - - // Try flushing the rest of the queue, but bail out on any failure. Shutdown time is unbounded, but it - // doesn't make sense to pick an arbitrary limit - a future version might add a new option to control this. - try - { - while (_queue.Reader.TryPeek(out _)) - { - while (_currentBatch.Count < _batchSizeLimit && - _queue.Reader.TryRead(out var next)) - { - _currentBatch.Enqueue(next); - } - - if (_currentBatch.Count != 0) - { - await _targetSink.EmitBatchAsync(_currentBatch).ConfigureAwait(false); - _currentBatch.Clear(); - } - } - } - catch (Exception ex) - { - WriteToSelfLog("failed emitting a batch during shutdown; dropping remaining queued events", ex); - } - } - - // Wait until `reader` has items to read. Returns `false` if the `timeout` task completes, or if the reader is cancelled. - async Task TryWaitToReadAsync(ChannelReader reader, Task timeout, CancellationToken cancellationToken) - { - var waitToRead = _cachedWaitToRead ?? reader.WaitToReadAsync(cancellationToken).AsTask(); - _cachedWaitToRead = null; - - var completed = await Task.WhenAny(timeout, waitToRead).ConfigureAwait(false); - - // Avoid unobserved task exceptions in the cancellation and failure cases. Note that we may not end up observing - // read task cancellation exceptions during shutdown, may be some room to improve. - if (completed is { Exception: not null, IsCanceled: false }) - { - WriteToSelfLog($"could not read from queue: {completed.Exception}"); - } - - if (completed == timeout) - { - // Dropping references to `waitToRead` will cause it and some supporting objects to leak; disposing it - // will break the channel and cause future attempts to read to fail. So, we cache and reuse it next time - // around the loop. - - _cachedWaitToRead = waitToRead; - return false; - } - - if (waitToRead.Status is not TaskStatus.RanToCompletion) - return false; - - return await waitToRead; + if (!_inheritanceApi || CanInclude(logEvent)) + _targetSink.Emit(logEvent); } /// @@ -331,83 +176,21 @@ public void Dispose() protected virtual void Dispose(bool disposing) { if (!disposing) return; - - SignalShutdown(); - - try - { - _runLoop.Wait(); - } - catch (Exception ex) - { - // E.g. the task was canceled before ever being run, or internally failed and threw - // an unexpected exception. - WriteToSelfLog("caught exception during disposal", ex); - } - - if (ReferenceEquals(_targetSink, this)) - { - // The sink is being used in the obsolete inheritance-based mode. - return; - } - - (_targetSink as IDisposable)?.Dispose(); + ((IDisposable)_targetSink).Dispose(); + GC.SuppressFinalize(this); } #if FEATURE_ASYNCDISPOSABLE /// public async ValueTask DisposeAsync() { - SignalShutdown(); - - try - { - await _runLoop.ConfigureAwait(false); - } - catch (Exception ex) - { - // E.g. the task was canceled before ever being run, or internally failed and threw - // an unexpected exception. - WriteToSelfLog("caught exception during async disposal", ex); - } - - if (ReferenceEquals(_targetSink, this)) - { - // The sink is being used in the obsolete inheritance-based mode. Old sinks won't - // override something like `DisposeAsyncCore()`; we just forward to the synchronous - // `Dispose()` method to ensure whatever cleanup they do still occurs. + await ((IAsyncDisposable)_targetSink).DisposeAsync(); + if (_inheritanceApi) Dispose(true); - return; - } - - if (_targetSink is IAsyncDisposable asyncDisposable) - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - else - (_targetSink as IDisposable)?.Dispose(); GC.SuppressFinalize(this); } #endif - - void SignalShutdown() - { - lock (_stateLock) - { - if (!_shutdownSignal.IsCancellationRequested) - { - // Relies on synchronization via `_stateLock`: once the writer is completed, subsequent attempts to - // complete it will throw. - _queue.Writer.Complete(); - _shutdownSignal.Cancel(); - } - } - } - - void WriteToSelfLog(string message, Exception? exception = null) - { - var ex = exception != null ? $"{Environment.NewLine}{exception}" : ""; - SelfLog.WriteLine($"PeriodicBatchingSink ({_targetSink}): {message}{ex}"); - } /// /// Determine whether a queued log event should be included in the batch. If diff --git a/test/Serilog.Sinks.PeriodicBatching.Tests/FailureAwareBatchSchedulerTests.cs b/test/Serilog.Sinks.PeriodicBatching.Tests/FailureAwareBatchSchedulerTests.cs deleted file mode 100644 index 2ac40e8..0000000 --- a/test/Serilog.Sinks.PeriodicBatching.Tests/FailureAwareBatchSchedulerTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Globalization; -using Xunit; - -namespace Serilog.Sinks.PeriodicBatching.Tests; - -public class FailureAwareBatchSchedulerTests -{ - static TimeSpan Period => TimeSpan.FromSeconds(2); - FailureAwareBatchScheduler Scheduler { get; } = new(Period); - - [Fact] - public void WhenNoFailuresHaveOccurredTheInitialIntervalIsUsed() - { - Assert.Equal(Period, Scheduler.NextInterval); - } - - [Fact] - public void WhenOneFailureHasOccurredTheInitialIntervalIsUsed() - { - Scheduler.MarkFailure(); - Assert.Equal(Period, Scheduler.NextInterval); - } - - [Fact] - public void WhenTwoFailuresHaveOccurredTheIntervalBacksOff() - { - Scheduler.MarkFailure(); - Scheduler.MarkFailure(); - Assert.Equal(TimeSpan.FromSeconds(10), Scheduler.NextInterval); - } - - [Fact] - public void WhenABatchSucceedsTheStatusResets() - { - Scheduler.MarkFailure(); - Scheduler.MarkFailure(); - Scheduler.MarkSuccess(); - Assert.Equal(Period, Scheduler.NextInterval); - } - - [Fact] - public void WhenThreeFailuresHaveOccurredTheIntervalBacksOff() - { - Scheduler.MarkFailure(); - Scheduler.MarkFailure(); - Scheduler.MarkFailure(); - Assert.Equal(TimeSpan.FromSeconds(20), Scheduler.NextInterval); - Assert.False(Scheduler.ShouldDropBatch); - } - - [Fact] - public void WhenEightFailuresHaveOccurredTheIntervalBacksOffAndBatchIsDropped() - { - for (var i = 0; i < 8; ++i) - { - Assert.False(Scheduler.ShouldDropBatch); - Scheduler.MarkFailure(); - } - Assert.Equal(TimeSpan.FromMinutes(10), Scheduler.NextInterval); - Assert.True(Scheduler.ShouldDropBatch); - Assert.False(Scheduler.ShouldDropQueue); - } - - [Fact] - public void WhenTenFailuresHaveOccurredTheQueueIsDropped() - { - for (var i = 0; i < 10; ++i) - { - Assert.False(Scheduler.ShouldDropQueue); - Scheduler.MarkFailure(); - } - Assert.True(Scheduler.ShouldDropQueue); - } - - [Fact] - public void AtTheDefaultIntervalRetriesForTenMinutesBeforeDroppingBatch() - { - var cumulative = TimeSpan.Zero; - do - { - Scheduler.MarkFailure(); - - if (!Scheduler.ShouldDropBatch) - cumulative += Scheduler.NextInterval; - } while (!Scheduler.ShouldDropBatch); - - Assert.False(Scheduler.ShouldDropQueue); - Assert.Equal(TimeSpan.Parse("00:10:32", CultureInfo.InvariantCulture), cumulative); - } - - [Fact] - public void AtTheDefaultIntervalRetriesForThirtyMinutesBeforeDroppingQueue() - { - var cumulative = TimeSpan.Zero; - do - { - Scheduler.MarkFailure(); - - if (!Scheduler.ShouldDropQueue) - cumulative += Scheduler.NextInterval; - } while (!Scheduler.ShouldDropQueue); - - Assert.Equal(TimeSpan.Parse("00:30:32", CultureInfo.InvariantCulture), cumulative); - } -}