簡體   English   中英

如何使用Observables進行輪詢?

[英]How do I implement polling using Observables?

我有一個參數化的rest調用,應該使用不同的參數每五秒鍾執行一次:

Observable<TResult> restCall = api.method1(param1);

我需要創建一個Observable<TResult> ,它將每隔5秒使用不同的param1值輪詢restCall。 如果api調用失敗,我需要得到一個錯誤,並在5秒鍾內進行下一個調用。 僅當restCall完成(成功/錯誤)時,才應該測量兩次調用之間的間隔。

我目前正在使用RxJava,但是.NET示例也不錯。

介紹

首先,我是一個.NET專家,我知道這種方法使用的一些習語在Java中沒有直接等效項。 但是,我是按您的意願行事,並以此為基礎繼續前進,因為這是.NET專家們會喜歡的一個好問題,並且希望它將引導您沿着rx-java的正確道路前進,這是我從未研究過的。 這是一個很長的答案,但主要是解釋-解決方案代碼本身很短!

兩者之一的使用

我們將需要首先整理一些工具以幫助該解決方案。 第一種是使用Either<TLeft, TRight>類型。 這很重要,因為每個調用都有兩個可能的結果, 要么是好結果,要么是錯誤。 但是我們需要將它們包裝為單一類型-我們不能使用OnError向后發送錯誤,因為這會終止結果流。 兩者看上去都像元組,因此更容易處理這種情況。 Rxx庫Either了非常完整和良好的實現,但這是一個簡單的用法通用示例,然后是一個對我們有用的簡單實現:

var goodResult = Either.Right<Exception,int>(1);
var exception = Either.Left<Exception,int>(new Exception());

/* base class for LeftValue and RightValue types */
public abstract class Either<TLeft, TRight>
{
    public abstract bool IsLeft { get; }
    public bool IsRight { get { return !IsLeft; } }
    public abstract TLeft Left { get; }
    public abstract TRight Right { get;  }    
}

public static class Either
{
    public sealed class LeftValue<TLeft, TRight> : Either<TLeft, TRight>
    {
        TLeft _leftValue;

        public LeftValue(TLeft leftValue)
        {
            _leftValue = leftValue;
        }

        public override TLeft Left { get { return _leftValue; } }
        public override TRight Right { get { return default(TRight); } }
        public override bool IsLeft { get { return true; } }
    }

    public sealed class RightValue<TLeft, TRight> : Either<TLeft, TRight>
    {
        TRight _rightValue;

        public RightValue(TRight rightValue)
        {
            _rightValue = rightValue;
        }

        public override TLeft Left { get { return default(TLeft); } }
        public override TRight Right { get { return _rightValue; } }
        public override bool IsLeft { get { return false; } }
    }

    // Factory functions to create left or right-valued Either instances
    public static Either<TLeft, TRight> Left<TLeft, TRight>(TLeft leftValue)
    {
        return new LeftValue<TLeft, TRight>(leftValue);
    }

    public static Either<TLeft, TRight> Right<TLeft, TRight>(TRight rightValue)
    {
        return new RightValue<TLeft, TRight>(rightValue);
    }
}

請注意,按照慣例,當使用Either建模成功或失敗時,將Right用作成功值,因為它當然是“ Right” :)

一些輔助功能

我將使用一些輔助函數來模擬您的問題的兩個方面。 首先,這是一個生成參數的工廠-每次調用它時,它將返回以1開頭的整數序列中的下一個整數:

// An infinite supply of parameters
private static int count = 0;
public int ParameterFactory()
{
    return ++count; 
}

接下來,這是一個函數,將您的Rest調用模擬為IObservable。 該函數接受一個整數,並且:

  • 如果整數是偶數,則返回一個Observable,並立即發送一個OnError。
  • 如果整數為奇數,則返回字符串,該字符串將整數與“ -ret”連接,但僅在經過一秒鍾之后。 我們將使用它來檢查輪詢間隔是否符合您的要求-作為完成的調用之間的暫停(無論它們花費多長時間),而不是規則的間隔。

這里是:

// A asynchronous function representing the REST call
public IObservable<string> SomeRestCall(int x)
{
    return x % 2 == 0
        ? Observable.Throw<string>(new Exception())
        : Observable.Return(x + "-ret").Delay(TimeSpan.FromSeconds(1));   
}

現在進行中

以下是我稱為Poll的合理通用可重用函數。 它接受將要輪詢的異步函數,該函數的參數工廠,所需的休息時間(無雙關!),最后是要使用的IScheduler。

我能想到的最簡單的方法是使用Observable.Create ,它使用調度程序來驅動結果流。 ScheduleAsync是使用.NET async / await表單進行ScheduleAsync的一種方式。 這是一個.NET慣用法,使您可以以命令式方式編寫異步代碼。 async關鍵字引入了一個異步函數,該函數然后可以在其主體中await一個或多個異步調用,並且僅在調用完成時才繼續。 我在這個問題中對此調度方式作了詳盡的解釋,其中包括較舊的遞歸方式,這種方式在rx-java方法中可能更容易實現。 代碼如下:

public IObservable<Either<Exception, TResult>> Poll<TResult, TArg>(
    Func<TArg, IObservable<TResult>> asyncFunction,
    Func<TArg> parameterFactory,
    TimeSpan interval,
    IScheduler scheduler)
{
    return Observable.Create<Either<Exception, TResult>>(observer =>
    {
        return scheduler.ScheduleAsync(async (ctrl, ct) => {
            while(!ct.IsCancellationRequested)
            {
                try
                {
                    var result = await asyncFunction(parameterFactory());
                    observer.OnNext(Either.Right<Exception,TResult>(result));
                }
                catch(Exception ex)
                {
                    observer.OnNext(Either.Left<Exception, TResult>(ex));
                }
                await ctrl.Sleep(interval, ct);
            }
        });        
    });    
}

總而言之, Observable.Create是創建IObservable的工廠,它使您可以更好地控制將結果發布到觀察者的方式。 它經常被忽略,而支持不必要的復雜基元組成。

在這種情況下,我們使用它來創建Either<TResult, Exception>的流,以便我們可以返回成功和失敗的輪詢結果。

Create函數接受一個觀察者,該觀察者表示我們通過OnNext / OnError / OnCompleted將結果傳遞給的訂閱者。 我們需要在.NET中的Create調用中返回一個IDisposable ,這是訂閱服務器可以通過其取消訂閱的句柄。 這在這里特別重要,因為否則輪詢將永遠進行下去,或者至少不會進行OnComplete

ScheduleAsync (或普通Schedule )的結果就是這樣的句柄。 處置后,它將取消我們計划的任何未決事件-從而終止輪詢循環。 在本例中,用於管理間隔的Sleep是可取消操作,盡管可以輕松修改Poll函數以接受也可以接受CancellationToken的可取消asyncFunction

ScheduleAsync方法接受一個將調度事件的函數。 它傳遞了兩個參數,第一個ctrl是調度程序本身。 第二個ct是CancellationToken,我們可以使用它來查看是否已請求取消(由訂閱服務器布置其訂閱句柄)。

輪詢本身是通過無限while循環執行的,該循環僅在CancellationToken指示已請求取消時才終止。

在循環中,我們可以使用async / await的魔力來異步調用輪詢功能,但仍將其包裝在異常處理程序中。 太棒了! 假設沒有錯誤,我們將結果作為Either正確值通過OnNext發送給觀察者。 如果存在異常,我們將其作為Either值發送給觀察者。 最后,我們使用調度程序上的Sleep函數在休息間隔之后安排一次喚醒調用-不要與Thread.Sleep調用混淆,該調用通常不會阻塞任何線程。 請注意,Sleep接受CancellationToken ,它也將被中止!

我想您會同意,這是async / await的一種很酷的用法,它可以簡化本來非常棘手的問題!

用法示例

最后,這是一些調用Poll測試代碼以及​​示例輸出-對於LINQPad風扇,此答案中的所有代碼將一起在LINQPad中運行,並引用Rx 2.1程序集:

void Main()
{
    var subscription = Poll(SomeRestCall,
                            ParameterFactory,
                            TimeSpan.FromSeconds(5),
                            ThreadPoolScheduler.Instance)
        .TimeInterval()                            
        .Subscribe(x => {
            Console.Write("Interval: " + x.Interval);
            var result = x.Value;
            if(result.IsRight)
                Console.WriteLine(" Success: " + result.Right);
            else
                Console.WriteLine(" Error: " + result.Left.Message);
        });

    Console.ReadLine();    
    subscription.Dispose();
}

Interval: 00:00:01.0027668 Success: 1-ret
Interval: 00:00:05.0012461 Error: Exception of type 'System.Exception' was thrown.
Interval: 00:00:06.0009684 Success: 3-ret
Interval: 00:00:05.0003127 Error: Exception of type 'System.Exception' was thrown.
Interval: 00:00:06.0113053 Success: 5-ret
Interval: 00:00:05.0013136 Error: Exception of type 'System.Exception' was thrown.

請注意,如果立即返回錯誤,則結果之間的間隔為5秒(輪詢間隔),或者為成功結果則為6秒(輪詢間隔加上模擬的REST調用持續時間)。

編輯-這是一個使用ScheduleAsync的替代實現,但使用舊式遞歸調度並且不使用async / await語法。 正如您所看到的,這比較麻煩-但它也支持取消可觀察到的asyncFunction。

    public IObservable<Either<Exception, TResult>> Poll<TResult, TArg>(
        Func<TArg, IObservable<TResult>> asyncFunction,
        Func<TArg> parameterFactory,
        TimeSpan interval,
        IScheduler scheduler)
    {
        return Observable.Create<Either<Exception, TResult>>(
            observer =>
                {
                    var disposable = new CompositeDisposable();
                    var funcDisposable = new SerialDisposable();
                    bool cancelRequested = false;
                    disposable.Add(Disposable.Create(() => { cancelRequested = true; }));
                    disposable.Add(funcDisposable);
                    disposable.Add(scheduler.Schedule(interval, self =>
                        {
                            funcDisposable.Disposable = asyncFunction(parameterFactory())
                                .Finally(() =>
                                    {
                                        if (!cancelRequested) self(interval);
                                    })
                                .Subscribe(
                                    res => observer.OnNext(Either.Right<Exception, TResult>(res)),
                                    ex => observer.OnNext(Either.Left<Exception, TResult>(ex)));
                        }));

                    return disposable;

                });
    }

請參閱我的其他答案,以獲取另一種避免.NET 4.5異步/等待功能且不使用計划調用的方法。

我希望這對rx-java家伙有所幫助!

我已經清理了直接使用Schedule調用的方法-使用其他答案中的Either類型-它也可以使用相同的測試代碼並給出相同的結果:

    public IObservable<Either<Exception, TResult>> Poll2<TResult, TArg>(
        Func<TArg, IObservable<TResult>> asyncFunction,
        Func<TArg> parameterFactory,
        TimeSpan interval,
        IScheduler scheduler)
    {
        return Observable.Create<Either<Exception, TResult>>(
            observer =>
                Observable.Defer(() => asyncFunction(parameterFactory()))
                          .Select(Either.Right<Exception, TResult>)
                          .Catch<Either<Exception, TResult>, Exception>(
                            ex => Observable.Return(Either.Left<Exception, TResult>(ex)))
                          .Concat(Observable.Defer(
                            () => Observable.Empty<Either<Exception, TResult>>()
                                            .Delay(interval, scheduler)))
                          .Repeat().Subscribe(observer));
    }

這具有適當的取消語義。

實施注意事項

  • 整個構造都使用Repeat(重復)來獲得循環行為。
  • 初始Defer用於確保每次迭代都傳遞不同的參數
  • Select將OnNext結果投影到右側的Either中
  • Catch將OnError結果投射到左側的Either中-請注意,此異常終止了當前可觀察到的asyncFunction,因此需要重復
  • Concat增加了間隔延遲

我認為調度版本更具可讀性,但是該版本不使用異步/等待,因此與.NET 4.0兼容。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM