Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Horde/Jobs/IJob.cs
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

1931 lines
62 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Horde.Acls;
using EpicGames.Horde.Agents;
using EpicGames.Horde.Agents.Leases;
using EpicGames.Horde.Agents.Pools;
using EpicGames.Horde.Agents.Sessions;
using EpicGames.Horde.Artifacts;
using EpicGames.Horde.Commits;
using EpicGames.Horde.Jobs.Bisect;
using EpicGames.Horde.Jobs.Graphs;
using EpicGames.Horde.Jobs.Templates;
using EpicGames.Horde.Jobs.Timing;
using EpicGames.Horde.Logs;
using EpicGames.Horde.Notifications;
using EpicGames.Horde.Streams;
using EpicGames.Horde.Ugs;
using EpicGames.Horde.Users;
using Microsoft.Extensions.Logging;
#pragma warning disable CA1716
namespace EpicGames.Horde.Jobs
{
/// <summary>
/// Document describing a job
/// </summary>
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 JobId Id { get; }
/// <summary>
/// The stream that this job belongs to
/// </summary>
public StreamId StreamId { get; }
/// <summary>
/// The template ref id
/// </summary>
public TemplateId 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>
/// Graph for this job
/// </summary>
public IGraph Graph { get; }
/// <summary>
/// Id of the user that started this job
/// </summary>
public UserId? StartedByUserId { get; }
/// <summary>
/// Id of the user that aborted this job. Set to null if the job is not aborted.
/// </summary>
public UserId? AbortedByUserId { get; }
/// <summary>
/// Optional reason for why the job was canceled
/// </summary>
public string? CancellationReason { get; }
/// <summary>
/// Identifier of the bisect task that started this job
/// </summary>
public BisectTaskId? StartedByBisectTaskId { get; }
/// <summary>
/// Name of the job.
/// </summary>
public string Name { get; }
/// <summary>
/// The commit to build
/// </summary>
public CommitIdWithOrder CommitId { get; }
/// <summary>
/// The code commit for this build
/// </summary>
public CommitIdWithOrder? CodeCommitId { get; }
/// <summary>
/// The preflight changelist number
/// </summary>
public CommitId? PreflightCommitId { get; }
/// <summary>
/// Description for the shelved change if running a preflight
/// </summary>
public string? PreflightDescription { 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>
/// Whether to promote issues by default based on the outcome of this job
/// </summary>
public bool PromoteIssuesByDefault { get; }
/// <summary>
/// Time that the job was created (in UTC)
/// </summary>
public DateTime CreateTimeUtc { get; }
/// <summary>
/// Options for executing the job
/// </summary>
public JobOptions? JobOptions { get; }
/// <summary>
/// Claims inherited from the user that started this job
/// </summary>
public IReadOnlyList<IAclClaim> Claims { get; }
/// <summary>
/// Array of jobstep runs
/// </summary>
public IReadOnlyList<IJobStepBatch> Batches { get; }
/// <summary>
/// Parameters for the job
/// </summary>
public IReadOnlyDictionary<ParameterId, string> Parameters { get; }
/// <summary>
/// Optional user-defined properties for this job
/// </summary>
public IReadOnlyList<string> Arguments { get; }
/// <summary>
/// Custom list of targets for the job. If null or empty, the list of targets is determined from the command line.
/// </summary>
public IReadOnlyList<string>? Targets { get; }
/// <summary>
/// Additional arguments for the job, when a set of parameters are applied.
/// </summary>
public IReadOnlyList<string> AdditionalArguments { get; }
/// <summary>
/// Environment variables for the job
/// </summary>
public IReadOnlyDictionary<string, string> Environment { get; }
/// <summary>
/// Issues associated with this job
/// </summary>
public IReadOnlyList<int> Issues { get; }
/// <summary>
/// Unique id for notifications
/// </summary>
public NotificationTriggerId? 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>
/// Mapping of label ids to notification trigger ids for notifications
/// </summary>
public IReadOnlyDictionary<int, NotificationTriggerId> LabelIdxToTriggerId { get; }
/// <summary>
/// List of reports for this step
/// </summary>
public IReadOnlyList<IJobReport>? Reports { get; }
/// <summary>
/// List of downstream job triggers
/// </summary>
public IReadOnlyList<IChainedJob> ChainedJobs { get; }
/// <summary>
/// The job which spawned this job
/// </summary>
public JobId? ParentJobId { get; }
/// <summary>
/// The job step which spawned this job
/// </summary>
public JobStepId? ParentJobStepId { get; }
/// <summary>
/// The last update time
/// </summary>
public DateTime UpdateTimeUtc { get; }
/// <summary>
/// The job meta data
/// </summary>
public List<string> Metadata { 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>
/// Gets the latest job state
/// </summary>
/// <param name="cancellationToken">Cancellation token for the operation</param>
Task<IJob?> RefreshAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Attempt to get a batch with the given id
/// </summary>
/// <param name="batchId">The job batch id</param>
/// <param name="batch">Receives the batch interface on success</param>
/// <returns>True if the batch was found</returns>
bool TryGetBatch(JobStepBatchId batchId, [NotNullWhen(true)] out IJobStepBatch? batch);
/// <summary>
/// Attempt to get a step with the given id
/// </summary>
/// <param name="stepId">The job step id</param>
/// <param name="step">Receives the step interface on success</param>
/// <returns>True if the step was found</returns>
bool TryGetStep(JobStepId stepId, [NotNullWhen(true)] out IJobStep? step);
/// <summary>
/// Attempt to delete the job
/// </summary>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the job was deleted. False if the job is not the latest revision.</returns>
Task<bool> TryDeleteAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Removes a job from the dispatch queue. Ignores the state of any batches still remaining to execute. Should only be used to correct for inconsistent state.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns></returns>
Task<IJob?> TryRemoveFromDispatchQueueAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Updates a new job
/// </summary>
/// <param name="name">Name of the job</param>
/// <param name="priority">Priority of the job</param>
/// <param name="autoSubmit">Automatically submit the job on completion</param>
/// <param name="autoSubmitChange">Changelist that was automatically submitted</param>
/// <param name="autoSubmitMessage"></param>
/// <param name="abortedByUserId">Name of the user that aborted the job</param>
/// <param name="notificationTriggerId">Id for a notification trigger</param>
/// <param name="reports">New reports</param>
/// <param name="arguments">New arguments for the job</param>
/// <param name="labelIdxToTriggerId">New trigger ID for a label in the job</param>
/// <param name="jobTrigger">New downstream job id</param>
/// <param name="cancellationReason">Optional reason why the job was canceled</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
Task<IJob?> TryUpdateJobAsync(string? name = null, Priority? priority = null, bool? autoSubmit = null, int? autoSubmitChange = null, string? autoSubmitMessage = null, UserId? abortedByUserId = null, NotificationTriggerId? notificationTriggerId = null, List<JobReport>? reports = null, List<string>? arguments = null, KeyValuePair<int, NotificationTriggerId>? labelIdxToTriggerId = null, KeyValuePair<TemplateId, JobId>? jobTrigger = null, string? cancellationReason = null, CancellationToken cancellationToken = default);
/// <summary>
/// Updates the state of a batch
/// </summary>
/// <param name="batchId">Unique id of the batch to update</param>
/// <param name="newLogId">The new log file id</param>
/// <param name="newState">New state of the jobstep</param>
/// <param name="newError">Error code for the batch</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the job was updated, false if it was deleted</returns>
Task<IJob?> TryUpdateBatchAsync(JobStepBatchId batchId, LogId? newLogId, JobStepBatchState? newState, JobStepBatchError? newError, CancellationToken cancellationToken = default);
/// <summary>
/// Update a jobstep state
/// </summary>
/// <param name="batchId">Unique id of the batch containing the step</param>
/// <param name="stepId">Unique id of the step to update</param>
/// <param name="newState">New state of the jobstep</param>
/// <param name="newOutcome">New outcome of the jobstep</param>
/// <param name="newError">New error annotation for this jobstep</param>
/// <param name="newAbortRequested">New state of request abort</param>
/// <param name="newAbortByUserId">New name of user that requested the abort</param>
/// <param name="newLogId">New log id for the jobstep</param>
/// <param name="newNotificationTriggerId">New id for a notification trigger</param>
/// <param name="newRetryByUserId">Whether the step should be retried</param>
/// <param name="newPriority">New priority for this step</param>
/// <param name="newReports">New report documents</param>
/// <param name="newProperties">Property changes. Any properties with a null value will be removed.</param>
/// <param name="newCancellationReason">The reason the job step was canceled</param>
/// <param name="newSpawnedJob">JobId of any job this step spawned</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the job was updated, false if it was deleted in the meantime</returns>
Task<IJob?> TryUpdateStepAsync(JobStepBatchId batchId, JobStepId stepId, JobStepState newState = default, JobStepOutcome newOutcome = default, JobStepError? newError = null, bool? newAbortRequested = null, UserId? newAbortByUserId = null, LogId? newLogId = null, NotificationTriggerId? newNotificationTriggerId = null, UserId? newRetryByUserId = null, Priority? newPriority = null, List<JobReport>? newReports = null, Dictionary<string, string?>? newProperties = null, string? newCancellationReason = null, JobId? newSpawnedJob = null, CancellationToken cancellationToken = default);
/// <summary>
/// Attempts to update the node groups to be executed for a job. Fails if another write happens in the meantime.
/// </summary>
/// <param name="newGraph">New graph for this job</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the groups were updated to the given list. False if another write happened first.</returns>
Task<IJob?> TryUpdateGraphAsync(IGraph newGraph, CancellationToken cancellationToken = default);
/// <summary>
/// Marks a job as skipped
/// </summary>
/// <param name="reason">Reason for this batch being failed</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>Updated version of the job</returns>
Task<IJob?> TrySkipAllBatchesAsync(JobStepBatchError reason, CancellationToken cancellationToken = default);
/// <summary>
/// Marks a batch as skipped
/// </summary>
/// <param name="batchId">The batch to mark as skipped</param>
/// <param name="reason">Reason for this batch being failed</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>Updated version of the job</returns>
Task<IJob?> TrySkipBatchAsync(JobStepBatchId batchId, JobStepBatchError reason, CancellationToken cancellationToken = default);
/// <summary>
/// Abort an agent's lease, and update the payload accordingly
/// </summary>
/// <param name="batchIdx">Index of the batch to cancel</param>
/// <param name="reason">Reason for this batch being failed</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the job is updated</returns>
Task<IJob?> TryFailBatchAsync(int batchIdx, JobStepBatchError reason, CancellationToken cancellationToken = default);
/// <summary>
/// Attempt to assign a lease to execute a batch
/// </summary>
/// <param name="batchIdx">Index of the batch</param>
/// <param name="poolId">The pool id</param>
/// <param name="agentId">New agent to execute the batch</param>
/// <param name="sessionId">Session of the agent that is to execute the batch</param>
/// <param name="leaseId">The lease unique id</param>
/// <param name="logId">Unique id of the log for the batch</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the batch is updated</returns>
Task<IJob?> TryAssignLeaseAsync(int batchIdx, PoolId poolId, AgentId agentId, SessionId sessionId, LeaseId leaseId, LogId logId, CancellationToken cancellationToken = default);
/// <summary>
/// Cancel a lease reservation on a batch (before it has started)
/// </summary>
/// <param name="batchIdx">Index of the batch to cancel</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>True if the job is updated</returns>
Task<IJob?> TryCancelLeaseAsync(int batchIdx, CancellationToken cancellationToken = default);
}
/// <summary>
/// Extension methods for jobs
/// </summary>
public static class JobExtensions
{
/// <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 waiting = 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
{
waiting = true;
}
}
}
}
return waiting ? 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 anySkipped = false;
bool anyWarnings = false;
bool anyFailed = false;
bool anyPending = false;
foreach (IJobStep step in steps)
{
anyPending |= step.IsPending();
anySkipped |= step.State == JobStepState.Aborted || step.State == JobStepState.Skipped;
anyFailed |= (step.Outcome == JobStepOutcome.Failure);
anyWarnings |= (step.Outcome == JobStepOutcome.Warnings);
}
JobStepState newState = anyPending ? JobStepState.Running : JobStepState.Completed;
JobStepOutcome newOutcome = anyFailed ? JobStepOutcome.Failure : anyWarnings ? JobStepOutcome.Warnings : anySkipped ? 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>
/// Find the latest step executing the given node
/// </summary>
/// <param name="job">The job being run</param>
/// <param name="nodeRef">Node to find</param>
/// <returns>The retried step information</returns>
public static JobStepRefId? FindLatestStepForNode(this IJob job, NodeRef nodeRef)
{
for (int batchIdx = job.Batches.Count - 1; batchIdx >= 0; batchIdx--)
{
IJobStepBatch batch = job.Batches[batchIdx];
if (batch.GroupIdx == nodeRef.GroupIdx)
{
for (int stepIdx = batch.Steps.Count - 1; stepIdx >= 0; stepIdx--)
{
IJobStep step = batch.Steps[stepIdx];
if (step.NodeIdx == nodeRef.NodeIdx)
{
return new JobStepRefId(job.Id, batch.Id, step.Id);
}
}
}
}
return null;
}
/// <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>
/// <param name="logger">Logger for any diagnostic messages</param>
/// <returns>Map of node to expected timing info</returns>
public static Dictionary<INode, TimingInfo> GetTimingInfo(this IJob job, IGraph graph, IJobTiming jobTiming, ILogger logger)
{
#pragma warning disable IDE0054 // Use compound assignment
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, logger);
TimeSpan? averageInitTime = GetAverageInitTime(graph, batch, jobTiming, logger);
// 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, logger, 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;
#pragma warning restore IDE0054 // Use compound assignment
}
/// <summary>
/// Gets the step completion info of the job.
/// </summary>
/// <param name="job">The job document</param>
/// <returns>The job's step completion info.</returns>
/// <remarks>Any steps not in a terminal state (<see cref="JobStepState.Completed"/>, <see cref="JobStepState.Aborted"/>, <see cref="JobStepState.Skipped"/>) are excluded in total counts except <see cref="JobStepsCompletionInfo.StepTotalCount"/>.</remarks>
public static JobStepsCompletionInfo GetStepCompletionInfo(this IJob job)
{
JobStepsCompletionInfo jobStepsCompletionInfo = new JobStepsCompletionInfo();
foreach (IJobStepBatch batch in job.Batches)
{
foreach (IJobStep step in batch.Steps)
{
jobStepsCompletionInfo.StepTotalCount++;
if (step.State != JobStepState.Completed && step.State != JobStepState.Aborted && step.State != JobStepState.Skipped)
{
continue;
}
if (step.FinishTimeUtc != null && step.StartTimeUtc != null)
{
TimeSpan? stepDuration = step.FinishTimeUtc.Value - step.StartTimeUtc;
if (stepDuration != null)
{
jobStepsCompletionInfo.JobStepsTotalTime += (float)stepDuration.Value.TotalSeconds;
}
}
switch (step.Outcome)
{
case JobStepOutcome.Success:
jobStepsCompletionInfo.StepPassCount++;
break;
case JobStepOutcome.Warnings:
jobStepsCompletionInfo.StepWarningCount++;
break;
case JobStepOutcome.Failure:
jobStepsCompletionInfo.StepFailureCount++;
break;
}
}
}
return jobStepsCompletionInfo;
}
/// <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>
/// <param name="logger">Logger for diagnostic info</param>
/// <returns>Wait time for the batch</returns>
public static TimeSpan? GetAverageWaitTime(IGraph graph, IJobStepBatch batch, IJobTiming jobTiming, ILogger logger)
{
TimeSpan? waitTime = null;
foreach (IJobStep step in batch.Steps)
{
INode node = graph.Groups[batch.GroupIdx].Nodes[step.NodeIdx];
if (jobTiming.TryGetStepTiming(node.Name, logger, 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>
/// <param name="logger">Logger for diagnostic messages</param>
/// <returns>Initialization time for this batch</returns>
public static TimeSpan? GetAverageInitTime(IGraph graph, IJobStepBatch batch, IJobTiming jobTiming, ILogger logger)
{
TimeSpan? initTime = null;
foreach (IJobStep step in batch.Steps)
{
INode node = graph.Groups[batch.GroupIdx].Nodes[step.NodeIdx];
if (jobTiming.TryGetStepTiming(node.Name, logger, 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="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, JobStepBatchId batchId, JobStepId stepId, [NotNullWhen(true)] out IJobStep? step)
{
if (!job.TryGetStep(stepId, out step) || step.Batch.Id != batchId)
{
step = null;
return false;
}
return true;
}
/// <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
GetLabelStateResponse response = new GetLabelStateResponse();
response.DashboardName = label.DashboardName;
response.DashboardCategory = label.DashboardCategory;
response.UgsName = label.UgsName;
response.UgsProject = label.UgsProject;
response.State = newState;
response.Outcome = newOutcome;
foreach (NodeRef includedNodeRef in label.IncludedNodes)
{
IJobStep? includedStep;
if (stepForNode.TryGetValue(includedNodeRef, out includedStep))
{
response.Steps.Add(includedStep.Id);
}
}
responses.Add(response);
}
// 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);
GetDefaultLabelStateResponse defaultResponse = new GetDefaultLabelStateResponse();
defaultResponse.State = otherState;
defaultResponse.Outcome = otherOutcome;
defaultResponse.Nodes.AddRange(unlabeledNodes.Select(x => graph.GetNode(x).Name));
defaultResponse.Steps.AddRange(unlabeledNodes.Select(x => stepForNode[x].Id));
return defaultResponse;
}
/// <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 anySkipped = false;
bool anyWarnings = false;
bool anyFailed = false;
bool anyPending = false;
foreach (NodeRef includedNode in label.IncludedNodes)
{
IJobStep? step;
if (stepForNodeRef.TryGetValue(includedNode, out step))
{
anyPending |= step.IsPending();
anySkipped |= step.State == JobStepState.Aborted || step.State == JobStepState.Skipped;
anyFailed |= (step.Outcome == JobStepOutcome.Failure);
anyWarnings |= (step.Outcome == JobStepOutcome.Warnings);
}
}
// Figure out the overall label state
newState = anyPending ? LabelState.Running : LabelState.Complete;
newOutcome = anyFailed ? LabelOutcome.Failure : anyWarnings ? LabelOutcome.Warnings : anySkipped ? 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>
#pragma warning disable IDE0060 // Remove unused parameter
public static Dictionary<int, UgsBadgeState> GetUgsBadgeStates(this IJob job, IGraph graph, IReadOnlyList<(LabelState, LabelOutcome)> labelStates)
#pragma warning restore IDE0060 // Remove unused parameter
{
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) = labelStates[labelIdx];
switch (state)
{
case LabelState.Complete:
{
switch (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>
/// Gets a key attached to all artifacts produced for a job
/// </summary>
public static string GetArtifactKey(this IJob job)
{
return $"job:{job.Id}";
}
/// <summary>
/// Gets a key attached to all artifacts produced for a job step
/// </summary>
public static string GetArtifactKey(this IJob job, IJobStep jobStep)
{
return $"job:{job.Id}/step:{jobStep.Id}";
}
/// <inheritdoc cref="IJob.TrySkipBatchAsync(JobStepBatchId, JobStepBatchError, CancellationToken)"/>
public static async Task<IJob?> SkipBatchAsync(this IJob job, JobStepBatchId batchId, JobStepBatchError error, CancellationToken cancellationToken)
{
for (; ; )
{
IJob? newJob = await job.TrySkipBatchAsync(batchId, error, cancellationToken);
if (newJob != null)
{
return newJob;
}
newJob = await job.RefreshAsync(cancellationToken);
if (newJob == null)
{
return null;
}
job = newJob;
}
}
/// <inheritdoc cref="IJob.TrySkipAllBatchesAsync(JobStepBatchError, CancellationToken)"/>
public static async Task<IJob?> SkipAllBatchesAsync(this IJob job, JobStepBatchError reason, CancellationToken cancellationToken)
{
for (; ; )
{
IJob? newJob = await job.TrySkipAllBatchesAsync(reason, cancellationToken);
if (newJob != null)
{
return newJob;
}
newJob = await job.RefreshAsync(cancellationToken);
if (newJob == null)
{
return null;
}
job = newJob;
}
}
}
/// <summary>
/// Stores information about a batch of job steps
/// </summary>
public interface IJobStepBatch
{
/// <summary>
/// Job that this batch belongs to
/// </summary>
public IJob Job { get; }
/// <summary>
/// Unique id for this group
/// </summary>
public JobStepBatchId Id { get; }
/// <summary>
/// The type of agent to execute this group
/// </summary>
public string AgentType { get; }
/// <summary>
/// The log file id for this batch
/// </summary>
public LogId? LogId { get; }
/// <summary>
/// The node group for this batch
/// </summary>
public INodeGroup Group { 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 SessionId? SessionId { get; }
/// <summary>
/// The lease that's executing this group
/// </summary>
public LeaseId? 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, JobStepId 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)
{
List<INode> batchNodes = batch.Steps.ConvertAll(x => groups[batch.GroupIdx].Nodes[x.NodeIdx]);
return GetStartDependencies(batchNodes, groups);
}
/// <summary>
/// Get the dependencies required for this batch to start, taking run-early nodes into account
/// </summary>
/// <param name="batchNodes">Nodes in 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(IEnumerable<INode> batchNodes, IReadOnlyList<INodeGroup> groups)
{
// Find all the nodes that this group will start with.
List<INode> nodes = new List<INode>(batchNodes);
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(batchNodes);
return dependencies;
}
}
/// <summary>
/// Embedded jobstep document
/// </summary>
public interface IJobStep
{
/// <summary>
/// Job that this step belongs to
/// </summary>
public IJob Job { get; }
/// <summary>
/// Batch that this step belongs to
/// </summary>
public IJobStepBatch Batch { get; }
/// <summary>
/// Unique ID assigned to this jobstep. A new id is generated whenever a jobstep's order is changed.
/// </summary>
public JobStepId Id { get; }
/// <summary>
/// The node for this step
/// </summary>
public INode Node { get; }
/// <summary>
/// Index of the node which this jobstep is to execute
/// </summary>
public int NodeIdx { get; }
/// <summary>
/// The name of this node
/// </summary>
public string Name { get; }
/// <summary>
/// References to inputs for this node
/// </summary>
public IReadOnlyList<JobStepOutputRef> Inputs { get; }
/// <summary>
/// List of output names
/// </summary>
public IReadOnlyList<string> OutputNames { get; }
/// <summary>
/// Indices of nodes which must have succeeded for this node to run
/// </summary>
public IReadOnlyList<JobStepId> InputDependencies { get; }
/// <summary>
/// Indices of nodes which must have completed for this node to run
/// </summary>
public IReadOnlyList<JobStepId> OrderDependencies { get; }
/// <summary>
/// Whether this node can be run multiple times
/// </summary>
public bool AllowRetry { get; }
/// <summary>
/// This node can start running early, before dependencies of other nodes in the same group are complete
/// </summary>
public bool RunEarly { get; }
/// <summary>
/// Whether to include warnings in the output (defaults to true)
/// </summary>
public bool Warnings { get; }
/// <summary>
/// List of credentials required for this node. Each entry maps an environment variable name to a credential in the form "CredentialName.PropertyName".
/// </summary>
public IReadOnlyDictionary<string, string>? Credentials { get; }
/// <summary>
/// Annotations for this node
/// </summary>
public IReadOnlyNodeAnnotations Annotations { get; }
/// <summary>
/// Metadata for this node
/// </summary>
public IReadOnlyList<string> Metadata { 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>
/// Error from executing this step
/// </summary>
public JobStepError Error { get; }
/// <summary>
/// The log id for this step
/// </summary>
public LogId? LogId { get; }
/// <summary>
/// Unique id for notifications
/// </summary>
public NotificationTriggerId? 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 UserId? RetriedByUserId { get; }
/// <summary>
/// Signal if a step should be aborted
/// </summary>
public bool AbortRequested { get; }
/// <summary>
/// If an abort is requested, stores the id of the user that requested it
/// </summary>
public UserId? AbortedByUserId { get; }
/// <summary>
/// Optional reason for why the job step was canceled
/// </summary>
public string? CancellationReason { get; }
/// <summary>
/// List of reports for this step
/// </summary>
public IReadOnlyList<IJobReport>? Reports { get; }
/// <summary>
/// List of jobs this step has spawned
/// </summary>
public IReadOnlyList<JobId>? SpawnedJobs { get; }
/// <summary>
/// Reports for this jobstep.
/// </summary>
public IReadOnlyDictionary<string, string>? Properties { get; }
}
/// <summary>
/// Extension methods for job steps
/// </summary>
public static class JobStepExtensions
{
/// <summary>
/// Determines if a jobstep state is completed, skipped, or aborted.
/// </summary>
/// <returns>True if the step is completed, skipped, or aborted</returns>
public static bool IsPendingState(JobStepState state)
{
return state != JobStepState.Aborted && state != JobStepState.Completed && state != JobStepState.Skipped;
}
/// <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)
=> IsPendingState(step.State);
/// <summary>
/// Determine if a step should be timed out
/// </summary>
/// <param name="step"></param>
/// <param name="utcNow"></param>
/// <returns></returns>
public static bool HasTimedOut(this IJobStep step, DateTime utcNow)
{
if (step.State == JobStepState.Running && step.StartTimeUtc != null)
{
TimeSpan elapsed = utcNow - step.StartTimeUtc.Value;
if (elapsed > TimeSpan.FromHours(24.0))
{
return true;
}
}
return false;
}
}
/// <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>
/// Copies this info to a repsonse object
/// </summary>
public void CopyToResponse(GetTimingInfoResponse response)
{
response.TotalWaitTime = (float?)TotalWaitTime?.TotalSeconds;
response.TotalInitTime = (float?)TotalInitTime?.TotalSeconds;
response.TotalTimeToComplete = (float?)TotalTimeToComplete?.TotalSeconds;
response.AverageTotalWaitTime = (float?)AverageTotalWaitTime?.TotalSeconds;
response.AverageTotalInitTime = (float?)AverageTotalInitTime?.TotalSeconds;
response.AverageTotalTimeToComplete = (float?)AverageTotalTimeToComplete?.TotalSeconds;
}
}
/// <summary>
/// Step completion information
/// </summary>
public class JobStepsCompletionInfo
{
/// <summary>
/// The total count of steps passed within a job.
/// </summary>
public int StepPassCount { get; set; }
/// <summary>
/// The total count of steps with a warning result within a job.
/// </summary>
public int StepWarningCount { get; set; }
/// <summary>
/// The total count of steps with a failure result within a job.
/// </summary>
public int StepFailureCount { get; set; }
/// <summary>
/// The total number of steps within a job.
/// </summary>
public int StepTotalCount { get; set; }
/// <summary>
/// The total step time for the job, in seconds.
/// </summary>
public float JobStepsTotalTime { get; set; }
/// <summary>
/// The pass count as compared to the total step count.
/// </summary>
public float PassRatio => StepTotalCount == 0 ? 0 : StepPassCount / (float)StepTotalCount;
/// <summary>
/// The pass and warning count as compared to the total step count.
/// </summary>
public float PassWithWarningRatio => StepTotalCount == 0 ? 0 : (StepPassCount + StepWarningCount) / (float)StepTotalCount;
/// <summary>
/// The warning count as compared to the total step count.
/// </summary>
public float WarningRatio => StepTotalCount == 0 ? 0 : StepWarningCount / (float)StepTotalCount;
/// <summary>
/// The failure count as compared to the total step count.
/// </summary>
public float FailureRatio => StepTotalCount == 0 ? 0 : StepFailureCount / (float)StepTotalCount;
}
/// <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 TemplateId TemplateRefId { get; }
/// <summary>
/// The triggered job id
/// </summary>
public JobId? JobId { get; }
/// <summary>
/// Whether to run the latest change, or default change for the template, when starting the new job. Uses same change as the triggering job by default.
/// </summary>
public bool UseDefaultChangeForTemplate { get; }
}
/// <summary>
/// Report for a job or jobstep
/// </summary>
public interface IJobReport
{
/// <summary>
/// Name of the report
/// </summary>
string Name { get; }
/// <summary>
/// Where to render the report
/// </summary>
JobReportPlacement Placement { get; }
/// <summary>
/// The artifact id
/// </summary>
ArtifactId? ArtifactId { get; }
/// <summary>
/// Inline data for the report
/// </summary>
string? Content { get; }
}
/// <summary>
/// Implementation of IReport
/// </summary>
public class JobReport : IJobReport
{
/// <inheritdoc/>
public string Name { get; set; } = String.Empty;
/// <inheritdoc/>
public JobReportPlacement Placement { get; set; }
/// <inheritdoc/>
public ArtifactId? ArtifactId { get; set; }
/// <inheritdoc/>
public string? Content { get; set; }
}
}