diff --git a/src/Runner.Listener/JobDispatcher.cs b/src/Runner.Listener/JobDispatcher.cs index bbc09593c..10076441e 100644 --- a/src/Runner.Listener/JobDispatcher.cs +++ b/src/Runner.Listener/JobDispatcher.cs @@ -24,7 +24,7 @@ namespace GitHub.Runner.Listener public interface IJobDispatcher : IRunnerService { bool Busy { get; } - TaskCompletionSource RunOnceJobCompleted { get; } + TaskCompletionSource RunOnceJobCompleted { get; } void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false); bool Cancel(JobCancelMessage message); Task WaitAsync(CancellationToken token); @@ -56,7 +56,7 @@ namespace GitHub.Runner.Listener // timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT private TimeSpan _channelTimeout; - private TaskCompletionSource _runOnceJobCompleted = new(); + private TaskCompletionSource _runOnceJobCompleted = new(); public event EventHandler JobStatus; @@ -82,7 +82,7 @@ namespace GitHub.Runner.Listener Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds."); } - public TaskCompletionSource RunOnceJobCompleted => _runOnceJobCompleted; + public TaskCompletionSource RunOnceJobCompleted => _runOnceJobCompleted; public bool Busy { get; private set; } @@ -340,18 +340,19 @@ namespace GitHub.Runner.Listener private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken) { + var jobResult = TaskResult.Succeeded; try { - await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken); + jobResult = await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken); } finally { Trace.Info("Fire signal for one time used runner."); - _runOnceJobCompleted.TrySetResult(true); + _runOnceJobCompleted.TrySetResult(jobResult); } } - private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken) + private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken) { Busy = true; try @@ -399,7 +400,7 @@ namespace GitHub.Runner.Listener { // renew job request task complete means we run out of retry for the first job request renew. Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker."); - return; + return TaskResult.Abandoned; } if (jobRequestCancellationToken.IsCancellationRequested) @@ -412,7 +413,7 @@ namespace GitHub.Runner.Listener // complete job request with result Cancelled await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled); - return; + return TaskResult.Canceled; } HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}"); @@ -523,7 +524,7 @@ namespace GitHub.Runner.Listener await renewJobRequest; // not finish the job request since the job haven't run on worker at all, we will not going to set a result to server. - return; + return TaskResult.Failed; } // we get first jobrequest renew succeed and start the worker process with the job message. @@ -604,7 +605,7 @@ namespace GitHub.Runner.Listener Trace.Error(detailInfo); } - return; + return TaskResultUtil.TranslateFromReturnCode(returnCode); } else if (completedTask == renewJobRequest) { @@ -706,6 +707,8 @@ namespace GitHub.Runner.Listener // complete job request await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel); + + return resultOnAbandonOrCancel; } finally { diff --git a/src/Runner.Listener/Runner.cs b/src/Runner.Listener/Runner.cs index a7577bca8..e20eb6256 100644 --- a/src/Runner.Listener/Runner.cs +++ b/src/Runner.Listener/Runner.cs @@ -324,8 +324,11 @@ namespace GitHub.Runner.Listener HostContext.EnableAuthMigration("EnableAuthMigrationByDefault"); } + // hosted runner only run one job and would like to know the result of the job for telemetry and alerting on failure spike. + var returnJobResultForHosted = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED")); + // Run the runner interactively or as service - return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral); + return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral || returnJobResultForHosted, returnJobResultForHosted); } else { @@ -401,7 +404,7 @@ namespace GitHub.Runner.Listener } //create worker manager, create message listener and start listening to the queue - private async Task RunAsync(RunnerSettings settings, bool runOnce = false) + private async Task RunAsync(RunnerSettings settings, bool runOnce = false, bool returnRunOnceJobResult = false) { try { @@ -580,6 +583,21 @@ namespace GitHub.Runner.Listener Trace.Info($"Ignore any exception after cancel message loop. {ex}"); } + if (returnRunOnceJobResult) + { + try + { + var jobResult = await jobDispatcher.RunOnceJobCompleted.Task; + return TaskResultUtil.TranslateToReturnCode(jobResult); + } + catch (Exception ex) + { + Trace.Error("run once job finished with error."); + Trace.Error(ex); + return Constants.Runner.ReturnCode.TerminatedError; + } + } + return Constants.Runner.ReturnCode.Success; } } @@ -866,14 +884,14 @@ namespace GitHub.Runner.Listener return Constants.Runner.ReturnCode.Success; } - private async Task ExecuteRunnerAsync(RunnerSettings settings, bool runOnce) + private async Task ExecuteRunnerAsync(RunnerSettings settings, bool runOnce, bool returnRunOnceJobResult) { int returnCode = Constants.Runner.ReturnCode.Success; bool restart = false; do { restart = false; - returnCode = await RunAsync(settings, runOnce); + returnCode = await RunAsync(settings, runOnce, returnRunOnceJobResult); if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed) { diff --git a/src/Test/L0/Listener/JobDispatcherL0.cs b/src/Test/L0/Listener/JobDispatcherL0.cs index 3b26233a4..a160ffba2 100644 --- a/src/Test/L0/Listener/JobDispatcherL0.cs +++ b/src/Test/L0/Listener/JobDispatcherL0.cs @@ -739,7 +739,8 @@ namespace GitHub.Runner.Common.Tests.Listener Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent."); if (jobDispatcher.RunOnceJobCompleted.Task.IsCompleted) { - Assert.True(await jobDispatcher.RunOnceJobCompleted.Task, "JobDispatcher should set task complete token to 'TRUE' for one time agent."); + var result = await jobDispatcher.RunOnceJobCompleted.Task; + Assert.Equal(TaskResult.Succeeded, result); } } } diff --git a/src/Test/L0/Listener/RunnerL0.cs b/src/Test/L0/Listener/RunnerL0.cs index 456f51cc9..c88bc8274 100644 --- a/src/Test/L0/Listener/RunnerL0.cs +++ b/src/Test/L0/Listener/RunnerL0.cs @@ -295,13 +295,13 @@ namespace GitHub.Runner.Common.Tests.Listener _messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny())) .Returns(Task.CompletedTask); - var runOnceJobCompleted = new TaskCompletionSource(); + var runOnceJobCompleted = new TaskCompletionSource(); _jobDispatcher.Setup(x => x.RunOnceJobCompleted) .Returns(runOnceJobCompleted); _jobDispatcher.Setup(x => x.Run(It.IsAny(), It.IsAny())) .Callback(() => { - runOnceJobCompleted.TrySetResult(true); + runOnceJobCompleted.TrySetResult(TaskResult.Succeeded); }); _jobNotification.Setup(x => x.StartClient(It.IsAny())) .Callback(() => @@ -399,13 +399,13 @@ namespace GitHub.Runner.Common.Tests.Listener _messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny())) .Returns(Task.CompletedTask); - var runOnceJobCompleted = new TaskCompletionSource(); + var runOnceJobCompleted = new TaskCompletionSource(); _jobDispatcher.Setup(x => x.RunOnceJobCompleted) .Returns(runOnceJobCompleted); _jobDispatcher.Setup(x => x.Run(It.IsAny(), It.IsAny())) .Callback(() => { - runOnceJobCompleted.TrySetResult(true); + runOnceJobCompleted.TrySetResult(TaskResult.Succeeded); }); _jobNotification.Setup(x => x.StartClient(It.IsAny())) .Callback(() => @@ -733,8 +733,8 @@ namespace GitHub.Runner.Common.Tests.Listener _configStore.Setup(x => x.IsServiceConfigured()).Returns(false); - var completedTask = new TaskCompletionSource(); - completedTask.SetResult(true); + var completedTask = new TaskCompletionSource(); + completedTask.SetResult(TaskResult.Succeeded); _jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask); //Act @@ -834,8 +834,8 @@ namespace GitHub.Runner.Common.Tests.Listener _configStore.Setup(x => x.IsServiceConfigured()).Returns(false); - var completedTask = new TaskCompletionSource(); - completedTask.SetResult(true); + var completedTask = new TaskCompletionSource(); + completedTask.SetResult(TaskResult.Succeeded); _jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask); //Act @@ -954,8 +954,8 @@ namespace GitHub.Runner.Common.Tests.Listener _configStore.Setup(x => x.IsServiceConfigured()).Returns(false); - var completedTask = new TaskCompletionSource(); - completedTask.SetResult(true); + var completedTask = new TaskCompletionSource(); + completedTask.SetResult(TaskResult.Succeeded); _jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask); //Act