Skip to content

Commit 4eacd63

Browse files
authored
Improve PublishBuildToMaestro retries (#16020)
1 parent 5f8663a commit 4eacd63

File tree

2 files changed

+132
-15
lines changed

2 files changed

+132
-15
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Net;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace Microsoft.DotNet.Build.Tasks.Feed.Tests
10+
{
11+
public class PublishBuildToMaestroTests
12+
{
13+
[Fact]
14+
public void GetTimeToWait_ReadsGitHubRateLimitingHeaders()
15+
{
16+
// Unauthenticated requests to github that are rate limited don't use the Retry-After header,
17+
// so we need to be able to handle their X-RateLimit-Reset header.
18+
19+
DateTime resetTime = DateTime.UtcNow.AddSeconds(5.0);
20+
long unixTime = (long)(resetTime - DateTime.UnixEpoch).TotalSeconds;
21+
var response = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Forbidden)
22+
{
23+
Headers =
24+
{
25+
{ "X-RateLimit-Remaining", "0" },
26+
{ "X-RateLimit-Reset", unixTime.ToString() },
27+
}
28+
};
29+
30+
TimeSpan? actual = PublishBuildToMaestro.GetTimeToWait(response);
31+
32+
actual.Should().NotBeNull();
33+
actual.Should().BeCloseTo(resetTime - DateTime.UtcNow, TimeSpan.FromSeconds(1));
34+
}
35+
36+
[Fact]
37+
public void GetTimeToWait_NotWaitWhenNotLimited()
38+
{
39+
DateTime resetTime = DateTime.UtcNow.AddSeconds(5.0);
40+
long unixTime = (long)(resetTime - DateTime.UnixEpoch).TotalSeconds;
41+
var response = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Forbidden)
42+
{
43+
Headers =
44+
{
45+
{ "X-RateLimit-Remaining", "1" },
46+
{ "X-RateLimit-Reset", unixTime.ToString() },
47+
}
48+
};
49+
50+
TimeSpan? actual = PublishBuildToMaestro.GetTimeToWait(response);
51+
52+
actual.Should().BeNull();
53+
}
54+
55+
[Fact]
56+
public void GetTimeToWait_UsesRetryAfterDuration()
57+
{
58+
var response = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Forbidden)
59+
{
60+
Headers =
61+
{
62+
{ "Retry-After", "5" },
63+
}
64+
};
65+
TimeSpan? actual = PublishBuildToMaestro.GetTimeToWait(response);
66+
actual.Should().NotBeNull();
67+
actual.Should().BeCloseTo(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(1));
68+
}
69+
70+
[Fact]
71+
public void GetTimeToWait_UsesRetryAfterDate()
72+
{
73+
DateTime resetTime = DateTime.UtcNow.AddSeconds(5.0);
74+
var response = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Forbidden)
75+
{
76+
Headers =
77+
{
78+
{ "Retry-After", resetTime.ToString("R") },
79+
}
80+
};
81+
TimeSpan? actual = PublishBuildToMaestro.GetTimeToWait(response);
82+
actual.Should().NotBeNull();
83+
actual.Should().BeCloseTo(resetTime - DateTime.UtcNow, TimeSpan.FromSeconds(1));
84+
}
85+
}
86+
}

src/Microsoft.DotNet.Build.Tasks.Feed/src/PublishBuildToMaestro.cs

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,12 @@ private void LookupForMatchingGitHubRepository(BuildIdentity buildIdentity)
529529
string repoIdentity = string.Empty;
530530
string gitHubHost = "github.com";
531531

532+
if (Environment.GetEnvironmentVariable("GITHUB_TOKEN") is string envGitHubToken)
533+
{
534+
client.DefaultRequestHeaders.Authorization =
535+
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", envGitHubToken);
536+
}
537+
532538
if (!Uri.TryCreate(buildIdentity.AzureDevOpsRepository, UriKind.Absolute, out Uri repoAddr))
533539
{
534540
throw new Exception($"Can't parse the repository URL: {buildIdentity.AzureDevOpsRepository}");
@@ -550,26 +556,20 @@ private void LookupForMatchingGitHubRepository(BuildIdentity buildIdentity)
550556
HttpResponseMessage response = client.GetAsync(url).Result;
551557

552558
const int MaxRetries = 5;
553-
for (int retry = 1; retry <= MaxRetries && response.StatusCode == System.Net.HttpStatusCode.TooManyRequests; retry++)
559+
for (int retry = 1;
560+
retry <= MaxRetries && (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests || response.StatusCode == System.Net.HttpStatusCode.Forbidden);
561+
retry++)
554562
{
555-
TimeSpan timeSpan;
556-
if (response.Headers.RetryAfter?.Delta != null)
557-
{
558-
timeSpan = response.Headers.RetryAfter.Delta.Value;
559-
}
560-
else if (response.Headers.RetryAfter?.Date != null)
561-
{
562-
timeSpan = response.Headers.RetryAfter.Date.Value - DateTimeOffset.UtcNow;
563-
}
564-
else
563+
TimeSpan? timeSpan = GetTimeToWait(response);
564+
565+
if (!timeSpan.HasValue)
565566
{
566-
const int defaultRetryAfterSeconds = 10;
567-
timeSpan = TimeSpan.FromSeconds(defaultRetryAfterSeconds);
567+
break;
568568
}
569569

570570
Log.LogMessage(MessageImportance.High,
571-
$"API rate limit exceeded, retrying in {timeSpan.TotalSeconds} seconds. Retry attempt: {retry}");
572-
Thread.Sleep(timeSpan);
571+
$"API rate limit exceeded, retrying in {timeSpan.Value.TotalSeconds} seconds. Retry attempt: {retry}");
572+
Thread.Sleep(timeSpan.Value);
573573
response = client.GetAsync(url).Result;
574574
}
575575

@@ -594,6 +594,37 @@ private void LookupForMatchingGitHubRepository(BuildIdentity buildIdentity)
594594
}
595595
}
596596

597+
public static TimeSpan? GetTimeToWait(HttpResponseMessage response)
598+
{
599+
if (response.Headers.RetryAfter != null)
600+
{
601+
if (response.Headers.RetryAfter.Delta.HasValue)
602+
{
603+
return response.Headers.RetryAfter.Delta.Value;
604+
}
605+
else if (response.Headers.RetryAfter.Date.HasValue)
606+
{
607+
return response.Headers.RetryAfter.Date.Value - DateTimeOffset.Now;
608+
}
609+
}
610+
611+
if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var values))
612+
{
613+
if (values.FirstOrDefault() != "0")
614+
{
615+
// apparently not rate limited, so no need to wait
616+
return null;
617+
}
618+
619+
values = response.Headers.GetValues("X-RateLimit-Reset");
620+
if (long.TryParse(values.FirstOrDefault(), out long resetTime))
621+
{
622+
return DateTimeOffset.FromUnixTimeSeconds(resetTime) - DateTimeOffset.Now;
623+
}
624+
}
625+
return null;
626+
}
627+
597628
public static string GetRepoName(string repoUrl)
598629
{
599630
// In case the URL comes in ending with a '/', prevent an indexing exception

0 commit comments

Comments
 (0)