fix: repair a missing entry sub-room under the bootstrap lock

From the local CodeRabbit review:
- Repair rooms that lack an OrderIndex 0 (entry) sub-room even when they
  have other sub-rooms, not only rooms with none.
- Every service runs the database bootstrap at startup, so on Postgres
  replicas booting together could hand out the same scene ids (max + 1)
  and one would fail startup on the primary key. Run the repair in a
  transaction holding the bootstrap's advisory lock.
This commit is contained in:
Alexa
2026-10-25 12:52:53 +02:00
parent 78c64d117a
commit cf7a021898
2 changed files with 35 additions and 7 deletions
@@ -118,6 +118,24 @@ public sealed class SubRoomsAndInstances2023Tests : IClassFixture<DorkNetServerF
Assert.Equal((0, 0), await service.RepairRoomScenesAsync());
}
[Fact]
public async Task Startup_repair_restores_a_missing_entry_sub_room_next_to_others()
{
var roomId = await SeedRoomAsync(1, accessibility: 0, scenes: new[] { "Home", "Back" });
await using var scope = _factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
await db.RoomScenes.Where(s => s.RoomId == roomId && s.OrderIndex == 0).ExecuteDeleteAsync();
var service = scope.ServiceProvider.GetRequiredService<RoomService>();
var (added, _) = await service.RepairRoomScenesAsync();
Assert.True(added >= 1);
var scenes = await db.RoomScenes.AsNoTracking().Where(s => s.RoomId == roomId)
.OrderBy(s => s.OrderIndex).Select(s => new { s.OrderIndex, s.Name }).ToListAsync();
Assert.Equal(new[] { (0, "Home"), (1, "Back") }, scenes.Select(s => (s.OrderIndex, s.Name)));
Assert.Equal((0, 0), await service.RepairRoomScenesAsync());
}
[Fact]
public async Task Instance_browser_lists_the_private_instance_with_its_players_for_allowed_callers()
{
+17 -7
View File
@@ -585,15 +585,24 @@ public class RoomService(DorkNetDbContext db)
///
/// <para>Scene ids are assigned explicitly (max + 1) for the same reason
/// the clone handler does: seeded rows were inserted with manual ids
/// without advancing the Postgres identity sequence.</para>
/// Returns (rooms given a scene, scenes renamed).</summary>
/// without advancing the Postgres identity sequence. Every service that
/// runs the database bootstrap calls this at startup, so on Postgres the
/// whole repair holds the bootstrap's advisory lock: replicas booting
/// together take turns instead of handing out the same ids.</para>
/// Returns (rooms given an entry scene, scenes renamed).</summary>
public async Task<(int ScenesAdded, int ScenesRenamed)> RepairRoomScenesAsync()
{
var roomsWithoutScenes = await db.Rooms
.Where(r => !r.IsDormRoom && !db.RoomScenes.Any(s => s.RoomId == r.Id))
await using var tx = await db.Database.BeginTransactionAsync();
if (db.Database.IsNpgsql())
await db.Database.ExecuteSqlRawAsync("SELECT pg_advisory_xact_lock(1146246987);");
// The entry sub-room is OrderIndex 0 (its blob is the room's own front
// door), so a room missing it is repaired even when it has others.
var roomsWithoutEntry = await db.Rooms
.Where(r => !r.IsDormRoom && !db.RoomScenes.Any(s => s.RoomId == r.Id && s.OrderIndex == 0))
.ToListAsync();
var nextSceneId = await db.RoomScenes.MaxAsync(s => (long?)s.Id) ?? 0L;
foreach (var room in roomsWithoutScenes)
foreach (var room in roomsWithoutEntry)
{
var scene = NewHomeScene(room);
scene.Id = ++nextSceneId;
@@ -606,9 +615,10 @@ public class RoomService(DorkNetDbContext db)
foreach (var scene in unnamed)
scene.Name = DefaultSubRoomName(scene.OrderIndex);
if (roomsWithoutScenes.Count > 0 || unnamed.Count > 0)
if (roomsWithoutEntry.Count > 0 || unnamed.Count > 0)
await db.SaveChangesAsync();
return (roomsWithoutScenes.Count, unnamed.Count);
await tx.CommitAsync();
return (roomsWithoutEntry.Count, unnamed.Count);
}
/// <summary>Run once at startup to backfill <see cref="RoomEntity"/>