Files
UnrealEngineUWP/Engine/Source/Programs/Horde/HordeServer/Models/Job.cs
Ben Marsh 6868e7c47d Missing files.
[CL 16507070 by Ben Marsh in ue5-main branch]
2021-05-30 17:11:29 -04:00

1492 lines
45 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using HordeServer.Api;
using HordeCommon;
using HordeServer.Utilities;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ReportPlacement = HordeCommon.Rpc.ReportPlacement;
using StreamId = HordeServer.Utilities.StringId<HordeServer.Models.IStream>;
using TemplateRefId = HordeServer.Utilities.StringId<HordeServer.Models.TemplateRef>;
using PoolId = HordeServer.Utilities.StringId<HordeServer.Models.IPool>;
namespace HordeServer.Models
{
/// <summary>
/// Report for a job or jobstep
/// </summary>
public interface IReport
{
/// <summary>
/// Name of the report
/// </summary>
string Name { get; }
/// <summary>
/// Where to render the report
/// </summary>
ReportPlacement Placement { get; }
/// <summary>
/// The artifact id
/// </summary>
ObjectId ArtifactId { get; }
}
/// <summary>
/// Implementation of IReport
/// </summary>
public class Report : IReport
{
/// <inheritdoc/>
public string Name { get; set; } = String.Empty;
/// <inheritdoc/>
public ReportPlacement Placement { get; set; }
/// <inheritdoc/>
public ObjectId ArtifactId { get; set; }
}
/// <summary>
/// Embedded jobstep document
/// </summary>
public interface IJobStep
{
/// <summary>
/// Unique ID assigned to this jobstep. A new id is generated whenever a jobstep's order is changed.
/// </summary>
public SubResourceId Id { get; }
/// <summary>
/// Index of the node which this jobstep is to execute
/// </summary>
public int NodeIdx { get; }
/// <summary>
/// Current state of the job step. This is updated automatically when runs complete.
/// </summary>
public JobStepState State { get; }
/// <summary>
/// Current outcome of the jobstep
/// </summary>
public JobStepOutcome Outcome { get; }
/// <summary>
/// The log id for this step
/// </summary>
public ObjectId? LogId { get; }
/// <summary>
/// Unique id for notifications
/// </summary>
public ObjectId? NotificationTriggerId { get; }
/// <summary>
/// Time at which the batch transitioned to the ready state (UTC).
/// </summary>
public DateTime? ReadyTimeUtc { get; }
/// <summary>
/// Time at which the batch transitioned to the executing state (UTC).
/// </summary>
public DateTime? StartTimeUtc { get; }
/// <summary>
/// Time at which the run finished (UTC)
/// </summary>
public DateTime? FinishTimeUtc { get; }
/// <summary>
/// Override for the priority of this step
/// </summary>
public Priority? Priority { get; }
/// <summary>
/// If a retry is requested, stores the name of the user that requested it
/// </summary>
public string? RetryByUser { get; }
/// <summary>
/// Signal if a step should be aborted
/// </summary>
public bool AbortRequested { get; }
/// <summary>
/// If an abort is requested, stores the name of the user that requested it
/// </summary>
public string? AbortByUser { get; }
/// <summary>
/// List of reports for this step
/// </summary>
public IReadOnlyList<IReport>? Reports { get; }
/// <summary>
/// Reports for this jobstep.
/// </summary>
public Dictionary<string, string>? Properties { get; }
}
/// <summary>
/// Extension methods for job steps
/// </summary>
public static class JobStepExtensions
{
/// <summary>
/// Determines if a jobstep has failed or is skipped. Can be used to determine whether dependent steps will be able to run.
/// </summary>
/// <returns>True if the step is failed or skipped</returns>
public static bool IsFailedOrSkipped(this IJobStep Step)
{
return Step.State == JobStepState.Skipped || Step.Outcome == JobStepOutcome.Failure;
}
/// <summary>
/// Determines if a jobstep is done by checking to see if it is completed, skipped, or aborted.
/// </summary>
/// <returns>True if the step is completed, skipped, or aborted</returns>
public static bool IsPending(this IJobStep Step)
{
return Step.State != JobStepState.Aborted && Step.State != JobStepState.Completed && Step.State != JobStepState.Skipped;
}
}
/// <summary>
/// Stores information about a batch of job steps
/// </summary>
public interface IJobStepBatch
{
/// <summary>
/// Unique id for this group
/// </summary>
public SubResourceId Id { get; }
/// <summary>
/// The log file id for this batch
/// </summary>
public ObjectId? LogId { get; }
/// <summary>
/// Index of the group being executed
/// </summary>
public int GroupIdx { get; }
/// <summary>
/// The state of this group
/// </summary>
public JobStepBatchState State { get; }
/// <summary>
/// Error associated with this group
/// </summary>
public JobStepBatchError Error { get; }
/// <summary>
/// Steps within this run
/// </summary>
public IReadOnlyList<IJobStep> Steps { get; }
/// <summary>
/// The pool that this agent was taken from
/// </summary>
public PoolId? PoolId { get; }
/// <summary>
/// The agent assigned to execute this group
/// </summary>
public AgentId? AgentId { get; }
/// <summary>
/// The agent session that is executing this group
/// </summary>
public ObjectId? SessionId { get; }
/// <summary>
/// The lease that's executing this group
/// </summary>
public ObjectId? LeaseId { get; }
/// <summary>
/// The weighted priority of this batch for the scheduler
/// </summary>
public int SchedulePriority { get; }
/// <summary>
/// Time at which the group became ready (UTC).
/// </summary>
public DateTime? ReadyTimeUtc { get; }
/// <summary>
/// Time at which the group started (UTC).
/// </summary>
public DateTime? StartTimeUtc { get; }
/// <summary>
/// Time at which the group finished (UTC)
/// </summary>
public DateTime? FinishTimeUtc { get; }
}
/// <summary>
/// Extension methods for IJobStepBatch
/// </summary>
public static class JobStepBatchExtensions
{
/// <summary>
/// Attempts to get a step with the given id
/// </summary>
/// <param name="Batch">The batch to search</param>
/// <param name="StepId">The step id</param>
/// <param name="Step">On success, receives the step object</param>
/// <returns>True if the step was found</returns>
public static bool TryGetStep(this IJobStepBatch Batch, SubResourceId StepId, [NotNullWhen(true)] out IJobStep Step)
{
Step = Batch.Steps.FirstOrDefault(x => x.Id == StepId);
return Step != null;
}
/// <summary>
/// Determines if new steps can be appended to this batch. We do not allow this after the last step has been completed, because the agent is shutting down.
/// </summary>
/// <param name="Batch">The batch to search</param>
/// <returns>True if new steps can be appended to this batch</returns>
public static bool CanBeAppendedTo(this IJobStepBatch Batch)
{
return Batch.State <= JobStepBatchState.Running;
}
/// <summary>
/// Gets the wait time for this batch
/// </summary>
/// <param name="Batch">The batch to search</param>
/// <returns>Wait time for the batch</returns>
public static TimeSpan? GetWaitTime(this IJobStepBatch Batch)
{
if (Batch.StartTimeUtc == null || Batch.ReadyTimeUtc == null)
{
return null;
}
else
{
return Batch.StartTimeUtc.Value - Batch.ReadyTimeUtc.Value;
}
}
/// <summary>
/// Gets the initialization time for this batch
/// </summary>
/// <param name="Batch">The batch to search</param>
/// <returns>Initialization time for this batch</returns>
public static TimeSpan? GetInitTime(this IJobStepBatch Batch)
{
if (Batch.StartTimeUtc != null)
{
foreach (IJobStep Step in Batch.Steps)
{
if (Step.StartTimeUtc != null)
{
return Step.StartTimeUtc - Batch.StartTimeUtc.Value;
}
}
}
return null;
}
/// <summary>
/// Get the dependencies required for this batch to start, taking run-early nodes into account
/// </summary>
/// <param name="Batch">The batch to search</param>
/// <param name="Groups">List of node groups</param>
/// <returns>Set of nodes that must have completed for this batch to start</returns>
public static HashSet<INode> GetStartDependencies(this IJobStepBatch Batch, IReadOnlyList<INodeGroup> Groups)
{
// Find all the nodes that this group will start with.
List<INode> Nodes = Batch.Steps.ConvertAll(x => Groups[Batch.GroupIdx].Nodes[x.NodeIdx]);
if (Nodes.Any(x => x.RunEarly))
{
Nodes.RemoveAll(x => !x.RunEarly);
}
// Find all their dependencies
HashSet<INode> Dependencies = new HashSet<INode>();
foreach (INode Node in Nodes)
{
Dependencies.UnionWith(Node.InputDependencies.Select(x => Groups[x.GroupIdx].Nodes[x.NodeIdx]));
Dependencies.UnionWith(Node.OrderDependencies.Select(x => Groups[x.GroupIdx].Nodes[x.NodeIdx]));
}
// Exclude all the dependencies within the same group
Dependencies.ExceptWith(Groups[Batch.GroupIdx].Nodes);
return Dependencies;
}
}
/// <summary>
/// Cumulative timing information to reach a certain point in a job
/// </summary>
public class TimingInfo
{
/// <summary>
/// Wait time on the critical path
/// </summary>
public TimeSpan? TotalWaitTime { get; set; }
/// <summary>
/// Sync time on the critical path
/// </summary>
public TimeSpan? TotalInitTime { get; set; }
/// <summary>
/// Duration to this point
/// </summary>
public TimeSpan? TotalTimeToComplete { get; set; }
/// <summary>
/// Average wait time to this point
/// </summary>
public TimeSpan? AverageTotalWaitTime { get; set; }
/// <summary>
/// Average sync time to this point
/// </summary>
public TimeSpan? AverageTotalInitTime { get; set; }
/// <summary>
/// Average duration to this point
/// </summary>
public TimeSpan? AverageTotalTimeToComplete { get; set; }
/// <summary>
/// Individual step timing information
/// </summary>
public IJobStepTiming? StepTiming { get; set; }
/// <summary>
/// Constructor
/// </summary>
public TimingInfo()
{
TotalWaitTime = TimeSpan.Zero;
TotalInitTime = TimeSpan.Zero;
TotalTimeToComplete = TimeSpan.Zero;
AverageTotalWaitTime = TimeSpan.Zero;
AverageTotalInitTime = TimeSpan.Zero;
AverageTotalTimeToComplete = TimeSpan.Zero;
}
/// <summary>
/// Copy constructor
/// </summary>
/// <param name="Other">The timing info object to copy from</param>
public TimingInfo(TimingInfo Other)
{
TotalWaitTime = Other.TotalWaitTime;
TotalInitTime = Other.TotalInitTime;
TotalTimeToComplete = Other.TotalTimeToComplete;
AverageTotalWaitTime = Other.AverageTotalWaitTime;
AverageTotalInitTime = Other.AverageTotalInitTime;
AverageTotalTimeToComplete = Other.AverageTotalTimeToComplete;
}
/// <summary>
/// Modifies this timing to wait for another timing
/// </summary>
/// <param name="Other">The other node to wait for</param>
public void WaitFor(TimingInfo Other)
{
if (TotalTimeToComplete != null)
{
if (Other.TotalTimeToComplete == null || Other.TotalTimeToComplete.Value > TotalTimeToComplete.Value)
{
TotalInitTime = Other.TotalInitTime;
TotalWaitTime = Other.TotalWaitTime;
TotalTimeToComplete = Other.TotalTimeToComplete;
}
}
if (AverageTotalTimeToComplete != null)
{
if (Other.AverageTotalTimeToComplete == null || Other.AverageTotalTimeToComplete.Value > AverageTotalTimeToComplete.Value)
{
AverageTotalInitTime = Other.AverageTotalInitTime;
AverageTotalWaitTime = Other.AverageTotalWaitTime;
AverageTotalTimeToComplete = Other.AverageTotalTimeToComplete;
}
}
}
/// <summary>
/// Waits for all the given timing info objects to complete
/// </summary>
/// <param name="Others">Other timing info objects to wait for</param>
public void WaitForAll(IEnumerable<TimingInfo> Others)
{
foreach(TimingInfo Other in Others)
{
WaitFor(Other);
}
}
/// <summary>
/// Constructs a new TimingInfo object which represents the last TimingInfo to finish
/// </summary>
/// <param name="Others">TimingInfo objects to wait for</param>
/// <returns>New TimingInfo instance</returns>
public static TimingInfo Max(IEnumerable<TimingInfo> Others)
{
TimingInfo TimingInfo = new TimingInfo();
TimingInfo.WaitForAll(Others);
return TimingInfo;
}
}
/// <summary>
/// Information about a chained job trigger
/// </summary>
public interface IChainedJob
{
/// <summary>
/// The target to monitor
/// </summary>
public string Target { get; }
/// <summary>
/// The template to trigger on success
/// </summary>
public TemplateRefId TemplateRefId { get; }
/// <summary>
/// The triggered job id
/// </summary>
public ObjectId? JobId { get; }
}
/// <summary>
/// Document describing a job
/// </summary>
[SuppressMessage("Compiler", "CA1056:URI parameters should not be strings")]
public interface IJob
{
/// <summary>
/// Job argument indicating a target that should be built
/// </summary>
public const string TargetArgumentPrefix = "-Target=";
/// <summary>
/// Name of the node which parses the buildgraph script
/// </summary>
public const string SetupNodeName = "Setup Build";
/// <summary>
/// Identifier for the job. Randomly generated.
/// </summary>
public ObjectId Id { get; }
/// <summary>
/// The stream that this job belongs to
/// </summary>
public StreamId StreamId { get; }
/// <summary>
/// The template ref id
/// </summary>
public TemplateRefId TemplateId { get; }
/// <summary>
/// The template that this job was created from
/// </summary>
public ContentHash? TemplateHash { get; }
/// <summary>
/// Hash of the graph definition
/// </summary>
public ContentHash GraphHash { get; }
/// <summary>
/// Id of the user that started this job
/// </summary>
public ObjectId? StartedByUserId { get; }
/// <summary>
/// Name of the user that started this job
/// </summary>
public string? StartedByUser { get; }
/// <summary>
/// Name of the user that aborted this job. Set to null if the job is not aborted.
/// </summary>
public string? AbortedByUser { get; }
/// <summary>
/// Name of the job.
/// </summary>
public string Name { get; }
/// <summary>
/// The changelist number to build
/// </summary>
public int Change { get; }
/// <summary>
/// The code changelist number for this build
/// </summary>
public int CodeChange { get; }
/// <summary>
/// The preflight changelist number
/// </summary>
public int PreflightChange { get; }
/// <summary>
/// The cloned preflight changelist number (if the prefight change is duplicated via p4 reshelve)
/// </summary>
public int ClonedPreflightChange { get; }
/// <summary>
/// Priority of this job
/// </summary>
public Priority Priority { get; }
/// <summary>
/// For preflights, submit the change if the job is successful
/// </summary>
public bool AutoSubmit { get; }
/// <summary>
/// The submitted changelist number
/// </summary>
public int? AutoSubmitChange { get; }
/// <summary>
/// Message produced by trying to auto-submit the change
/// </summary>
public string? AutoSubmitMessage { get; }
/// <summary>
/// Whether to update issues based on the outcome of this job
/// </summary>
public bool UpdateIssues { get; }
/// <summary>
/// Time that the job was created (in UTC)
/// </summary>
public DateTime CreateTimeUtc { get; }
/// <summary>
/// Largest value of the CombinedPriority value for batches in the ready state.
/// </summary>
public int SchedulePriority { get; }
/// <summary>
/// Array of jobstep runs
/// </summary>
public IReadOnlyList<IJobStepBatch> Batches { get; }
/// <summary>
/// Optional user-defined properties for this job
/// </summary>
public IReadOnlyList<string> Arguments { get; }
/// <summary>
/// Issues associated with this job
/// </summary>
public IReadOnlyList<int> Issues { get; }
/// <summary>
/// Unique id for notifications
/// </summary>
public ObjectId? NotificationTriggerId { get; }
/// <summary>
/// Whether to show badges in UGS for this job
/// </summary>
public bool ShowUgsBadges { get; }
/// <summary>
/// Whether to show alerts in UGS for this job
/// </summary>
public bool ShowUgsAlerts { get; }
/// <summary>
/// Notification channel for this job.
/// </summary>
public string? NotificationChannel { get; }
/// <summary>
/// Notification channel filter for this job.
/// </summary>
public string? NotificationChannelFilter { get; }
/// <summary>
/// URL to a Helix Swarm server to call once job finishes. Used for adding build outcome to Swarm reviews.
/// </summary>
public string? HelixSwarmCallbackUrl { get; }
/// <summary>
/// Mapping of label ids to notification trigger ids for notifications
/// </summary>
public IReadOnlyDictionary<int, ObjectId> LabelIdxToTriggerId { get; }
/// <summary>
/// List of reports for this step
/// </summary>
public IReadOnlyList<IReport>? Reports { get; }
/// <summary>
/// List of downstream job triggers
/// </summary>
public IReadOnlyList<IChainedJob> ChainedJobs { get; }
/// <summary>
/// Next id for batches or groups
/// </summary>
public SubResourceId NextSubResourceId { get; }
/// <summary>
/// The last update time
/// </summary>
public DateTime UpdateTimeUtc { get; }
/// <summary>
/// The ACL for this job.
/// </summary>
public Acl? Acl { get; }
/// <summary>
/// Update counter for this document. Any updates should compare-and-swap based on the value of this counter, or increment it in the case of server-side updates.
/// </summary>
public int UpdateIndex { get; }
}
/// <summary>
/// Extension methods for jobs
/// </summary>
public static class JobExtensions
{
/// <summary>
/// Attempts to get a step with a given ID
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="StepId">The step id</param>
/// <param name="Step">On success, receives the step object</param>
/// <returns>True if the step was found</returns>
public static bool TryGetStep(this IJob Job, SubResourceId StepId, [NotNullWhen(true)] out IJobStep? Step)
{
foreach (IJobStepBatch Batch in Job.Batches)
{
if (Batch.TryGetStep(StepId, out Step))
{
return true;
}
}
Step = null;
return false;
}
/// <summary>
/// Gets the current job state
/// </summary>
/// <param name="Job">The job document</param>
/// <returns>Job state</returns>
public static JobState GetState(this IJob Job)
{
bool bWaiting = false;
foreach (IJobStepBatch Batch in Job.Batches)
{
foreach (IJobStep Step in Batch.Steps)
{
if (Step.State == JobStepState.Running)
{
return JobState.Running;
}
else if (Step.State == JobStepState.Ready || Step.State == JobStepState.Waiting)
{
if (Batch.State == JobStepBatchState.Starting || Batch.State == JobStepBatchState.Running)
{
return JobState.Running;
}
else
{
bWaiting = true;
}
}
}
}
return bWaiting ? JobState.Waiting : JobState.Complete;
}
/// <summary>
/// Gets the outcome for a particular named target. May be an aggregate or node name.
/// </summary>
/// <param name="Job">The job to check</param>
/// <returns>The step outcome</returns>
public static (JobStepState, JobStepOutcome) GetTargetState(this IJob Job)
{
IReadOnlyDictionary<NodeRef, IJobStep> NodeToStep = GetStepForNodeMap(Job);
return GetTargetState(NodeToStep.Values);
}
/// <summary>
/// Gets the outcome for a particular named target. May be an aggregate or node name.
/// </summary>
/// <param name="Job">The job to check</param>
/// <param name="Graph">Graph for the job</param>
/// <param name="Target">Target to find an outcome for</param>
/// <returns>The step outcome</returns>
public static (JobStepState, JobStepOutcome)? GetTargetState(this IJob Job, IGraph Graph, string? Target)
{
if (Target == null)
{
return GetTargetState(Job);
}
NodeRef NodeRef;
if (Graph.TryFindNode(Target, out NodeRef))
{
IJobStep? Step;
if (Job.TryGetStepForNode(NodeRef, out Step))
{
return (Step.State, Step.Outcome);
}
else
{
return null;
}
}
IAggregate? Aggregate;
if (Graph.TryFindAggregate(Target, out Aggregate))
{
IReadOnlyDictionary<NodeRef, IJobStep> StepForNode = GetStepForNodeMap(Job);
List<IJobStep> Steps = new List<IJobStep>();
foreach (NodeRef AggregateNodeRef in Aggregate.Nodes)
{
IJobStep? Step;
if (!StepForNode.TryGetValue(AggregateNodeRef, out Step))
{
return null;
}
Steps.Add(Step);
}
return GetTargetState(Steps);
}
return null;
}
/// <summary>
/// Gets the outcome for a particular named target. May be an aggregate or node name.
/// </summary>
/// <param name="Steps">Steps to include</param>
/// <returns>The step outcome</returns>
public static (JobStepState, JobStepOutcome) GetTargetState(IEnumerable<IJobStep> Steps)
{
bool bAnySkipped = false;
bool bAnyWarnings = false;
bool bAnyFailed = false;
bool bAnyPending = false;
foreach (IJobStep Step in Steps)
{
bAnyPending |= Step.IsPending();
bAnySkipped |= Step.State == JobStepState.Aborted || Step.State == JobStepState.Skipped;
bAnyFailed |= (Step.Outcome == JobStepOutcome.Failure);
bAnyWarnings |= (Step.Outcome == JobStepOutcome.Warnings);
}
JobStepState NewState = bAnyPending ? JobStepState.Running : JobStepState.Completed;
JobStepOutcome NewOutcome = bAnyFailed ? JobStepOutcome.Failure : bAnyWarnings ? JobStepOutcome.Warnings : bAnySkipped ? JobStepOutcome.Unspecified : JobStepOutcome.Success;
return (NewState, NewOutcome);
}
/// <summary>
/// Gets the outcome for a particular named target. May be an aggregate or node name.
/// </summary>
/// <param name="Job">The job to check</param>
/// <param name="Graph">Graph for the job</param>
/// <param name="Target">Target to find an outcome for</param>
/// <returns>The step outcome</returns>
public static JobStepOutcome GetTargetOutcome(this IJob Job, IGraph Graph, string Target)
{
NodeRef NodeRef;
if (Graph.TryFindNode(Target, out NodeRef))
{
IJobStep? Step;
if (Job.TryGetStepForNode(NodeRef, out Step))
{
return Step.Outcome;
}
else
{
return JobStepOutcome.Unspecified;
}
}
IAggregate? Aggregate;
if (Graph.TryFindAggregate(Target, out Aggregate))
{
IReadOnlyDictionary<NodeRef, IJobStep> StepForNode = GetStepForNodeMap(Job);
bool Warnings = false;
foreach (NodeRef AggregateNodeRef in Aggregate.Nodes)
{
IJobStep? Step;
if (!StepForNode.TryGetValue(AggregateNodeRef, out Step))
{
return JobStepOutcome.Unspecified;
}
if (Step.Outcome == JobStepOutcome.Failure)
{
return JobStepOutcome.Failure;
}
Warnings |= (Step.Outcome == JobStepOutcome.Warnings);
}
return Warnings ? JobStepOutcome.Warnings : JobStepOutcome.Success;
}
return JobStepOutcome.Unspecified;
}
/// <summary>
/// Gets the job step for a particular node
/// </summary>
/// <param name="Job">The job to search</param>
/// <param name="NodeRef">The node ref</param>
/// <param name="JobStep">Receives the jobstep on success</param>
/// <returns>True if the jobstep was founds</returns>
public static bool TryGetStepForNode(this IJob Job, NodeRef NodeRef, [NotNullWhen(true)] out IJobStep? JobStep)
{
JobStep = null;
foreach (IJobStepBatch Batch in Job.Batches)
{
if (Batch.GroupIdx == NodeRef.GroupIdx)
{
foreach (IJobStep BatchStep in Batch.Steps)
{
if (BatchStep.NodeIdx == NodeRef.NodeIdx)
{
JobStep = BatchStep;
}
}
}
}
return JobStep != null;
}
/// <summary>
/// Gets a dictionary that maps <see cref="NodeRef"/> objects to their associated
/// <see cref="IJobStep"/> objects on a <see cref="IJob"/>.
/// </summary>
/// <param name="Job">The job document</param>
/// <returns>Map of <see cref="NodeRef"/> to <see cref="IJobStep"/></returns>
public static IReadOnlyDictionary<NodeRef, IJobStep> GetStepForNodeMap(this IJob Job)
{
Dictionary<NodeRef, IJobStep> StepForNode = new Dictionary<NodeRef, IJobStep>();
foreach (IJobStepBatch Batch in Job.Batches)
{
foreach (IJobStep BatchStep in Batch.Steps)
{
NodeRef BatchNodeRef = new NodeRef(Batch.GroupIdx, BatchStep.NodeIdx);
StepForNode[BatchNodeRef] = BatchStep;
}
}
return StepForNode;
}
/// <summary>
/// Gets the estimated timing info for all nodes in the job
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="Graph">Graph for this job</param>
/// <param name="JobTiming">Job timing information</param>
/// <returns>Map of node to expected timing info</returns>
public static Dictionary<INode, TimingInfo> GetTimingInfo(this IJob Job, IGraph Graph, IJobTiming JobTiming)
{
TimeSpan CurrentTime = DateTime.UtcNow - Job.CreateTimeUtc;
Dictionary<INode, TimingInfo> NodeToTimingInfo = Graph.Groups.SelectMany(x => x.Nodes).ToDictionary(x => x, x => new TimingInfo());
foreach (IJobStepBatch Batch in Job.Batches)
{
INodeGroup Group = Graph.Groups[Batch.GroupIdx];
// Step through the batch, keeping track of the time that things finish.
TimingInfo TimingInfo = new TimingInfo();
// Wait for the dependencies for the batch to start
HashSet<INode> DependencyNodes = Batch.GetStartDependencies(Graph.Groups);
TimingInfo.WaitForAll(DependencyNodes.Select(x => NodeToTimingInfo[x]));
// If the batch has actually started, correct the expected time to use this instead
if (Batch.StartTimeUtc != null)
{
TimingInfo.TotalTimeToComplete = Batch.StartTimeUtc - Job.CreateTimeUtc;
}
// Get the average times for this batch
TimeSpan? AverageWaitTime = GetAverageWaitTime(Graph, Batch, JobTiming);
TimeSpan? AverageInitTime = GetAverageInitTime(Graph, Batch, JobTiming);
// Update the wait times and initialization times along this path
TimingInfo.TotalWaitTime = TimingInfo.TotalWaitTime + (Batch.GetWaitTime() ?? AverageWaitTime);
TimingInfo.TotalInitTime = TimingInfo.TotalInitTime + (Batch.GetInitTime() ?? AverageInitTime);
// Update the average wait and initialization times too
TimingInfo.AverageTotalWaitTime = TimingInfo.AverageTotalWaitTime + AverageWaitTime;
TimingInfo.AverageTotalInitTime = TimingInfo.AverageTotalInitTime + AverageInitTime;
// Step through the batch, updating the expected times as we go
foreach (IJobStep Step in Batch.Steps)
{
INode Node = Group.Nodes[Step.NodeIdx];
// Get the timing for this step
IJobStepTiming? StepTimingInfo;
JobTiming.TryGetStepTiming(Node.Name, out StepTimingInfo);
// If the step has already started, update the actual time to reach this point
if(Step.StartTimeUtc != null)
{
TimingInfo.TotalTimeToComplete = Step.StartTimeUtc.Value - Job.CreateTimeUtc;
}
// If the step hasn't started yet, make sure the start time is later than the current time
if (Step.StartTimeUtc == null && CurrentTime > TimingInfo.TotalTimeToComplete)
{
TimingInfo.TotalTimeToComplete = CurrentTime;
}
// Wait for all the node dependencies to complete
TimingInfo.WaitForAll(Graph.GetDependencies(Node).Select(x => NodeToTimingInfo[x]));
// If the step has actually finished, correct the time to use that instead
if (Step.FinishTimeUtc != null)
{
TimingInfo.TotalTimeToComplete = Step.FinishTimeUtc.Value - Job.CreateTimeUtc;
}
else
{
TimingInfo.TotalTimeToComplete = TimingInfo.TotalTimeToComplete + NullableTimeSpanFromSeconds(StepTimingInfo?.AverageDuration);
}
// If the step hasn't finished yet, make sure the start time is later than the current time
if (Step.FinishTimeUtc == null && CurrentTime > TimingInfo.TotalTimeToComplete)
{
TimingInfo.TotalTimeToComplete = CurrentTime;
}
// Update the average time to complete
TimingInfo.AverageTotalTimeToComplete = TimingInfo.AverageTotalTimeToComplete + NullableTimeSpanFromSeconds(StepTimingInfo?.AverageDuration);
// Add it to the lookup
TimingInfo NodeTimingInfo = new TimingInfo(TimingInfo);
NodeTimingInfo.StepTiming = StepTimingInfo;
NodeToTimingInfo[Node] = NodeTimingInfo;
}
}
return NodeToTimingInfo;
}
/// <summary>
/// Gets the average wait time for this batch
/// </summary>
/// <param name="Graph">Graph for the job</param>
/// <param name="Batch">The batch to get timing info for</param>
/// <param name="JobTiming">The job timing information</param>
/// <returns>Wait time for the batch</returns>
public static TimeSpan? GetAverageWaitTime(IGraph Graph, IJobStepBatch Batch, IJobTiming JobTiming)
{
TimeSpan? WaitTime = null;
foreach (IJobStep Step in Batch.Steps)
{
INode Node = Graph.Groups[Batch.GroupIdx].Nodes[Step.NodeIdx];
if(JobTiming.TryGetStepTiming(Node.Name, out IJobStepTiming? TimingInfo))
{
if (TimingInfo.AverageWaitTime != null)
{
TimeSpan StepWaitTime = TimeSpan.FromSeconds(TimingInfo.AverageWaitTime.Value);
if (WaitTime == null || StepWaitTime > WaitTime.Value)
{
WaitTime = StepWaitTime;
}
}
}
}
return WaitTime;
}
/// <summary>
/// Gets the average initialization time for this batch
/// </summary>
/// <param name="Graph">Graph for the job</param>
/// <param name="Batch">The batch to get timing info for</param>
/// <param name="JobTiming">The job timing information</param>
/// <returns>Initialization time for this batch</returns>
public static TimeSpan? GetAverageInitTime(IGraph Graph, IJobStepBatch Batch, IJobTiming JobTiming)
{
TimeSpan? InitTime = null;
foreach (IJobStep Step in Batch.Steps)
{
INode Node = Graph.Groups[Batch.GroupIdx].Nodes[Step.NodeIdx];
if (JobTiming.TryGetStepTiming(Node.Name, out IJobStepTiming? TimingInfo))
{
if (TimingInfo.AverageInitTime != null)
{
TimeSpan StepInitTime = TimeSpan.FromSeconds(TimingInfo.AverageInitTime.Value);
if (InitTime == null || StepInitTime > InitTime.Value)
{
InitTime = StepInitTime;
}
}
}
}
return InitTime;
}
/// <summary>
/// Creates a nullable timespan from a nullable number of seconds
/// </summary>
/// <param name="Seconds">The number of seconds to construct from</param>
/// <returns>TimeSpan object</returns>
static TimeSpan? NullableTimeSpanFromSeconds(float? Seconds)
{
if (Seconds == null)
{
return null;
}
else
{
return TimeSpan.FromSeconds(Seconds.Value);
}
}
/// <summary>
/// Attempts to get a batch with the given id
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="BatchId">The batch id</param>
/// <param name="Batch">On success, receives the batch object</param>
/// <returns>True if the batch was found</returns>
public static bool TryGetBatch(this IJob Job, SubResourceId BatchId, [NotNullWhen(true)] out IJobStepBatch? Batch)
{
Batch = Job.Batches.FirstOrDefault(x => x.Id == BatchId);
return Batch != null;
}
/// <summary>
/// Attempts to get a batch with the given id
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="BatchId">The batch id</param>
/// <param name="StepId">The step id</param>
/// <param name="Step">On success, receives the step object</param>
/// <returns>True if the batch was found</returns>
public static bool TryGetStep(this IJob Job, SubResourceId BatchId, SubResourceId StepId, [NotNullWhen(true)] out IJobStep? Step)
{
IJobStepBatch? Batch;
if (!TryGetBatch(Job, BatchId, out Batch))
{
Step = null;
return false;
}
return Batch.TryGetStep(StepId, out Step);
}
/// <summary>
/// Finds the set of nodes affected by a label
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="Graph">Graph definition for the job</param>
/// <param name="LabelIdx">Index of the label. -1 or Graph.Labels.Count are treated as referring to the default lable.</param>
/// <returns>Set of nodes affected by the given label</returns>
public static HashSet<NodeRef> GetNodesForLabel(this IJob Job, IGraph Graph, int LabelIdx)
{
if (LabelIdx != -1 && LabelIdx != Graph.Labels.Count)
{
// Return all the nodes included by the label
return new HashSet<NodeRef>(Graph.Labels[LabelIdx].IncludedNodes);
}
else
{
// Set of nodes which are not covered by an existing label, initially containing everything
HashSet<NodeRef> UnlabeledNodes = new HashSet<NodeRef>();
for (int GroupIdx = 0; GroupIdx < Graph.Groups.Count; GroupIdx++)
{
INodeGroup Group = Graph.Groups[GroupIdx];
for (int NodeIdx = 0; NodeIdx < Group.Nodes.Count; NodeIdx++)
{
UnlabeledNodes.Add(new NodeRef(GroupIdx, NodeIdx));
}
}
// Remove all the nodes that are part of an active label
IReadOnlyDictionary<NodeRef, IJobStep> StepForNode = Job.GetStepForNodeMap();
foreach (ILabel Label in Graph.Labels)
{
if (Label.RequiredNodes.Any(x => StepForNode.ContainsKey(x)))
{
UnlabeledNodes.ExceptWith(Label.IncludedNodes);
}
}
return UnlabeledNodes;
}
}
/// <summary>
/// Create a list of aggregate responses, combining the graph definitions with the state of the job
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="Graph">Graph definition for the job</param>
/// <param name="Responses">List to receive all the responses</param>
/// <returns>The default label state</returns>
public static GetDefaultLabelStateResponse? GetLabelStateResponses(this IJob Job, IGraph Graph, List<GetLabelStateResponse> Responses)
{
// Create a lookup from noderef to step information
IReadOnlyDictionary<NodeRef, IJobStep> StepForNode = Job.GetStepForNodeMap();
// Set of nodes which are not covered by an existing label, initially containing everything
HashSet<NodeRef> UnlabeledNodes = new HashSet<NodeRef>();
for (int GroupIdx = 0; GroupIdx < Graph.Groups.Count; GroupIdx++)
{
INodeGroup Group = Graph.Groups[GroupIdx];
for (int NodeIdx = 0; NodeIdx < Group.Nodes.Count; NodeIdx++)
{
UnlabeledNodes.Add(new NodeRef(GroupIdx, NodeIdx));
}
}
// Create the responses
foreach (ILabel Label in Graph.Labels)
{
// Refresh the state for this label
LabelState NewState = LabelState.Unspecified;
foreach (NodeRef RequiredNodeRef in Label.RequiredNodes)
{
if (StepForNode.ContainsKey(RequiredNodeRef))
{
NewState = LabelState.Complete;
break;
}
}
// Refresh the outcome
LabelOutcome NewOutcome = LabelOutcome.Success;
if (NewState == LabelState.Complete)
{
GetLabelState(Label.IncludedNodes, StepForNode, out NewState, out NewOutcome);
UnlabeledNodes.ExceptWith(Label.IncludedNodes);
}
// Create the response
Responses.Add(new GetLabelStateResponse(NewState, NewOutcome));
}
// Remove all the nodes that don't have a step
UnlabeledNodes.RemoveWhere(x => !StepForNode.ContainsKey(x));
// Remove successful "setup build" nodes from the list
if (Graph.Groups.Count > 1 && Graph.Groups[0].Nodes.Count > 0)
{
INode Node = Graph.Groups[0].Nodes[0];
if (Node.Name == IJob.SetupNodeName)
{
NodeRef NodeRef = new NodeRef(0, 0);
if (UnlabeledNodes.Contains(NodeRef))
{
IJobStep Step = StepForNode[NodeRef];
if (Step.State == JobStepState.Completed && Step.Outcome == JobStepOutcome.Success && Responses.Count > 0)
{
UnlabeledNodes.Remove(NodeRef);
}
}
}
}
// Add a response for everything not included elsewhere.
GetLabelState(UnlabeledNodes, StepForNode, out LabelState OtherState, out LabelOutcome OtherOutcome);
return new GetDefaultLabelStateResponse(OtherState, OtherOutcome, UnlabeledNodes.Select(x => Graph.GetNode(x).Name).ToList());
}
/// <summary>
/// Get the states of all labels for this job
/// </summary>
/// <param name="Job">The job to get states for</param>
/// <param name="Graph">The graph for this job</param>
/// <returns>Collection of label states by label index</returns>
public static IReadOnlyList<(LabelState, LabelOutcome)> GetLabelStates(this IJob Job, IGraph Graph)
{
IReadOnlyDictionary<NodeRef, IJobStep> StepForNodeRef = Job.GetStepForNodeMap();
List<(LabelState, LabelOutcome)> States = new List<(LabelState, LabelOutcome)>();
for (int Idx = 0; Idx < Graph.Labels.Count; Idx++)
{
ILabel Label = Graph.Labels[Idx];
// Default the label to the unspecified state
LabelState NewState = LabelState.Unspecified;
LabelOutcome NewOutcome = LabelOutcome.Unspecified;
// Check if the label should be included
if (Label.RequiredNodes.Any(x => StepForNodeRef.ContainsKey(x)))
{
// Combine the state of the steps contributing towards this label
bool bAnySkipped = false;
bool bAnyWarnings = false;
bool bAnyFailed = false;
bool bAnyPending = false;
foreach (NodeRef IncludedNode in Label.IncludedNodes)
{
IJobStep? Step;
if (StepForNodeRef.TryGetValue(IncludedNode, out Step))
{
bAnyPending |= Step.IsPending();
bAnySkipped |= Step.State == JobStepState.Aborted || Step.State == JobStepState.Skipped;
bAnyFailed |= (Step.Outcome == JobStepOutcome.Failure);
bAnyWarnings |= (Step.Outcome == JobStepOutcome.Warnings);
}
}
// Figure out the overall label state
NewState = bAnyPending ? LabelState.Running : LabelState.Complete;
NewOutcome = bAnyFailed ? LabelOutcome.Failure : bAnyWarnings ? LabelOutcome.Warnings : bAnySkipped? LabelOutcome.Unspecified : LabelOutcome.Success;
}
States.Add((NewState, NewOutcome));
}
return States;
}
/// <summary>
/// Get the states of all UGS badges for this job
/// </summary>
/// <param name="Job">The job to get states for</param>
/// <param name="Graph">The graph for this job</param>
/// <returns>List of badge states</returns>
public static Dictionary<int, UgsBadgeState> GetUgsBadgeStates(this IJob Job, IGraph Graph)
{
IReadOnlyList<(LabelState, LabelOutcome)> LabelStates = GetLabelStates(Job, Graph);
return Job.GetUgsBadgeStates(Graph, LabelStates);
}
/// <summary>
/// Get the states of all UGS badges for this job
/// </summary>
/// <param name="Job">The job to get states for</param>
/// <param name="Graph">The graph for this job</param>
/// <param name="LabelStates">The existing label states to get the UGS badge states from</param>
/// <returns>List of badge states</returns>
public static Dictionary<int, UgsBadgeState> GetUgsBadgeStates(this IJob Job, IGraph Graph, IReadOnlyList<(LabelState, LabelOutcome)> LabelStates)
{
Dictionary<int, UgsBadgeState> UgsBadgeStates = new Dictionary<int, UgsBadgeState>();
for (int LabelIdx = 0; LabelIdx < LabelStates.Count; ++LabelIdx)
{
if (Graph.Labels[LabelIdx].UgsName == null)
{
continue;
}
(LabelState State, LabelOutcome Outcome) Label = LabelStates[LabelIdx];
switch (Label.State)
{
case LabelState.Complete:
{
switch (Label.Outcome)
{
case LabelOutcome.Success:
{
UgsBadgeStates.Add(LabelIdx, UgsBadgeState.Success);
break;
}
case LabelOutcome.Warnings:
{
UgsBadgeStates.Add(LabelIdx, UgsBadgeState.Warning);
break;
}
case LabelOutcome.Failure:
{
UgsBadgeStates.Add(LabelIdx, UgsBadgeState.Failure);
break;
}
case LabelOutcome.Unspecified:
{
UgsBadgeStates.Add(LabelIdx, UgsBadgeState.Skipped);
break;
}
}
break;
}
case LabelState.Running:
{
UgsBadgeStates.Add(LabelIdx, UgsBadgeState.Starting);
break;
}
case LabelState.Unspecified:
{
UgsBadgeStates.Add(LabelIdx, UgsBadgeState.Skipped);
break;
}
}
}
return UgsBadgeStates;
}
/// <summary>
/// Gets the state of a job, as a label that includes all steps
/// </summary>
/// <param name="Job">The job to query</param>
/// <param name="StepForNode">Map from node to step</param>
/// <param name="NewState">Receives the state of the label</param>
/// <param name="NewOutcome">Receives the outcome of the label</param>
public static void GetJobState(this IJob Job, IReadOnlyDictionary<NodeRef, IJobStep> StepForNode, out LabelState NewState, out LabelOutcome NewOutcome)
{
List<NodeRef> Nodes = new List<NodeRef>();
foreach (IJobStepBatch Batch in Job.Batches)
{
foreach (IJobStep Step in Batch.Steps)
{
Nodes.Add(new NodeRef(Batch.GroupIdx, Step.NodeIdx));
}
}
GetLabelState(Nodes, StepForNode, out NewState, out NewOutcome);
}
/// <summary>
/// Gets the state of a label
/// </summary>
/// <param name="IncludedNodes">Nodes to include in this label</param>
/// <param name="StepForNode">Map from node to step</param>
/// <param name="NewState">Receives the state of the label</param>
/// <param name="NewOutcome">Receives the outcome of the label</param>
public static void GetLabelState(IEnumerable<NodeRef> IncludedNodes, IReadOnlyDictionary<NodeRef, IJobStep> StepForNode, out LabelState NewState, out LabelOutcome NewOutcome)
{
NewState = LabelState.Complete;
NewOutcome = LabelOutcome.Success;
foreach (NodeRef IncludedNodeRef in IncludedNodes)
{
IJobStep? IncludedStep;
if (StepForNode.TryGetValue(IncludedNodeRef, out IncludedStep))
{
// Update the state
if (IncludedStep.State != JobStepState.Completed && IncludedStep.State != JobStepState.Skipped && IncludedStep.State != JobStepState.Aborted)
{
NewState = LabelState.Running;
}
// Update the outcome
if (IncludedStep.State == JobStepState.Skipped || IncludedStep.State == JobStepState.Aborted || IncludedStep.Outcome == JobStepOutcome.Failure)
{
NewOutcome = LabelOutcome.Failure;
}
else if (IncludedStep.Outcome == JobStepOutcome.Warnings && NewOutcome == LabelOutcome.Success)
{
NewOutcome = LabelOutcome.Warnings;
}
}
}
}
/// <summary>
/// Creates a response containing information about this object
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="Graph">The graph definition</param>
/// <param name="bIncludeBatches">Whether to include the job batches in the response</param>
/// <param name="bIncludeLabels">Whether to include the job aggregates in the response</param>
/// <param name="bIncludeAcl">Whether to include the ACL in the response</param>
/// <returns>The response object</returns>
public static GetJobResponse ToResponse(this IJob Job, IGraph Graph, bool bIncludeBatches, bool bIncludeLabels, bool bIncludeAcl)
{
GetAclResponse? AclResponse = (bIncludeAcl && Job.Acl != null) ? new GetAclResponse(Job.Acl) : null;
GetJobResponse Response = new GetJobResponse(Job, AclResponse);
if (bIncludeBatches || bIncludeLabels)
{
if (bIncludeBatches)
{
Response.Batches = Job.Batches.ConvertAll(x => new GetBatchResponse(x));
}
if (bIncludeLabels)
{
Response.Labels = new List<GetLabelStateResponse>();
Response.DefaultLabel = GetLabelStateResponses(Job, Graph, Response.Labels);
}
}
return Response;
}
/// <summary>
/// Creates a response containing information about this object
/// </summary>
/// <param name="Job">The job document</param>
/// <param name="Graph">The graph for this job</param>
/// <param name="bIncludeAcl">Whether to include the ACL in the response</param>
/// <param name="Filter">Filter for the properties to return</param>
/// <returns>Object containing the requested properties</returns>
public static object ToResponse(this IJob Job, IGraph Graph, bool bIncludeAcl, PropertyFilter? Filter)
{
if(Filter == null)
{
return Job.ToResponse(Graph, true, true, bIncludeAcl);
}
else
{
return Filter.ApplyTo(Job.ToResponse(Graph, Filter.Includes(nameof(GetJobResponse.Batches)), Filter.Includes(nameof(GetJobResponse.Labels)) || Filter.Includes(nameof(GetJobResponse.DefaultLabel)), bIncludeAcl));
}
}
/// <summary>
/// Creates an RPC response object
/// </summary>
/// <param name="Job">The job document</param>
/// <returns></returns>
public static HordeCommon.Rpc.GetJobResponse ToRpcResponse(this IJob Job)
{
HordeCommon.Rpc.GetJobResponse Response = new HordeCommon.Rpc.GetJobResponse();
Response.StreamId = Job.StreamId.ToString();
Response.Change = Job.Change;
Response.CodeChange = Job.CodeChange;
Response.PreflightChange = Job.PreflightChange;
Response.ClonedPreflightChange = Job.ClonedPreflightChange;
Response.Arguments.Add(Job.Arguments);
return Response;
}
}
/// <summary>
/// Projection of a job definition to just include permissions info
/// </summary>
public interface IJobPermissions
{
/// <summary>
/// ACL for the job
/// </summary>
public Acl? Acl { get; }
/// <summary>
/// The stream containing
/// </summary>
public StreamId StreamId { get; }
}
}