Skip to content

Commit 4060ec4

Browse files
authored
Properly fix website pin migration (#1066)
Found another pagination issue with this migration right after merging the previous PR, so I've (partially) reworked it to iterate users instead of pin relations. This does make the newly introduced feature to alter the processed and total counts in the `JobState` unused for now, but I have not removed it because it might eventually become necessary, if a different job ever needs to delete entities.
2 parents 5f7e4a9 + dad7fa4 commit 4060ec4

4 files changed

Lines changed: 137 additions & 29 deletions

File tree

Refresh.Database/GameDatabaseContext.Pins.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ private IEnumerable<PinProgressRelation> GetPinProgressesByUser(GameUser user, b
169169
public DatabaseList<PinProgressRelation> GetPinProgressesByUser(GameUser user, bool isBeta, TokenPlatform platform, int skip, int count)
170170
=> new(this.GetPinProgressesByUser(user, isBeta, platform), skip, count);
171171

172+
public IQueryable<PinProgressRelation> GetAllPinProgressesByUserAndId(GameUser user, long pinId)
173+
=> this.PinProgressRelations.Where(p => p.PublisherId == user.UserId && p.PinId == pinId);
174+
172175
public PinProgressRelation? GetUserPinProgress(long pinId, GameUser user, bool isBeta, TokenPlatform platform)
173176
=> this.PinProgressRelations.FirstOrDefault(p => p.PinId == pinId && p.PublisherId == user.UserId
174177
&& (p.IsBeta == isBeta && p.Platform == platform || p.Platform == TokenPlatform.Website));
Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
using MongoDB.Bson;
2-
using Refresh.Common.Time;
3-
using Refresh.Database.Models.Authentication;
1+
using Refresh.Database.Models.Authentication;
42
using Refresh.Database.Models.Pins;
53
using Refresh.Database.Models.Relations;
4+
using Refresh.Database.Models.Users;
65
using Refresh.Workers;
76

87
namespace Refresh.Interfaces.Workers.Migrations;
98

10-
public class CorrectWebsitePinProgressPlatformMigration : MigrationJob<PinProgressRelation>
9+
// Easier to do this by user than by pin relation, to avoid various pagination issues
10+
public class CorrectWebsitePinProgressPlatformMigration : MigrationJob<GameUser>
1111
{
1212
private readonly List<long> WebsitePinIds =
1313
[
@@ -16,36 +16,25 @@ public class CorrectWebsitePinProgressPlatformMigration : MigrationJob<PinProgre
1616
(long)ServerPins.SignIntoWebsite,
1717
];
1818

19-
protected override IQueryable<PinProgressRelation> SortAndFilter(IQueryable<PinProgressRelation> query)
19+
protected override IQueryable<GameUser> SortAndFilter(IQueryable<GameUser> query)
2020
{
21-
return query
22-
.Where(p => this.WebsitePinIds.Contains(p.PinId))
23-
.OrderBy(p => p.PinId);
21+
return query.OrderBy(u => u.UserId);
2422
}
2523

26-
protected override int Migrate(WorkContext context, PinProgressRelation[] batch)
24+
protected override int Migrate(WorkContext context, GameUser[] batch)
2725
{
28-
int pinsLeft = batch.Length;
29-
30-
foreach (long pinId in this.WebsitePinIds)
26+
foreach (GameUser user in batch)
3127
{
32-
IEnumerable<IGrouping<ObjectId, PinProgressRelation>> pinsByUser = batch
33-
.Where(r => r.PinId == pinId)
34-
.GroupBy(r => r.PublisherId);
35-
36-
foreach (IEnumerable<PinProgressRelation> group in pinsByUser)
28+
foreach (long pinId in this.WebsitePinIds)
3729
{
38-
// Should never happen, but just incase
39-
if (!group.Any()) continue;
30+
List<PinProgressRelation> pinsByUserAndId = context.Database.GetAllPinProgressesByUserAndId(user, pinId).ToList();
31+
if (pinsByUserAndId.Count <= 0) continue; // no need to deduplicate if there is nothing (do still migrate if there's only 1 pin, to overwrite its platform)
4032

41-
// Find best one by the current user
42-
PinProgressRelation relationToMigrate = group.MaxBy(r => r.Progress)!;
43-
List<PinProgressRelation> relationsToRemove = group.ToList();
33+
PinProgressRelation relationToMigrate = pinsByUserAndId.MaxBy(r => r.Progress)!;
4434

45-
foreach (PinProgressRelation relation in group)
35+
foreach (PinProgressRelation relation in pinsByUserAndId)
4636
{
4737
context.Database.RemovePinProgress(relation, false);
48-
pinsLeft--;
4938
}
5039

5140
// Now take the best progress we've just got and add it as a website pin, preserving other old metadata
@@ -61,11 +50,10 @@ protected override int Migrate(WorkContext context, PinProgressRelation[] batch)
6150
};
6251

6352
context.Database.AddPinProgress(newRelation, false);
64-
pinsLeft++;
6553
}
6654
}
6755

6856
context.Database.SaveChanges();
69-
return pinsLeft;
57+
return batch.Length;
7058
}
7159
}

Refresh.Workers/MigrationJob.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public override void ExecuteJob(WorkContext context)
5656
/// <returns>
5757
/// The number of entities in the batch, minus the number of entities removed from DB during this migration.
5858
/// E.g. if 1000 items are in the given batch, and 5 got removed from DB during migration,
59-
/// the returned number will be 1000 - 5 = 995
59+
/// the returned number will be 1000 - 5 = 995.
60+
/// This allows you to write migration jobs, which need to delete entities from the given batch, without breaking pagination done by the worker.
6061
/// </returns>
6162
protected abstract int Migrate(WorkContext context, TEntity[] batch);
6263
}

RefreshTests.GameServer/Tests/Workers/PinMigrationTests.cs

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Refresh.Database.Models.Authentication;
22
using Refresh.Database.Models.Pins;
3+
using Refresh.Database.Models.Relations;
34
using Refresh.Database.Models.Users;
45
using Refresh.Database.Models.Workers;
56
using Refresh.Interfaces.Workers.Migrations;
@@ -46,7 +47,7 @@ public void WebsitePinMigrationDoesNotBreakPagination()
4647
CorrectWebsitePinProgressPlatformMigration job = new();
4748
context.Database.UpdateOrCreateJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, new MigrationJobState()
4849
{
49-
Total = 100 // Since we already have to manually create the state
50+
Total = 50,
5051
}, WorkerClass.Refresh);
5152
context.Database.Refresh();
5253

@@ -68,6 +69,121 @@ public void WebsitePinMigrationDoesNotBreakPagination()
6869
Assert.That(jobState.Total, Is.EqualTo(50));
6970
Assert.That(jobState.Complete, Is.True);
7071

71-
Assert.That(context.Database.GetTotalPinProgresses(), Is.EqualTo(50));
72+
Assert.That(context.Database.GetTotalPinProgresses(), Is.EqualTo(50)); // Pins were deduplicated
73+
}
74+
75+
[Test]
76+
[TestCase(ServerPins.SignIntoWebsite)]
77+
[TestCase(ServerPins.HeartPlayerOnWebsite)]
78+
[TestCase(ServerPins.QueueLevelOnWebsite)]
79+
public void WebsitePinMigrationProperlyOverwritesPlatform(long pinId)
80+
{
81+
using TestContext context = this.GetServer();
82+
83+
GameUser user = context.CreateUser();
84+
context.Database.AddPinProgress(new()
85+
{
86+
PinId = pinId,
87+
Progress = 20,
88+
Publisher = user,
89+
FirstPublished = new(),
90+
LastUpdated = new(),
91+
IsBeta = false,
92+
Platform = TokenPlatform.RPCS3,
93+
}, true);
94+
95+
context.Database.AddPinProgress(new()
96+
{
97+
PinId = pinId,
98+
Progress = 30,
99+
Publisher = user,
100+
FirstPublished = new(),
101+
LastUpdated = new(),
102+
IsBeta = true,
103+
Platform = TokenPlatform.RPCS3,
104+
}, true);
105+
106+
Assert.That(context.Database.GetTotalPinProgresses(), Is.EqualTo(2));
107+
108+
// Prepare migration
109+
CorrectWebsitePinProgressPlatformMigration job = new();
110+
context.Database.UpdateOrCreateJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, new MigrationJobState()
111+
{
112+
Total = 1
113+
}, WorkerClass.Refresh);
114+
context.Database.Refresh();
115+
116+
object? stateObject = context.Database.GetJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh);
117+
Assert.That(stateObject, Is.Not.Null);
118+
job.JobState = stateObject!;
119+
120+
// Migrate
121+
job.ExecuteJob(context.GetWorkContext());
122+
context.Database.Refresh();
123+
124+
List<PinProgressRelation> relations = context.Database.GetAllPinProgressesByUserAndId(user, pinId).ToList();
125+
Assert.That(relations.Count, Is.EqualTo(1));
126+
Assert.That(relations[0].Platform, Is.EqualTo(TokenPlatform.Website));
127+
Assert.That(relations[0].Progress, Is.EqualTo(30));
128+
}
129+
130+
[Test]
131+
[TestCase(ServerPins.SignIntoWebsite, ServerPins.CherryShooterLbp3ChallengeMedal)]
132+
[TestCase(ServerPins.HeartPlayerOnWebsite, ServerPins.TopFourthOfXCommunityLevelsWithOver50Scores)]
133+
[TestCase(ServerPins.QueueLevelOnWebsite, 420)]
134+
public void WebsitePinMigrationOnlyMigratesWebsitePins(long websitePinId, long otherPinId)
135+
{
136+
using TestContext context = this.GetServer();
137+
138+
GameUser user = context.CreateUser();
139+
context.Database.AddPinProgress(new()
140+
{
141+
PinId = websitePinId,
142+
Progress = 20,
143+
Publisher = user,
144+
FirstPublished = new(),
145+
LastUpdated = new(),
146+
IsBeta = false,
147+
Platform = TokenPlatform.RPCS3,
148+
}, true);
149+
150+
context.Database.AddPinProgress(new()
151+
{
152+
PinId = otherPinId,
153+
Progress = 30,
154+
Publisher = user,
155+
FirstPublished = new(),
156+
LastUpdated = new(),
157+
IsBeta = true,
158+
Platform = TokenPlatform.RPCS3,
159+
}, true);
160+
161+
Assert.That(context.Database.GetTotalPinProgresses(), Is.EqualTo(2));
162+
163+
// Prepare migration
164+
CorrectWebsitePinProgressPlatformMigration job = new();
165+
context.Database.UpdateOrCreateJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, new MigrationJobState()
166+
{
167+
Total = 1
168+
}, WorkerClass.Refresh);
169+
context.Database.Refresh();
170+
171+
object? stateObject = context.Database.GetJobState(typeof(CorrectWebsitePinProgressPlatformMigration).Name, typeof(MigrationJobState), WorkerClass.Refresh);
172+
Assert.That(stateObject, Is.Not.Null);
173+
job.JobState = stateObject!;
174+
175+
// Migrate
176+
job.ExecuteJob(context.GetWorkContext());
177+
context.Database.Refresh();
178+
179+
List<PinProgressRelation> websitePins = context.Database.GetAllPinProgressesByUserAndId(user, websitePinId).ToList();
180+
Assert.That(websitePins.Count, Is.EqualTo(1));
181+
Assert.That(websitePins[0].Platform, Is.EqualTo(TokenPlatform.Website));
182+
Assert.That(websitePins[0].Progress, Is.EqualTo(20));
183+
184+
List<PinProgressRelation> otherPins = context.Database.GetAllPinProgressesByUserAndId(user, otherPinId).ToList();
185+
Assert.That(otherPins.Count, Is.EqualTo(1));
186+
Assert.That(otherPins[0].Platform, Is.EqualTo(TokenPlatform.RPCS3));
187+
Assert.That(otherPins[0].Progress, Is.EqualTo(30));
72188
}
73189
}

0 commit comments

Comments
 (0)