// 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);
}
}
}
}