Как реализовать поддержку CancellationToken в DropNet?

Я хочу получить асинхронный доступ к API DropBox в приложении MonoTouch.
Я подумал, что было бы удобно использовать DropNet, который сам полагается на RestSharp.

Обе библиотеки работают хорошо, но перегрузки DropNet, возвращающие Task, не дают возможности связать запросы с токенами отмены.

Вот как выглядит их реализация:

public Task<IRestResponse> GetThumbnailTask(string path, ThumbnailSize size)
{
    if (!path.StartsWith("/")) path = "/" + path;
    var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
    return ExecuteTask(ApiType.Content, request, cancel);
}

ExecuteTask основана на TaskCompletionSource и была изначально автор Лорен Кемпе:

public static class RestClientExtensions
{
    public static Task<TResult> ExecuteTask<TResult>(
        this IRestClient client, IRestRequest request
        ) where TResult : new()
    {
        var tcs = new TaskCompletionSource<TResult>();

        WaitCallback asyncWork = _ => {
            try {
                client.ExecuteAsync<TResult>(request,
                    (response, asynchandle) => {
                        if (response.StatusCode != HttpStatusCode.OK) {
                            tcs.SetException(new DropboxException(response));
                        } else {
                            tcs.SetResult(response.Data);
                        }
                    }
                );
            } catch (Exception exc) {
                    tcs.SetException(exc);
            }
        };

        return ExecuteTask(asyncWork, tcs);
    }


    public static Task<IRestResponse> ExecuteTask(
        this IRestClient client, IRestRequest request
        )
    {
        var tcs = new TaskCompletionSource<IRestResponse>();

        WaitCallback asyncWork = _ => {
            try {
                client.ExecuteAsync(request,
                    (response, asynchandle) => {
                        if (response.StatusCode != HttpStatusCode.OK) {
                            tcs.SetException(new DropboxException(response));
                        } else {
                            tcs.SetResult(response);
                        }
                    }
                );
            } catch (Exception exc) {
                    tcs.SetException(exc);
            }
        };

        return ExecuteTask(asyncWork, tcs);
   }

    private static Task<TResult> ExecuteTask<TResult>(
        WaitCallback asyncWork, TaskCompletionSource<TResult> tcs
        )
    {
        ThreadPool.QueueUserWorkItem(asyncWork);
        return tcs.Task;
    }
}

Как изменить или расширить этот код, чтобы он поддерживал отмену с помощью CancellationToken?
Я хотел бы назвать его так:

var task = dropbox.GetThumbnailTask(
    "/test.jpg", ThumbnailSize.ExtraLarge2, _token
);

person Dan Abramov    schedule 12.11.2012    source источник


Ответы (1)


Поскольку CancellationToken является типом значения, мы можем добавить его как необязательный параметр в API со значением по умолчанию и избежать проверок null, что приятно.

public Task<IRestResponse> GetThumbnailTask(
    string path, ThumbnailSize size, CancellationToken cancel = default(CancellationToken)
) {
    if (!path.StartsWith("/")) path = "/" + path;
    var request = _requestHelper.CreateThumbnailRequest(path, size, Root);
    return ExecuteTask(ApiType.Content, request, cancel);
}

Теперь метод RestSharp ExecuteAsync возвращает RestRequestAsyncHandle, который инкапсулирует базовый HttpWebRequest вместе с методом Abort. Вот как мы отменяем вещи.

public static Task<TResult> ExecuteTask<TResult>(
    this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken)
    ) where TResult : new()
{
    var tcs = new TaskCompletionSource<TResult>();
    try {
        var async = client.ExecuteAsync<TResult>(request, (response, _) => {
            if (cancel.IsCancellationRequested || response == null)
                return;

            if (response.StatusCode != HttpStatusCode.OK) {
                tcs.TrySetException(new DropboxException(response));
            } else {
                tcs.TrySetResult(response.Data);
            }
        });

        cancel.Register(() => {
            async.Abort();
            tcs.TrySetCanceled();
        });
    } catch (Exception ex) {
        tcs.TrySetException(ex);
    }

    return tcs.Task;
}

public static Task<IRestResponse> ExecuteTask(this IRestClient client, IRestRequest request, CancellationToken cancel = default(CancellationToken))
{
    var tcs = new TaskCompletionSource<IRestResponse>();
    try {
        var async = client.ExecuteAsync<IRestResponse>(request, (response, _) => {
            if (cancel.IsCancellationRequested || response == null)
                return;

            if (response.StatusCode != HttpStatusCode.OK) {
                tcs.TrySetException(new DropboxException(response));
            } else {
                tcs.TrySetResult(response);
            }
        });

        cancel.Register(() => {
            async.Abort();
            tcs.TrySetCanceled();
        });
    } catch (Exception ex) {
        tcs.TrySetException(ex);
    }

    return tcs.Task;
}

Наконец, реализация Лорен помещает запросы в пуле потоков, но я не вижу причин для этого — ExecuteAsync сам по себе асинхронен. Так что я этого не делаю.

И это все для отмены операций DropNet.

Я также сделал несколько настроек, которые могут быть вам полезны.

Поскольку у меня не было возможности планировать Tasks DropBox, не прибегая к переносу ExecuteTask вызовов в другие Task, я решил найти оптимальный уровень параллелизма для запросов, который для меня оказался 4, и установить его явно:

static readonly Uri DropboxContentHost = new Uri("https://api-content.dropbox.com");

static DropboxService()
{
    var point = ServicePointManager.FindServicePoint(DropboxContentHost);
    point.ConnectionLimit = 4;
}

Я также был доволен тем, что оставил необработанные исключения задач гнить в аду, так что я сделал следующее:

TaskScheduler.UnobservedTaskException += (sender, e) => {
    e.SetObserved();
};

Последнее наблюдение: вы не должны вызывать Start() для задачи, возвращаемой DropNet, потому что задача запускается сразу. Если вам это не нравится, вам придется обернуть ExecuteTask еще одной «настоящей» задачей, не подкрепленной TaskCompletionSource.

person Dan Abramov    schedule 12.11.2012