使用RX的buffer运算符可以在出现一定数量的结果后或在指定时间后(以较早者为准)创建批次。这在将结果传递给另一台机器上的数据库时非常有用,其中一个数据库希望保持延迟,但避免发送大量请求(每个结果一个)。
我有一个额外的要求,即保留结果到数据库的顺序(有些是更新,必须在相应的添加之后)。这意味着,如果传出请求出现故障,它们就不会重叠。
理想情况下,如果先前的数据库请求尚未返回,则每个缓冲区应该继续填充,即使它通常会发出,因为这会最小化延迟和进入数据库的请求数。
如何修改以下代码才能使其正常工作?
source
.Buffer(TimeSpan.FromSeconds(1), 25)
.Subscribe(async batch => await SendToDatabase(batch));
答案 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));
但是,这仍然会在等待时产生批次,因此不是一个完美的解决方案。