Rx推拉窗 - 正确清理?

时间:2014-03-13 05:26:19

标签: .net system.reactive

我一直在尝试构建一个可以配置为使用复合规则跳闸的断路器,如:

  • 在45秒内发生2次超时异常;或
  • 50分钟内任何其他类型的例外

因此认为最好的方法是使用滑动窗口(或者任何你想要调用它们的时间缓冲区)。

根据我在网上找到的内容并拼凑自己,我写了这个简单的控制台应用程序:

static IEnumerable<ConsoleKeyInfo> KeyPresses()
{
    ConsoleKeyInfo key;
    do
    {
        key = Console.ReadKey();
        yield return key;
    } while (key.Key != ConsoleKey.Escape);
}

static IObservable<int> TimeLimitedThreshold<T>(IObservable<T> source, TimeSpan timeLimit, int threshold)
{
    return source.Window(source, _ => Observable.Timer(timeLimit))
        .Select(x => x.Count())
        .Merge()
        .Where(count => count >= threshold)
        .Take(1);
}

static void Main(string[] args)
{
    Console.WriteLine("Starting");

    var timeLimit = TimeSpan.FromSeconds(5);
    const int threshold = 3;
    var keys = KeyPresses().ToObservable(Scheduler.Default).Publish().RefCount();

    var thresholdHit = TimeLimitedThreshold(keys, timeLimit, threshold);
    thresholdHit.Subscribe(count => Console.WriteLine("THRESHOLD BREACHED! Count is: {0}", count));

    // block the main thread so we don't terminate
    keys.Where(key => key.Key == ConsoleKey.Escape).FirstAsync().Wait();
    Console.WriteLine("Finished");
}

(如果我把它放在gist或pastebin而不是问题中,请说出来)

现在这似乎做了我想要的,如果我在5秒内按任意键3次或更多次“THRESHOLD BREACHED !!!”打印一次,没有其他事情发生。

我的问题是:

  • 在使用Rx.Net时,TimeLimitedThreshold函数是否处于合理的抽象级别?
  • 我应该在创建Observable.Timer()?
  • 时注入要使用的调度程序
  • 如果有的话,我会错过哪些清理工作?关于Rx.NET的内存使用确实令我感到困惑。

2 个答案:

答案 0 :(得分:2)

我可能会通过以下方式编写阈值函数,利用Timestamp组合子。

    public static IObservable<U> TimeLimitedThreshold
        <T,U>
        ( this IObservable<T> source
        , int count
        , TimeSpan timeSpan
        , Func<IList<T>,U> selector
        , IScheduler scheduler = null
        )
    {
        var tmp = scheduler == null
            ? source.Timestamp()
            : source.Timestamp(scheduler);

        return  tmp
             .Buffer(count, 1).Where(b=>b.Count==count)
             .Select(b => new { b, span = b.Last().Timestamp - b.First().Timestamp })
             .Where(o => o.span <= timeSpan)
             .Select(o => selector(o.b.Select(ts=>ts.Value).ToList()));
    }

作为触发器触发时的一个额外的便利,满足触发器的完整缓冲区被提供给选择器函数。

例如

 var keys = KeyPresses().ToObservable(Scheduler.Default).Publish().RefCount();
 IObservable<string> fastKeySequences = keys.TimeLimitedThreshHold
     ( 3
     , TimeSpan.FromSeconds(5)
     , keys => String.Join("", keys)
     );

提供了额外的IScheduler参数,因为Timestamp方法有一个额外的重载,需要一个。如果您想要一个不根据内部时钟跟踪时间的自定义调度程序,这可能很有用。出于测试目的,使用historical scheduler可能很有用,然后您需要额外的过载。

这是一个完全有效的测试,显示了调度的使用。 (使用XUnit和FluentAssertions作为Should()。Be(..))

public class TimeLimitedThresholdSpec  : ReactiveTest
{

    TestScheduler _Scheduler = new TestScheduler();
    [Fact]
    public void ShouldWork()
    {
        var o = _Scheduler.CreateColdObservable
            ( OnNext(100, "A")
            , OnNext(200, "B")
            , OnNext(250, "C")
            , OnNext(255, "D")
            , OnNext(258, "E")
            , OnNext(600, "F")
            );

        var fixture = o
            .TimeLimitedThreshold
                (3
                , TimeSpan.FromTicks(20)
                , b => String.Join("", b)
                , _Scheduler
                );

        var actual = _Scheduler
            .Start(()=>fixture, created:0, subscribed:1, disposed:1000);
        actual.Messages.Count.Should().Be(1);
        actual.Messages[0].Value.Value.Should().Be("CDE");


    }

}

订阅并采用以下方式

IDisposable subscription = fastKeySequences.Subscribe(s=>Console.WriteLine(s));

当您想要取消订阅(清理内存和资源)时,您将处理订阅。简单地

subscription.Dispose()

答案 1 :(得分:2)

这是一种替代方法,它使用单个延迟来支持缓冲区和定时器。它没有给你发送事件 - 它只是在有违规时发出信号 - 但是它使用的内存较少,因为它没有太多。

public static class ObservableExtensions
{
    public static IObservable<Unit> TimeLimitedThreshold<TSource>(
        this IObservable<TSource> source,
        long threshold,
        TimeSpan timeLimit,
        IScheduler s)
    {
        var events = source.Publish().RefCount();
        var count = events.Select(_ => 1)
                        .Merge(events.Select(_ => -1)
                                    .Delay(timeLimit, s));                                               
        return count.Scan((x,y) => x + y)              
                    .Where(c => c == threshold)
                    .Select(_ => Unit.Default);           
    }
}

Publish().RefCount()用于避免订阅多个来源。该查询将所有事件投影到1,并将延迟的事件流投影到-1,然后生成一个运行总计。如果运行总计达到阈值,我们发出一个信号(Unit.Default是Rx类型,表示没有有效负载的事件)。这是一个测试(只在带有nuget rx-testing的LINQPad中运行):

void Main()
{    
    var s = new TestScheduler();
    var source = s.CreateColdObservable(
        new Recorded<Notification<int>>(100, Notification.CreateOnNext(1)),
        new Recorded<Notification<int>>(200, Notification.CreateOnNext(2)),
        new Recorded<Notification<int>>(300, Notification.CreateOnNext(3)),
        new Recorded<Notification<int>>(330, Notification.CreateOnNext(4)));

    var results = s.CreateObserver<Unit>();

    source.TimeLimitedThreshold(
        2,
        TimeSpan.FromTicks(30),
        s).Subscribe(results);

    s.Start();

    ReactiveAssert.AssertEqual(
        results.Messages,
        new List<Recorded<Notification<Unit>>> {
            new Recorded<Notification<Unit>>(
                330, Notification.CreateOnNext(Unit.Default))
        });
}

修改

在Matthew Finlay的观察结果显示上面的内容也会随着阈值的传递而消失&#34;在下降的路上#34;我添加了这个版本,只检查正向的阈值交叉:< / p>

public static class ObservableExtensions
{
    public static IObservable<Unit> TimeLimitedThreshold<TSource>(
        this IObservable<TSource> source,
        long threshold,
        TimeSpan timeLimit,
        IScheduler s)
    {
        var events = source.Publish().RefCount();
        var count = events.Select(_ => 1)
                        .Merge(events.Select(_ => -1)
                                    .Delay(timeLimit, s));                                                   

        return count.Scan((x,y) => x + y)          
                    .Scan(new { Current = 0, Last = 0},
                          (x,y) => new { Current = y, Last = x.Current })                        
                    .Where(c => c.Current == threshold && c.Last < threshold)
                    .Select(_ => Unit.Default);                          
    }
}