in src/WebJobs.Extensions/Extensions/Timers/Listener/TimerListener.cs [292:387]
internal async Task InvokeJobFunction(DateTimeOffset invocationTime, bool isPastDue = false, bool runOnStartup = false, DateTimeOffset? originalSchedule = null)
{
try
{
await _invocationLock.WaitAsync();
// if Cancel, Stop, or Dispose have been called, skip the invocation
// since we're stopping the listener
if (_cancellationTokenSource.IsCancellationRequested)
{
return;
}
CancellationToken token = _cancellationTokenSource.Token;
ScheduleStatus timerInfoStatus = null;
if (ScheduleMonitor != null)
{
timerInfoStatus = ScheduleStatus;
}
TimerInfo timerInfo = new TimerInfo(_schedule, timerInfoStatus, isPastDue);
// Build up trigger details that will be logged if the timer is running at a different time
// than originally scheduled.
IDictionary<string, string> details = new Dictionary<string, string>();
if (isPastDue)
{
details[UnscheduledInvocationReasonKey] = "IsPastDue";
}
else if (runOnStartup)
{
details[UnscheduledInvocationReasonKey] = "RunOnStartup";
}
if (originalSchedule.HasValue)
{
details[OriginalScheduleKey] = originalSchedule.Value.ToString("o");
}
try
{
if (timerInfo?.ScheduleStatus is not null)
{
details[ScheduleStatusKey] = JsonConvert.SerializeObject(timerInfo.ScheduleStatus, _serializerSettings);
}
}
catch
{
// best effort
}
TriggeredFunctionData input = new TriggeredFunctionData
{
TriggerValue = timerInfo,
TriggerDetails = details
};
try
{
await _executor.TryExecuteAsync(input, token);
}
catch
{
// We don't want any function errors to stop the execution
// schedule. Invocation errors are already logged.
}
// If the trigger fired before it was officially scheduled (likely under 1 second due to clock skew),
// adjust the invocation time forward for the purposes of calculating the next occurrence.
// Without this, it's possible to set the 'Next' value to the same time twice in a row,
// which results in duplicate triggers if the site restarts.
DateTimeOffset adjustedInvocationTime = invocationTime;
if (!isPastDue && !runOnStartup && ScheduleStatus?.Next > invocationTime)
{
adjustedInvocationTime = ScheduleStatus.Next;
}
// Create the Last value with the adjustedInvocationTime; otherwise, the listener will
// consider this a schedule change when the host next starts.
ScheduleStatus = new ScheduleStatus
{
Last = adjustedInvocationTime.LocalDateTime,
Next = _schedule.GetNextOccurrence(adjustedInvocationTime.LocalDateTime),
LastUpdated = adjustedInvocationTime.LocalDateTime
};
if (ScheduleMonitor != null)
{
await ScheduleMonitor.UpdateStatusAsync(_timerLookupName, ScheduleStatus);
_logger.LogDebug($"Function '{_functionLogName}' updated status: Last='{ScheduleStatus.Last.ToString("o")}', Next='{ScheduleStatus.Next.ToString("o")}', LastUpdated='{ScheduleStatus.LastUpdated.ToString("o")}'");
}
}
finally
{
_invocationLock.Release();
}
}