-
Notifications
You must be signed in to change notification settings - Fork 12
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
Add command for applying simple mod multiplier changes #246
Changes from 1 commit
d172e7a
fb821f8
dac88a3
2125c18
9fa327f
289c88a
1428000
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Dapper; | ||
using JetBrains.Annotations; | ||
using McMaster.Extensions.CommandLineUtils; | ||
using MySqlConnector; | ||
using osu.Server.QueueProcessor; | ||
|
||
namespace osu.Server.Queues.ScoreStatisticsProcessor.Commands.Score | ||
{ | ||
[Command("change-mod-multiplier", Description = "Changes a mod's multiplier globally by adjusting all relevant scores' totals.")] | ||
public class ChangeModMultiplierCommand | ||
{ | ||
[Option(CommandOptionType.SingleValue, Template = "-r|--ruleset-id", Description = "Required. The ID of the ruleset for the mod whose multiplier is being adjusted.")] | ||
public int? RulesetId { get; set; } | ||
|
||
[Option(CommandOptionType.SingleValue, Template = "-m|--mod", Description = "Required. The acronym of the mod whose multiplier is being adjusted.")] | ||
public string? ModAcronym { get; set; } | ||
|
||
[Option(CommandOptionType.SingleValue, Template = "--old", Description = "Required. The old multiplier of the mod being adjusted.")] | ||
public double? OldMultiplier { get; set; } | ||
|
||
[Option(CommandOptionType.SingleValue, Template = "--new", Description = "Required. The new multiplier of the mod being adjusted.")] | ||
public double? NewMultiplier { get; set; } | ||
|
||
[Option(CommandOptionType.SingleValue, Template = "--start-id", Description = "The ID of the `scores` table row to start processing from.")] | ||
public ulong? StartId { get; set; } | ||
|
||
[Option(CommandOptionType.SingleOrNoValue, Template = "--batch-size", Description = "The maximum number of scores to fetch in each batch.")] | ||
public int BatchSize { get; set; } = 5000; | ||
|
||
[Option(CommandOptionType.SingleOrNoValue, Template = "--dry-run", Description = "Do not actually change any score totals, just display what would be done.")] | ||
public bool DryRun { get; set; } | ||
|
||
private readonly StringBuilder sqlBuffer = new StringBuilder(); | ||
private readonly ElasticQueuePusher elasticQueueProcessor = new ElasticQueuePusher(); | ||
private readonly HashSet<ElasticQueuePusher.ElasticScoreItem> elasticItems = new HashSet<ElasticQueuePusher.ElasticScoreItem>(); | ||
|
||
[UsedImplicitly] | ||
public async Task<int> OnExecuteAsync(CommandLineApplication app, CancellationToken cancellationToken) | ||
{ | ||
if (RulesetId == null | ||
|| string.IsNullOrEmpty(ModAcronym) | ||
|| OldMultiplier == null | ||
|| NewMultiplier == null) | ||
{ | ||
await Console.Error.WriteLineAsync("One or more required parameters is missing."); | ||
app.ShowHelp(false); | ||
return 1; | ||
} | ||
|
||
if (NewMultiplier.Value == OldMultiplier.Value) | ||
{ | ||
Console.WriteLine("New and old multipliers are equal - there is nothing to do."); | ||
return 0; | ||
} | ||
|
||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(OldMultiplier.Value, nameof(OldMultiplier)); | ||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(NewMultiplier.Value, nameof(NewMultiplier)); | ||
|
||
Console.WriteLine(); | ||
Console.WriteLine($"Changing multiplier of mod {ModAcronym} in ruleset {RulesetId} from {OldMultiplier} to {NewMultiplier}"); | ||
Console.WriteLine($"Indexing to elasticsearch queue(s) {elasticQueueProcessor.ActiveQueues}"); | ||
|
||
if (DryRun) | ||
Console.WriteLine("RUNNING IN DRY RUN MODE."); | ||
|
||
Thread.Sleep(5000); | ||
|
||
ulong lastId = StartId ?? 0; | ||
int converted = 0; | ||
int skipped = 0; | ||
|
||
using var conn = DatabaseAccess.GetConnection(); | ||
|
||
while (!cancellationToken.IsCancellationRequested) | ||
{ | ||
var scoresToAdjust = (await conn.QueryAsync<ScoreToAdjust>( | ||
"SELECT `id`, `total_score` " | ||
+ "FROM `scores` " | ||
+ "WHERE `id` BETWEEN @lastId AND (@lastId + @batchSize - 1) " | ||
+ "AND `ruleset_id` = @rulesetId " | ||
+ "AND JSON_SEARCH(`data`, 'one', @modAcronym, NULL, '$.mods[*].acronym') IS NOT NULL", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wasn't sure whether to do this sql-side or c#-side. I think this should be somewhat painless because |
||
new | ||
{ | ||
lastId = lastId, | ||
batchSize = BatchSize, | ||
rulesetId = RulesetId, | ||
modAcronym = ModAcronym, | ||
})).ToArray(); | ||
|
||
foreach (var score in scoresToAdjust) | ||
{ | ||
uint oldTotal = score.total_score; | ||
score.total_score = (uint)(score.total_score / OldMultiplier * NewMultiplier); | ||
Console.WriteLine($"{score.id}: total score change from {oldTotal} to {score.total_score}"); | ||
|
||
sqlBuffer.AppendLine($"UPDATE `scores` SET `total_score` = {score.total_score} WHERE `id` = {score.id};"); | ||
elasticItems.Add(new ElasticQueuePusher.ElasticScoreItem { ScoreId = (long?)score.id }); | ||
} | ||
|
||
flush(conn); | ||
|
||
lastId += (ulong)BatchSize; | ||
converted += scoresToAdjust.Length; | ||
skipped += BatchSize - scoresToAdjust.Length; | ||
|
||
Console.WriteLine($"Processed up to ID {lastId} ({converted} converted {skipped} skipped)"); | ||
|
||
if (lastId > await conn.QuerySingleAsync<ulong>("SELECT MAX(`id`) FROM `scores`")) | ||
{ | ||
Console.WriteLine("All done!"); | ||
break; | ||
} | ||
} | ||
|
||
flush(conn, force: true); | ||
return 0; | ||
} | ||
|
||
private void flush(MySqlConnection conn, bool force = false) | ||
{ | ||
int bufferLength = sqlBuffer.Length; | ||
|
||
if (bufferLength == 0) | ||
return; | ||
|
||
if (bufferLength > 1024 || force) | ||
{ | ||
if (!DryRun) | ||
{ | ||
Console.WriteLine(); | ||
Console.WriteLine($"Flushing sql batch ({bufferLength:N0} bytes)"); | ||
conn.Execute(sqlBuffer.ToString()); | ||
|
||
if (elasticItems.Count > 0) | ||
{ | ||
elasticQueueProcessor.PushToQueue(elasticItems.ToList()); | ||
Console.WriteLine($"Queued {elasticItems.Count} items for indexing"); | ||
} | ||
} | ||
|
||
elasticItems.Clear(); | ||
sqlBuffer.Clear(); | ||
} | ||
} | ||
|
||
[SuppressMessage("ReSharper", "InconsistentNaming")] | ||
private class ScoreToAdjust | ||
{ | ||
public ulong id; | ||
public uint total_score; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using JetBrains.Annotations; | ||
using McMaster.Extensions.CommandLineUtils; | ||
using osu.Server.Queues.ScoreStatisticsProcessor.Commands.Score; | ||
|
||
namespace osu.Server.Queues.ScoreStatisticsProcessor.Commands | ||
{ | ||
[Command(Name = "score", Description = "Runs batch processing on score totals for scores and users.")] | ||
[Subcommand(typeof(ChangeModMultiplierCommand))] | ||
public class ScoreCommands | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would have probably just put the command in maintenance, given there's already similar score repair commands in there? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the time of writing I wasn't sure if there were going to be any others, for e.g. user total recalculations. But I think it's pretty clear to me that it's not required anymore, will move. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{ | ||
[UsedImplicitly] | ||
public Task<int> OnExecuteAsync(CommandLineApplication app, CancellationToken _) | ||
{ | ||
app.ShowHelp(false); | ||
return Task.FromResult(1); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda weird to have optional arguments which are required (usually I'd use standard arguments in place of this, but I guess your goal is to have things named?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I didn't want to have them purely positional to prevent screw-ups from swapping them or something.