Skip to content

Commit

Permalink
Actually remove dungeon sessions (#862)
Browse files Browse the repository at this point in the history
Today, when you finish a dungeon, the session remains in the cache for
the remainder of its expiry time, because issues were previously
encountered with the client retrying record requests if they take too
long, and then encountering errors because the session had been deleted
on the first request.

Now that the codebase makes better use of CancellationToken, this is
much less likely if we put the eviction of the session right at the end
of the response method. It is still possible if the request is cancelled
during serialization, however. If we encounter issues we can make the
RemoveDungeon method instead mark the session to be removed in 1
minute's time or similar.

Also includes a refactoring of the DungeonService to implement a
unit-of-work pattern, where the current session can be cached and
modified in-memory without round-trips to the cache. This is primarily
aimed at the DungeonStart logic which can require several updates after
creating the session, arising from how the code is structured.

Finally, makes sessions keyed by viewer ID, so that it isn't possible to
get another player's session. It probably wasn't really possible before,
given the astronomical likelihood of guessing/colliding GUIDs, but it
makes sense to segregate this data by tenant all the same. **This key
change is breaking, and the server should be taken down during the
deployment.**
  • Loading branch information
SapiensAnatis authored Jun 8, 2024
1 parent d375c0f commit 42a0a56
Show file tree
Hide file tree
Showing 25 changed files with 365 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ await AddToDatabase(
}
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

DungeonRecordRecordResponse response = (
await Client.PostMsgpack<DungeonRecordRecordResponse>(
Expand Down Expand Up @@ -229,7 +229,7 @@ await Client.PostMsgpack<MemoryEventActivateResponse>(
}
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

DungeonRecordRecordResponse response = (
await Client.PostMsgpack<DungeonRecordRecordResponse>(
Expand Down Expand Up @@ -280,7 +280,7 @@ await Client.PostMsgpack<MemoryEventActivateResponse>(
}
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

DungeonRecordRecordResponse response = (
await Client.PostMsgpack<DungeonRecordRecordResponse>(
Expand Down Expand Up @@ -864,7 +864,7 @@ public async Task Record_HandlesNonExistentQuestData()
}
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

DragaliaResponse<DungeonRecordRecordResponse> response =
await Client.PostMsgpack<DungeonRecordRecordResponse>(
Expand Down Expand Up @@ -988,7 +988,7 @@ await AddToDatabase(
EnemyList = new Dictionary<int, IEnumerable<AtgenEnemy>>()
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

(
await Client.PostMsgpack<DungeonRecordRecordResponse>(
Expand Down Expand Up @@ -1043,7 +1043,7 @@ await AddToDatabase(
EnemyList = new Dictionary<int, IEnumerable<AtgenEnemy>>()
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

(
await Client.PostMsgpack<DungeonRecordRecordResponse>(
Expand Down Expand Up @@ -1209,8 +1209,13 @@ await Client.PostMsgpack<DungeonRecordRecordResponse>("/dungeon_record/record",
response.UpdateDataList.UserData.TutorialStatus.Should().Be(20501);
}

private async Task<string> StartDungeon(DungeonSession session) =>
await Services.GetRequiredService<IDungeonService>().StartDungeon(session);
private async Task<string> StartDungeon(DungeonSession session)
{
string key = this.DungeonService.CreateSession(session);
await this.DungeonService.SaveSession(CancellationToken.None);

return key;
}

private void SetupPhotonAuthentication()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -869,10 +869,22 @@ await this.Client.PostMsgpack<SummonRequestResponse>(

response.Data.ResultUnitList.Should().Contain(x => x.Rarity == 5);

this.ApiContext.PlayerBannerData.AsNoTracking()
.First(x => x.SummonBannerId == TestGalaBannerId)
.SummonCountSinceLastFiveStar.Should()
.Be(0);
SummonGetOddsDataResponse oddsResponse = (
await this.Client.PostMsgpack<SummonGetOddsDataResponse>(
"summon/get_odds_data",
new SummonGetOddsDataRequest(TestBannerId)
)
).Data;

oddsResponse
.OddsRateList.Normal.RarityList.Should()
.BeEquivalentTo(
[
new AtgenRarityList { Rarity = 5, TotalRate = "4.00%" },
new AtgenRarityList { Rarity = 4, TotalRate = "16.00%" },
new AtgenRarityList { Rarity = 3, TotalRate = "80.00%" },
]
);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ await this.AddRangeToDatabase(
WallLevel = wallLevel + 1 // Client passes (db wall level + 1)
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

WallRecordRecordResponse response = (
await Client.PostMsgpack<WallRecordRecordResponse>(
Expand Down Expand Up @@ -160,7 +160,7 @@ await this.AddRangeToDatabase(
WallLevel = wallLevel
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

WallRecordRecordResponse response = (
await Client.PostMsgpack<WallRecordRecordResponse>(
Expand Down Expand Up @@ -239,7 +239,7 @@ await this.AddRangeToDatabase(
WallLevel = 6
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

WallRecordRecordResponse response = (
await Client.PostMsgpack<WallRecordRecordResponse>(
Expand Down Expand Up @@ -309,7 +309,7 @@ await this.AddRangeToDatabase(
WallLevel = 80
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

WallRecordRecordResponse response = (
await Client.PostMsgpack<WallRecordRecordResponse>(
Expand All @@ -329,4 +329,12 @@ await Client.PostMsgpack<WallRecordRecordResponse>(

missionNotice.Should().BeNull();
}

private async Task<string> StartDungeon(DungeonSession session)
{
string key = this.DungeonService.CreateSession(session);
await this.DungeonService.SaveSession(CancellationToken.None);

return key;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ await AddToDatabase(
WallLevel = expectedWallLevel
};

string key = await Services.GetRequiredService<IDungeonService>().StartDungeon(mockSession);
string key = await this.StartDungeon(mockSession);

WallFailResponse response = (
await Client.PostMsgpack<WallFailResponse>(
Expand Down Expand Up @@ -256,4 +256,12 @@ await this.Client.PostMsgpack<ResultCodeResponse>(

response.DataHeaders.ResultCode.Should().Be(ResultCode.CommonInvalidArgument);
}

private async Task<string> StartDungeon(DungeonSession session)
{
string key = this.DungeonService.CreateSession(session);
await this.DungeonService.SaveSession(CancellationToken.None);

return key;
}
}
20 changes: 19 additions & 1 deletion DragaliaAPI/DragaliaAPI.Integration.Test/TestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Database.Entities.Abstract;
using DragaliaAPI.Extensions;
using DragaliaAPI.Features.Dungeon;
using DragaliaAPI.Features.Fort;
using DragaliaAPI.Models;
using DragaliaAPI.Models.Options;
using DragaliaAPI.Services;
using DragaliaAPI.Services.Api;
using DragaliaAPI.Shared.PlayerDetails;
Expand All @@ -13,8 +15,11 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using Npgsql;

Expand All @@ -34,10 +39,12 @@ public class TestFixture
protected const string SessionId = "session_id";

private readonly CustomWebApplicationFactory factory;
private readonly IPlayerIdentityService stubPlayerIdentityService;

protected TestFixture(CustomWebApplicationFactory factory, ITestOutputHelper testOutputHelper)
{
this.factory = factory;

this.TestOutputHelper = testOutputHelper;

this.Client = this.CreateClient();
Expand All @@ -52,11 +59,20 @@ protected TestFixture(CustomWebApplicationFactory factory, ITestOutputHelper tes
this.SeedDatabase().Wait();
this.SeedCache().Wait();

this.stubPlayerIdentityService = new StubPlayerIdentityService(this.ViewerId);

DbContextOptions<ApiContext> options = this.Services.GetRequiredService<
DbContextOptions<ApiContext>
>();
this.ApiContext = new ApiContext(options, new StubPlayerIdentityService(this.ViewerId));
this.ApiContext = new ApiContext(options, this.stubPlayerIdentityService);
this.ApiContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

this.DungeonService = new DungeonService(
this.Services.GetRequiredService<IDistributedCache>(),
this.Services.GetRequiredService<IOptionsMonitor<RedisCachingOptions>>(),
this.stubPlayerIdentityService,
NullLogger<DungeonService>.Instance
);
}

protected DateTimeOffset LastDailyReset { get; }
Expand Down Expand Up @@ -84,6 +100,8 @@ protected TestFixture(CustomWebApplicationFactory factory, ITestOutputHelper tes

protected IMapper Mapper { get; }

protected IDungeonService DungeonService { get; }

/// <summary>
/// Instance of <see cref="ApiContext"/> to use for setting up / interrogating the database in tests.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace DragaliaAPI.Shared.Definitions.Enums;
/// </summary>
public enum VariationTypes
{
None = 0,
Normal = 1,
Hard = 2,
VeryHard = 3,
Expand Down
28 changes: 20 additions & 8 deletions DragaliaAPI/DragaliaAPI.Test/Controllers/DungeonControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,28 @@ public async Task Fail_IsMultiFalse_ReturnsExpectedResponse()
}
};

this.mockDungeonService.Setup(x => x.FinishDungeon("my key"))
this.mockDungeonService.Setup(x => x.GetSession("my key", CancellationToken.None))
.ReturnsAsync(
new DungeonSession()
{
QuestData = MasterAsset.QuestData.Get(questId),
Party = new List<PartySettingList>(),
Party = [],
IsMulti = false,
SupportViewerId = 4,
QuestData = MasterAsset.QuestData[questId]
}
);

this.mockDungeonService.Setup(x => x.RemoveSession("my key", CancellationToken.None))
.Returns(Task.CompletedTask);

this.mockDungeonRecordHelperService.Setup(x => x.ProcessHelperDataSolo(4))
.ReturnsAsync((userSupportList, supportDetailList));

DungeonFailResponse? response = (
await this.dungeonController.Fail(new DungeonFailRequest() { DungeonKey = "my key" })
await this.dungeonController.Fail(
new DungeonFailRequest() { DungeonKey = "my key" },
CancellationToken.None
)
).GetData<DungeonFailResponse>();

response.Should().NotBeNull();
Expand Down Expand Up @@ -126,23 +132,29 @@ public async Task Fail_IsMultiTrue_RespondsExpectedResponse()
}
};

this.mockDungeonService.Setup(x => x.FinishDungeon("my key"))
this.mockDungeonService.Setup(x => x.GetSession("my key", CancellationToken.None))
.ReturnsAsync(
new DungeonSession()
{
QuestData = MasterAsset.QuestData.Get(questId),
Party = new List<PartySettingList>(),
Party = [],
IsMulti = true,
QuestData = MasterAsset.QuestData[questId]
}
);

this.mockDungeonService.Setup(x => x.RemoveSession("my key", CancellationToken.None))
.Returns(Task.CompletedTask);

this.mockDungeonRecordHelperService.Setup(x => x.ProcessHelperDataMulti())
.ReturnsAsync((userSupportList, supportDetailList));

this.mockMatchingService.Setup(x => x.GetIsHost()).ReturnsAsync(false);

DungeonFailResponse? response = (
await this.dungeonController.Fail(new DungeonFailRequest() { DungeonKey = "my key" })
await this.dungeonController.Fail(
new DungeonFailRequest() { DungeonKey = "my key" },
CancellationToken.None
)
).GetData<DungeonFailResponse>();

response.Should().NotBeNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using DragaliaAPI.Models.Generated;
using DragaliaAPI.Models.Options;
using DragaliaAPI.Shared.MasterAsset;
using DragaliaAPI.Test.Utils;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -33,6 +34,7 @@ public DungeonServiceTest()
dungeonService = new DungeonService(
testCache,
this.mockOptions.Object,
IdentityTestUtils.MockPlayerDetailsService.Object,
this.mockLogger.Object
);
}
Expand All @@ -50,8 +52,11 @@ public async Task StartDungeon_CanGetAfterwards()
}
};

string key = await dungeonService.StartDungeon(session);
string key = dungeonService.CreateSession(session);
await dungeonService.SaveSession(CancellationToken.None);

(await dungeonService.GetDungeon(key)).Should().BeEquivalentTo(session);
(await dungeonService.GetSession(key, CancellationToken.None))
.Should()
.BeEquivalentTo(session);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ public async Task Record_ReturnsData()
WallLevel = wallLevel
};

mockDungeonService.Setup(x => x.FinishDungeon(dungeonKey)).ReturnsAsync(session);
this.mockDungeonService.Setup(x => x.GetSession(dungeonKey, CancellationToken.None))
.ReturnsAsync(session);

mockDungeonService
.Setup(x => x.RemoveSession(dungeonKey, CancellationToken.None))
.Returns(Task.CompletedTask);

mockWallService.Setup(x => x.GetQuestWall(wallId)).ReturnsAsync(playerQuestWall);
mockWallService.Setup(x => x.LevelupQuestWall(wallId)).Returns(Task.CompletedTask);
Expand Down
12 changes: 8 additions & 4 deletions DragaliaAPI/DragaliaAPI/Extensions/DistributedCacheExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ public static class DistributedCacheExtensions
{
public static async Task<TObject?> GetJsonAsync<TObject>(
this IDistributedCache cache,
string key
string key,
CancellationToken cancellationToken = default
)
where TObject : class
{
string? json = await cache.GetStringAsync(key);
string? json = await cache.GetStringAsync(key, cancellationToken);
if (json == null)
{
return default;
}

return JsonSerializer.Deserialize<TObject>(json);
}
Expand All @@ -21,6 +24,7 @@ public static Task SetJsonAsync(
this IDistributedCache cache,
string key,
object entry,
DistributedCacheEntryOptions options
) => cache.SetStringAsync(key, JsonSerializer.Serialize(entry), options);
DistributedCacheEntryOptions options,
CancellationToken cancellationToken = default
) => cache.SetStringAsync(key, JsonSerializer.Serialize(entry), options, cancellationToken);
}
Loading

0 comments on commit 42a0a56

Please sign in to comment.