Files
pgsql-jellyfin/ASYNC_CONVERSION_EXAMPLE.cs
T
wjones 86883cd5c6 Refactor PostgreSQL provider: multi-schema & async prep
- 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.
2026-02-23 09:38:22 -05:00

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)
);
}