From f7369e0d682cc27e0e210e1135b1959abc4e1058 Mon Sep 17 00:00:00 2001 From: Davran Dilshat Date: Sun, 5 Sep 2021 14:47:46 +0100 Subject: [PATCH] create UserIdLookupCache to get user ID when importing scores --- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 35 ++++---- .../Scoring/ScoreManager_UserIdLookupCache.cs | 85 +++++++++++++++++++ 3 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Scoring/ScoreManager_UserIdLookupCache.cs diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f2d575550a..484cc23161 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -263,7 +263,7 @@ namespace osu.Game dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig, true)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); // this should likely be moved to ArchiveModelManager when another case appears where it is necessary diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3c99dd6637..fcf214268a 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Platform; @@ -27,7 +28,7 @@ using osu.Game.Users; namespace osu.Game.Scoring { - public class ScoreManager : DownloadableArchiveModelManager + public partial class ScoreManager : DownloadableArchiveModelManager { public override IEnumerable HandledExtensions => new[] { ".osr" }; @@ -44,10 +45,13 @@ namespace osu.Game.Scoring [CanBeNull] private readonly OsuConfigManager configManager; + [CanBeNull] + private readonly UserIdLookupCache userIdLookupCache; + private IAPIProvider api { get; set; } public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null, - Func difficulties = null, OsuConfigManager configManager = null) + Func difficulties = null, OsuConfigManager configManager = null, bool performOnlineLookups = false) : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { this.rulesets = rulesets; @@ -55,6 +59,9 @@ namespace osu.Game.Scoring this.difficulties = difficulties; this.configManager = configManager; this.api = api; + + if (performOnlineLookups) + userIdLookupCache = new UserIdLookupCache(api); } protected override ScoreInfo CreateModel(ArchiveReader archive) @@ -76,30 +83,20 @@ namespace osu.Game.Scoring } } - private readonly Dictionary previouslyLookedUpUsernames = new Dictionary(); - - protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override async Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { // These scores only provide the user's username but we need the user's ID too. - if (model.UserID <= 1 && model.UserString != null) + if (model.UserID <= 1 && model.UserString != null && userIdLookupCache != null) { - if (previouslyLookedUpUsernames.TryGetValue(model.UserString, out User user)) + try { - model.UserID = user.Id; - return Task.CompletedTask; + model.UserID = await userIdLookupCache.GetUserIdAsync(model.UserString, cancellationToken).ConfigureAwait(false); } - - var request = new GetUserRequest(model.UserString); - request.Success += u => + catch (Exception e) { - model.UserID = u.Id; - previouslyLookedUpUsernames.TryAdd(model.UserString, u); - }; - - api?.Queue(request); + LogForModel(model, $"Online retrieval failed for {model.User} ({e.Message})", e); + } } - - return Task.CompletedTask; } protected override void ExportModelTo(ScoreInfo model, Stream outputStream) diff --git a/osu.Game/Scoring/ScoreManager_UserIdLookupCache.cs b/osu.Game/Scoring/ScoreManager_UserIdLookupCache.cs new file mode 100644 index 0000000000..dc7e244f14 --- /dev/null +++ b/osu.Game/Scoring/ScoreManager_UserIdLookupCache.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Database; + +namespace osu.Game.Scoring +{ + public partial class ScoreManager + { + private class UserIdLookupCache : MemoryCachingComponent + { + private readonly IAPIProvider api; + + public UserIdLookupCache(IAPIProvider api) + { + this.api = api; + } + + /// + /// Perform an API lookup on the specified username, returning the associated ID. + /// + /// The username to lookup. + /// An optional cancellation token. + /// The user ID, or 1 if the user does not exist or the request could not be satisfied. + public Task GetUserIdAsync(string username, CancellationToken token = default) => GetAsync(username, token); + + protected override async Task ComputeValueAsync(string lookup, CancellationToken token = default) + => await queryUserId(lookup).ConfigureAwait(false); + + private readonly Queue<(string username, TaskCompletionSource)> pendingUserTasks = new Queue<(string, TaskCompletionSource)>(); + private Task pendingRequestTask; + private readonly object taskAssignmentLock = new object(); + + private Task queryUserId(string username) + { + lock (taskAssignmentLock) + { + var tcs = new TaskCompletionSource(); + + // Add to the queue. + pendingUserTasks.Enqueue((username, tcs)); + + // Create a request task if there's not already one. + if (pendingRequestTask == null) + createNewTask(); + + return tcs.Task; + } + } + + private void performLookup() + { + (string username, TaskCompletionSource task) next; + + lock (taskAssignmentLock) + { + next = pendingUserTasks.Dequeue(); + } + + var request = new GetUserRequest(next.username); + + // rather than queueing, we maintain our own single-threaded request stream. + // todo: we probably want retry logic here. + api.Perform(request); + + // Create a new request task if there's still more users to query. + lock (taskAssignmentLock) + { + pendingRequestTask = null; + if (pendingUserTasks.Count > 0) + createNewTask(); + } + + next.task.SetResult(request.Result?.Id ?? 1); + } + + private void createNewTask() => pendingRequestTask = Task.Run(performLookup); + } + } +}