Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use new HelixAPI /job/{job}/results endpoint #15230

Merged
merged 14 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/Microsoft.DotNet.Helix/Client/CSharp/generated-code/Job.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public partial interface IJob
Task<Models.JobCreationResult> NewAsync(
Models.JobCreationRequest body,
string idempotencyKey,
bool? returnSas = default,
CancellationToken cancellationToken = default
);

Expand All @@ -33,6 +34,11 @@ public partial interface IJob
CancellationToken cancellationToken = default
);

Task<Models.JobResultsUri> ResultsAsync(
string job,
CancellationToken cancellationToken = default
);

Task<Models.JobPassFail> PassFailAsync(
string job,
CancellationToken cancellationToken = default
Expand Down Expand Up @@ -77,6 +83,7 @@ public Job(HelixApi client)
public async Task<Models.JobCreationResult> NewAsync(
Models.JobCreationRequest body,
string idempotencyKey,
bool? returnSas = default,
CancellationToken cancellationToken = default
)
{
Expand Down Expand Up @@ -118,6 +125,11 @@ public Job(HelixApi client)
_req.Headers.Add("Idempotency-Key", idempotencyKey);
}

if (returnSas != default(bool?))
{
_req.Headers.Add("return-sas", returnSas.ToString());
}

if (body != default(Models.JobCreationRequest))
{
_req.Content = RequestContent.Create(Encoding.UTF8.GetBytes(Client.Serialize(body)));
Expand Down Expand Up @@ -268,6 +280,81 @@ internal async Task OnListFailed(Request req, Response res)
throw ex;
}

partial void HandleFailedResultsRequest(RestApiException ex);

public async Task<Models.JobResultsUri> ResultsAsync(
string job,
CancellationToken cancellationToken = default
)
{

if (string.IsNullOrEmpty(job))
{
throw new ArgumentNullException(nameof(job));
}

const string apiVersion = "2019-06-17";

var _baseUri = Client.Options.BaseUri;
var _url = new RequestUriBuilder();
_url.Reset(_baseUri);
_url.AppendPath(
"/api/jobs/{job}/results".Replace("{job}", Uri.EscapeDataString(Client.Serialize(job))),
false);

_url.AppendQuery("api-version", Client.Serialize(apiVersion));


using (var _req = Client.Pipeline.CreateRequest())
{
_req.Uri = _url;
_req.Method = RequestMethod.Get;

using (var _res = await Client.SendAsync(_req, cancellationToken).ConfigureAwait(false))
{
if (_res.Status < 200 || _res.Status >= 300)
{
await OnResultsFailed(_req, _res).ConfigureAwait(false);
}

if (_res.ContentStream == null)
{
await OnResultsFailed(_req, _res).ConfigureAwait(false);
}

using (var _reader = new StreamReader(_res.ContentStream))
{
var _content = await _reader.ReadToEndAsync().ConfigureAwait(false);
var _body = Client.Deserialize<Models.JobResultsUri>(_content);
return _body;
}
}
}
}

internal async Task OnResultsFailed(Request req, Response res)
{
string content = null;
if (res.ContentStream != null)
{
using (var reader = new StreamReader(res.ContentStream))
{
content = await reader.ReadToEndAsync().ConfigureAwait(false);
}
}

var ex = new RestApiException<Models.ApiError>(
req,
res,
content,
Client.Deserialize<Models.ApiError>(content)
);
HandleFailedResultsRequest(ex);
HandleFailedRequest(ex);
Client.OnFailedRequest(ex);
throw ex;
}

partial void HandleFailedPassFailRequest(RestApiException ex);

public async Task<Models.JobPassFail> PassFailAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ public bool IsValid
{
return false;
}
if (string.IsNullOrEmpty(ResultsUriRSAS))
{
return false;
}
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using Newtonsoft.Json;

namespace Microsoft.DotNet.Helix.Client.Models
{
public partial class JobResultsUri
{
public JobResultsUri()
{
}

[JsonProperty("ResultsUri")]
public string ResultsUri { get; set; }

[JsonProperty("ResultsUriRSAS")]
public string ResultsUriRSAS { get; set; }
}
}
11 changes: 0 additions & 11 deletions src/Microsoft.DotNet.Helix/JobSender/ISentJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,6 @@ public interface ISentJob
/// </summary>
string HelixCancellationToken { get; }

/// <summary>
/// URI for blob storage container with the results.
/// </summary>
string ResultsContainerUri { get; }

/// <summary>
/// Shared Access Signature for access to the container with results.
/// Used for internal builds.
/// </summary>
string ResultsContainerReadSAS { get; }

/// <summary>
/// Poll for the job to actually finish inside Helix.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.DotNet.Helix/JobSender/JobDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ public async Task<ISentJob> SendAsync(Action<string> log, CancellationToken canc
}

string jobStartIdentifier = Guid.NewGuid().ToString("N");
var newJob = await JobApi.NewAsync(creationRequest, jobStartIdentifier, cancellationToken).ConfigureAwait(false);
var newJob = await JobApi.NewAsync(creationRequest, jobStartIdentifier, cancellationToken: cancellationToken).ConfigureAwait(false);

return new SentJob(JobApi, newJob, newJob.ResultsUri, newJob.ResultsUriRSAS);
return new SentJob(JobApi, newJob);
}

private void WarnForImpendingRemoval(Action<string> log, QueueInfo queueInfo)
Expand Down
6 changes: 1 addition & 5 deletions src/Microsoft.DotNet.Helix/JobSender/SentJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,16 @@ namespace Microsoft.DotNet.Helix.Client
{
internal class SentJob : ISentJob
{
public SentJob(IJob jobApi, JobCreationResult newJob, string resultsContainerUri, string resultsContainerReadSAS)
public SentJob(IJob jobApi, JobCreationResult newJob)
{
JobApi = jobApi;
CorrelationId = newJob.Name;
HelixCancellationToken = newJob.CancellationToken;
ResultsContainerUri = resultsContainerUri;
ResultsContainerReadSAS = resultsContainerReadSAS;
}

public IJob JobApi { get; }
public string CorrelationId { get; }
public string HelixCancellationToken { get; }
public string ResultsContainerUri { get; }
public string ResultsContainerReadSAS { get; }

public Task<JobPassFail> WaitAsync(int pollingIntervalMs = 10000, CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public class DownloadFromResultsContainer : HelixTask, ICancelableTask
[Required]
public ITaskItem[] MetadataToWrite { get; set; }

public string ResultsContainerReadSAS { get; set; }

private const string MetadataFile = "metadata.txt";

private readonly CancellationTokenSource _cancellationSource = new CancellationTokenSource();
Expand Down Expand Up @@ -74,6 +72,7 @@ private async Task DownloadFilesForWorkItem(ITaskItem workItem, string directory

// Use the Helix API to get the last possible iteration of the work item's execution
var allAvailableFiles = await HelixApi.WorkItem.ListFilesAsync(workItemName, JobId, true, ct);
var resultsUri = await HelixApi.Job.ResultsAsync(JobId, ct);

DirectoryInfo destinationDir = Directory.CreateDirectory(Path.Combine(directoryPath, workItemName));
foreach (string file in filesToDownload)
Expand Down Expand Up @@ -109,14 +108,14 @@ private async Task DownloadFilesForWorkItem(ITaskItem workItem, string directory
// If we have no read SAS token from the build, make a best-effort attempt using the URL from the Helix API.
// For restricted queues, there will be no read SAS token available to use in the Helix API's result
// (but hopefully the 'else' branch will be hit in this case)
if (string.IsNullOrEmpty(ResultsContainerReadSAS))
if (string.IsNullOrEmpty(resultsUri.ResultsUriRSAS))
{
blob = new BlobClient(new Uri(fileAvailableForDownload.Link), blobClientOptions);
}
else
{
var strippedFileUri = new Uri(fileAvailableForDownload.Link.Substring(0, fileAvailableForDownload.Link.LastIndexOf('?')));
blob = new BlobClient(strippedFileUri, new AzureSasCredential(ResultsContainerReadSAS), blobClientOptions);
blob = new BlobClient(strippedFileUri, new AzureSasCredential(resultsUri.ResultsUriRSAS), blobClientOptions);
}
await blob.DownloadToAsync(destinationFile);
}
Expand Down
14 changes: 0 additions & 14 deletions src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,6 @@ public static class MetadataNames
[Output]
public string JobCancellationToken { get; set; }

/// <summary>
/// When the task finishes, the results container uri should be available in case we want to download files.
/// </summary>
[Output]
public string ResultsContainerUri { get; set; }

/// <summary>
/// If the job is internal, we need to give the DownloadFromResultsContainer task the Write SAS to download files.
/// </summary>
[Output]
public string ResultsContainerReadSAS { get; set; }

/// <summary>
/// A collection of commands that will run for each work item before any work item commands.
/// Use a semicolon to delimit these and escape semicolons by percent coding them ('%3B').
Expand Down Expand Up @@ -270,8 +258,6 @@ protected override async Task ExecuteCore(CancellationToken cancellationToken)
ISentJob job = await def.SendAsync(msg => Log.LogMessageFromText(msg, MessageImportance.Normal), cancellationToken);
JobCorrelationId = job.CorrelationId;
JobCancellationToken = job.HelixCancellationToken;
ResultsContainerUri = job.ResultsContainerUri;
ResultsContainerReadSAS = job.ResultsContainerReadSAS;
cancellationToken.ThrowIfCancellationRequested();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,11 @@
HelixProperties="@(HelixProperties)">
<Output TaskParameter="JobCorrelationId" PropertyName="HelixJobId"/>
<Output TaskParameter="JobCancellationToken" PropertyName="HelixJobCancellationToken"/>
<Output TaskParameter="ResultsContainerUri" PropertyName="HelixResultsContainer"/>
<Output TaskParameter="ResultsContainerReadSAS" PropertyName="HelixResultsContainerReadSAS"/>
</SendHelixJob>
<ItemGroup>
<SentJob Include="$(HelixJobId)">
<WorkItemCount>@(HelixWorkItem->Count())</WorkItemCount>
<HelixTargetQueue>$(HelixTargetQueue)</HelixTargetQueue>
<ResultsContainerUri>$(HelixResultsContainer)</ResultsContainerUri>
<ResultsContainerReadSAS>$(HelixResultsContainerReadSAS)</ResultsContainerReadSAS>
<HelixJobCancellationToken>$(HelixJobCancellationToken)</HelixJobCancellationToken>
</SentJob>
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@
<_shouldDownloadResults Condition="'@(_workItemsWithDownloadMetadata)' != '' AND '$(HelixResultsDestinationDir)' != ''">true</_shouldDownloadResults>
</PropertyGroup>

<Warning Text="DownloadFromResultsContainer will be skipped for job %(SentJob.Identity) because results container uri is empty" Condition="'%(SentJob.ResultsContainerUri)' == '' AND $(_shouldDownloadResults)" />

<DownloadFromResultsContainer
Condition="$(_shouldDownloadResults) AND '%(SentJob.ResultsContainerUri)' != ''"
Condition="$(_shouldDownloadResults)"
AccessToken="$(HelixAccessToken)"
WorkItems="@(_workItemsWithDownloadMetadata)"
OutputDirectory="$(HelixResultsDestinationDir)"
MetadataToWrite="@(HelixDownloadResultsMetadata)"
JobId="%(SentJob.Identity)"
ResultsContainerReadSAS="%(SentJob.ResultsContainerReadSAS)" />
JobId="%(SentJob.Identity)" />
</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ public void NotNullCheck(TextWriter output, object context, Action<TextWriter, o
}
}

[HelperMethod]
public string MaybeCallToString(TypeReference reference)
{
if (reference != TypeReference.String)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I see this is only called in not null scenarios, but it still feels like a null check should occur in this method just in case someone else tries to consume this or someone tries to refactor the calling code and the check gets lost.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole code generator is broken currently, and its outputs don't match the HTTP client code exactly. I had to make several more changes to get it to produce reasonable code (e.g., the generator doesn't add licensing comments at the start of every file).

My understanding is that the API surface doesn't change that often anymore and making the generator more robust isn't a high priority at the moment. I only committed this change because it is necessary to make the new API surface work with the generated code and somebody might find it useful if the ever need to actually fix the code generator. I'd rather avoid making any further changes to the generator in this PR.

{
return ".ToString()";
}

return string.Empty;
}

public string GetDefaultExpression(TypeReference reference, bool required)
{
string typeElement = ResolveReference(reference, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ namespace {{pascalCaseNs Namespace}}

if ({{#notNullCheck Type Required}}{{camelCase Name}}{{/notNullCheck}})
{
_req.Headers.Add("{{Name}}", {{camelCase Name}});
_req.Headers.Add("{{Name}}", {{camelCase Name}}{{maybeCallToString Type}});
}
{{/each}}
{{#with BodyParameter}}
Expand Down
Loading