public virtual async Task <ScheduledCallbackState> ScheduleCallbackAsync(ScheduledCallback callback) { // the next execution time is computed from the time the current method is called, as the // caller may hang on to the ScheduledCallback for some time before calling the current method. // we set the time here, before adding it to the collection below, so that any callback // in the collection will certainly have a next execution time. callback.NextExecution = DateTime.Now + callback.Delay; if (callback.State != ScheduledCallbackState.Created) { SensusException.Report("Attemped to schedule callback " + callback.Id + ", which is in the " + callback.State + " state and not the " + ScheduledCallbackState.Created + " state."); callback.State = ScheduledCallbackState.Unknown; } else if (_idCallback.TryAdd(callback.Id, callback)) { callback.InvocationId = Guid.NewGuid().ToString(); BatchNextExecutionWithToleratedDelay(callback); // the state needs to be updated after batching is performed, so that other callbacks don't // attempt to batch with it, but before the invocations are requested, so that if the invocation // comes back immediately (e.g., being scheduled in the past) the callback is scheduled and // ready to run. callback.State = ScheduledCallbackState.Scheduled; await RequestLocalInvocationAsync(callback); } else { SensusServiceHelper.Get().Logger.Log("Attempted to schedule duplicate callback for " + callback.Id + ".", LoggingLevel.Normal, GetType()); } return(callback.State); }
/// <summary> /// Batches the <see cref="ScheduledCallback.NextExecution"/> value within the parameters of toleration (<see cref="ScheduledCallback.DelayToleranceBefore"/> /// and <see cref="ScheduledCallback.DelayToleranceAfter"/>), given the <see cref="ScheduledCallback"/>s that are already scheduled to run. /// </summary> /// <param name="callback">Callback.</param> private void BatchNextExecutionWithToleratedDelay(ScheduledCallback callback) { callback.Batched = false; // if delay tolerance is allowed, look for other scheduled callbacks in range of the delay tolerance. if (callback.DelayToleranceTotal.Ticks > 0) { DateTime rangeStart = callback.NextExecution.Value - callback.DelayToleranceBefore; DateTime rangeEnd = callback.NextExecution.Value + callback.DelayToleranceAfter; ScheduledCallback closestCallbackInRange = _idCallback.Values.Where(existingCallback => existingCallback != callback && // the current callback will already have been added to the collection. don't consider it. existingCallback.NextExecution.Value >= rangeStart && // consider callbacks within range of the current existingCallback.NextExecution.Value <= rangeEnd && // consider callbacks within range of the current !existingCallback.Batched && // don't consider batching with other callbacks that are themselves batched, as this can potentially create batch cycling if the delay tolerance values are large. existingCallback.State == ScheduledCallbackState.Scheduled) // consider callbacks that are already scheduled. we don't want to batch with callbacks that are, e.g., running or recently completed. // get existing callback with execution time closest to the current callback's time .OrderBy(existingCallback => Math.Abs(callback.NextExecution.Value.Ticks - existingCallback.NextExecution.Value.Ticks)) // there might not be a callback within range .FirstOrDefault(); // use the closest if there is one in range if (closestCallbackInRange != null) { SensusServiceHelper.Get().Logger.Log("Batching callback " + callback.Id + ":" + Environment.NewLine + "\tCurrent time: " + callback.NextExecution + Environment.NewLine + "\tRange: " + rangeStart + " -- " + rangeEnd + Environment.NewLine + "\tNearest: " + closestCallbackInRange.Id + Environment.NewLine + "\tNew time: " + closestCallbackInRange.NextExecution, LoggingLevel.Normal, GetType()); callback.NextExecution = closestCallbackInRange.NextExecution; callback.Batched = true; } } }
public string ScheduleRepeatingCallback(ScheduledCallback callback, int initialDelayMS, int repeatDelayMS, bool repeatLag) { string callbackId = AddCallback(callback); ScheduleRepeatingCallback(callbackId, initialDelayMS, repeatDelayMS, repeatLag); return(callbackId); }
public string ScheduleOneTimeCallback(ScheduledCallback callback, int delayMS) { string callbackId = AddCallback(callback); ScheduleOneTimeCallback(callbackId, delayMS); return(callbackId); }
/// <summary> /// Unschedules the callback, first cancelling any executions that are currently running and then removing the callback from the scheduler. /// </summary> /// <param name="callback">Callback.</param> public async Task UnscheduleCallbackAsync(ScheduledCallback callback) { if (callback == null) { return; } SensusServiceHelper.Get().Logger.Log("Unscheduling callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); // interrupt any current executions CancelRaisedCallback(callback); // remove from the scheduler _idCallback.TryRemove(callback.Id, out ScheduledCallback removedCallback); CancelLocalInvocation(callback); #if __IOS__ await CancelRemoteInvocationAsync(callback); #else await Task.CompletedTask; #endif SensusServiceHelper.Get().Logger.Log("Unscheduled callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); }
private string AddCallback(ScheduledCallback callback) { // treat the callback as if it were brand new, even if it might have been previously used (e.g., if it's being reschedueld). set a // new ID and cancellation token. callback.Id = Guid.NewGuid().ToString(); callback.Canceller = new CancellationTokenSource(); _idCallback.TryAdd(callback.Id, callback); return(callback.Id); }
/// <summary> /// Unschedules the callback. /// </summary> /// <returns>Task.</returns> /// <param name="id">Identifier.</param> public async Task UnscheduleCallbackAsync(string id) { ScheduledCallback callback = TryGetCallback(id); if (callback != null) { await UnscheduleCallbackAsync(callback); } }
public bool ScheduleOneTimeCallback(ScheduledCallback callback, TimeSpan delay) { if (!_idCallback.TryAdd(callback.Id, callback)) { return(false); } ScheduleOneTimeCallbackPlatformSpecific(callback.Id, delay); return(true); }
public bool ScheduleRepeatingCallback(ScheduledCallback callback, TimeSpan initialDelay, TimeSpan repeatDelay, bool repeatLag) { if (!_idCallback.TryAdd(callback.Id, callback)) { return(false); } ScheduleRepeatingCallbackPlatformSpecific(callback.Id, initialDelay, repeatDelay, repeatLag); return(true); }
/// <summary> /// Cancels a callback that has been raised and is currently executing. /// </summary> /// <param name="callback">Callback.</param> public void CancelRaisedCallback(ScheduledCallback callback) { if (callback == null) { return; } callback.Canceller.Cancel(); SensusServiceHelper.Get().Logger.Log("Cancelled callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); }
public bool ContainsCallback(ScheduledCallback callback) { // we should never get a null callback id, but it seems that we are from android. if (callback?.Id == null) { SensusException.Report("Attempted to check scheduling status of callback with null id."); return(false); } else { return(_idCallback.ContainsKey(callback.Id)); } }
/// <summary> /// Unschedules the callback, first cancelling any executions that are currently running and then removing the callback from the scheduler. /// </summary> /// <param name="callback">Callback.</param> public void UnscheduleCallback(ScheduledCallback callback) { if (callback != null) { SensusServiceHelper.Get().Logger.Log("Unscheduling callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); // interrupt any current executions CancelRaisedCallback(callback); // remove from the scheduler ScheduledCallback removedCallback; _idCallback.TryRemove(callback.Id, out removedCallback); // tell the current platform cancel its hook into the system's callback system UnscheduleCallbackPlatformSpecific(callback); SensusServiceHelper.Get().Logger.Log("Unscheduled callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); } }
public ScheduledCallbackState ScheduleCallback(ScheduledCallback callback) { if (callback.State != ScheduledCallbackState.Created) { SensusException.Report("Attemped to schedule callback " + callback.Id + ", which is in the " + callback.State + " state and not the " + ScheduledCallbackState.Created + " state."); callback.State = ScheduledCallbackState.Unknown; } else if (_idCallback.TryAdd(callback.Id, callback)) { callback.NextExecution = DateTime.Now + callback.Delay; callback.State = ScheduledCallbackState.Scheduled; ScheduleCallbackPlatformSpecific(callback); } else { SensusException.Report("Attempted to schedule duplicate callback for " + callback.Id + "."); } return(callback.State); }
/// <summary> /// Requests remote invocation for a <see cref="ScheduledCallback"/>, to be delivered in parallel with the /// local invocation loop on iOS. Only one of these (the remote or local) will ultimately be allowed to /// run -- whichever arrives first. /// </summary> /// <returns>Task.</returns> /// <param name="callback">Callback.</param> private async Task RequestRemoteInvocationAsync(ScheduledCallback callback) { // not all callbacks are associated with a protocol (e.g., the app-level health test). because push notifications are // currently tied to the remote data store of the protocol, we don't currently provide PNR support for such callbacks. // on race conditions, it might be the case that the system attempts to schedule a duplicate callback. if this happens // the duplicate will not be assigned a next execution, and the system will try to unschedule/delete it. skip such // callbacks below. if (callback.Protocol != null && callback.NextExecution.HasValue) { try { // the request id must differentiate the current device. furthermore, it needs to identify the // request as one for a callback. lastly, it needs to identify the particular callback that it // targets. the id does not include the callback invocation, as any newer requests for the // callback should obsolete older requests. string id = SensusServiceHelper.Get().DeviceId + "." + SENSUS_CALLBACK_KEY + "." + callback.Id; PushNotificationUpdate update = new PushNotificationUpdate { Type = PushNotificationUpdateType.Callback, Content = JObject.Parse("{" + "\"callback-id\":" + JsonConvert.ToString(callback.Id) + "," + "\"invocation-id\":" + JsonConvert.ToString(callback.InvocationId) + "}") }; PushNotificationRequest request = new PushNotificationRequest(id, SensusServiceHelper.Get().DeviceId, callback.Protocol, update, PushNotificationRequest.LocalFormat, callback.NextExecution.Value, callback.PushNotificationBackendKey); await SensusContext.Current.Notifier.SendPushNotificationRequestAsync(request, CancellationToken.None); } catch (Exception ex) { SensusException.Report("Exception while sending push notification request for scheduled callback: " + ex.Message, ex); } } }
/// <summary> /// Raises a callback. /// </summary> /// <returns>Async task</returns> /// <param name="callback">Callback to raise.</param> /// <param name="notifyUser">If set to <c>true</c>, then notify user that the callback is being raised.</param> /// <param name="scheduleRepeatCallback">Platform-specific action to execute to schedule the next execution of the callback.</param> /// <param name="letDeviceSleepCallback">Action to execute when the system should be allowed to sleep.</param> public virtual Task RaiseCallbackAsync(ScheduledCallback callback, bool notifyUser, Action scheduleRepeatCallback, Action letDeviceSleepCallback) { return(Task.Run(async() => { try { if (callback == null) { throw new NullReferenceException("Attemped to raise null callback."); } // the same callback cannot be run multiple times concurrently, so drop the current callback if it's already running. multiple // callers might compete for the same callback, but only one will win the lock below and it will exclude all others until the // the callback has finished executing. bool runCallbackNow = false; lock (callback) { if (callback.State == ScheduledCallbackState.Scheduled) { runCallbackNow = true; callback.State = ScheduledCallbackState.Running; } } if (runCallbackNow) { try { if (callback.Canceller.IsCancellationRequested) { throw new Exception("Callback " + callback.Id + " was cancelled before it was raised."); } else { SensusServiceHelper.Get().Logger.Log("Raising callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); if (notifyUser) { SensusContext.Current.Notifier.IssueNotificationAsync("Sensus", callback.UserNotificationMessage, callback.Id, callback.Protocol, true, callback.DisplayPage); } // if the callback specified a timeout, request cancellation at the specified time. if (callback.CallbackTimeout.HasValue) { callback.Canceller.CancelAfter(callback.CallbackTimeout.Value); } await callback.Action(callback.Id, callback.Canceller.Token, letDeviceSleepCallback); } } catch (Exception raiseException) { SensusException.Report("Callback " + callback.Id + " threw an exception: " + raiseException.Message, raiseException); } finally { // the cancellation token source for the current callback might have been canceled. if this is a repeating callback then we'll need a new // cancellation token source because they cannot be reset and we're going to use the same scheduled callback again for the next repeat. // if we enter the _idCallback lock before CancelRaisedCallback does, then the next raise will be cancelled. if CancelRaisedCallback enters the // _idCallback lock first, then the cancellation token source will be overwritten here and the cancel will not have any effect on the next // raise. the latter case is a reasonable outcome, since the purpose of CancelRaisedCallback is to terminate a callback that is currently in // progress, and the current callback is no longer in progress. if the desired outcome is complete discontinuation of the repeating callback // then UnscheduleRepeatingCallback should be used -- this method first cancels any raised callbacks and then removes the callback entirely. try { if (callback.RepeatDelay.HasValue) { callback.Canceller = new CancellationTokenSource(); } } catch (Exception ex) { SensusException.Report("Exception while assigning new callback canceller.", ex); } finally { callback.State = ScheduledCallbackState.Completed; // schedule callback again if it is still scheduled with a valid repeat delay if (ContainsCallback(callback) && callback.RepeatDelay.HasValue && callback.RepeatDelay.Value.Ticks > 0 && scheduleRepeatCallback != null) { // if this repeating callback is allowed to lag, schedule the next execution from the current time. if (callback.AllowRepeatLag.Value) { callback.NextExecution = DateTime.Now + callback.RepeatDelay.Value; } else { // otherwise, schedule the next execution from the time at which the current callback was supposed to be raised. callback.NextExecution = callback.NextExecution.Value + callback.RepeatDelay.Value; // if we've lagged so long that the next execution is already in the past, just reschedule for now. this will cause // the rescheduled callback to be raised as soon as possible, subject to delays in the systems scheduler (e.g., on // android most alarms do not come back immediately, even if requested). if (callback.NextExecution.Value < DateTime.Now) { callback.NextExecution = DateTime.Now; } } callback.State = ScheduledCallbackState.Scheduled; scheduleRepeatCallback(); } else { UnscheduleCallback(callback); } } } } else { throw new Exception("Callback " + callback.Id + " was already running. Not running again."); } } catch (Exception ex) { SensusException.Report("Failed to raise callback: " + ex.Message, ex); } })); }
public virtual Task RaiseCallbackAsync(string callbackId, bool repeating, TimeSpan repeatDelay, bool repeatLag, bool notifyUser, Action <DateTime> scheduleRepeatCallback, Action letDeviceSleepCallback) { DateTime callbackStartTime = DateTime.Now; return(Task.Run(async() => { try { ScheduledCallback scheduledCallback = null; // do we have callback information for the passed callbackId? we might not, in the case where the callback is canceled by the user and the system fires it subsequently. if (!_idCallback.TryGetValue(callbackId, out scheduledCallback)) { SensusServiceHelper.Get().Logger.Log("Callback " + callbackId + " is not valid. Unscheduling.", LoggingLevel.Normal, GetType()); UnscheduleCallback(callbackId); } if (scheduledCallback != null) { // the same callback action cannot be run multiple times concurrently. drop the current callback if it's already running. multiple // callers might compete for the same callback, but only one will win the lock below and it will exclude all others until it has executed. bool actionAlreadyRunning = true; lock (scheduledCallback) { if (!scheduledCallback.Running) { actionAlreadyRunning = false; scheduledCallback.Running = true; } } if (actionAlreadyRunning) { SensusServiceHelper.Get().Logger.Log("Callback \"" + scheduledCallback.Id + "\" is already running. Not running again.", LoggingLevel.Normal, GetType()); } else { try { if (scheduledCallback.Canceller.IsCancellationRequested) { SensusServiceHelper.Get().Logger.Log("Callback \"" + scheduledCallback.Id + "\" was cancelled before it was raised.", LoggingLevel.Normal, GetType()); } else { SensusServiceHelper.Get().Logger.Log("Raising callback \"" + scheduledCallback.Id + "\".", LoggingLevel.Normal, GetType()); if (notifyUser) { SensusContext.Current.Notifier.IssueNotificationAsync("Sensus", scheduledCallback.UserNotificationMessage, callbackId, scheduledCallback.ProtocolId, true, scheduledCallback.DisplayPage); } // if the callback specified a timeout, request cancellation at the specified time. if (scheduledCallback.CallbackTimeout.HasValue) { scheduledCallback.Canceller.CancelAfter(scheduledCallback.CallbackTimeout.Value); } await scheduledCallback.Action(callbackId, scheduledCallback.Canceller.Token, letDeviceSleepCallback); } } catch (Exception ex) { string errorMessage = "Callback \"" + scheduledCallback.Id + "\" failed: " + ex.Message; SensusServiceHelper.Get().Logger.Log(errorMessage, LoggingLevel.Normal, GetType()); SensusException.Report(errorMessage, ex); } finally { // the cancellation token source for the current callback might have been canceled. if this is a repeating callback then we'll need a new // cancellation token source because they cannot be reset and we're going to use the same scheduled callback again for the next repeat. // if we enter the _idCallback lock before CancelRaisedCallback does, then the next raise will be cancelled. if CancelRaisedCallback enters the // _idCallback lock first, then the cancellation token source will be overwritten here and the cancel will not have any effect on the next // raise. the latter case is a reasonable outcome, since the purpose of CancelRaisedCallback is to terminate a callback that is currently in // progress, and the current callback is no longer in progress. if the desired outcome is complete discontinuation of the repeating callback // then UnscheduleRepeatingCallback should be used -- this method first cancels any raised callbacks and then removes the callback entirely. try { if (repeating) { lock (_idCallback) { scheduledCallback.Canceller = new CancellationTokenSource(); } } } catch (Exception) { } finally { // if we marked the callback as running, ensure that we unmark it (note we're nested within two finally blocks so // this will always execute). this will allow others to run the callback. lock (scheduledCallback) { scheduledCallback.Running = false; } // schedule callback again if it was a repeating callback and is still scheduled with a valid repeat delay if (repeating && CallbackIsScheduled(callbackId) && repeatDelay.Ticks >= 0 && scheduleRepeatCallback != null) { DateTime nextCallbackTime; // if this repeating callback is allowed to lag, schedule the repeat from the current time. if (repeatLag) { nextCallbackTime = DateTime.Now + repeatDelay; } else { // otherwise, schedule the repeat from the time at which the current callback was raised. nextCallbackTime = callbackStartTime + repeatDelay; } scheduleRepeatCallback(nextCallbackTime); } else { UnscheduleCallback(callbackId); } } } } } } catch (Exception ex) { string errorMessage = "Failed to raise callback: " + ex.Message; SensusServiceHelper.Get().Logger.Log(errorMessage, LoggingLevel.Normal, GetType()); try { // TODO: Report } catch (Exception) { } } })); }
protected abstract void UnscheduleCallbackPlatformSpecific(ScheduledCallback callback);
/// <summary> /// Raises a <see cref="ScheduledCallback"/>. This involves initiating the <see cref="ScheduledCallback"/>, setting up cancellation timing /// for the callback's action based on the <see cref="ScheduledCallback.Timeout"/>, and scheduling the next invocation of the /// <see cref="ScheduledCallback"/> in the case of repeating <see cref="ScheduledCallback"/>s. See <see cref="CancelRaisedCallback(ScheduledCallback)"/> /// for how to cancel a <see cref="ScheduledCallback"/> after it has been raised. Unlike other methods called via the app's entry points /// (e.g., push notifications, alarms, etc.), this method does not take a <see cref="CancellationToken"/>. The reason for this is that /// raising <see cref="ScheduledCallback"/>s is done from several locations, and cancellation is only needed due to background considerations /// on iOS. So we've centralized background-sensitive cancellation into the iOS override of this method. /// </summary> /// <returns>Async task</returns> /// <param name="callback">Callback to raise.</param> /// <param name="invocationId">Identifier of invocation.</param> public virtual async Task RaiseCallbackAsync(ScheduledCallback callback, string invocationId) { try { if (callback == null) { throw new NullReferenceException("Attemped to raise null callback."); } if (SensusServiceHelper.Get() == null) { throw new NullReferenceException("Attempted to raise callback with null service helper."); } // the same callback must not be run multiple times concurrently, so drop the current callback if it's already running. multiple // callers might compete for the same callback, but only one will win the lock below and it will exclude all others until the // the callback has finished executing. furthermore, the callback must not run multiple times in sequence (e.g., if the callback // is raised by the local scheduling system and then later by a remote push notification). this is handled by tracking invocation // identifiers, which are only runnable once. string initiationError = null; lock (callback) { if (callback.State != ScheduledCallbackState.Scheduled) { initiationError += "Callback " + callback.Id + " is not scheduled. Current state: " + callback.State; } if (invocationId != callback.InvocationId) { initiationError += (initiationError == null ? "" : ". ") + "Invocation ID provided for callback " + callback.Id + " does not match the one on record."; } if (initiationError == null) { callback.State = ScheduledCallbackState.Running; } } if (initiationError == null) { try { if (callback.Canceller.IsCancellationRequested) { SensusServiceHelper.Get().Logger.Log("Callback " + callback.Id + " was cancelled before it was raised.", LoggingLevel.Normal, GetType()); } else { SensusServiceHelper.Get().Logger.Log("Raising callback " + callback.Id + ".", LoggingLevel.Normal, GetType()); #if __ANDROID__ // on android we wouldn't have yet notified the user using the callback's message. on ios, the // message would have already been displayed to the user if the app was in the background. on // ios we do not display callback messages if the app is foregrounded. see the notification // delegate for how this is done. await SensusContext.Current.Notifier.IssueNotificationAsync("Sensus", callback.UserNotificationMessage, callback.Id, true, callback.Protocol, null, callback.NotificationUserResponseAction, callback.NotificationUserResponseMessage); #endif // if the callback specified a timeout, request cancellation at the specified time. if (callback.Timeout.HasValue) { callback.Canceller.CancelAfter(callback.Timeout.Value); } await callback.ActionAsync(callback.Canceller.Token); } } catch (Exception raiseException) { SensusException.Report("Callback " + callback.Id + " threw an exception: " + raiseException.Message, raiseException); } finally { // the cancellation token source for the current callback might have been canceled. if this is a repeating callback then we'll need a new // cancellation token source because they cannot be reset and we're going to use the same scheduled callback again for the next repeat. // if we enter the _idCallback lock before CancelRaisedCallback does, then the next raise will be cancelled. if CancelRaisedCallback enters the // _idCallback lock first, then the cancellation token source will be overwritten here and the cancel will not have any effect on the next // raise. the latter case is a reasonable outcome, since the purpose of CancelRaisedCallback is to terminate a callback that is currently in // progress, and the current callback is no longer in progress. if the desired outcome is complete discontinuation of the repeating callback // then UnscheduleRepeatingCallback should be used -- this method first cancels any raised callbacks and then removes the callback entirely. try { if (callback.RepeatDelay.HasValue) { callback.Canceller = new CancellationTokenSource(); } } catch (Exception ex) { SensusException.Report("Exception while assigning new callback canceller.", ex); } finally { callback.State = ScheduledCallbackState.Completed; // schedule callback again if it is still scheduled with a valid repeat delay if (ContainsCallback(callback) && callback.RepeatDelay.HasValue && callback.RepeatDelay.Value.Ticks > 0) { callback.NextExecution = DateTime.Now + callback.RepeatDelay.Value; callback.InvocationId = Guid.NewGuid().ToString(); // set the new invocation ID before resetting the state so that concurrent callers won't run (their invocation IDs won't match) BatchNextExecutionWithToleratedDelay(callback); // the state needs to be updated after batching is performed, so that other callbacks don't // attempt to batch with it, but before the invocations are requested, so that if the invocation // comes back immediately (e.g., being scheduled in the past) the callback is scheduled and // ready to run. callback.State = ScheduledCallbackState.Scheduled; await RequestLocalInvocationAsync(callback); #if __IOS__ await RequestRemoteInvocationAsync(callback); #endif } else { await UnscheduleCallbackAsync(callback); } } } } else { SensusServiceHelper.Get().Logger.Log("Initiation error for callback " + callback.Id + ": " + initiationError, LoggingLevel.Normal, GetType()); } } catch (Exception ex) { SensusException.Report("Exception raising callback: " + ex.Message, ex); } }
protected abstract void CancelLocalInvocation(ScheduledCallback callback);
protected abstract Task RequestLocalInvocationAsync(ScheduledCallback callback);
private async Task CancelRemoteInvocationAsync(ScheduledCallback callback) { await SensusContext.Current.Notifier.DeletePushNotificationRequestAsync(callback.PushNotificationBackendKey, callback.Protocol, CancellationToken.None); }