Skip to content

Commit 76a28f5

Browse files
committed
Use eTag when fetching GitHub resources
1 parent 7561385 commit 76a28f5

File tree

12 files changed

+294
-100
lines changed

12 files changed

+294
-100
lines changed

src/Maestro/Maestro.DataProviders/RemoteFactory.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class RemoteFactory : IRemoteFactory
2424
private readonly IGitHubTokenProvider _gitHubTokenProvider;
2525
private readonly IAzureDevOpsTokenProvider _azdoTokenProvider;
2626
private readonly IServiceProvider _serviceProvider;
27+
private readonly IRedisCacheClient _redisCacheClient;
2728

2829
public RemoteFactory(
2930
BuildAssetRegistryContext context,
@@ -33,6 +34,7 @@ public RemoteFactory(
3334
DarcRemoteMemoryCache memoryCache,
3435
OperationManager operations,
3536
IProcessManager processManager,
37+
IRedisCacheClient redisCacheClient,
3638
ILoggerFactory loggerFactory,
3739
IServiceProvider serviceProvider)
3840
{
@@ -44,6 +46,7 @@ public RemoteFactory(
4446
_azdoTokenProvider = azdoTokenProvider;
4547
_cache = memoryCache;
4648
_serviceProvider = serviceProvider;
49+
_redisCacheClient = redisCacheClient;
4750
}
4851

4952
public async Task<IRemote> CreateRemoteAsync(string repoUrl)
@@ -87,7 +90,8 @@ private async Task<IRemoteGitRepo> GetRemoteGitClient(string repoUrl)
8790
new Microsoft.DotNet.DarcLib.GitHubTokenProvider(_gitHubTokenProvider),
8891
_processManager,
8992
_loggerFactory.CreateLogger<GitHubClient>(),
90-
_cache.Cache),
93+
_cache.Cache,
94+
_redisCacheClient),
9195

9296
GitRepoType.AzureDevOps => new AzureDevOpsClient(
9397
_azdoTokenProvider,

src/Microsoft.DotNet.Darc/Darc/Helpers/RemoteFactory.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ internal class RemoteFactory : IRemoteFactory
1717
private readonly ILoggerFactory _loggerFactory;
1818
private readonly ICommandLineOptions _options;
1919
private readonly IServiceProvider _serviceProvider;
20+
private readonly IRedisCacheClient _redisCacheClient;
2021

2122
public RemoteFactory(
2223
ILoggerFactory loggerFactory,
2324
ICommandLineOptions options,
24-
IServiceProvider serviceProvider)
25+
IServiceProvider serviceProvider,
26+
IRedisCacheClient redisCacheClient)
2527
{
2628
_loggerFactory = loggerFactory;
2729
_options = options;
2830
_serviceProvider = serviceProvider;
31+
_redisCacheClient = redisCacheClient;
2932
}
3033

3134
public Task<IRemote> CreateRemoteAsync(string repoUrl)
@@ -52,10 +55,10 @@ private IRemoteGitRepo CreateRemoteGitClient(ICommandLineOptions options, string
5255
new GitHubClient(
5356
options.GetGitHubTokenProvider(),
5457
new ProcessManager(_loggerFactory.CreateLogger<IProcessManager>(), options.GitLocation),
55-
_loggerFactory.CreateLogger<GitHubClient>(),
5658
temporaryRepositoryRoot,
57-
// Caching not in use for Darc local client.
58-
null),
59+
null, // Memory Caching not in use for Darc local client.
60+
_redisCacheClient,
61+
_loggerFactory.CreateLogger<GitHubClient>()),
5962

6063
GitRepoType.AzureDevOps =>
6164
new AzureDevOpsClient(

src/Microsoft.DotNet.Darc/DarcLib/GitHubClient.cs

Lines changed: 105 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Collections.Immutable;
76
using System.IO;
87
using System.Linq;
98
using System.Net;
109
using System.Net.Http;
1110
using System.Reflection;
11+
using System.Security.Policy;
1212
using System.Text;
1313
using System.Text.RegularExpressions;
1414
using System.Threading.Tasks;
@@ -44,6 +44,7 @@ public class GitHubClient : RemoteRepoBase, IRemoteGitRepo
4444
private readonly string _userAgent = $"DarcLib-{DarcLibVersion}";
4545
private IGitHubClient? _lazyClient = null;
4646
private readonly Dictionary<(string, string, string?), string> _gitRefCommitCache;
47+
private readonly IRedisCacheClient _cache;
4748

4849
static GitHubClient()
4950
{
@@ -57,17 +58,19 @@ public GitHubClient(
5758
IRemoteTokenProvider remoteTokenProvider,
5859
IProcessManager processManager,
5960
ILogger logger,
60-
IMemoryCache? cache)
61-
: this(remoteTokenProvider, processManager, logger, null, cache)
61+
IMemoryCache? cache,
62+
IRedisCacheClient redisClient)
63+
: this(remoteTokenProvider, processManager, null, cache, redisClient, logger)
6264
{
6365
}
6466

6567
public GitHubClient(
6668
IRemoteTokenProvider remoteTokenProvider,
6769
IProcessManager processManager,
68-
ILogger logger,
6970
string? temporaryRepositoryPath,
70-
IMemoryCache? cache)
71+
IMemoryCache? cache,
72+
IRedisCacheClient redisClient,
73+
ILogger logger)
7174
: base(remoteTokenProvider, processManager, temporaryRepositoryPath, cache, logger)
7275
{
7376
_tokenProvider = remoteTokenProvider;
@@ -78,6 +81,7 @@ public GitHubClient(
7881
NullValueHandling = NullValueHandling.Ignore
7982
};
8083
_gitRefCommitCache = [];
84+
_cache = redisClient;
8185
}
8286

8387
public bool AllowRetries { get; set; } = true;
@@ -299,28 +303,18 @@ public async Task<IEnumerable<int>> SearchPullRequestsAsync(
299303
public async Task<Models.PullRequest> GetPullRequestAsync(string pullRequestUrl)
300304
{
301305
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
302-
Octokit.PullRequest pr = await GetClient(owner, repo).PullRequest.Get(owner, repo, id);
303306

304-
PrStatus status;
305-
if (pr.State == ItemState.Closed)
306-
{
307-
status = pr.Merged == true ? PrStatus.Merged : PrStatus.Closed;
308-
}
309-
else
310-
{
311-
status = PrStatus.Open;
312-
}
307+
IGitHubClient client = GetClient(owner, repo);
313308

314-
return new Models.PullRequest
315-
{
316-
Title = pr.Title,
317-
Description = pr.Body,
318-
BaseBranch = pr.Base.Ref,
319-
HeadBranch = pr.Head.Ref,
320-
Status = status,
321-
UpdatedAt = pr.UpdatedAt,
322-
TargetBranchCommitSha = pr.Head.Sha,
323-
};
309+
var resourceUri = ApiUrls.PullRequest(owner, repo, id);
310+
311+
Models.PullRequest result = await RequestResourceUsingEtagsAsync<Models.PullRequest, Octokit.PullRequest>(
312+
pullRequestUrl,
313+
resourceUri,
314+
client,
315+
GithubResourceConverters.ConvertPullRequest);
316+
317+
return result;
324318
}
325319

326320
/// <summary>
@@ -403,7 +397,7 @@ public async Task MergeDependencyPullRequestAsync(string pullRequestUrl, MergePu
403397

404398
IGitHubClient gitHubClient = GetClient(owner, repo);
405399

406-
Octokit.PullRequest pr = await gitHubClient.PullRequest.Get(owner, repo, id);
400+
Models.PullRequest pr = await GetPullRequestAsync(pullRequestUrl);
407401

408402
var mergePullRequest = new MergePullRequest
409403
{
@@ -425,11 +419,11 @@ public async Task MergeDependencyPullRequestAsync(string pullRequestUrl, MergePu
425419
{
426420
try
427421
{
428-
await gitHubClient.Git.Reference.Delete(owner, repo, $"heads/{pr.Head.Ref}");
422+
await gitHubClient.Git.Reference.Delete(owner, repo, $"heads/{pr.HeadBranch}");
429423
}
430424
catch (Exception ex)
431425
{
432-
_logger.LogInformation("Couldn't delete branch {sourceBranch} - {message}", pr.Head.Ref, ex.Message);
426+
_logger.LogInformation("Couldn't delete branch {sourceBranch} - {message}", pr.HeadBranch, ex.Message);
433427
}
434428
}
435429
}
@@ -449,26 +443,25 @@ public async Task CreateOrUpdatePullRequestMergeStatusInfoAsync(string pullReque
449443
{
450444
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
451445
var client = GetClient(owner, repo);
452-
// Get the sha of the latest commit for the current PR
453-
string prSha = (await client.PullRequest.Get(owner, repo, id))?.Head?.Sha
454-
?? throw new InvalidOperationException("We cannot find the sha of the pull request");
446+
447+
Models.PullRequest pr = await GetPullRequestAsync(pullRequestUrl);
455448

456449
// Get a list of all the merge policies checks runs for the current PR
457450
List<CheckRun> existingChecksRuns =
458-
(await client.Check.Run.GetAllForReference(owner, repo, prSha))
451+
(await client.Check.Run.GetAllForReference(owner, repo, pr.HeadBranchCommitSha))
459452
.CheckRuns.Where(e => e.ExternalId.StartsWith(MergePolicyConstants.MaestroMergePolicyCheckRunPrefix)).ToList();
460453

461-
var toBeAdded = evaluations.Where(e => existingChecksRuns.All(c => c.ExternalId != CheckRunId(e, prSha)));
462-
var toBeUpdated = existingChecksRuns.Where(c => evaluations.Any(e => c.ExternalId == CheckRunId(e, prSha)));
463-
var toBeDeleted = existingChecksRuns.Where(c => evaluations.All(e => c.ExternalId != CheckRunId(e, prSha)));
454+
var toBeAdded = evaluations.Where(e => existingChecksRuns.All(c => c.ExternalId != CheckRunId(e, pr.HeadBranchCommitSha)));
455+
var toBeUpdated = existingChecksRuns.Where(c => evaluations.Any(e => c.ExternalId == CheckRunId(e, pr.HeadBranchCommitSha)));
456+
var toBeDeleted = existingChecksRuns.Where(c => evaluations.All(e => c.ExternalId != CheckRunId(e, pr.HeadBranchCommitSha)));
464457

465458
foreach (var newCheckRunValidation in toBeAdded)
466459
{
467-
await client.Check.Run.Create(owner, repo, CheckRunForAdd(newCheckRunValidation, prSha));
460+
await client.Check.Run.Create(owner, repo, CheckRunForAdd(newCheckRunValidation, pr.HeadBranchCommitSha));
468461
}
469462
foreach (var updatedCheckRun in toBeUpdated)
470463
{
471-
MergePolicyEvaluationResult eval = evaluations.Last(e => updatedCheckRun.ExternalId == CheckRunId(e, prSha));
464+
MergePolicyEvaluationResult eval = evaluations.Last(e => updatedCheckRun.ExternalId == CheckRunId(e, pr.HeadBranchCommitSha));
472465
if (eval.IsCachedResult)
473466
{
474467
_logger.LogInformation("Not updating check run {checkRunId} for PR {pullRequestUrl} because the merge policy was not re-evaluated.",
@@ -907,13 +900,12 @@ public async Task<IList<Check>> GetPullRequestChecksAsync(string pullRequestUrl)
907900
{
908901
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
909902

910-
var commits = await GetClient(owner, repo).Repository.PullRequest.Commits(owner, repo, id);
911-
var lastCommitSha = commits[commits.Count - 1].Sha;
903+
Models.PullRequest pr = await GetPullRequestAsync(pullRequestUrl);
912904

913905
return
914906
[
915-
.. await GetChecksFromStatusApiAsync(owner, repo, lastCommitSha),
916-
.. await GetChecksFromChecksApiAsync(owner, repo, lastCommitSha),
907+
.. await GetChecksFromStatusApiAsync(owner, repo, pr.HeadBranchCommitSha),
908+
.. await GetChecksFromChecksApiAsync(owner, repo, pr.HeadBranchCommitSha),
917909
];
918910
}
919911

@@ -929,31 +921,22 @@ public async Task<IList<Review>> GetLatestPullRequestReviewsAsync(string pullReq
929921
{
930922
(string owner, string repo, int id) = ParsePullRequestUri(pullRequestUrl);
931923

932-
var reviews = await GetClient(owner, repo).Repository.PullRequest.Review.GetAll(owner, repo, id);
924+
IGitHubClient client = GetClient(owner, repo);
925+
var pullRequestReviewsUri = ApiUrls.PullRequestReviews(owner, repo, id);
933926

934-
var actionableReviews = reviews
935-
.Where(r => r.State != PullRequestReviewState.Commented) // filter out reviews that don't affect approval/RFC
936-
.GroupBy(r => r.User.Login)
937-
.Select(g => g.OrderByDescending(r => r.SubmittedAt).First()) // pick each user's most recent review
938-
.Select(review => new Review(TranslateReviewState(review.State.Value), pullRequestUrl))
939-
.ToList();
927+
var pullRequestReviews = await RequestResourceUsingEtagsAsync<PullRequestReviews, List<PullRequestReview>>(
928+
pullRequestUrl,
929+
pullRequestReviewsUri,
930+
client,
931+
GithubResourceConverters.ConvertPullRequestReviews);
940932

941-
return actionableReviews;
942-
}
933+
var newestActionableReviews = pullRequestReviews.Reviews
934+
.Where(r => r.Status != ReviewState.Commented) // filter out reviews that don't affect approval/RFC
935+
.GroupBy(r => r.User)
936+
.Select(g => (Review) g.OrderByDescending(r => r.SubmittedAt).First()) // pick each user's most recent review
937+
.ToList();
943938

944-
private static ReviewState TranslateReviewState(PullRequestReviewState state)
945-
{
946-
return state switch
947-
{
948-
PullRequestReviewState.Approved => ReviewState.Approved,
949-
PullRequestReviewState.ChangesRequested => ReviewState.ChangesRequested,
950-
PullRequestReviewState.Commented => ReviewState.Commented,
951-
// A PR comment could be dismissed by a new push, so this does not count as a rejection.
952-
// Change to a comment
953-
PullRequestReviewState.Dismissed => ReviewState.Commented,
954-
PullRequestReviewState.Pending => ReviewState.Pending,
955-
_ => throw new NotImplementedException($"Unexpected pull request review state {state}"),
956-
};
939+
return newestActionableReviews;
957940
}
958941

959942
private async Task<IList<Check>> GetChecksFromStatusApiAsync(string owner, string repo, string @ref)
@@ -1315,4 +1298,63 @@ public async Task<List<string>> GetPullRequestCommentsAsync(string pullRequestUr
13151298

13161299
return comments.Select(comment => comment.Body).ToList();
13171300
}
1301+
1302+
/// <summary>
1303+
/// This method fills a functionality that's currently missing from Octokit: fetching Github resources using eTags.
1304+
/// eTags allow us to cache mutable resources and efficiently check if the resource has changed on Github since the last fetch.
1305+
/// </summary>
1306+
/// <typeparam name="T">The domain class of the resource in our server</typeparam>
1307+
/// <typeparam name="K">The class of the resource in Octokit</typeparam>
1308+
/// <param name="resourceKey">The key used to cache the resource in redis</param>
1309+
/// <param name="resourceUri">The uri used to request the resource from Github</param>
1310+
/// <param name="client">The github client that makes the request</param>
1311+
/// <param name="resourceConverter">Function to convert the resource from Octokit to our domain class</param>
1312+
/// <returns>The resource of type T</returns>
1313+
/// <exception cref="DarcException"></exception>
1314+
protected virtual async Task<T> RequestResourceUsingEtagsAsync<T, K>(
1315+
string resourceKey,
1316+
Uri resourceUri,
1317+
IGitHubClient client,
1318+
Func<K, T> resourceConverter)
1319+
where T : class, IGithubEtagResource
1320+
{
1321+
var cachedResource = await _cache.TryGetAsync<T>(resourceKey);
1322+
string? entityTag = cachedResource?.Etag;
1323+
var headers = new Dictionary<string, string>
1324+
{
1325+
{ "Accept", "application/vnd.github.v3+json" },
1326+
};
1327+
if (entityTag != null)
1328+
{
1329+
headers.Add("If-None-Match", entityTag);
1330+
}
1331+
var response = await client.Connection.Get<K>(resourceUri, headers);
1332+
if (response.HttpResponse.StatusCode == HttpStatusCode.NotModified && cachedResource != null)
1333+
{
1334+
// TODO: Add telemetry for cache hits to measure the impact of this optimization.
1335+
return cachedResource;
1336+
}
1337+
else
1338+
{
1339+
if (response.HttpResponse.StatusCode == HttpStatusCode.OK)
1340+
{
1341+
string? etag = response.HttpResponse.Headers
1342+
.FirstOrDefault(h => h.Key.Equals("Etag", StringComparison.OrdinalIgnoreCase))
1343+
.Value;
1344+
1345+
var resource = resourceConverter(response.Body);
1346+
1347+
if (etag != null)
1348+
{
1349+
resource.Etag = etag;
1350+
await _cache.TrySetAsync<T>(resourceKey, resource);
1351+
}
1352+
return resource;
1353+
}
1354+
else
1355+
{
1356+
throw new DarcException($"Failed to get {typeof(T).Name} from GitHub. Status code: {response.HttpResponse.StatusCode}");
1357+
}
1358+
}
1359+
}
13181360
}

src/Microsoft.DotNet.Darc/DarcLib/GitRepoFactory.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class GitRepoFactory : IGitRepoFactory
2424
private readonly IFileSystem _fileSystem;
2525
private readonly ILoggerFactory _loggerFactory;
2626
private readonly string? _temporaryPath = null;
27+
private readonly IRedisCacheClient _redisCacheClient;
2728

2829
public GitRepoFactory(
2930
IRemoteTokenProvider remoteTokenProvider,
@@ -32,7 +33,8 @@ public GitRepoFactory(
3233
IProcessManager processManager,
3334
IFileSystem fileSystem,
3435
ILoggerFactory loggerFactory,
35-
string temporaryPath)
36+
string temporaryPath,
37+
IRedisCacheClient redisCacheClient)
3638
{
3739
_remoteTokenProvider = remoteTokenProvider;
3840
_azdoTokenProvider = azdoTokenProvider;
@@ -41,6 +43,7 @@ public GitRepoFactory(
4143
_fileSystem = fileSystem;
4244
_loggerFactory = loggerFactory;
4345
_temporaryPath = temporaryPath;
46+
_redisCacheClient = redisCacheClient;
4447
}
4548

4649
public IGitRepo CreateClient(string repoUri) => GitRepoUrlUtils.ParseTypeFromUri(repoUri) switch
@@ -54,10 +57,11 @@ public GitRepoFactory(
5457
GitRepoType.GitHub => new GitHubClient(
5558
_remoteTokenProvider,
5659
_processManager,
57-
_loggerFactory.CreateLogger<GitHubClient>(),
5860
_temporaryPath,
5961
// Caching not in use for Darc local client.
60-
null),
62+
null,
63+
_redisCacheClient,
64+
_loggerFactory.CreateLogger<GitHubClient>()),
6165

6266
GitRepoType.Local => new LocalLibGit2Client(
6367
_remoteTokenProvider,

0 commit comments

Comments
 (0)