Exemple #1
0
        public virtual void Run()
        {
            running = true;
            HttpClient httpClient;

            if (client == null)
            {
                // This is a race condition that can be reproduced by calling cbpuller.start() and cbpuller.stop()
                // directly afterwards.  What happens is that by the time the Changetracker thread fires up,
                // the cbpuller has already set this.client to null.  See issue #109
                Log.W(Database.Tag, "ChangeTracker run() loop aborting because client == null");
                return;
            }
            if (mode == ChangeTracker.ChangeTrackerMode.Continuous)
            {
                // there is a failing unit test for this, and from looking at the code the Replication
                // object will never use Continuous mode anyway.  Explicitly prevent its use until
                // it is demonstrated to actually work.
                throw new RuntimeException("ChangeTracker does not correctly support continuous mode"
                                           );
            }
            httpClient = client.GetHttpClient();
            ChangeTrackerBackoff backoff = new ChangeTrackerBackoff();

            while (running)
            {
                Uri url = GetChangesFeedURL();
                request = new HttpRequestMessage(url.ToString());
                AddRequestHeaders(request);
                // if the URL contains user info AND if this a DefaultHttpClient
                // then preemptively set the auth credentials
                if (url.GetUserInfo() != null)
                {
                    Log.V(Database.Tag, "url.getUserInfo(): " + url.GetUserInfo());
                    if (url.GetUserInfo().Contains(":") && !url.GetUserInfo().Trim().Equals(":"))
                    {
                        string[] userInfoSplit = url.GetUserInfo().Split(":");
                        throw new NotImplementedException();
//						Credentials creds = new UsernamePasswordCredentials(URIUtils.Decode(userInfoSplit
//							[0]), URIUtils.Decode(userInfoSplit[1]));
//						if (httpClient is DefaultHttpClient)
//						{
//							DefaultHttpClient dhc = (DefaultHttpClient)httpClient;
//							MessageProcessingHandler preemptiveAuth = new _MessageProcessingHandler_212(creds
//								);
//                            dhc.AddRequestInterceptor((HttpWebRequest request, HttpContext context)=>
//                                {
//                                    AuthState authState = (AuthState)context.GetAttribute(ClientContext.TargetAuthState
//                                    );
//                                    CredentialsProvider credsProvider = (CredentialsProvider)context.GetAttribute(ClientContext
//                                        .CredsProvider);
//                                    HttpHost targetHost = (HttpHost)context.GetAttribute(ExecutionContext.HttpTargetHost
//                                    );
//                                    if (authState.GetAuthScheme() == null)
//                                    {
//                                        AuthScope authScope = new AuthScope(targetHost.GetHostName(), targetHost.GetPort(
//                                        ));
//                                        authState.SetAuthScheme(new BasicScheme());
//                                        authState.SetCredentials(creds);
//                                    }
//                                }, 0);
//						}
                    }
                    else
                    {
                        Log.W(Database.Tag, "ChangeTracker Unable to parse user info, not setting credentials"
                              );
                    }
                }
                try
                {
                    string maskedRemoteWithoutCredentials = GetChangesFeedURL().ToString();
                    maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials.ReplaceAll("://.*:.*@"
                                                                                               , "://---:---@");
                    Log.V(Database.Tag, "Making request to " + maskedRemoteWithoutCredentials);
                    HttpResponse response = httpClient.Execute(request);
                    StatusLine   status   = response.GetStatusLine();
                    if (status.GetStatusCode() >= 300)
                    {
                        Log.E(Database.Tag, "Change tracker got error " + Sharpen.Extensions.ToString(status
                                                                                                      .GetStatusCode()));
                        string msg = string.Format(status.ToString());
                        this.error = new CouchbaseLiteException(msg, new Status(status.GetStatusCode()));
                        Stop();
                    }
                    HttpEntity  entity = response.GetEntity();
                    InputStream input  = null;
                    if (entity != null)
                    {
                        input = entity.GetContent();
                        if (mode == ChangeTracker.ChangeTrackerMode.LongPoll)
                        {
                            IDictionary <string, object> fullBody = Manager.GetObjectMapper().ReadValue <IDictionary
                                                                                                         >(input);
                            bool responseOK = ReceivedPollResponse(fullBody);
                            if (mode == ChangeTracker.ChangeTrackerMode.LongPoll && responseOK)
                            {
                                Log.V(Database.Tag, "Starting new longpoll");
                                continue;
                            }
                            else
                            {
                                Log.W(Database.Tag, "Change tracker calling stop");
                                Stop();
                            }
                        }
                        else
                        {
                            JsonFactory jsonFactory = Manager.GetObjectMapper().GetJsonFactory();
                            JsonParser  jp          = jsonFactory.CreateJsonParser(input);
                            while (jp.CurrentToken() != JsonToken.StartArray)
                            {
                            }
                            // ignore these tokens
                            while (jp.CurrentToken() == JsonToken.StartObject)
                            {
                                IDictionary <string, object> change = (IDictionary)Manager.GetObjectMapper().ReadValue
                                                                      <IDictionary>(jp);
                                if (!ReceivedChange(change))
                                {
                                    Log.W(Database.Tag, string.Format("Received unparseable change line from server: %s"
                                                                      , change));
                                }
                            }
                            Stop();
                            break;
                        }
                        backoff.ResetBackoff();
                    }
                }
                catch (Exception e)
                {
                    if (!running && e is IOException)
                    {
                    }
                    else
                    {
                        // in this case, just silently absorb the exception because it
                        // frequently happens when we're shutting down and have to
                        // close the socket underneath our read.
                        Log.E(Database.Tag, "Exception in change tracker", e);
                    }
                    backoff.SleepAppropriateAmountOfTime();
                }
            }
            Log.V(Database.Tag, "Change tracker run loop exiting");
        }
        // TODO: Needs to refactored into smaller calls. Each continuation could be its own method, for example.
        public void Run()
        {
            running = true;
            HttpClient httpClient;

            if (client == null)
            {
                // This is a race condition that can be reproduced by calling cbpuller.start() and cbpuller.stop()
                // directly afterwards.  What happens is that by the time the Changetracker thread fires up,
                // the cbpuller has already set this.client to null.  See issue #109
                Log.W(Tag, this + ": ChangeTracker run() loop aborting because client == null");
                return;
            }
            if (mode == ChangeTracker.ChangeTrackerMode.Continuous)
            {
                // there is a failing unit test for this, and from looking at the code the Replication
                // object will never use Continuous mode anyway.  Explicitly prevent its use until
                // it is demonstrated to actually work.
                throw new RuntimeException("ChangeTracker does not correctly support continuous mode");
            }

            httpClient = client.GetHttpClient();
            backoff    = new ChangeTrackerBackoff();

            var shouldBreak = false;

            while (running)
            {
                if (tokenSource.Token.IsCancellationRequested)
                {
                    break;
                }

                var url = GetChangesFeedURL();
                Request = new HttpRequestMessage(HttpMethod.Get, url);

                AddRequestHeaders(Request);

                // if the URL contains user info AND if this a DefaultHttpClient
                // then preemptively set/update the auth credentials
                if (url.UserInfo != null)
                {
                    Log.V(Tag, "url.getUserInfo(): " + url.GetUserInfo());

                    var credentials = Request.ToCredentialsFromUri();
                    if (credentials != null)
                    {
                        var handler = client.HttpHandler;
                        if (handler.Credentials == null || !handler.Credentials.Equals(credentials))
                        {
                            client.HttpHandler.Credentials = credentials;
                        }
                    }
                    else
                    {
                        Log.W(Tag, this + ": ChangeTracker Unable to parse user info, not setting credentials");
                    }
                }

                try
                {
                    var requestStatus = CurrentRequest == null
                        ? TaskStatus.Canceled
                        : CurrentRequest.Status;

                    Log.V(Tag, this + ": Current Request Status: " + requestStatus);

                    if (requestStatus == TaskStatus.Running || requestStatus == TaskStatus.WaitingForActivation)
                    {
                        //System.Threading.Thread.Sleep(5000);
                        continue;
                    }
                    var maskedRemoteWithoutCredentials = GetChangesFeedURL().ToString();
                    maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials.ReplaceAll("://.*:.*@", "://---:---@");
                    Log.V(Tag, this + ": Making request to " + maskedRemoteWithoutCredentials);
                    if (tokenSource.Token.IsCancellationRequested)
                    {
                        break;
                    }
                    CurrentRequest = httpClient.SendAsync(Request)
                                     .ContinueWith <HttpResponseMessage>(request =>
                    {
                        if (request.Status != System.Threading.Tasks.TaskStatus.RanToCompletion && request.IsFaulted)
                        {
                            Log.E(Tag, this + ": Change tracker got error " + Extensions.ToString(request.Status));
                            throw request.Exception;
                        }
                        return(request.Result);
                    }, this.tokenSource.Token)
                                     .ContinueWith <Task <Byte[]> >((request) =>
                    {
                        var status = request.Result.StatusCode;
                        if ((Int32)status >= 300)
                        {
                            var msg = String.Format("Change tracker got error: {0}", status);
                            Log.E(Tag, msg);
                            Error = new CouchbaseLiteException(msg, new Status(status.GetStatusCode()));
                            Stop();
                        }
                        return(request.Result.Content.ReadAsByteArrayAsync());
                    }, this.tokenSource.Token)
                                     .ContinueWith((Task <Task <Byte[]> > response) =>
                    {
                        if (response.Status != System.Threading.Tasks.TaskStatus.RanToCompletion &&
                            !response.IsFaulted &&
                            response.Result != null)
                        {
                            return;
                        }

                        var result = response.Result.Result;

                        if (mode == ChangeTrackerMode.LongPoll)
                        {
                            var fullBody   = Manager.GetObjectMapper().ReadValue <IDictionary <string, object> >(result.AsEnumerable());
                            var responseOK = ReceivedPollResponse(fullBody);
                            if (mode == ChangeTracker.ChangeTrackerMode.LongPoll && responseOK)
                            {
                                Log.V(Tag, this + ": Starting new longpoll");
                                backoff.ResetBackoff();
                                return;
                            }
                            else
                            {
                                Log.W(Tag, this + ": Change tracker calling stop");
                                Stop();
                            }
                        }
                        else
                        {
                            var results      = Manager.GetObjectMapper().ReadValue <IDictionary <String, Object> >(result.AsEnumerable());
                            var resultsValue = results["results"] as Newtonsoft.Json.Linq.JArray;
                            foreach (var item in resultsValue)
                            {
                                IDictionary <String, Object> change = null;
                                try {
                                    change = item.ToObject <IDictionary <String, Object> >();
                                } catch (Exception) {
                                    Log.E(Tag, this + string.Format(": Received unparseable change line from server: {0}", change));
                                }
                                if (!ReceivedChange(change))
                                {
                                    Log.W(Tag, this + string.Format(": Received unparseable change line from server: {0}", change));
                                }
                            }
                            Stop();
                            shouldBreak = true;
                            return;
                        }
                        backoff.ResetBackoff();
                    }, tokenSource.Token);
                }
                catch (Exception e)
                {
                    if (!running && e is IOException)
                    {
                        // swallow
                    }
                    else
                    {
                        // in this case, just silently absorb the exception because it
                        // frequently happens when we're shutting down and have to
                        // close the socket underneath our read.
                        Log.E(Tag, this + ": Exception in change tracker", e);
                    }
                    backoff.SleepAppropriateAmountOfTime();
                }
                if (shouldBreak)
                {
                    break;
                }
            }
            if (!tokenSource.Token.IsCancellationRequested)
            {   // Handle cancellation requests while we are waiting.
                // e.g. when Stop() is called from another thread.
                try {
                    CurrentRequest.Wait(tokenSource.Token);
                } catch (Exception) {
                    Log.V(Tag, this + ": Run loop was cancelled.");
                }
            }
            Log.V(Tag, this + ": Change tracker run loop exiting");
        }
        // TODO: Needs to refactored into smaller calls. Each continuation could be its own method, for example.
        public void Run()
        {
            IsRunning = true;

            if (client == null)
            {
                // This is a race condition that can be reproduced by calling cbpuller.start() and cbpuller.stop()
                // directly afterwards.  What happens is that by the time the Changetracker thread fires up,
                // the cbpuller has already set this.client to null.  See issue #109
                Log.W(Tag, "ChangeTracker run() loop aborting because client == null");
                return;
            }

            if (tokenSource.IsCancellationRequested)
            {
                tokenSource.Dispose();
                tokenSource = new CancellationTokenSource();
            }

            backoff = new ChangeTrackerBackoff();

            while (IsRunning && !tokenSource.Token.IsCancellationRequested)
            {
//                if (changesRequestTask != null && !changesRequestTask.IsCanceled && !changesRequestTask.IsFaulted)
//                {
//                    Thread.Sleep(500);
//                    continue;
//                }
                var httpClient = client.GetHttpClient();

                if (Request != null)
                {
                    Request.Dispose();
                    Request = null;
                }

                var url = GetChangesFeedURL();
                if (UsePost)
                {
                    Request = new HttpRequestMessage(HttpMethod.Post, url);
                    var body = GetChangesFeedPostBody();
                    Request.Content = new StringContent(body);
                    Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                }
                else
                {
                    Request = new HttpRequestMessage(HttpMethod.Get, url);
                }

                AddRequestHeaders(Request);

                var authHeader = AuthUtils.GetAuthenticationHeaderValue(Authenticator, Request.RequestUri);
                if (authHeader != null)
                {
                    httpClient.DefaultRequestHeaders.Authorization = authHeader;
                }

                var maskedRemoteWithoutCredentials = url.ToString();
                maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials.ReplaceAll("://.*:.*@", "://---:---@");
                Log.V(Tag, "Making request to " + maskedRemoteWithoutCredentials);

                if (tokenSource.Token.IsCancellationRequested)
                {
                    break;
                }

                Task <HttpResponseMessage> changesRequestTask = null;
                Task <HttpResponseMessage> successHandler;
                Task <Boolean>             errorHandler;

                try {
                    changesFeedRequestTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token);

                    var evt = new ManualResetEvent(false);
                    //successHandler.ConfigureAwait(false).GetAwaiter().OnCompleted(()=>evt.Set());
                    //                    ChangeFeedResponseHandler(response);


                    var info = httpClient.SendAsync(
                        Request,
                        HttpCompletionOption.ResponseContentRead,
                        changesFeedRequestTokenSource.Token
                        );
                    var infoAwaiter = info.ConfigureAwait(false).GetAwaiter();
                    infoAwaiter.OnCompleted(() =>
                                            evt.Set()
                                            );
                    evt.WaitOne(ManagerOptions.Default.RequestTimeout);

                    changesRequestTask = info; //Task.FromResult(info.Result);

                    successHandler = changesRequestTask.ContinueWith <HttpResponseMessage>(
                        ChangeFeedResponseHandler,
                        changesFeedRequestTokenSource.Token,
                        TaskContinuationOptions.LongRunning | TaskContinuationOptions.OnlyOnRanToCompletion,
                        WorkExecutor.Scheduler
                        );

                    errorHandler = changesRequestTask.ContinueWith(t =>
                    {
                        if (t.IsCanceled)
                        {
                            return(false); // Not a real error.
                        }
                        var err = t.Exception.Flatten();
                        Log.D(Tag, "ChangeFeedResponseHandler faulted.", err.InnerException ?? err);
                        Error = err.InnerException ?? err;
                        return(true); // a real error.
                    }, changesFeedRequestTokenSource.Token, TaskContinuationOptions.OnlyOnFaulted, WorkExecutor.Scheduler);

                    try {
                        var completedTask = Task.WhenAll(successHandler, errorHandler);
                        completedTask.Wait((Int32)ManagerOptions.Default.RequestTimeout.TotalMilliseconds, changesFeedRequestTokenSource.Token);
                        Log.D(Tag, "Finished processing changes feed.");
                    } catch (Exception ex) {
                        // Swallow TaskCancelledExceptions, which will always happen
                        // if either errorHandler or successHandler don't need to fire.
                        if (!(ex.InnerException is TaskCanceledException))
                        {
                            throw ex;
                        }
                    } finally {
                        changesRequestTask.Dispose();
                        changesRequestTask = null;
                        successHandler.Dispose();
                        successHandler = null;
                        errorHandler.Dispose();
                        errorHandler = null;
                        Request.Dispose();
                        Request = null;
                        changesFeedRequestTokenSource.Dispose();
                        if (httpClient != null)
                        {
                            httpClient.Dispose();
                        }
                    }
                }
                catch (Exception e)
                {
                    if (!IsRunning && e.InnerException is IOException)
                    {
                        // swallow
                    }
                    else
                    {
                        // in this case, just silently absorb the exception because it
                        // frequently happens when we're shutting down and have to
                        // close the socket underneath our read.
                        Log.E(Tag, "Exception in change tracker", e);
                    }
                    backoff.SleepAppropriateAmountOfTime();
                }
                finally
                {
                    if (mode == ChangeTrackerMode.OneShot)
                    {
                        Stop();
                    }
                }
//                var singleRequestTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token);
//                var cTask = httpClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, singleRequestTokenSource.Token);
//                var bTask = cTask
//                    .ContinueWith<HttpResponseMessage>(t =>
//                    {
//                        if (!IsRunning)
//                        {
//                            return null;
//                            // swallow
//                        }
//                        if (t.IsFaulted && t.Exception.InnerException is IOException)
//                        {
//                            // in this case, just silently absorb the exception because it
//                            // frequently happens when we're shutting down and have to
//                            // close the socket underneath our read.
//                            Log.E(Tag, "Exception in change tracker", t.Exception);
//                            return null;
//                        }
//                        if (!singleRequestTokenSource.IsCancellationRequested && t.Exception != null)
//                        {
//                            var e = t.Exception.InnerException as WebException;
//                            var status = (HttpStatusCode)e.Status;
//                            if ((Int32)status >= 300 && !Misc.IsTransientError(status))
//                            {
//                                var response = t.Result;
//                                var msg = response.Content != null
//                                    ? String.Format("Change tracker got error with status code: {0}", status)
//                                    : String.Format("Change tracker got error with status code: {0} and null response content", status);
//                                Log.E(Tag, msg);
//                                Error = new CouchbaseLiteException(msg, new Status(status.GetStatusCode()));
//                                Stop();
//                            }
//                            backoff.SleepAppropriateAmountOfTime();
//                        }
//                        return t.Result;
//                    }, singleRequestTokenSource.Token, TaskContinuationOptions.OnlyOnFaulted, WorkExecutor.Scheduler)
//                    .ContinueWith<HttpResponseMessage>(ChangeFeedResponseHandler, singleRequestTokenSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, WorkExecutor.Scheduler)
//                    .ContinueWith<HttpResponseMessage>(t =>
//                    {
//                        Log.D(Tag, "ChangeFeedResponseHandler finished.");
//                        singleRequestTokenSource.Token.ThrowIfCancellationRequested();
//                        if (t != null) t.Result.Dispose();
//                        return null;
//                    }, singleRequestTokenSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, WorkExecutor.Scheduler);
//                changesRequestTask = bTask;
//                    .ContinueWith((t) =>
//                    {
//                        Log.D(Tag, "ChangeFeedResponseHandler faulted.");
//                    }, singleRequestTokenSource.Token, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent, TaskScheduler.Default);
            }
        }
        // TODO: Needs to refactored into smaller calls. Each continuation could be its own method, for example.
        public void Run()
        {
            IsRunning = true;

            if (client == null)
            {
                // This is a race condition that can be reproduced by calling cbpuller.start() and cbpuller.stop()
                // directly afterwards.  What happens is that by the time the Changetracker thread fires up,
                // the cbpuller has already set this.client to null.  See issue #109
                Log.W(Tag, "ChangeTracker run() loop aborting because client == null");
                return;
            }

            if (tokenSource.IsCancellationRequested)
            {
                tokenSource.Dispose();
                tokenSource = new CancellationTokenSource();
            }

            backoff = new ChangeTrackerBackoff();

            while (IsRunning && !tokenSource.Token.IsCancellationRequested)
            {
                if (Request != null)
                {
                    Request.Dispose();
                    Request = null;
                }

                var url = GetChangesFeedURL();
                if (UsePost)
                {
                    Request = new HttpRequestMessage(HttpMethod.Post, url);
                    var body = GetChangesFeedPostBody();
                    Request.Content = new StringContent(body);
                    Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                }
                else
                {
                    Request = new HttpRequestMessage(HttpMethod.Get, url);
                }

                AddRequestHeaders(Request);

                var maskedRemoteWithoutCredentials = url.ToString();
                maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials.ReplaceAll("://.*:.*@", "://---:---@");
                Log.V(Tag, "Making request to " + maskedRemoteWithoutCredentials);

                if (tokenSource.Token.IsCancellationRequested)
                {
                    break;
                }

                Task <HttpResponseMessage> changesRequestTask = null;
                Task <HttpResponseMessage> successHandler;
                Task <Boolean>             errorHandler;

                HttpClient httpClient = null;
                try {
                    httpClient = client.GetHttpClient();
                    var authHeader = AuthUtils.GetAuthenticationHeaderValue(Authenticator, Request.RequestUri);
                    if (authHeader != null)
                    {
                        httpClient.DefaultRequestHeaders.Authorization = authHeader;
                    }

                    changesFeedRequestTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token);

                    var evt = new ManualResetEvent(false);

                    // We do this akward set of calls in order
                    // to help minimize the frequency of the error:
                    //
                    //   "Cannot re-call start of asynchronous method
                    //    while a previous call is still in progress."
                    //
                    // There's got to be a better way to deal with this.
                    var info = httpClient.SendAsync(
                        Request,
                        changesFeedRequestTokenSource.Token
                        );
                    var infoAwaiter = info.ConfigureAwait(false).GetAwaiter();
                    infoAwaiter.OnCompleted(() =>
                                            evt.Set()
                                            );
                    evt.WaitOne(ManagerOptions.Default.RequestTimeout);

                    changesRequestTask = info;

                    successHandler = changesRequestTask.ContinueWith <HttpResponseMessage>(
                        ChangeFeedResponseHandler,
                        changesFeedRequestTokenSource.Token,
                        TaskContinuationOptions.LongRunning | TaskContinuationOptions.OnlyOnRanToCompletion,
                        WorkExecutor.Scheduler
                        );

                    errorHandler = changesRequestTask.ContinueWith(t =>
                    {
                        if (t.IsCanceled)
                        {
                            return(false); // Not a real error.
                        }
                        var err = t.Exception.Flatten();
                        Log.D(Tag, "ChangeFeedResponseHandler faulted.", err.InnerException ?? err);
                        Error = err.InnerException ?? err;
                        backoff.SleepAppropriateAmountOfTime();
                        return(true); // a real error.
                    }, changesFeedRequestTokenSource.Token, TaskContinuationOptions.OnlyOnFaulted, WorkExecutor.Scheduler);

                    try
                    {
                        var completedTask = Task.WhenAll(successHandler, errorHandler);
                        completedTask.Wait((Int32)ManagerOptions.Default.RequestTimeout.TotalMilliseconds, changesFeedRequestTokenSource.Token);
                        Log.D(Tag, "Finished processing changes feed.");
                    }
                    catch (Exception ex) {
                        // Swallow TaskCancelledExceptions, which will always happen
                        // if either errorHandler or successHandler don't need to fire.
                        if (!(ex.InnerException is TaskCanceledException))
                        {
                            throw ex;
                        }
                    }
                    finally
                    {
                        if (changesRequestTask.IsCompleted)
                        {
                            changesRequestTask.Dispose();
                        }
                        changesRequestTask = null;

                        if (successHandler.IsCompleted)
                        {
                            successHandler.Dispose();
                        }

                        successHandler = null;

                        if (errorHandler.IsCompleted)
                        {
                            errorHandler.Dispose();
                        }

                        errorHandler = null;

                        Request.Dispose();
                        Request = null;

                        changesFeedRequestTokenSource.Dispose();
                        changesFeedRequestTokenSource = null;
                    }
                }
                catch (Exception e)
                {
                    if (!IsRunning && e.InnerException is IOException)
                    {
                        // swallow
                    }
                    else
                    {
                        // in this case, just silently absorb the exception because it
                        // frequently happens when we're shutting down and have to
                        // close the socket underneath our read.
                        Log.E(Tag, "Exception in change tracker", e);
                    }
                    backoff.SleepAppropriateAmountOfTime();
                }
                finally
                {
                    if (httpClient != null)
                    {
                        httpClient.Dispose();
                    }

                    if (mode == ChangeTrackerMode.OneShot)
                    {
                        Stop();
                    }
                }
            }
        }