using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using CompatApiClient; using Microsoft.Extensions.Caching.Memory; using Octokit; namespace GithubClient; public class Client { private readonly GitHubClient client; private static readonly TimeSpan PrStatusCacheTime = TimeSpan.FromMinutes(3); private static readonly TimeSpan IssueStatusCacheTime = TimeSpan.FromMinutes(30); private static readonly MemoryCache StatusesCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromMinutes(1) }); private static readonly MemoryCache IssuesCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromMinutes(30) }); public static int RateLimit { get; private set; } public static int RateLimitRemaining { get; private set; } public static DateTime RateLimitResetTime { get; private set; } public Client(string? githubToken) { client = new(new ProductHeaderValue(ApiConfig.ProductName, ApiConfig.ProductVersion)); if (githubToken is {Length: >0}) client.Credentials = new(githubToken); } public async Task GetPrInfoAsync(int pr, CancellationToken cancellationToken) { if (StatusesCache.TryGetValue(pr, out PullRequest? result)) { ApiConfig.Log.Debug($"Returned {nameof(PullRequest)} for {pr} from cache"); return result; } try { result = await client.PullRequest.Get("RPCS3", "rpcs3", pr).WaitAsync(cancellationToken).ConfigureAwait(false); UpdateRateLimitStats(); } catch (Exception e) { ApiConfig.Log.Error(e); } if (result == null) { ApiConfig.Log.Debug($"Failed to get {nameof(PullRequest)}, returning empty result"); return new(pr); } StatusesCache.Set(pr, result, PrStatusCacheTime); ApiConfig.Log.Debug($"Cached {nameof(PullRequest)} for {pr} for {PrStatusCacheTime}"); return result; } public async Task GetIssueInfoAsync(int issue, CancellationToken cancellationToken) { if (IssuesCache.TryGetValue(issue, out Issue? result)) { ApiConfig.Log.Debug($"Returned {nameof(Issue)} for {issue} from cache"); return result; } try { result = await client.Issue.Get("RPCS3", "rpcs3", issue).WaitAsync(cancellationToken).ConfigureAwait(false); UpdateRateLimitStats(); IssuesCache.Set(issue, result, IssueStatusCacheTime); ApiConfig.Log.Debug($"Cached {nameof(Issue)} for {issue} for {IssueStatusCacheTime}"); return result; } catch (Exception e) { ApiConfig.Log.Error(e); } ApiConfig.Log.Debug($"Failed to get {nameof(Issue)}, returning empty result"); return new(); } public Task?> GetOpenPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new() { State = ItemStateFilter.Open }, cancellationToken); public Task?> GetClosedPrsAsync(CancellationToken cancellationToken) => GetPrsWithStatusAsync(new() { State = ItemStateFilter.Closed, SortProperty = PullRequestSort.Updated, SortDirection = SortDirection.Descending }, cancellationToken); private async Task?> GetPrsWithStatusAsync(PullRequestRequest filter, CancellationToken cancellationToken) { var statusUri = "https://api.github.com/repos/RPCS3/rpcs3/pulls?state=" + filter; if (StatusesCache.TryGetValue(statusUri, out IReadOnlyList? result)) { ApiConfig.Log.Debug("Returned list of opened PRs from cache"); return result; } try { result = await client.PullRequest.GetAllForRepository("RPCS3", "rpcs3", filter).WaitAsync(cancellationToken).ConfigureAwait(false); UpdateRateLimitStats(); StatusesCache.Set(statusUri, result, PrStatusCacheTime); foreach (var prInfo in result) StatusesCache.Set(prInfo.Number, prInfo, PrStatusCacheTime); ApiConfig.Log.Debug($"Cached list of open PRs for {PrStatusCacheTime}"); } catch (Exception e) { ApiConfig.Log.Error(e); } return result; } private void UpdateRateLimitStats() { var apiInfo = client.GetLastApiInfo(); if (apiInfo == null) return; RateLimit = apiInfo.RateLimit.Limit; RateLimitRemaining = apiInfo.RateLimit.Remaining; RateLimitResetTime = DateTimeOffset.FromUnixTimeSeconds(apiInfo.RateLimit.ResetAsUtcEpochSeconds).UtcDateTime; if (RateLimitRemaining < 10) ApiConfig.Log.Warn($"Github rate limit is low: {RateLimitRemaining} out of {RateLimit}, will be reset on {RateLimitResetTime:u}"); } }