86883cd5c6
- Refactor migrations and provider to use multiple PostgreSQL schemas, each matching a legacy SQLite database (activitylog, authentication, displaypreferences, library, users). - All tables, foreign keys, and indexes are now schema-qualified; Down migration drops tables by schema. - Provider ensures schemas exist before migrations; entities are mapped to correct schemas in OnModelCreating. - Add support for max-pool-size, min-pool-size, and multiplexing connection options; update logging accordingly. - VACUUM ANALYZE now runs per schema during scheduled optimization. - TruncateAllTablesAsync now truncates tables with schema qualification. - README updated with schema structure, new options, and multiplexing warnings. - CacheDecorator now calls async repository methods using .GetAwaiter().GetResult(), with documentation. - Lays groundwork for full async/await and multiplexing support in the database layer.
263 lines
9.0 KiB
C#
263 lines
9.0 KiB
C#
// Example: Converting DeleteItem from Sync to Async
|
|
// File: Jellyfin.Server.Implementations\Item\BaseItemRepository.cs
|
|
|
|
// ==========================================
|
|
// BEFORE (Current Synchronous Version)
|
|
// ==========================================
|
|
public void DeleteItem(params IReadOnlyList<Guid> ids)
|
|
{
|
|
if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
|
|
{
|
|
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
|
|
}
|
|
|
|
using var context = _dbProvider.CreateDbContext();
|
|
using var transaction = context.Database.BeginTransaction();
|
|
|
|
var date = (DateTime?)DateTime.UtcNow;
|
|
var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
|
|
|
|
// Remove conflicting UserData
|
|
context.UserData
|
|
.Join(
|
|
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
|
|
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
|
|
userData => new { userData.UserId, userData.CustomDataKey },
|
|
(placeholder, userData) => placeholder)
|
|
.Where(e => e.ItemId == PlaceholderId)
|
|
.ExecuteDelete();
|
|
|
|
// Detach user data
|
|
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteUpdate(e => e
|
|
.SetProperty(f => f.RetentionDate, date)
|
|
.SetProperty(f => f.ItemId, PlaceholderId));
|
|
|
|
// Delete related entities
|
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
|
|
|
|
var peopleIds = context.PeopleBaseItemMap
|
|
.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.Select(f => f.PeopleId)
|
|
.Distinct()
|
|
.ToArray(); // 🔴 SYNC: ToArray()
|
|
|
|
context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.Peoples.WhereOneOrMany(peopleIds, e => e.Id)
|
|
.Where(e => e.BaseItems!.Count == 0)
|
|
.ExecuteDelete();
|
|
|
|
context.SaveChanges(); // 🔴 SYNC: SaveChanges()
|
|
transaction.Commit(); // 🔴 SYNC: Commit()
|
|
}
|
|
|
|
// ==========================================
|
|
// AFTER (Async Version)
|
|
// ==========================================
|
|
public async Task DeleteItemAsync(IReadOnlyList<Guid> ids, CancellationToken cancellationToken = default)
|
|
{
|
|
if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
|
|
{
|
|
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
|
|
}
|
|
|
|
await using var context = _dbProvider.CreateDbContext(); // ✅ await using
|
|
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken); // ✅ BeginTransactionAsync
|
|
|
|
var date = (DateTime?)DateTime.UtcNow;
|
|
|
|
// ✅ Convert TraverseHirachyDown to async
|
|
var relatedItems = await GetRelatedItemsAsync(ids, context, cancellationToken);
|
|
|
|
// Remove conflicting UserData
|
|
await context.UserData
|
|
.Join(
|
|
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
|
|
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
|
|
userData => new { userData.UserId, userData.CustomDataKey },
|
|
(placeholder, userData) => placeholder)
|
|
.Where(e => e.ItemId == PlaceholderId)
|
|
.ExecuteDeleteAsync(cancellationToken); // ✅ ExecuteDeleteAsync
|
|
|
|
// Detach user data
|
|
await context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteUpdateAsync(e => e
|
|
.SetProperty(f => f.RetentionDate, date)
|
|
.SetProperty(f => f.ItemId, PlaceholderId),
|
|
cancellationToken); // ✅ ExecuteUpdateAsync
|
|
|
|
// Delete related entities (can be done in parallel if no dependencies)
|
|
await Task.WhenAll(
|
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken),
|
|
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken)
|
|
);
|
|
|
|
// Delete base items
|
|
await context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
// Get people IDs to check for orphans
|
|
var peopleIds = await context.PeopleBaseItemMap
|
|
.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.Select(f => f.PeopleId)
|
|
.Distinct()
|
|
.ToArrayAsync(cancellationToken); // ✅ ToArrayAsync
|
|
|
|
await context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await context.Peoples.WhereOneOrMany(peopleIds, e => e.Id)
|
|
.Where(e => e.BaseItems!.Count == 0)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
|
|
await context.SaveChangesAsync(cancellationToken); // ✅ SaveChangesAsync
|
|
await transaction.CommitAsync(cancellationToken); // ✅ CommitAsync
|
|
}
|
|
|
|
// Helper method - also converted to async
|
|
private async Task<Guid[]> GetRelatedItemsAsync(
|
|
IReadOnlyList<Guid> ids,
|
|
JellyfinDbContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var relatedItems = new HashSet<Guid>();
|
|
foreach (var id in ids)
|
|
{
|
|
var related = await TraverseHirachyDownAsync(id, context, cancellationToken);
|
|
foreach (var item in related)
|
|
{
|
|
relatedItems.Add(item);
|
|
}
|
|
}
|
|
return relatedItems.ToArray();
|
|
}
|
|
|
|
// ==========================================
|
|
// Interface Change Required
|
|
// ==========================================
|
|
|
|
// In IItemRepository.cs:
|
|
|
|
// BEFORE:
|
|
void DeleteItem(params IReadOnlyList<Guid> ids);
|
|
|
|
// AFTER:
|
|
Task DeleteItemAsync(IReadOnlyList<Guid> ids, CancellationToken cancellationToken = default);
|
|
|
|
// ==========================================
|
|
// Calling Code Changes
|
|
// ==========================================
|
|
|
|
// In LibraryManager.cs or similar:
|
|
|
|
// BEFORE:
|
|
public void DeleteItem(BaseItem item)
|
|
{
|
|
_itemRepository.DeleteItem(new[] { item.Id });
|
|
}
|
|
|
|
// AFTER:
|
|
public async Task DeleteItemAsync(BaseItem item, CancellationToken cancellationToken = default)
|
|
{
|
|
await _itemRepository.DeleteItemAsync(new[] { item.Id }, cancellationToken);
|
|
}
|
|
|
|
// ==========================================
|
|
// API Controller Changes
|
|
// ==========================================
|
|
|
|
// In ItemsController.cs:
|
|
|
|
// BEFORE:
|
|
[HttpDelete("{id}")]
|
|
public ActionResult DeleteItem([FromRoute] Guid id)
|
|
{
|
|
var item = _libraryManager.GetItem(id);
|
|
if (item is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
_libraryManager.DeleteItem(item);
|
|
return NoContent();
|
|
}
|
|
|
|
// AFTER:
|
|
[HttpDelete("{id}")]
|
|
public async Task<ActionResult> DeleteItem(
|
|
[FromRoute] Guid id,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var item = await _libraryManager.GetItemAsync(id, cancellationToken);
|
|
if (item is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
await _libraryManager.DeleteItemAsync(item, cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
// ==========================================
|
|
// Performance Optimization: Parallel Deletes
|
|
// ==========================================
|
|
|
|
// When deleting multiple independent entities, use Task.WhenAll:
|
|
|
|
// BEFORE (Sequential):
|
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
|
|
|
|
// AFTER (Parallel):
|
|
await Task.WhenAll(
|
|
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken),
|
|
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken),
|
|
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken),
|
|
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId)
|
|
.ExecuteDeleteAsync(cancellationToken)
|
|
);
|
|
|
|
// ⚡ This can significantly improve performance for operations with many independent deletes!
|
|
|
|
// ==========================================
|
|
// Testing the Async Version
|
|
// ==========================================
|
|
|
|
[Fact]
|
|
public async Task DeleteItemAsync_ValidId_DeletesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var itemId = Guid.NewGuid();
|
|
await _repository.SaveItemAsync(new BaseItem { Id = itemId }, CancellationToken.None);
|
|
|
|
// Act
|
|
await _repository.DeleteItemAsync(new[] { itemId }, CancellationToken.None);
|
|
|
|
// Assert
|
|
var item = await _repository.RetrieveItemAsync(itemId, CancellationToken.None);
|
|
Assert.Null(item);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteItemAsync_Cancelled_ThrowsOperationCanceledException()
|
|
{
|
|
// Arrange
|
|
var itemId = Guid.NewGuid();
|
|
var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<OperationCanceledException>(
|
|
async () => await _repository.DeleteItemAsync(new[] { itemId }, cts.Token)
|
|
);
|
|
}
|