public async Task BindAsync_ReturnsExpectedTriggerData() { ParameterInfo parameter = GetType().GetMethod("TestTimerJob").GetParameters()[0]; MethodInfo methodInfo = (MethodInfo)parameter.Member; string timerName = string.Format("{0}.{1}", methodInfo.DeclaringType.FullName, methodInfo.Name); Mock<ScheduleMonitor> mockScheduleMonitor = new Mock<ScheduleMonitor>(MockBehavior.Strict); ScheduleStatus status = new ScheduleStatus(); mockScheduleMonitor.Setup(p => p.GetStatusAsync(timerName)).ReturnsAsync(status); TimerTriggerAttribute attribute = parameter.GetCustomAttribute<TimerTriggerAttribute>(); TimersConfiguration config = new TimersConfiguration(); config.ScheduleMonitor = mockScheduleMonitor.Object; TestTraceWriter trace = new TestTraceWriter(); TimerTriggerBinding binding = new TimerTriggerBinding(parameter, attribute, config, trace); // when we bind to a non-TimerInfo (e.g. in a Dashboard invocation) a new // TimerInfo is created, with the ScheduleStatus populated FunctionBindingContext functionContext = new FunctionBindingContext(Guid.NewGuid(), CancellationToken.None, trace); ValueBindingContext context = new ValueBindingContext(functionContext, CancellationToken.None); TriggerData triggerData = (TriggerData)(await binding.BindAsync("", context)); TimerInfo timerInfo = (TimerInfo)triggerData.ValueProvider.GetValue(); Assert.Same(status, timerInfo.ScheduleStatus); // when we pass in a TimerInfo that is used TimerInfo expected = new TimerInfo(attribute.Schedule, status); triggerData = (TriggerData)(await binding.BindAsync(expected, context)); timerInfo = (TimerInfo)triggerData.ValueProvider.GetValue(); Assert.Same(expected, timerInfo); }
/// <summary> /// Checks whether the schedule is currently past due. /// </summary> /// <remarks> /// On startup, all schedules are checked to see if they are past due. Any /// timers that are past due will be executed immediately by default. Subclasses can /// change this behavior by inspecting the current time and schedule to determine /// whether it should be considered past due. /// </remarks> /// <param name="timerName">The name of the timer to check.</param> /// <param name="now">The time to check.</param> /// <param name="schedule">The <see cref="TimerSchedule"/></param> /// <param name="lastStatus">The last recorded status, or null if the status has never been recorded.</param> /// <returns>A non-zero <see cref="TimeSpan"/> if the schedule is past due, otherwise <see cref="TimeSpan.Zero"/>.</returns> public virtual async Task <TimeSpan> CheckPastDueAsync(string timerName, DateTime now, TimerSchedule schedule, ScheduleStatus lastStatus) { DateTime recordedNextOccurrence; if (lastStatus == null) { // If we've never recorded a status for this timer, write an initial // status entry. This ensures that for a new timer, we've captured a // status log for the next occurrence even though no occurrence has happened yet // (ensuring we don't miss an occurrence) DateTime nextOccurrence = schedule.GetNextOccurrence(now); lastStatus = new ScheduleStatus { Last = default(DateTime), Next = nextOccurrence }; await UpdateStatusAsync(timerName, lastStatus); recordedNextOccurrence = nextOccurrence; } else { DateTime expectedNextOccurrence; if (lastStatus.Last == default(DateTime)) { // there have been no executions of the function yet, so compute // from now expectedNextOccurrence = schedule.GetNextOccurrence(now); } else { // compute the next occurrence from the last expectedNextOccurrence = schedule.GetNextOccurrence(lastStatus.Last); } // ensure that the schedule hasn't been updated since the last // time we checked, and if it has, update the status to use the new schedule if (lastStatus.Next != expectedNextOccurrence) { lastStatus.Last = default(DateTime); lastStatus.Next = expectedNextOccurrence; await UpdateStatusAsync(timerName, lastStatus); } recordedNextOccurrence = lastStatus.Next; } if (now > recordedNextOccurrence) { // if now is after the last next occurrence we recorded, we know we've missed // at least one schedule instance and we are past due return(now - recordedNextOccurrence); } else { // not past due return(TimeSpan.Zero); } }
/// <summary> /// Checks whether the schedule is currently past due. /// </summary> /// <remarks> /// On startup, all schedules are checked to see if they are past due. Any /// timers that are past due will be executed immediately by default. Subclasses can /// change this behavior by inspecting the current time and schedule to determine /// whether it should be considered past due. /// </remarks> /// <param name="timerName">The name of the timer to check.</param> /// <param name="now">The time to check.</param> /// <param name="schedule">The <see cref="TimerSchedule"/></param> /// <param name="lastStatus">The last recorded status, or null if the status has never been recorded.</param> /// <returns>A non-zero <see cref="TimeSpan"/> if the schedule is past due, otherwise <see cref="TimeSpan.Zero"/>.</returns> public virtual async Task<TimeSpan> CheckPastDueAsync(string timerName, DateTime now, TimerSchedule schedule, ScheduleStatus lastStatus) { DateTime recordedNextOccurrence; if (lastStatus == null) { // If we've never recorded a status for this timer, write an initial // status entry. This ensures that for a new timer, we've captured a // status log for the next occurrence even though no occurrence has happened yet // (ensuring we don't miss an occurrence) DateTime nextOccurrence = schedule.GetNextOccurrence(now); lastStatus = new ScheduleStatus { Last = default(DateTime), Next = nextOccurrence }; await UpdateStatusAsync(timerName, lastStatus); recordedNextOccurrence = nextOccurrence; } else { // ensure that the schedule hasn't been updated since the last // time we checked, and if it has, update the status DateTime expectedNextOccurrence; if (lastStatus.Last == default(DateTime)) { // there have been no executions of the function yet, so compute // from now expectedNextOccurrence = schedule.GetNextOccurrence(now); } else { // compute the next occurrence from the last expectedNextOccurrence = schedule.GetNextOccurrence(lastStatus.Last); } if (lastStatus.Next != expectedNextOccurrence) { lastStatus.Next = expectedNextOccurrence; await UpdateStatusAsync(timerName, lastStatus); } recordedNextOccurrence = lastStatus.Next; } if (now > recordedNextOccurrence) { // if now is after the last next occurrence we recorded, we know we've missed // at least one schedule instance and we are past due return now - recordedNextOccurrence; } else { // not past due return TimeSpan.Zero; } }
public async Task InvokeJobFunction_UpdatesScheduleMonitor() { DateTime lastOccurrence = DateTime.Now; DateTime nextOccurrence = _attribute.Schedule.GetNextOccurrence(lastOccurrence); ScheduleStatus status = new ScheduleStatus { Last = lastOccurrence, Next = nextOccurrence }; _mockScheduleMonitor.Setup(p => p.UpdateStatusAsync(_testTimerName, It.Is<ScheduleStatus>(q => q.Last == lastOccurrence && q.Next == nextOccurrence))) .Returns(Task.FromResult(true)); await _listener.InvokeJobFunction(lastOccurrence, false); _listener.Dispose(); }
public async Task UpdateStatusAsync_MultipleFunctions() { // update status for 3 functions ScheduleStatus expected = new ScheduleStatus { Last = DateTime.Now.Subtract(TimeSpan.FromMinutes(5)), Next = DateTime.Now.AddMinutes(5) }; for (int i = 0; i < 3; i++) { await _scheduleMonitor.UpdateStatusAsync(TestTimerName + i.ToString(), expected); } CloudBlockBlob[] statuses = _scheduleMonitor.TimerStatusDirectory.ListBlobs(useFlatBlobListing: true).Cast<CloudBlockBlob>().ToArray(); Assert.Equal(3, statuses.Length); Assert.Equal("timers/testhostid/TestProgram.TestTimer0/status", statuses[0].Name); Assert.Equal("timers/testhostid/TestProgram.TestTimer1/status", statuses[1].Name); Assert.Equal("timers/testhostid/TestProgram.TestTimer2/status", statuses[2].Name); }
public async Task GetStatusAsync_ReturnsExpectedStatus() { // no status, so should return null ScheduleStatus status = await _scheduleMonitor.GetStatusAsync(TestTimerName); Assert.Null(status); // update the status ScheduleStatus expected = new ScheduleStatus { Last = DateTime.Now.Subtract(TimeSpan.FromMinutes(5)), Next = DateTime.Now.AddMinutes(5) }; await _scheduleMonitor.UpdateStatusAsync(TestTimerName, expected); // expect the status to be returned status = await _scheduleMonitor.GetStatusAsync(TestTimerName); Assert.Equal(expected.Last, status.Last); Assert.Equal(expected.Next, status.Next); }
/// <inheritdoc/> public override async Task UpdateStatusAsync(string timerName, ScheduleStatus status) { string statusLine; using (StringWriter stringWriter = new StringWriter()) { _serializer.Serialize(stringWriter, status); statusLine = stringWriter.ToString(); } try { CloudBlockBlob statusBlob = GetStatusBlobReference(timerName); await statusBlob.UploadTextAsync(statusLine); } catch { // best effort } }
/// <inheritdoc/> public override async Task UpdateStatusAsync(string timerName, ScheduleStatus status) { string statusLine; using (StringWriter stringWriter = new StringWriter()) { _serializer.Serialize(stringWriter, status); statusLine = stringWriter.ToString(); } try { CloudBlockBlob statusBlob = GetStatusBlobReference(timerName); await statusBlob.UploadTextAsync(statusLine); } catch (Exception ex) { // best effort _logger.LogError(ex, $"Function '{timerName}' failed to update the timer trigger status."); } }
public async Task StartAsync_SchedulePastDue_InvokesJobFunctionImmediately() { // Set this to true to ensure that the function is only executed once // In this case, because it is run on startup due to being behind schedule, // it shouldn't be run twice. _attribute.RunOnStartup = true; ScheduleStatus status = new ScheduleStatus(); _mockScheduleMonitor.Setup(p => p.GetStatusAsync(_testTimerName)).ReturnsAsync(status); DateTime lastOccurrence = default(DateTime); TimeSpan pastDueAmount = TimeSpan.FromMinutes(3); _mockScheduleMonitor.Setup(p => p.CheckPastDueAsync(_testTimerName, It.IsAny<DateTime>(), It.IsAny<TimerSchedule>(), status)) .Callback<string, DateTime, TimerSchedule, ScheduleStatus>((mockTimerName, mockNow, mockNext, mockStatus) => { lastOccurrence = mockNow; }) .ReturnsAsync(pastDueAmount); _mockScheduleMonitor.Setup(p => p.UpdateStatusAsync(_testTimerName, It.IsAny<ScheduleStatus>())) .Callback<string, ScheduleStatus>((mockTimerName, mockStatus) => { Assert.Equal(lastOccurrence, mockStatus.Last); DateTime expectedNextOccurrence = _attribute.Schedule.GetNextOccurrence(lastOccurrence); Assert.Equal(expectedNextOccurrence, mockStatus.Next); }) .Returns(Task.FromResult(true)); CancellationToken cancellationToken = new CancellationToken(); await _listener.StartAsync(cancellationToken); TimerInfo timerInfo = (TimerInfo)_triggeredFunctionData.TriggerValue; Assert.Same(status, timerInfo.ScheduleStatus); Assert.True(timerInfo.IsPastDue); _mockTriggerExecutor.Verify(p => p.TryExecuteAsync(It.IsAny<TriggeredFunctionData>(), It.IsAny<CancellationToken>()), Times.Once()); _listener.Dispose(); }
/// <inheritdoc/> public override Task UpdateStatusAsync(string timerName, ScheduleStatus status) { string statusLine; using (StringWriter stringWriter = new StringWriter()) { _serializer.Serialize(stringWriter, status); statusLine = stringWriter.ToString(); } string statusFileName = GetStatusFileName(timerName); try { File.WriteAllText(statusFileName, statusLine); } catch { // best effort } return(Task.FromResult(true)); }
/// <summary> /// Updates the schedule status for the specified timer. /// </summary> /// <param name="timerName">The name of the timer.</param> /// <param name="status">The new schedule status.</param> public abstract Task UpdateStatusAsync(string timerName, ScheduleStatus status);
/// <summary> /// Checks whether the schedule is currently past due. /// </summary> /// <remarks> /// On startup, all schedules are checked to see if they are past due. Any /// timers that are past due will be executed immediately by default. Subclasses can /// change this behavior by inspecting the current time and schedule to determine /// whether it should be considered past due. /// </remarks> /// <param name="timerName">The name of the timer to check.</param> /// <param name="now">The time to check.</param> /// <param name="schedule">The <see cref="TimerSchedule"/>.</param> /// <param name="lastStatus">The last recorded status, or null if the status has never been recorded.</param> /// <returns>A non-zero <see cref="TimeSpan"/> if the schedule is past due, otherwise <see cref="TimeSpan.Zero"/>.</returns> public virtual async Task <TimeSpan> CheckPastDueAsync(string timerName, DateTime now, TimerSchedule schedule, ScheduleStatus lastStatus) { DateTime recordedNextOccurrence; if (lastStatus == null) { // If we've never recorded a status for this timer, write an initial // status entry. This ensures that for a new timer, we've captured a // status log for the next occurrence even though no occurrence has happened yet // (ensuring we don't miss an occurrence) DateTime nextOccurrence = schedule.GetNextOccurrence(now); lastStatus = new ScheduleStatus { Last = default(DateTime), Next = nextOccurrence, LastUpdated = now }; await UpdateStatusAsync(timerName, lastStatus); recordedNextOccurrence = nextOccurrence; } else { DateTime expectedNextOccurrence; // Track the time that was used to create 'expectedNextOccurrence'. DateTime lastUpdated; if (lastStatus.Last != default(DateTime)) { // If we have a 'Last' value, we know that we used this to calculate 'Next' // in a previous invocation. expectedNextOccurrence = schedule.GetNextOccurrence(lastStatus.Last); lastUpdated = lastStatus.Last; } else if (lastStatus.LastUpdated != default(DateTime)) { // If the trigger has never fired, we won't have 'Last', but we will have // 'LastUpdated', which tells us the last time that we used to calculate 'Next'. expectedNextOccurrence = schedule.GetNextOccurrence(lastStatus.LastUpdated); lastUpdated = lastStatus.LastUpdated; } else { // If we do not have 'LastUpdated' or 'Last', we don't have enough information to // properly calculate 'Next', so we'll calculate it from the current time. expectedNextOccurrence = schedule.GetNextOccurrence(now); lastUpdated = now; } // ensure that the schedule hasn't been updated since the last // time we checked, and if it has, update the status to use the new schedule if (lastStatus.Next != expectedNextOccurrence) { // if the schedule has changed and the next occurrence is in the past, // recalculate it based on the current time as we don't want it to register // immediately as 'past due'. if (now > expectedNextOccurrence) { expectedNextOccurrence = schedule.GetNextOccurrence(now); lastUpdated = now; } lastStatus.Last = default(DateTime); lastStatus.Next = expectedNextOccurrence; lastStatus.LastUpdated = lastUpdated; await UpdateStatusAsync(timerName, lastStatus); } recordedNextOccurrence = lastStatus.Next; } if (now > recordedNextOccurrence) { // if now is after the last next occurrence we recorded, we know we've missed // at least one schedule instance and we are past due return(now - recordedNextOccurrence); } else { // not past due return(TimeSpan.Zero); } }
public async Task UpdateAsync_WritesStatusToFile() { DateTime now = DateTime.Now; DateTime expectedNext = DateTime.Now + TimeSpan.FromMinutes(1); File.Delete(_statusFile); Assert.False(File.Exists(_statusFile)); ScheduleStatus status = new ScheduleStatus { Last = now, Next = expectedNext }; await _monitor.UpdateStatusAsync(_testTimerName, status); Assert.True(File.Exists(_statusFile)); VerifyScheduleStatus(now, expectedNext); now = expectedNext; expectedNext = now + TimeSpan.FromMinutes(1); status = new ScheduleStatus { Last = now, Next = expectedNext }; await _monitor.UpdateStatusAsync(_testTimerName, status); VerifyScheduleStatus(now, expectedNext); }
public async Task StartAsync_ScheduleNotPastDue_DoesNotInvokeJobFunctionImmediately() { ScheduleStatus status = new ScheduleStatus(); _mockScheduleMonitor.Setup(p => p.GetStatusAsync(_testTimerName)).ReturnsAsync(status); TimeSpan pastDueAmount = TimeSpan.Zero; _mockScheduleMonitor.Setup(p => p.CheckPastDueAsync(_testTimerName, It.IsAny<DateTime>(), It.IsAny<TimerSchedule>(), status)) .ReturnsAsync(pastDueAmount); CancellationToken cancellationToken = new CancellationToken(); await _listener.StartAsync(cancellationToken); _mockTriggerExecutor.Verify(p => p.TryExecuteAsync(It.IsAny<TriggeredFunctionData>(), It.IsAny<CancellationToken>()), Times.Never()); _listener.Dispose(); }
public async Task CheckPastDueAsync_ScheduleUpdate_UpdatesStatusFile() { Mock<TimerSchedule> mockSchedule = new Mock<TimerSchedule>(MockBehavior.Strict); DateTime now = DateTime.Now; DateTime next = now + TimeSpan.FromDays(2); ScheduleStatus status = new ScheduleStatus { Last = now, Next = next }; await _monitor.UpdateStatusAsync(_testTimerName, status); mockSchedule.Setup(p => p.GetNextOccurrence(It.IsAny<DateTime>())).Returns(next); TimeSpan pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); // now adjust the schedule DateTime adjustedNext = next - TimeSpan.FromDays(1); mockSchedule.Setup(p => p.GetNextOccurrence(It.IsAny<DateTime>())).Returns(adjustedNext); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); ScheduleStatus updatedStatus = await _monitor.GetStatusAsync(_testTimerName); Assert.Equal(adjustedNext, updatedStatus.Next); now = now + TimeSpan.FromHours(23); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); now = now + TimeSpan.FromHours(1); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); now = now + TimeSpan.FromHours(1); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.FromHours(1), pastDueAmount); }
public async Task CheckPastDueAsync_ReturnsExpectedResult() { Mock<TimerSchedule> mockSchedule = new Mock<TimerSchedule>(MockBehavior.Strict); DateTime now = DateTime.Now; DateTime next = now + TimeSpan.FromDays(1); ScheduleStatus status = new ScheduleStatus { Last = now, Next = next }; mockSchedule.Setup(p => p.GetNextOccurrence(It.IsAny<DateTime>())).Returns(next); TimeSpan pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); now = now + TimeSpan.FromHours(23); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); now = now + TimeSpan.FromHours(1); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.Zero, pastDueAmount); now = now + TimeSpan.FromHours(1); pastDueAmount = await _monitor.CheckPastDueAsync(_testTimerName, now, mockSchedule.Object, status); Assert.Equal(TimeSpan.FromHours(1), pastDueAmount); }
/// <summary> /// Constructs a new instance /// </summary> /// <param name="schedule">The timer trigger schedule.</param> /// <param name="status">The current schedule status.</param> /// <param name="isPastDue">True if the schedule is past due, false otherwise.</param> public TimerInfo(TimerSchedule schedule, ScheduleStatus status, bool isPastDue = false) { Schedule = schedule; ScheduleStatus = status; IsPastDue = isPastDue; }
/// <inheritdoc/> public override Task UpdateStatusAsync(string timerName, ScheduleStatus status) { string statusLine; using (StringWriter stringWriter = new StringWriter()) { _serializer.Serialize(stringWriter, status); statusLine = stringWriter.ToString(); } string statusFileName = GetStatusFileName(timerName); try { File.WriteAllText(statusFileName, statusLine); } catch { // best effort } return Task.FromResult(true); }