public static void SuspendResumeTrialFinal() { var scheduler = new TestScheduler(); int suspendOnCount = 3; TimeSpan suspendCountDuration = TimeSpan.FromSeconds(5); TimeSpan suspendDuration = TimeSpan.FromSeconds(15); var subject = new Subject <int>(); var source = subject.Timestamp(scheduler).Select(i => new MetaValue <int>(i.Value, i.Timestamp, Emit.Value)); var head = source.Take(2); var tail = source .Buffer(3, 1) .Where(b => b.Count == 3) .Select(b => { var lastInBuffer = b.Last(); var firstInBuffer = b.First(); var diff = lastInBuffer.Timestamp.Subtract(firstInBuffer.Timestamp); return(new MetaValue <int>(lastInBuffer.Value, lastInBuffer.Timestamp, diff <= suspendCountDuration ? Emit.Suspend : Emit.Value)); }); var rectified = tail.Scan( ScannedValueWithRectification <int> .Empty, (enrichedAccumulatedValue, currentMetaValue) => { if (enrichedAccumulatedValue.StartOfSuspendWindow != DateTimeOffset.MinValue) { var endOfSuspend = enrichedAccumulatedValue.StartOfSuspendWindow.Add(suspendDuration); if (currentMetaValue.Timestamp < endOfSuspend) { return(new ScannedValueWithRectification <int>( null, enrichedAccumulatedValue.StartOfSuspendWindow, enrichedAccumulatedValue.RemainingNumberOfValuesToRectify)); } } if (enrichedAccumulatedValue.RemainingNumberOfValuesToRectify > 0) { // Rectify the first N-1 values after a resume to Emit.Value var rectifiedCurrent = new MetaValue <int>(currentMetaValue.Value, currentMetaValue.Timestamp, Emit.Value); // Preserve the information of the suspend window until all necessary values have been rectified return(new ScannedValueWithRectification <int>( rectifiedCurrent, enrichedAccumulatedValue.StartOfSuspendWindow, enrichedAccumulatedValue.RemainingNumberOfValuesToRectify - 1)); } return(currentMetaValue.Kind == Emit.Suspend ? new ScannedValueWithRectification <int>(null, currentMetaValue.Timestamp, 2) : new ScannedValueWithRectification <int>(currentMetaValue, DateTimeOffset.MinValue, 0)); //Emit value }) .Where(ev => ev.MetaValue != null) .Select(ev => ev.MetaValue); var prefinal = head.Merge(rectified); prefinal.Subscribe(Console.WriteLine); var final = prefinal.Select(m => m.Value); /********************************************************************** * * source --1---------2----------3-4-5-6-7--------8---------9------ * **********************************************************************/ scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2000).Ticks); subject.OnNext(1); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10000).Ticks); subject.OnNext(2); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(11000).Ticks); subject.OnNext(3); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2000).Ticks); subject.OnNext(4); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1500).Ticks); subject.OnNext(5); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1250).Ticks); subject.OnNext(6); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2250).Ticks); subject.OnNext(7); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(9000).Ticks); subject.OnNext(8); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10000).Ticks); subject.OnNext(9); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(22000).Ticks); subject.OnNext(10); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2000).Ticks); subject.OnNext(11); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2000).Ticks); subject.OnNext(12); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(12000).Ticks); subject.OnNext(13); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1000).Ticks); subject.OnNext(14); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2000).Ticks); subject.OnNext(15); //should be emitted outside of suspend scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1000).Ticks); subject.OnNext(16); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(2000).Ticks); subject.OnNext(17); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(10000).Ticks); subject.OnNext(18); scheduler.AdvanceBy(TimeSpan.FromMilliseconds(1).Ticks); subject.OnCompleted(); }
public ScannedValue(MetaValue <T> metaValue, DateTimeOffset startOfSuspendWindow) { MetaValue = metaValue; StartOfSuspendWindow = startOfSuspendWindow; }
public static IObservable <T> SuspendDuringFloodWithRectification <T>( this IObservable <T> source, int maxElementsPerWindow, TimeSpan windowDuration, TimeSpan suspendDuration, IScheduler scheduler) { scheduler = scheduler ?? new TestScheduler(); var internalSource = source.Timestamp(scheduler).Select(i => new MetaValue <T>(i.Value, i.Timestamp, Emit.Value)); // The first max-1 elements will always be emitted (no need to enrich them) var head = source.Take(maxElementsPerWindow - 1); // Enrich the rest of the elements with information about the absolute time they were emitted (see ... below) var tail = internalSource // Apply a buffer in order to determine if the last element in the buffer (the element of importance) is supposed to introduce a suspension .Buffer(maxElementsPerWindow, 1) // The last max-1 buffers are incomplete and can be safely ignored, because the last buffer of size max contains that last element in the observable .Where(b => b.Count == maxElementsPerWindow) .Select(b => { var lastInBuffer = b.Last(); var firstInBuffer = b.First(); var diff = lastInBuffer.Timestamp.Subtract(firstInBuffer.Timestamp); //... also enrich the value with information about whether or not it is supposed to introduce a suspension // (based on the time difference to the first element in the buffer) return(new MetaValue <T>(lastInBuffer.Value, lastInBuffer.Timestamp, diff <= windowDuration ? Emit.Suspend : Emit.Value)); }); // Run through the meta values produced and ignore the ones emitted during a suspend, // while rectifing (emitting) the ones wrongly identified as suspended (caused by them being in buffers with values that should have been ignored) var rectifiedTail = tail.Scan( ScannedValueWithRectification <T> .Empty, (accumulator, metaValue) => { if (accumulator.StartOfSuspendWindow != DateTimeOffset.MinValue) { var endOfSuspend = accumulator.StartOfSuspendWindow.Add(suspendDuration); if (metaValue.Timestamp < endOfSuspend) // Ignore (return null) values emitted during an ongoing suspend window { return(new ScannedValueWithRectification <T>( null, accumulator.StartOfSuspendWindow, accumulator.RemainingNumberOfValuesToRectify)); } } if (accumulator.RemainingNumberOfValuesToRectify > 0) { // Rectify the first max-1 values after a resume to Emit.Value. There is a philosofical reason for this: if one value // (for which there is always a buffer in which it is the last value) is deemed as introducing a suspension due // to values from the original source that ended up being ignored because they were emitted during an active suspension, // I don't find it logical for this value to still be considered as introducing a suspension. It should be considered as // the 1st value emitted after a suspend, and because suspendDuration >> windowDuration it should always emit a value, same goes // for all max-1 values that follow an end of a suspension interval. var rectifiedCurrent = new MetaValue <T>(metaValue.Value, metaValue.Timestamp, Emit.Value); // Preserve the information of the suspend window until all necessary values have been rectified return(new ScannedValueWithRectification <T>( rectifiedCurrent, accumulator.StartOfSuspendWindow, accumulator.RemainingNumberOfValuesToRectify - 1)); } return(metaValue.Kind == Emit.Suspend ? new ScannedValueWithRectification <T>(null, metaValue.Timestamp, maxElementsPerWindow - 1) // Begin a suspend window without emitting the value that triggered it : new ScannedValueWithRectification <T>(metaValue, DateTimeOffset.MinValue, 0)); // Emit a value }) .Where(sv => sv.MetaValue != null) // This is the part that effectively actually ignores the values under a suspend window .Select(ev => ev.MetaValue.Value); return(head.Merge(rectifiedTail)); }
public ScannedValueWithRectification(MetaValue <T> metaValue, DateTimeOffset startOfSuspendWindow, int remainingNumberOfValuesToRectify) : base(metaValue, startOfSuspendWindow) { RemainingNumberOfValuesToRectify = remainingNumberOfValuesToRectify; }