diff --git a/.gitignore b/.gitignore index 703913b..c57a1e9 100644 --- a/.gitignore +++ b/.gitignore @@ -236,3 +236,6 @@ _Pvt_Extensions .fake/ BenchmarkDotNet.Artifacts/ +.idea +*.orig + diff --git a/README.md b/README.md index 592bce2..56bf721 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,53 @@ -# Serilog.Sinks.PeriodicBatching +# Serilog.Sinks.PeriodicBatching [![Build status](https://ci.appveyor.com/api/projects/status/w2agqyd8rn0jur9y?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-periodicbatching) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.periodicbatching.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.periodicbatching/) -A base for Serilog sinks that batch and asynchronously send events to a slow/remote target. +A wrapper for Serilog sinks that asynchronously emits events in batches, useful when logging to a slow and/or remote target. -[![Build status](https://ci.appveyor.com/api/projects/status/w2agqyd8rn0jur9y?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-periodicbatching) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.periodicbatching.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.periodicbatching/) +### Getting started -* [Documentation](https://github.com/serilog/serilog/wiki) +Sinks that, for performance reasons, need to emit events in batches, can be implemented using `PeriodicBatchingSink` +from this package. -Copyright © 2016 Serilog Contributors - Provided under the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html). +First, install the package into your Sink project: + +``` +dotnet add package Serilog.Sinks.PeriodicBatching +``` + +Then, instead of implementing Serilog's `ILogEventSink`, implement `IBatchedLogEventSink` in your sink class: + +```csharp +class ExampleBatchedSink : IBatchedLogEventSink +{ + public async Task EmitBatchAsync(IEnumerable batch) + { + foreach (var logEvent in batch) + Console.WriteLine(logEvent); + } + + public Task OnEmptyBatchAsync() { } +} +``` + +Finally, in your sink's configuration method, construct a `PeriodicBatchingSink` that wraps your batched sink: + +```csharp +public static class LoggerSinkExampleConfiguration +{ + public static LoggerConfiguration Example(this LoggerSinkConfiguration loggerSinkConfiguration) + { + var exampleSink = new ExampleBatchedSink(); + + var batchingOptions = new PeriodicBatchingSinkOptions + { + BatchSize = 100, + Period = TimeSpan.FromSeconds(2), + EagerlyEmitFirstEvent = true, + QueueSizeLimit = 10000 + }; + + var batchingSink = new PeriodicBatchingSink(exampleSink, batchingOptions); + + return loggerSinkConfiguration.Sink(batchingSink); + } +} +``` diff --git a/appveyor.yml b/appveyor.yml index 8bfe9e5..03eb001 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,6 @@ version: '{build}' skip_tags: true -image: Visual Studio 2017 -configuration: Release +image: Visual Studio 2019 install: - ps: mkdir -Force ".\build\" | Out-Null - ps: Invoke-WebRequest "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.ps1" -OutFile ".\build\installcli.ps1" diff --git a/serilog-sinks-periodicbatching.sln.DotSettings b/serilog-sinks-periodicbatching.sln.DotSettings new file mode 100644 index 0000000..f398bf5 --- /dev/null +++ b/serilog-sinks-periodicbatching.sln.DotSettings @@ -0,0 +1,6 @@ + + True + True + True + True + True \ No newline at end of file diff --git a/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj b/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj index 07c6c28..feeaf68 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj +++ b/src/Serilog.Sinks.PeriodicBatching/Serilog.Sinks.PeriodicBatching.csproj @@ -1,12 +1,13 @@ - + The periodic batching sink for Serilog - 2.2.0 + 2.3.0 Serilog Contributors net45;netstandard1.1;netstandard1.2;netstandard2.0 true Serilog.Sinks.PeriodicBatching + Serilog ../../assets/Serilog.snk true true diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BatchedConnectionStatus.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BatchedConnectionStatus.cs index 0327190..39d3769 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BatchedConnectionStatus.cs +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BatchedConnectionStatus.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2020 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ 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 availabilty fluctuates, the class tracks the number of failed attempts, each time + /// 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 diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BoundedConcurrentQueue.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BoundedConcurrentQueue.cs index be33450..94e9ad4 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BoundedConcurrentQueue.cs +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/BoundedConcurrentQueue.cs @@ -1,36 +1,45 @@ -using System; +// Copyright 2013-2020 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. + +using System; using System.Collections.Concurrent; using System.Threading; namespace Serilog.Sinks.PeriodicBatching { - class BoundedConcurrentQueue + class BoundedConcurrentQueue { - const int NON_BOUNDED = -1; + public const int Unbounded = -1; readonly ConcurrentQueue _queue = new ConcurrentQueue(); readonly int _queueLimit; int _counter; - public BoundedConcurrentQueue() - { - _queueLimit = NON_BOUNDED; - } - - public BoundedConcurrentQueue(int queueLimit) + public BoundedConcurrentQueue(int? queueLimit = null) { - if (queueLimit <= 0) - throw new ArgumentOutOfRangeException(nameof(queueLimit), "queue limit must be positive"); + if (queueLimit.HasValue && queueLimit <= 0) + throw new ArgumentOutOfRangeException(nameof(queueLimit), "Queue limit must be positive, or `null` to indicate unbounded."); - _queueLimit = queueLimit; + _queueLimit = queueLimit ?? Unbounded; } public int Count => _queue.Count; public bool TryDequeue(out T item) { - if (_queueLimit == NON_BOUNDED) + if (_queueLimit == Unbounded) return _queue.TryDequeue(out item); var result = false; @@ -50,7 +59,7 @@ public bool TryDequeue(out T item) public bool TryEnqueue(T item) { - if (_queueLimit == NON_BOUNDED) + if (_queueLimit == Unbounded) { _queue.Enqueue(item); return true; diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/IBatchedLogEventSink.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/IBatchedLogEventSink.cs new file mode 100644 index 0000000..27357ea --- /dev/null +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/IBatchedLogEventSink.cs @@ -0,0 +1,38 @@ +// Copyright 2013-2020 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. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Serilog.Events; + +namespace Serilog.Sinks.PeriodicBatching +{ + /// + /// Interface for targets that accept events in batches. + /// + public interface IBatchedLogEventSink + { + /// + /// Emit a batch of log events, running asynchronously. + /// + /// The batch of events to emit. + Task EmitBatchAsync(IEnumerable batch); + + /// + /// Allows sinks to perform periodic work without requiring additional threads + /// or timers (thus avoiding additional flush/shut-down complexity). + /// + Task OnEmptyBatchAsync(); + } +} diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs index b89f21b..40b032b 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSink.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2020 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -21,6 +20,8 @@ using Serilog.Events; using System.Threading; +// ReSharper disable MemberCanBePrivate.Global, UnusedMember.Global, VirtualMemberNeverOverridden.Global, ClassWithVirtualMembersNeverInherited.Global + namespace Serilog.Sinks.PeriodicBatching { /// @@ -33,9 +34,16 @@ namespace Serilog.Sinks.PeriodicBatching /// that want to change this behavior need to either implement from scratch, or /// embed retry logic in the batch emitting functions. /// - public abstract class PeriodicBatchingSink : ILogEventSink, IDisposable + public class PeriodicBatchingSink : ILogEventSink, IDisposable, IBatchedLogEventSink { + /// + /// Constant used to indicate that the internal queue shouldn't be limited. + /// + public const int NoQueueLimit = BoundedConcurrentQueue.Unbounded; + + readonly IBatchedLogEventSink _batchedLogEventSink; readonly int _batchSizeLimit; + readonly bool _eagerlyEmitFirstEvent; readonly BoundedConcurrentQueue _queue; readonly BatchedConnectionStatus _status; readonly Queue _waitingBatch = new Queue(); @@ -48,29 +56,73 @@ public abstract class PeriodicBatchingSink : ILogEventSink, IDisposable bool _started; /// - /// Construct a sink posting to the specified database. + /// Construct a . + /// + /// A to send log event batches to. Batches and empty + /// batch notifications will not be sent concurrently. When the is disposed, + /// it will dispose this object if possible. + /// Options controlling behavior of the sink. + public PeriodicBatchingSink(IBatchedLogEventSink batchedSink, PeriodicBatchingSinkOptions options) + : this(options) + { + _batchedLogEventSink = batchedSink ?? throw new ArgumentNullException(nameof(batchedSink)); + } + + /// + /// Construct a . New code should avoid subclassing + /// and use + /// + /// instead. /// /// The maximum number of events to include in a single batch. /// The time to wait between checking for event batches. protected PeriodicBatchingSink(int batchSizeLimit, TimeSpan period) + : this(new PeriodicBatchingSinkOptions + { + BatchSizeLimit = batchSizeLimit, + Period = period, + EagerlyEmitFirstEvent = true, + QueueLimit = null + }) { - _batchSizeLimit = batchSizeLimit; - _queue = new BoundedConcurrentQueue(); - _status = new BatchedConnectionStatus(period); - - _timer = new PortableTimer(cancel => OnTick()); + _batchedLogEventSink = this; } /// - /// Construct a sink posting to the specified database. + /// Construct a . New code should avoid subclassing + /// and use + /// + /// instead. /// /// The maximum number of events to include in a single batch. /// The time to wait between checking for event batches. - /// Maximum number of events in the queue. + /// Maximum number of events in the queue - use for an unbounded queue. protected PeriodicBatchingSink(int batchSizeLimit, TimeSpan period, int queueLimit) - : this(batchSizeLimit, period) + : this(new PeriodicBatchingSinkOptions + { + BatchSizeLimit = batchSizeLimit, + Period = period, + EagerlyEmitFirstEvent = true, + QueueLimit = queueLimit + }) { - _queue = new BoundedConcurrentQueue(queueLimit); + _batchedLogEventSink = 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 = new BoundedConcurrentQueue(options.QueueLimit); + _status = new BatchedConnectionStatus(options.Period); + _eagerlyEmitFirstEvent = options.EagerlyEmitFirstEvent; + _timer = new PortableTimer(cancel => OnTick()); } void CloseAndFlush() @@ -88,9 +140,12 @@ void CloseAndFlush() // This is the place where SynchronizationContext.Current is unknown and can be != null // so we prevent possible deadlocks here for sync-over-async downstream implementations ResetSyncContextAndWait(OnTick); + + if (_batchedLogEventSink != this) + (_batchedLogEventSink as IDisposable)?.Dispose(); } - void ResetSyncContextAndWait(Func taskFactory) + static void ResetSyncContextAndWait(Func taskFactory) { var prevContext = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); @@ -154,9 +209,8 @@ async Task OnTick() bool batchWasFull; do { - LogEvent next; while (_waitingBatch.Count < _batchSizeLimit && - _queue.TryDequeue(out next)) + _queue.TryDequeue(out var next)) { if (CanInclude(next)) _waitingBatch.Enqueue(next); @@ -164,11 +218,11 @@ async Task OnTick() if (_waitingBatch.Count == 0) { - await OnEmptyBatchAsync(); + await _batchedLogEventSink.OnEmptyBatchAsync(); return; } - await EmitBatchAsync(_waitingBatch); + await _batchedLogEventSink.EmitBatchAsync(_waitingBatch); batchWasFull = _waitingBatch.Count >= _batchSizeLimit; _waitingBatch.Clear(); @@ -188,8 +242,7 @@ async Task OnTick() if (_status.ShouldDropQueue) { - LogEvent evt; - while (_queue.TryDequeue(out evt)) { } + while (_queue.TryDequeue(out _)) { } } lock (_stateLock) @@ -230,11 +283,20 @@ public void Emit(LogEvent logEvent) if (_unloading) return; if (!_started) { - // Special handling to try to get the first event across as quickly - // as possible to show we're alive! _queue.TryEnqueue(logEvent); _started = true; - SetTimer(TimeSpan.Zero); + + if (_eagerlyEmitFirstEvent) + { + // Special handling to try to get the first event across as quickly + // as possible to show we're alive! + SetTimer(TimeSpan.Zero); + } + else + { + SetTimer(_status.NextInterval); + } + return; } } @@ -247,9 +309,10 @@ public void Emit(LogEvent logEvent) /// Determine whether a queued log event should be included in the batch. If /// an override returns false, the event will be dropped. /// - /// - /// - protected virtual bool CanInclude(LogEvent evt) + /// An event to test for inclusion. + /// True if the event should be included in the batch; otherwise, false. + // ReSharper disable once UnusedParameter.Global + protected virtual bool CanInclude(LogEvent logEvent) { return true; } @@ -276,5 +339,8 @@ protected virtual async Task OnEmptyBatchAsync() { OnEmptyBatch(); } + + Task IBatchedLogEventSink.EmitBatchAsync(IEnumerable batch) => EmitBatchAsync(batch); + Task IBatchedLogEventSink.OnEmptyBatchAsync() => OnEmptyBatchAsync(); } } diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSinkOptions.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSinkOptions.cs new file mode 100644 index 0000000..d0a8fbc --- /dev/null +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PeriodicBatchingSinkOptions.cs @@ -0,0 +1,47 @@ +// Copyright 2013-2020 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. + +using System; + +namespace Serilog.Sinks.PeriodicBatching +{ + /// + /// Initialization options for . + /// + public class PeriodicBatchingSinkOptions + { + /// + /// Eagerly emit a batch containing the first received event, regardless of + /// the target batch size or batching time. This helps with perceived "liveness" + /// when running/debugging applications interactively. The default is true. + /// + public bool EagerlyEmitFirstEvent { get; set; } = true; + + /// + /// The maximum number of events to include in a single batch. The default is 1000. + /// + public int BatchSizeLimit { get; set; } = 1000; + + /// + /// The time to wait between checking for event batches. The default is two seconds. + /// + public TimeSpan Period { get; set; } = TimeSpan.FromSeconds(2); + + /// + /// Maximum number of events to hold in the sink's internal queue, or null + /// for an unbounded queue. The default is 10000. + /// + public int? QueueLimit { get; set; } = 100000; + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PortableTimer.cs b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PortableTimer.cs index 1dfcdaf..7b5eb29 100644 --- a/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PortableTimer.cs +++ b/src/Serilog.Sinks.PeriodicBatching/Sinks/PeriodicBatching/PortableTimer.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2020 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/BatchedConnectionStatusTests.cs b/test/Serilog.Sinks.PeriodicBatching.Tests/BatchedConnectionStatusTests.cs similarity index 75% rename from test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/BatchedConnectionStatusTests.cs rename to test/Serilog.Sinks.PeriodicBatching.Tests/BatchedConnectionStatusTests.cs index a29e84e..f94375c 100644 --- a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/BatchedConnectionStatusTests.cs +++ b/test/Serilog.Sinks.PeriodicBatching.Tests/BatchedConnectionStatusTests.cs @@ -1,33 +1,32 @@ using System; using System.Globalization; using Xunit; -using Serilog.Sinks.PeriodicBatching; -namespace Serilog.Tests.Sinks.PeriodicBatching +namespace Serilog.Sinks.PeriodicBatching.Tests { public class BatchedConnectionStatusTests { - readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(2); + readonly TimeSpan _defaultPeriod = TimeSpan.FromSeconds(2); [Fact] public void WhenNoFailuresHaveOccurredTheRegularIntervalIsUsed() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); - Assert.Equal(DefaultPeriod, bcs.NextInterval); + var bcs = new BatchedConnectionStatus(_defaultPeriod); + Assert.Equal(_defaultPeriod, bcs.NextInterval); } [Fact] public void WhenOneFailureHasOccurredTheRegularIntervalIsUsed() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); bcs.MarkFailure(); - Assert.Equal(DefaultPeriod, bcs.NextInterval); + Assert.Equal(_defaultPeriod, bcs.NextInterval); } [Fact] public void WhenTwoFailuresHaveOccurredTheIntervalBacksOff() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); bcs.MarkFailure(); bcs.MarkFailure(); Assert.Equal(TimeSpan.FromSeconds(10), bcs.NextInterval); @@ -36,17 +35,17 @@ public void WhenTwoFailuresHaveOccurredTheIntervalBacksOff() [Fact] public void WhenABatchSucceedsTheStatusResets() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); bcs.MarkFailure(); bcs.MarkFailure(); bcs.MarkSuccess(); - Assert.Equal(DefaultPeriod, bcs.NextInterval); + Assert.Equal(_defaultPeriod, bcs.NextInterval); } [Fact] public void WhenThreeFailuresHaveOccurredTheIntervalBacksOff() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); bcs.MarkFailure(); bcs.MarkFailure(); bcs.MarkFailure(); @@ -57,7 +56,7 @@ public void WhenThreeFailuresHaveOccurredTheIntervalBacksOff() [Fact] public void When8FailuresHaveOccurredTheIntervalBacksOffAndBatchIsDropped() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); for (var i = 0; i < 8; ++i) { Assert.False(bcs.ShouldDropBatch); @@ -71,7 +70,7 @@ public void When8FailuresHaveOccurredTheIntervalBacksOffAndBatchIsDropped() [Fact] public void When10FailuresHaveOccurredTheQueueIsDropped() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); for (var i = 0; i < 10; ++i) { Assert.False(bcs.ShouldDropQueue); @@ -83,7 +82,7 @@ public void When10FailuresHaveOccurredTheQueueIsDropped() [Fact] public void AtTheDefaultIntervalRetriesFor10MinutesBeforeDroppingBatch() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); var cumulative = TimeSpan.Zero; do { @@ -100,7 +99,7 @@ public void AtTheDefaultIntervalRetriesFor10MinutesBeforeDroppingBatch() [Fact] public void AtTheDefaultIntervalRetriesFor30MinutesBeforeDroppingQueue() { - var bcs = new BatchedConnectionStatus(DefaultPeriod); + var bcs = new BatchedConnectionStatus(_defaultPeriod); var cumulative = TimeSpan.Zero; do { diff --git a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/BoundedConcurrentQueueTests.cs b/test/Serilog.Sinks.PeriodicBatching.Tests/BoundedConcurrentQueueTests.cs similarity index 74% rename from test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/BoundedConcurrentQueueTests.cs rename to test/Serilog.Sinks.PeriodicBatching.Tests/BoundedConcurrentQueueTests.cs index 973e9bb..d65f193 100644 --- a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/BoundedConcurrentQueueTests.cs +++ b/test/Serilog.Sinks.PeriodicBatching.Tests/BoundedConcurrentQueueTests.cs @@ -1,18 +1,17 @@ -using Serilog.Sinks.PeriodicBatching; -using System; +using System; using Xunit; -namespace Serilog.Tests.Sinks.PeriodicBatching +namespace Serilog.Sinks.PeriodicBatching.Tests { public class BoundedConcurrentQueueTests { [Fact] public void WhenBoundedShouldNotExceedLimit() { - var limit = 100; + const int limit = 100; var queue = new BoundedConcurrentQueue(limit); - for (int i = 0; i < limit * 2; i++) + for (var i = 0; i < limit * 2; i++) { queue.TryEnqueue(i); } diff --git a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/PeriodicBatchingSinkTests.cs b/test/Serilog.Sinks.PeriodicBatching.Tests/PeriodicBatchingSinkTests.cs similarity index 50% rename from test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/PeriodicBatchingSinkTests.cs rename to test/Serilog.Sinks.PeriodicBatching.Tests/PeriodicBatchingSinkTests.cs index 5cae7e6..6f19975 100644 --- a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/PeriodicBatchingSinkTests.cs +++ b/test/Serilog.Sinks.PeriodicBatching.Tests/PeriodicBatchingSinkTests.cs @@ -2,26 +2,25 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Xunit; using Serilog.Events; -using Serilog.Sinks.PeriodicBatching; using Serilog.Tests.Support; -namespace Serilog.Tests.Sinks.PeriodicBatching +namespace Serilog.Sinks.PeriodicBatching.Tests { - class InMemoryPeriodicBatchingSink : PeriodicBatchingSink + class InMemoryBatchedSink : IBatchedLogEventSink, IDisposable { readonly TimeSpan _batchEmitDelay; readonly object _stateLock = new object(); - bool _isDisposed; bool _stopped; - // Post-mortem only + // Postmortem only public bool WasCalledAfterDisposal { get; private set; } public IList> Batches { get; } + public bool IsDisposed { get; private set; } - public InMemoryPeriodicBatchingSink(int batchSizeLimit, TimeSpan period, TimeSpan batchEmitDelay) - : base(batchSizeLimit, period) + public InMemoryBatchedSink(TimeSpan batchEmitDelay) { _batchEmitDelay = batchEmitDelay; Batches = new List>(); @@ -35,25 +34,29 @@ public void Stop() } } - protected override void EmitBatch(IEnumerable events) + public Task EmitBatchAsync(IEnumerable events) { lock (_stateLock) { if (_stopped) - return; + return Task.FromResult(0); - if (_isDisposed) + if (IsDisposed) WasCalledAfterDisposal = true; Thread.Sleep(_batchEmitDelay); Batches.Add(events.ToList()); } + + return Task.FromResult(0); } - protected override void Dispose(bool disposing) + public Task OnEmptyBatchAsync() => Task.FromResult(0); + + public void Dispose() { - base.Dispose(disposing); - _isDisposed = true; + lock (_stateLock) + IsDisposed = true; } } @@ -67,38 +70,47 @@ public class PeriodicBatchingSinkTests [Fact] public void WhenAnEventIsEnqueuedItIsWrittenToABatch_OnFlush() { - var pbs = new InMemoryPeriodicBatchingSink(2, TinyWait, TimeSpan.Zero); + var bs = new InMemoryBatchedSink(TimeSpan.Zero); + var pbs = new PeriodicBatchingSink(bs, new PeriodicBatchingSinkOptions + { BatchSizeLimit = 2, Period = TinyWait, EagerlyEmitFirstEvent = true }); var evt = Some.InformationEvent(); pbs.Emit(evt); pbs.Dispose(); - Assert.Equal(1, pbs.Batches.Count); - Assert.Equal(1, pbs.Batches[0].Count); - Assert.Same(evt, pbs.Batches[0][0]); - Assert.False(pbs.WasCalledAfterDisposal); + Assert.Equal(1, bs.Batches.Count); + Assert.Equal(1, bs.Batches[0].Count); + Assert.Same(evt, bs.Batches[0][0]); + Assert.True(bs.IsDisposed); + Assert.False(bs.WasCalledAfterDisposal); } [Fact] public void WhenAnEventIsEnqueuedItIsWrittenToABatch_OnTimer() { - var pbs = new InMemoryPeriodicBatchingSink(2, TinyWait, TimeSpan.Zero); + var bs = new InMemoryBatchedSink(TimeSpan.Zero); + var pbs = new PeriodicBatchingSink(bs, new PeriodicBatchingSinkOptions + { BatchSizeLimit = 2, Period = TinyWait, EagerlyEmitFirstEvent = true }); var evt = Some.InformationEvent(); pbs.Emit(evt); Thread.Sleep(TinyWait + TinyWait); - pbs.Stop(); - Assert.Equal(1, pbs.Batches.Count); - Assert.False(pbs.WasCalledAfterDisposal); + bs.Stop(); + pbs.Dispose(); + Assert.Equal(1, bs.Batches.Count); + Assert.True(bs.IsDisposed); + Assert.False(bs.WasCalledAfterDisposal); } [Fact] public void WhenAnEventIsEnqueuedItIsWrittenToABatch_FlushWhileRunning() { - var pbs = new InMemoryPeriodicBatchingSink(2, MicroWait, TinyWait + TinyWait); + var bs = new InMemoryBatchedSink(TinyWait + TinyWait); + var pbs = new PeriodicBatchingSink(bs, new PeriodicBatchingSinkOptions { BatchSizeLimit = 2, Period = MicroWait, EagerlyEmitFirstEvent = true }); var evt = Some.InformationEvent(); pbs.Emit(evt); Thread.Sleep(TinyWait); pbs.Dispose(); - Assert.Equal(1, pbs.Batches.Count); - Assert.False(pbs.WasCalledAfterDisposal); + Assert.Equal(1, bs.Batches.Count); + Assert.True(bs.IsDisposed); + Assert.False(bs.WasCalledAfterDisposal); } } } diff --git a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/PortableTimerTests.cs b/test/Serilog.Sinks.PeriodicBatching.Tests/PortableTimerTests.cs similarity index 73% rename from test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/PortableTimerTests.cs rename to test/Serilog.Sinks.PeriodicBatching.Tests/PortableTimerTests.cs index 70e9d4e..05ee73e 100644 --- a/test/Serilog.Sinks.PeriodicBatching.Tests/Sinks/PeriodicBatching/PortableTimerTests.cs +++ b/test/Serilog.Sinks.PeriodicBatching.Tests/PortableTimerTests.cs @@ -1,20 +1,21 @@ -using Serilog.Debugging; -using Serilog.Sinks.PeriodicBatching; -using System; +using System; using System.Threading; using System.Threading.Tasks; +using Serilog.Debugging; using Xunit; #pragma warning disable 1998 -namespace Serilog.Tests.Sinks.PeriodicBatching +// ReSharper disable AccessToModifiedClosure + +namespace Serilog.Sinks.PeriodicBatching.Tests { public class PortableTimerTests { [Fact] public void WhenItStartsItWaitsUntilHandled_OnDispose() { - bool wasCalled = false; + var wasCalled = false; var barrier = new Barrier(participantCount: 2); @@ -36,10 +37,10 @@ async delegate [Fact] public void WhenWaitingShouldCancel_OnDispose() { - bool wasCalled = false; - bool writtenToSelflog = false; + var wasCalled = false; + var writtenToSelfLog = false; - SelfLog.Enable(_ => writtenToSelflog = true); + SelfLog.Enable(_ => writtenToSelfLog = true); using (var timer = new PortableTimer(async delegate { await Task.Delay(50); wasCalled = true; })) { @@ -49,41 +50,47 @@ public void WhenWaitingShouldCancel_OnDispose() Thread.Sleep(100); Assert.False(wasCalled, "tick handler was called"); - Assert.False(writtenToSelflog, "message was written to SelfLog"); + Assert.False(writtenToSelfLog, "message was written to SelfLog"); } [Fact] public void WhenActiveShouldCancel_OnDispose() { - bool wasCalled = false; - bool writtenToSelflog = false; + var wasCalled = false; + var writtenToSelfLog = false; - SelfLog.Enable(_ => writtenToSelflog = true); + SelfLog.Enable(_ => writtenToSelfLog = true); var barrier = new Barrier(participantCount: 2); using (var timer = new PortableTimer( async token => { + // ReSharper disable once MethodSupportsCancellation barrier.SignalAndWait(); - await Task.Delay(50); + // ReSharper disable once MethodSupportsCancellation + await Task.Delay(20); wasCalled = true; - await Task.Delay(50, token); + Interlocked.MemoryBarrier(); + await Task.Delay(100, token); })) { timer.Start(TimeSpan.FromMilliseconds(20)); barrier.SignalAndWait(); } + + Thread.Sleep(100); + Interlocked.MemoryBarrier(); Assert.True(wasCalled, "tick handler wasn't called"); - Assert.True(writtenToSelflog, "message wasn't written to SelfLog"); + Assert.True(writtenToSelfLog, "message wasn't written to SelfLog"); } [Fact] public void WhenDisposedWillThrow_OnStart() { - bool wasCalled = false; + var wasCalled = false; var timer = new PortableTimer(async delegate { wasCalled = true; }); timer.Start(TimeSpan.FromMilliseconds(100)); timer.Dispose(); @@ -95,7 +102,7 @@ public void WhenDisposedWillThrow_OnStart() [Fact] public void WhenOverlapsShouldProcessOneAtTime_OnTick() { - bool userHandlerOverlapped = false; + var userHandlerOverlapped = false; PortableTimer timer = null; timer = new PortableTimer( @@ -105,6 +112,7 @@ public void WhenOverlapsShouldProcessOneAtTime_OnTick() { try { + // ReSharper disable once PossibleNullReferenceException timer.Start(TimeSpan.Zero); Thread.Sleep(20); } @@ -130,6 +138,7 @@ public void WhenOverlapsShouldProcessOneAtTime_OnTick() public void CanBeDisposedFromMultipleThreads() { PortableTimer timer = null; + // ReSharper disable once PossibleNullReferenceException timer = new PortableTimer(async _ => timer.Start(TimeSpan.FromMilliseconds(1))); timer.Start(TimeSpan.Zero); @@ -139,5 +148,3 @@ public void CanBeDisposedFromMultipleThreads() } } } - -#pragma warning restore 1998 \ No newline at end of file