web-dev-qa-db-ja.com

再試行可能なブロックの正しい完了の実装

Teaser:皆さん、この質問は再試行ポリシーの実装方法に関するものではありません。これは、TPLDataflowブロックが正しく完了することについてです。

この質問は、ほとんどが私の前の質問の続きです ITargetBlock内のポリシーを再試行してください 。この質問への答えは、TransformBlock(ソース)とTransformManyBlock(ターゲット)を利用する@svickのスマートソリューションでした。残っている唯一の問題は、このブロックを正しい方法で完了することです:最初にすべての再試行が完了するのを待ってから、ターゲットブロックを完了します。これが私が最終的に得たものです(これは単なるスニペットであり、スレッドセーフではないretriesセットにあまり注意を払わないでください):

var retries = new HashSet<RetryingMessage<TInput>>();

TransformManyBlock<RetryableMessage<TInput>, TOutput> target = null;
target = new TransformManyBlock<RetryableMessage<TInput>, TOutput>(
    async message =>
    {
        try
        {
            var result = new[] { await transform(message.Data) };
            retries.Remove(message);
            return result;
        }
        catch (Exception ex)
        {
            message.Exceptions.Add(ex);
            if (message.RetriesRemaining == 0)
            {
                if (failureHandler != null)
                    failureHandler(message.Exceptions);

                retries.Remove(message);
            }
            else
            {
                retries.Add(message);
                message.RetriesRemaining--;

                Task.Delay(retryDelay)
                    .ContinueWith(_ => target.Post(message));
            }
            return null;
        }
    }, dataflowBlockOptions);

source.LinkTo(target);

source.Completion.ContinueWith(async _ =>
{
    while (target.InputCount > 0 || retries.Any())
        await Task.Delay(100);

    target.Complete();
});

ある種のポーリングを実行し、処理を待機しているメッセージがまだあり、再試行が必要なメッセージがないかどうかを確認するという考え方です。しかし、このソリューションでは、ポーリングのアイデアは好きではありません。

はい、再試行を追加/削除するロジックを別のクラスにカプセル化できます。再試行のセットが空になったときに何らかのアクションを実行しますが、target.InputCount > 0状態に対処するにはどうすればよいですか?ブロックに保留中のメッセージがないときに呼び出されるようなコールバックはないため、遅延の少ないループでtarget.ItemCountを検証することが唯一のオプションのようです。

誰かがこれを達成するためのより賢い方法を知っていますか?

53
Alex

Hwcverweの回答とJamieSeeのコメントを組み合わせることが理想的な解決策になる可能性があります。

まず、複数のイベントを作成する必要があります。

var signal  = new ManualResetEvent(false);
var completedEvent = new ManualResetEvent(false);

次に、オブザーバーを作成し、TransformManyBlockにサブスクライブする必要があります。これにより、関連するイベントが発生したときに通知が届きます。

var observer = new RetryingBlockObserver<TOutput>(completedEvent);
var observable = target.AsObservable();
observable.Subscribe(observer);

オブザーバブルは非常に簡単です。

private class RetryingBlockObserver<T> : IObserver<T> {
        private ManualResetEvent completedEvent;

        public RetryingBlockObserver(ManualResetEvent completedEvent) {                
            this.completedEvent = completedEvent;
        }

        public void OnCompleted() {
            completedEvent.Set();
        }

        public void OnError(Exception error) {
            //TODO
        }

        public void OnNext(T value) {
            //TODO
        }
    }

そして、シグナル、完了(すべてのソースアイテムの枯渇)、またはその両方を待つことができます

 source.Completion.ContinueWith(async _ => {

            WaitHandle.WaitAll(completedEvent, signal);
            // Or WaitHandle.WaitAny, depending on your needs!

            target.Complete();
        });

WaitAllの結果値を調べて、どのイベントが設定されたかを理解し、それに応じて対応することができます。他のイベントをコードに追加してオブザーバーに渡すこともできるので、必要なときにそれらを設定できます。たとえば、エラーが発生したときに、動作を区別して異なる応答をすることができます。

1

たぶん ManualResetEvent があなたのためにトリックをすることができます。

パブリックプロパティをTransformManyBlockに追加します

private ManualResetEvent _signal  = new ManualResetEvent(false);
public ManualResetEvent Signal { get { return _signal; } }

そして、ここに行きます:

var retries = new HashSet<RetryingMessage<TInput>>();

TransformManyBlock<RetryableMessage<TInput>, TOutput> target = null;
target = new TransformManyBlock<RetryableMessage<TInput>, TOutput>(
    async message =>
    {
        try
        {
            var result = new[] { await transform(message.Data) };
            retries.Remove(message);

            // Sets the state of the event to signaled, allowing one or more waiting threads to proceed
            if(!retries.Any()) Signal.Set(); 
            return result;
        }
        catch (Exception ex)
        {
            message.Exceptions.Add(ex);
            if (message.RetriesRemaining == 0)
            {
                if (failureHandler != null)
                    failureHandler(message.Exceptions);

                retries.Remove(message);

                // Sets the state of the event to signaled, allowing one or more waiting threads to proceed
                if(!retries.Any()) Signal.Set(); 
            }
            else
            {
                retries.Add(message);
                message.RetriesRemaining--;

                Task.Delay(retryDelay)
                    .ContinueWith(_ => target.Post(message));
            }
            return null;
        }
    }, dataflowBlockOptions);

source.LinkTo(target);

source.Completion.ContinueWith(async _ =>
{
    //Blocks the current thread until the current WaitHandle receives a signal.
    target.Signal.WaitOne();

    target.Complete();
});

どこにいるのかわかりませんtarget.InputCountが設定されます。だからあなたが変える場所でtarget.InputCount次のコードを追加できます。

if(InputCount == 0)  Signal.Set();
2
hwcverwe