Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Morpheus.Tests/ChannelServiceConcurrencyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Morpheus.Database;
using Morpheus.Database.Models;
using Morpheus.Services;

namespace Morpheus.Tests;

public class ChannelServiceConcurrencyTests
{
[Fact]
public async Task TryGetCreateChannel_WhenAnotherHandlerCreatesChannel_ReturnsPersistedChannel()
{
await using SqliteConnection connection = new("Data Source=:memory:");
await connection.OpenAsync();

DbContextOptions<DB> options = new DbContextOptionsBuilder<DB>()
.UseSqlite(connection)
.Options;
await using (DB setup = new(options))
await setup.Database.EnsureCreatedAsync();

await using RacingDb db = new(options);
db.InsertCompetingChannelOnNextSave = true;
ChannelService service = new(db, new LogsService(new LogQueue()));

Channel result = await service.TryGetCreateChannel(123, "current-name");

Assert.Equal((ulong)123, result.DiscordId);
Assert.Equal("current-name", result.Name);
Assert.Equal(1, await db.Channels.CountAsync());
}

private sealed class RacingDb(DbContextOptions<DB> options) : DB(options)
{
public bool InsertCompetingChannelOnNextSave { get; set; }

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
if (InsertCompetingChannelOnNextSave)
{
InsertCompetingChannelOnNextSave = false;
ChangeTracker.Clear();
Channels.Add(new Channel
{
DiscordId = 123,
Name = "stale-name"
});
await base.SaveChangesAsync(cancellationToken);
throw new DbUpdateException("Simulated concurrent unique-key conflict.");
}

return await base.SaveChangesAsync(cancellationToken);
}
}
}
22 changes: 21 additions & 1 deletion Services/ChannelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,27 @@ public async Task<Channel> TryGetCreateChannel(ulong discordId, string name)
};

await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
try
{
await dbContext.SaveChangesAsync();
}
catch (DbUpdateException)
{
// Another handler may have created the same Discord channel after our initial lookup.
// Clear the failed insert and use the row protected by the unique DiscordId index.
dbContext.ChangeTracker.Clear();
Channel? concurrentChannel = await dbContext.Channels.FirstOrDefaultAsync(c => c.DiscordId == discordId);
if (concurrentChannel == null)
throw;

if (concurrentChannel.Name != name)
{
concurrentChannel.Name = name;
await dbContext.SaveChangesAsync();
}

return concurrentChannel;
}

logsService.Log($"New channel created {name}", Discord.LogSeverity.Verbose);

Expand Down