在RX中,如何创建缓冲区的速度不会超过它们的处理速度

时间:2018-03-24 14:44:48

标签: c# async-await system.reactive

使用RX的buffer运算符可以在出现一定数量的结果后或在指定时间后(以较早者为准)创建批次。这在将结果传递给另一台机器上的数据库时非常有用,其中一个数据库希望保持延迟,但避免发送大量请求(每个结果一个)。

我有一个额外的要求,即保留结果到数据库的顺序(有些是更新,必须在相应的添加之后)。这意味着,如果传出请求出现故障,它们就不会重叠。

理想情况下,如果先前的数据库请求尚未返回,则每个缓冲区应该继续填充,即使它通常会发出,因为这会最小化延迟和进入数据库的请求数。

如何修改以下代码才能使其正常工作?

source
  .Buffer(TimeSpan.FromSeconds(1), 25)
  .Subscribe(async batch => await SendToDatabase(batch));

2 个答案:

答案 0 :(得分:1)

我编写了一个新的可观察扩展名BufferAndAct来执行此操作。

总之,它需要一个时间间隔,一个(项目数)和一个应用于每个批次的操作。当时间间隔到期或达到项目数量时,它会尝试对批次执行操作,但在上一个批次完成之前,它将永远不会开始对新批次执行操作,因此对可能的大小没有限制批量。可以进行修改以使其与Buffer的其他一些重载相符。

它使用了另一个扩展Split,其作用类似于Buffer的重载之一,将源项目的可观察对象转换为可观察的源项目可观察量,在收到信号时将它们拆分输入可观察的。

BufferAndAct使用Split创建一个observable,当在源observable上发出正常的,定时的缓冲区时给出一个tick,并在释放实际缓冲区时重置。这可能是后来的,因为当目前没有正在进行的请求时,还有另一个可观察的时间点。通过zipping这两个滴答,Buffer可以在满足两个条件时立即发出批次。

用法如下:

source
  .BufferAndAct(TimeSpan.FromSeconds(1), 25, async batch =>
    await SendToDatabase(batch)
  )
  .Subscribe(r => {})

两个扩展的来源:

public static IObservable<TDest> BufferAndAct<TSource, TDest>(
        this IObservable<TSource> source,
        TimeSpan timeSpan,
        int count,
        Func<IList<TSource>, Task<TDest>> action
)
{
    return new AnonymousObservable<TDest>(observer =>
    {
        var actionStartedObserver = new Subject<Unit>();
        var actionCompleteObserver = new Subject<Unit>();
        var published = source.Publish();
        var batchReady = published.Select(i => Unit.Default).Split(actionStartedObserver).Select(s => s.Buffer(timeSpan, count).Select(u => Unit.Default).Take(1)).Concat();
        var disposable = published.Buffer(Observable.Zip(actionCompleteObserver.StartWith(Unit.Default), batchReady)).SelectMany(async list =>
        {
            actionStartedObserver.OnNext(Unit.Default);
            try
            {
                return await action(list);
            }
            finally
            {
                actionCompleteObserver.OnNext(Unit.Default);
            }
        }).Finally(() => {}).Subscribe(observer);
        published.Connect();
        return Disposable.Create(() =>
        {
            disposable.Dispose();
            actionCompleteObserver.Dispose();
        });
    });
}

public static IObservable<Unit> BufferAndAct<TSource>(
        this IObservable<TSource> source,
        TimeSpan timeSpan,
        int count,
        Func<IList<TSource>, Task> action
)
{
    return BufferAndAct(source, timeSpan, count, s =>
    {
        action(s);
        return Task.FromResult(Unit.Default);
    });
}

public static IObservable<IObservable<TSource>> Split<TSource>(
        this IObservable<TSource> source,
        IObservable<Unit> boundaries
)
{
    return Observable.Create<IObservable<TSource>>(observer =>
    {
        var tuple = Split(observer);
        var d1 = boundaries.Subscribe(tuple.Item2);
        var d2 = source.Subscribe(tuple.Item1);
        return Disposable.Create(() =>
        {
            d2.Dispose();
            d1.Dispose();
        });
    });
}

private static Tuple<IObserver<TSource>, IObserver<Unit>> Split<TSource>(this IObserver<IObservable<TSource>> output)
{
    ReplaySubject<TSource> obs = null;
    var completed = 0; // int not bool to use in interlocked
    Action newObservable = () =>
    {
        obs?.OnCompleted();
        obs = new ReplaySubject<TSource>();
        output.OnNext(obs);
    };
    Action completeOutput = () =>
    {
        if (Interlocked.CompareExchange(ref completed, 0, 1) == 1)
        {
            output.OnCompleted();
        }
    };
    newObservable();
    return new Tuple<IObserver<TSource>, IObserver<Unit>>(Observer.Create<TSource>(obs.OnNext, output.OnError, () =>
    {
        obs.OnCompleted();
        completeOutput();
    }), Observer.Create<Unit>(s => newObservable(), output.OnError, () => completeOutput()));
}

答案 1 :(得分:0)

要强制传出请求等待,直到前一个请求在处理之前返回,有一个RX技巧会将每个结果转换为一个observable,只有在完成处理后才会完成。通过将这些与concat组合,下一个将在上一个完成之前不会开始。

source
  .Buffer(TimeSpan.FromSeconds(1), 25)
  .Select(batch => 
    Observable.FromAsync(async () => 
      await SendToDatabase(batch)
    )
  )
  .Concat()
  .Subscribe(async batch => await SendToDatabase(batch));

但是,这仍然会在等待时产生批次,因此不是一个完美的解决方案。