// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using EpicGames.Horde.Commits; using EpicGames.Horde.Jobs.Templates; namespace EpicGames.Horde.Jobs.Schedules { /// /// Schedule for a template /// public interface ISchedule { /// /// Whether the schedule should be enabled /// bool Enabled { get; } /// /// Maximum number of builds that can be active at once /// int MaxActive { get; } /// /// Maximum number of changes the schedule can fall behind head revision. If greater than zero, builds will be triggered for every submitted changelist until the backlog is this size. /// int MaxChanges { get; } /// /// Whether the build requires a change to be submitted /// bool RequireSubmittedChange { get; } /// /// Gate allowing the schedule to trigger /// IScheduleGate? Gate { get; } /// /// Commit tags for this schedule /// IReadOnlyList Commits { get; } /// /// Roles to impersonate for this schedule /// IReadOnlyList? Claims { get; } /// /// Last changelist number that this was triggered for /// CommitIdWithOrder? LastTriggerCommitId { get; } /// /// Gets the last trigger time, in UTC /// DateTime LastTriggerTimeUtc { get; } /// /// List of jobs that are currently active /// IReadOnlyList ActiveJobs { get; } /// /// Patterns for starting this scheduled job /// IReadOnlyList Patterns { get; } /// /// Files that should cause the job to trigger /// IReadOnlyList? Files { get; } /// /// Parameters for the template /// IReadOnlyDictionary TemplateParameters { get; } } /// /// Claim granted to a schedule /// public interface IScheduleClaim { /// /// The claim type /// string Type { get; } /// /// The claim value /// string Value { get; } } /// /// Required gate for starting a schedule /// public interface IScheduleGate { /// /// The template containing the dependency /// TemplateId TemplateId { get; } /// /// Target to wait for /// string Target { get; } } /// /// Pattern for executing a schedule /// public interface ISchedulePattern { /// /// Days of the week to run this schedule on. If null, the schedule will run every day. /// IReadOnlyList? DaysOfWeek { get; } /// /// Time during the day for the first schedule to trigger. Measured in minutes from midnight. /// ScheduleTimeOfDay MinTime { get; } /// /// Time during the day for the last schedule to trigger. Measured in minutes from midnight. /// ScheduleTimeOfDay? MaxTime { get; } /// /// Interval between each schedule triggering /// ScheduleInterval? Interval { get; } } /// /// Extension methods for schedules /// public static class ScheduleExtensions { /// /// Gets the next trigger time for a schedule /// /// /// /// public static DateTime? GetNextTriggerTimeUtc(this ISchedule schedule, TimeZoneInfo timeZone) { return schedule.GetNextTriggerTimeUtc(schedule.LastTriggerTimeUtc, timeZone); } /// /// Get the next time that the schedule will trigger /// /// Schedule to query /// Last time at which the schedule triggered /// Timezone to evaluate the trigger /// Next time at which the schedule will trigger public static DateTime? GetNextTriggerTimeUtc(this ISchedule schedule, DateTime lastTimeUtc, TimeZoneInfo timeZone) { DateTime? nextTriggerTimeUtc = null; foreach (ISchedulePattern pattern in schedule.Patterns) { DateTime patternTriggerTime = pattern.GetNextTriggerTimeUtc(lastTimeUtc, timeZone); if (nextTriggerTimeUtc == null || patternTriggerTime < nextTriggerTimeUtc) { nextTriggerTimeUtc = patternTriggerTime; } } return nextTriggerTimeUtc; } /// /// Calculates the trigger index based on the given time in minutes /// /// Pattern to query /// Time for the last trigger /// The timezone for running the schedule /// Index of the trigger public static DateTime GetNextTriggerTimeUtc(this ISchedulePattern pattern, DateTime lastTimeUtc, TimeZoneInfo timeZone) { // Convert last time into the correct timezone for running the scheule DateTimeOffset lastTime = TimeZoneInfo.ConvertTime((DateTimeOffset)lastTimeUtc, timeZone); // Get the base time (ie. the start of this day) for anchoring the schedule DateTimeOffset baseTime = new DateTimeOffset(lastTime.Year, lastTime.Month, lastTime.Day, 0, 0, 0, lastTime.Offset); for (; ; ) { if (pattern.DaysOfWeek == null || pattern.DaysOfWeek.Contains(baseTime.DayOfWeek)) { // Get the last time in minutes from the start of this day int lastTimeMinutes = (int)(lastTime - baseTime).TotalMinutes; // Get the time of the first trigger of this day. If the last time is less than this, this is the next trigger. if (lastTimeMinutes < pattern.MinTime.Minutes) { return baseTime.AddMinutes(pattern.MinTime.Minutes).UtcDateTime; } // Otherwise, get the time for the last trigger in the day. if (pattern.Interval != null && pattern.Interval.Minutes > 0) { int actualMaxTime = pattern.MaxTime?.Minutes ?? ((24 * 60) - 1); if (lastTimeMinutes < actualMaxTime) { int lastIndex = (lastTimeMinutes - pattern.MinTime.Minutes) / pattern.Interval.Minutes; int nextIndex = lastIndex + 1; int nextTimeMinutes = pattern.MinTime.Minutes + (nextIndex * pattern.Interval.Minutes); if (nextTimeMinutes <= actualMaxTime) { return baseTime.AddMinutes(nextTimeMinutes).UtcDateTime; } } } } baseTime = baseTime.AddDays(1.0); } } } }