diff --git a/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs b/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs index b2e1d90..deccd06 100644 --- a/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs +++ b/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs @@ -20,9 +20,9 @@ namespace LiquidProjections.Owin { internal class CustomNancyBootstrapper : DefaultNancyBootstrapper { - private readonly ProjectionStats stats; + private readonly IProjectionStats stats; - public CustomNancyBootstrapper(ProjectionStats stats) + public CustomNancyBootstrapper(IProjectionStats stats) { this.stats = stats; } diff --git a/Src/LiquidProjections.Owin/MiddlewareExtensions.cs b/Src/LiquidProjections.Owin/MiddlewareExtensions.cs index a5aad4f..39446ca 100644 --- a/Src/LiquidProjections.Owin/MiddlewareExtensions.cs +++ b/Src/LiquidProjections.Owin/MiddlewareExtensions.cs @@ -6,7 +6,7 @@ namespace LiquidProjections.Owin { public static class MiddlewareExtensions { - public static IAppBuilder UseLiquidProjections(this IAppBuilder appBuilder, ProjectionStats stats) + public static IAppBuilder UseLiquidProjections(this IAppBuilder appBuilder, IProjectionStats stats) { appBuilder.Map("/projectionStats", a => a.UseNancy(new NancyOptions { diff --git a/Src/LiquidProjections.Owin/StatisticsModule.cs b/Src/LiquidProjections.Owin/StatisticsModule.cs index d0e41e2..740aa2c 100644 --- a/Src/LiquidProjections.Owin/StatisticsModule.cs +++ b/Src/LiquidProjections.Owin/StatisticsModule.cs @@ -16,15 +16,15 @@ namespace LiquidProjections.Owin { internal class StatisticsModule : NancyModule { - public StatisticsModule(ProjectionStats stats, IResourceLinker resourceLinker) + public StatisticsModule(IProjectionStats stats, IResourceLinker resourceLinker) { Get("/", args => { - var results = stats.GetForAllProjectors().OrderBy(p => p.ProjectorId).Select(p => new ProjectorSummary + var results = stats.OrderBy(p => p.ProjectorId).Select(p => new ProjectorSummary { ProjectorId = p.ProjectorId, - LastCheckpoint = stats[p.ProjectorId].LastCheckpoint.Checkpoint, - LastCheckpointUpdatedUtc = stats[p.ProjectorId].LastCheckpoint.TimestampUtc, + LastCheckpoint = p.LastCheckpoint.Checkpoint, + LastCheckpointUpdatedUtc = p.LastCheckpoint.TimestampUtc, Url = Context.Request.Url + $"/{p.ProjectorId}" }); @@ -34,13 +34,13 @@ public StatisticsModule(ProjectionStats stats, IResourceLinker resourceLinker) Get("/{id}", args => { string id = args.Id; - + return new { ProjectorId = id, - LastCheckpoint = stats[id].LastCheckpoint.Checkpoint, - LastCheckpointUpdatedUtc = stats[id].LastCheckpoint.TimestampUtc, - Properties = stats[id].GetProperties().Select(p => new ProjectorProperty + LastCheckpoint = stats.Get(id).LastCheckpoint.Checkpoint, + LastCheckpointUpdatedUtc = stats.Get(id).LastCheckpoint.TimestampUtc, + Properties = stats.Get(id).GetProperties().Select(p => new ProjectorProperty { Key = p.Key, Value = p.Value.Value, @@ -60,7 +60,7 @@ public StatisticsModule(ProjectionStats stats, IResourceLinker resourceLinker) return new ProjectorEventCollection { ProjectorId = id, - Events = stats[id].GetEvents().Select(@event => new ProjectorEvent + Events = stats.Get(id).GetEvents().Select(@event => new ProjectorEvent { Body = @event.Body, TimestampUtc = @event.TimestampUtc @@ -72,7 +72,7 @@ public StatisticsModule(ProjectionStats stats, IResourceLinker resourceLinker) { string id = args.Id; - TimeSpan? eta = stats[id].GetTimeToReach(args.targetCheckpoint); + TimeSpan? eta = stats.GetTimeToReach(id, args.targetCheckpoint); return new { diff --git a/Src/LiquidProjections/LiquidProjections.csproj b/Src/LiquidProjections/LiquidProjections.csproj index e198fa2..960426d 100644 --- a/Src/LiquidProjections/LiquidProjections.csproj +++ b/Src/LiquidProjections/LiquidProjections.csproj @@ -1,11 +1,9 @@  - netstandard1.1 false - TRACE;DEBUG;NETSTANDARD1_1;LIBLOG_PORTABLE bin\Debug\netstandard1.1\LiquidProjections.xml @@ -13,7 +11,6 @@ True - TRACE;RELEASE;LIBLOG_PORTABLE;NETSTANDARD1_1 bin\Release\netstandard1.1\LiquidProjections.xml @@ -21,15 +18,12 @@ True - - - \ No newline at end of file diff --git a/Src/LiquidProjections/Statistics/IProjectionStats.cs b/Src/LiquidProjections/Statistics/IProjectionStats.cs new file mode 100644 index 0000000..ad04421 --- /dev/null +++ b/Src/LiquidProjections/Statistics/IProjectionStats.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace LiquidProjections.Statistics +{ + public interface IProjectionStats : IEnumerable + { + /// + /// Should be called to track the progress of a projector and use that to calculate an ETA. + /// + void TrackProgress(string projectorId, long checkpoint); + + /// + /// Can be used to store projector-specific properties that characterize the projector's configuration or state. + /// + /// + /// Each property is identified by a . This class only keeps the latest value + /// for each property. + /// + void StoreProperty(string projectorId, string name, string value); + + /// + /// Can be used to store information that happened that can help diagnose the state or failure of a projector. + /// + void LogEvent(string projectorId, string body); + + /// + /// Gets the speed in transactions per minute based on a weighted average over the last + /// ten minutes, or null if there is not enough information yet. + /// + /// + float? GetSpeed(string projectorId); + + /// + /// Calculates the expected time for the projector identified by to reach a + /// certain based on a weighted average over the last + /// ten minutes, or null if there is not enough information yet. Use to report + /// progress. + /// + TimeSpan? GetTimeToReach(string projectorId, long targetCheckpoint); + + /// + /// Gets the stats for an individual projector. + /// + IProjectorStats Get(string projectorId); + } +} \ No newline at end of file diff --git a/Src/LiquidProjections/Statistics/ProjectionStats.cs b/Src/LiquidProjections/Statistics/ProjectionStats.cs index b6abcd4..968e3cb 100644 --- a/Src/LiquidProjections/Statistics/ProjectionStats.cs +++ b/Src/LiquidProjections/Statistics/ProjectionStats.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,7 @@ namespace LiquidProjections.Statistics /// /// Provides a thread-safe place to store all kinds of run-time information about the progress of a projector. /// - public class ProjectionStats + public class ProjectionStats : IProjectionStats { private readonly Func nowUtc; private readonly ConcurrentDictionary stats = new ConcurrentDictionary(); @@ -67,10 +68,16 @@ public void LogEvent(string projectorId, string body) return this[projectorId].GetTimeToReach(targetCheckpoint); } + /// + public IProjectorStats Get(string projectorId) + { + return this[projectorId]; + } + /// /// Gets the statistics for a particular projector. /// - public ProjectorStats this[string projectorId] + private ProjectorStats this[string projectorId] { get { @@ -78,9 +85,14 @@ public ProjectorStats this[string projectorId] } } - public IEnumerable GetForAllProjectors() + public IEnumerator GetEnumerator() + { + return stats.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { - return stats.ToArray().Select(projectorStatsById => projectorStatsById.Value); + return GetEnumerator(); } } } diff --git a/Src/LiquidProjections/Statistics/ProjectorStats.cs b/Src/LiquidProjections/Statistics/ProjectorStats.cs index 5004d7d..c232dae 100644 --- a/Src/LiquidProjections/Statistics/ProjectorStats.cs +++ b/Src/LiquidProjections/Statistics/ProjectorStats.cs @@ -5,13 +5,29 @@ namespace LiquidProjections.Statistics { + public interface IProjectorStats + { + string ProjectorId { get; } + TimestampedCheckpoint LastCheckpoint { get; } + + /// + /// Gets a snapshot of the properties stored for this projector at the time of calling. + /// + IDictionary GetProperties(); + + /// + /// Gets a snapshot of the events stored for this projector at the time of calling. + /// + IReadOnlyList GetEvents(); + } + /// /// Contains statistics and information about a particular projector. /// /// /// An instance of this class is safe for use in multi-threaded solutions. /// - public class ProjectorStats + public class ProjectorStats : IProjectorStats { private readonly object eventsSyncObject = new object(); private readonly object progressSyncObject = new object(); diff --git a/Tests/LiquidProjections.Specs/ProjectionStatsSpecs.cs b/Tests/LiquidProjections.Specs/ProjectionStatsSpecs.cs index d726fa6..1a088b4 100644 --- a/Tests/LiquidProjections.Specs/ProjectionStatsSpecs.cs +++ b/Tests/LiquidProjections.Specs/ProjectionStatsSpecs.cs @@ -29,7 +29,7 @@ public void When_checking_in_multiple_times_for_a_projector_it_should_remember_t //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - var projectorStats = stats.GetForAllProjectors().Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; + var projectorStats = stats.Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; projectorStats.LastCheckpoint.Checkpoint.Should().Be(2000); projectorStats.LastCheckpoint.TimestampUtc.Should().Be(nowUtc); } @@ -53,7 +53,7 @@ public void When_multiple_properties_are_registered_under_the_same_name_it_shoul //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - var projectorStats = stats.GetForAllProjectors().Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; + var projectorStats = stats.Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; projectorStats.GetProperties().Should().ContainKey("theName"); projectorStats.GetProperties()["theName"].Should().BeEquivalentTo(new @@ -83,7 +83,7 @@ public void When_multiple_properties_are_registered_under_different_names_it_sho //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - var projectorStats = stats.GetForAllProjectors().Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; + var projectorStats = stats.Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; projectorStats.GetProperties().Should().ContainKey("aName"); projectorStats.GetProperties()["aName"].Should().BeEquivalentTo(new @@ -121,7 +121,7 @@ public void When_multiple_events_are_registered_it_should_remember_their_timesta //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - var projectorStats = stats.GetForAllProjectors().Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; + var projectorStats = stats.Should().ContainSingle(s => s.ProjectorId == "myProjector").Subject; projectorStats.GetEvents().Should().BeEquivalentTo(new[] { new