/// <summary> /// Start polling the node-dss server with a GET request, and continue to do so /// until <see cref="StopPollingAsync"/> is called. /// </summary> /// <returns>Returns <c>true</c> if polling effectively started with this call.</returns> /// <remarks> /// The <see cref="LocalPeerId"/> field must be set before calling this method. /// This method can safely be called multiple times, and will do nothing if /// polling is already underway or waiting to be stopped. /// </remarks> public bool StartPollingAsync(CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(LocalPeerId)) { throw new InvalidOperationException("Cannot start polling with empty LocalId."); } lock (_pollingLock) { if (_isPolling) { return(false); } _isPolling = true; _cancellationTokenSource = new CancellationTokenSource(); } RaisePropertyChanged("IsPolling"); // Build the GET polling request string requestUri = $"{_httpServerAddress}data/{LocalPeerId}"; long lastPollTimeTicks = DateTime.UtcNow.Ticks; long pollTimeTicks = TimeSpan.FromMilliseconds(PollTimeMs).Ticks; var masterTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken); var masterToken = masterTokenSource.Token; masterToken.Register(() => { lock (_pollingLock) { _isPolling = false; _cancellationTokenSource.Dispose(); _cancellationTokenSource = null; } RaisePropertyChanged("IsPolling"); Interlocked.Exchange(ref _connectedEventFired, 0); OnDisconnect?.Invoke(); OnPollingDone?.Invoke(); masterTokenSource.Dispose(); }); // Prepare the repeating poll task. // In order to poll at the specified frequency but also avoid overlapping requests, // use a repeating task which re-schedule itself on completion, either immediately // if the polling delay is elapsed, or at a later time otherwise. async void PollServer() { try { // Polling loop while (true) { masterToken.ThrowIfCancellationRequested(); // Send GET request to DSS server. lastPollTimeTicks = DateTime.UtcNow.Ticks; HttpResponseMessage response = await _httpClient.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, masterToken); // On first successful HTTP request, raise the connected event if (0 == Interlocked.Exchange(ref _connectedEventFired, 1)) { OnConnect?.Invoke(); } masterToken.ThrowIfCancellationRequested(); // In order to avoid exceptions in GetStreamAsync() when the server returns a non-success status code (e.g. 404), // first get the HTTP headers, check the status code, then if successful wait for content. if (response.IsSuccessStatusCode) { string jsonMsg = await response.Content.ReadAsStringAsync(); masterToken.ThrowIfCancellationRequested(); var jsonSettings = new JsonSerializerSettings { Error = (object s, ErrorEventArgs e) => throw new Exception("JSON error: " + e.ErrorContext.Error.Message) }; Message msg = JsonConvert.DeserializeObject <Message>(jsonMsg, jsonSettings); if (msg != null) { OnMessage?.Invoke(msg); } else { throw new Exception("Failed to deserialize signaler message from JSON."); } } masterToken.ThrowIfCancellationRequested(); // Delay next loop iteration if current polling was faster than target poll duration long curTime = DateTime.UtcNow.Ticks; long deltaTicks = curTime - lastPollTimeTicks; long remainTicks = pollTimeTicks - deltaTicks; if (remainTicks > 0) { int waitTimeMs = new TimeSpan(remainTicks).Milliseconds; await Task.Delay(waitTimeMs); } } } catch (OperationCanceledException) { // Manual cancellation via UI, do not report error } catch (Exception ex) { OnFailure?.Invoke(ex); } } // Start the poll task immediately Task.Run(PollServer, masterToken); return(true); }
/// <summary> /// Start polling the node-dss server with a GET request, and continue to do so /// until <see cref="StopPollingAsync"/> is called. /// </summary> /// <returns>Returns <c>true</c> if polling effectively started with this call.</returns> /// <remarks> /// The <see cref="LocalPeerId"/> field must be set before calling this method. /// This method can safely be called multiple times, and will do nothing if /// polling is already underway or waiting to be stopped. /// </remarks> public bool StartPollingAsync() { if (string.IsNullOrWhiteSpace(LocalPeerId)) { throw new Exception("Cannot start polling with empty LocalId."); } if (Interlocked.CompareExchange(ref _isPolling, 1, 0) == 1) { // Already polling return(false); } // Build the GET polling request string requestUri = $"{_httpServerAddress}data/{LocalPeerId}"; long lastPollTimeTicks = DateTime.UtcNow.Ticks; long pollTimeTicks = TimeSpan.FromMilliseconds(PollTimeMs).Ticks; _cancellationTokenSource = new CancellationTokenSource(); var token = _cancellationTokenSource.Token; token.Register(() => { Interlocked.Exchange(ref _isPolling, 0); // TODO - Potentially a race condition here if StartPollingAsync() called, // but would be even worse with the exchange being after, as // StopPollingAsync() would attempt to read from it while being disposed. _cancellationTokenSource.Dispose(); _cancellationTokenSource = null; OnPollingDone?.Invoke(); }); // Prepare the repeating poll task. // In order to poll at the specified frequency but also avoid overlapping requests, // use a repeating task which re-schedule itself on completion, either immediately // if the polling delay is elapsed, or at a later time otherwise. Action nextAction = null; nextAction = () => { token.ThrowIfCancellationRequested(); // Send GET request to DSS server. // In order to avoid exceptions in GetStreamAsync() when the server returns a non-success status code (e.g. 404), // first get the HTTP headers, check the status code, then if successful wait for content. lastPollTimeTicks = DateTime.UtcNow.Ticks; _httpClient.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead).ContinueWith((getTask) => { if (getTask.Exception != null) { OnFailure?.Invoke(getTask.Exception); return; } token.ThrowIfCancellationRequested(); HttpResponseMessage response = getTask.Result; if (response.IsSuccessStatusCode) { response.Content.ReadAsStringAsync().ContinueWith((readTask) => { if (readTask.Exception != null) { OnFailure?.Invoke(readTask.Exception); return; } token.ThrowIfCancellationRequested(); var jsonMsg = readTask.Result; var msg = JsonConvert.DeserializeObject <Message>(jsonMsg); if (msg != null) { OnMessage?.Invoke(msg); } else { OnFailure?.Invoke(new Exception($"Failed to deserialize SignalerMessage object from JSON.")); } }); } // Some time may have passed waiting for GET content; check token again for responsiveness token.ThrowIfCancellationRequested(); // Repeat task to continue polling long curTime = DateTime.UtcNow.Ticks; long deltaTicks = curTime - lastPollTimeTicks; if (deltaTicks >= pollTimeTicks) { // Previous GET task took more time than polling delay, execute ASAP Task.Run(nextAction, token); } else { // Previous GET task took less time than polling delay, schedule next polling long remainTicks = pollTimeTicks - deltaTicks; int nextScheduleTimeMs = (int)(new TimeSpan(remainTicks).TotalMilliseconds); Task.Delay(nextScheduleTimeMs, token).ContinueWith(_ => nextAction(), token); } }); }; // Start the first poll task immediately Task.Run(nextAction, token); return(true); }