Files
pgsql-jellyfin/docs/ASYNC_MIGRATION_PLAN.md
T
wjones 534b0cde91 Ensure initial user exists before startup wizard completes
Add logic to initialize at least one user if the startup wizard is not completed, both during application startup and in the startup user API. After user creation, reload the user entity with all related navigation properties to ensure the in-memory user object is fully populated. Also update build and publish metadata files.
2026-02-23 15:42:19 -05:00

9.4 KiB

Jellyfin Async Migration Plan

🎯 Objective

Convert Jellyfin database operations from synchronous to asynchronous to enable PostgreSQL multiplexing and improve performance.

📋 Migration Phases

Phase 1: Repository Interfaces (Breaking Changes)

Estimated Effort: 2-3 weeks

Files to Update:

  • MediaBrowser.Controller\Persistence\IItemRepository.cs
  • MediaBrowser.Controller\Library\IUserManager.cs
  • MediaBrowser.Controller\Persistence\IUserDataRepository.cs
  • All other repository interfaces

Changes:

// BEFORE
void DeleteItem(params IReadOnlyList<Guid> ids);
void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken);
BaseItem RetrieveItem(Guid id);
QueryResult<BaseItem> GetItems(InternalItemsQuery filter);

// AFTER
Task DeleteItemAsync(IReadOnlyList<Guid> ids, CancellationToken cancellationToken = default);
Task SaveItemsAsync(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken = default);
Task<BaseItem?> RetrieveItemAsync(Guid id, CancellationToken cancellationToken = default);
Task<QueryResult<BaseItem>> GetItemsAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default);

Phase 2: Repository Implementations

Estimated Effort: 3-4 weeks

Files to Update:

  • Jellyfin.Server.Implementations\Item\BaseItemRepository.cs
  • Jellyfin.Server.Implementations\Users\UserManager.cs
  • All repository implementation classes

Key Changes:

1. SaveChanges → SaveChangesAsync
// BEFORE
context.SaveChanges();
transaction.Commit();

// AFTER
await context.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
2. ExecuteDelete → ExecuteDeleteAsync
// BEFORE
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();

// AFTER
await context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
    .ExecuteDeleteAsync(cancellationToken);
3. ExecuteUpdate → ExecuteUpdateAsync
// BEFORE
context.UserData.ExecuteUpdate(e => e
    .SetProperty(f => f.RetentionDate, date));

// AFTER
await context.UserData.ExecuteUpdateAsync(e => e
    .SetProperty(f => f.RetentionDate, date), cancellationToken);
4. ToArray/ToList → ToArrayAsync/ToListAsync
// BEFORE
var items = context.BaseItems.Where(x => x.Id == id).ToArray();

// AFTER
var items = await context.BaseItems
    .Where(x => x.Id == id)
    .ToArrayAsync(cancellationToken);
5. FirstOrDefault → FirstOrDefaultAsync
// BEFORE
var item = context.BaseItems.FirstOrDefault(x => x.Id == id);

// AFTER
var item = await context.BaseItems
    .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
6. Any → AnyAsync
// BEFORE
if (context.BaseItems.Any(x => x.Id == id))

// AFTER
if (await context.BaseItems.AnyAsync(x => x.Id == id, cancellationToken))
7. Count → CountAsync
// BEFORE
var count = context.BaseItems.Count();

// AFTER
var count = await context.BaseItems.CountAsync(cancellationToken);

Phase 3: Service Layer

Estimated Effort: 4-6 weeks

Files to Update:

  • Emby.Server.Implementations\Library\LibraryManager.cs
  • Emby.Server.Implementations\Library\MediaSourceManager.cs
  • All service classes that call repositories

Pattern:

// BEFORE
public BaseItem GetItem(Guid id)
{
    return _itemRepository.RetrieveItem(id);
}

// AFTER
public async Task<BaseItem?> GetItemAsync(Guid id, CancellationToken cancellationToken = default)
{
    return await _itemRepository.RetrieveItemAsync(id, cancellationToken);
}

Phase 4: API Controllers

Estimated Effort: 3-4 weeks

Files to Update:

  • Jellyfin.Api\Controllers\ItemsController.cs
  • Jellyfin.Api\Controllers\UsersController.cs
  • All API controller classes

Pattern:

// BEFORE
[HttpGet("{id}")]
public ActionResult<BaseItemDto> GetItem([FromRoute] Guid id)
{
    var item = _libraryManager.GetItem(id);
    return item is null ? NotFound() : Ok(item);
}

// AFTER
[HttpGet("{id}")]
public async Task<ActionResult<BaseItemDto>> GetItem(
    [FromRoute] Guid id, 
    CancellationToken cancellationToken)
{
    var item = await _libraryManager.GetItemAsync(id, cancellationToken);
    return item is null ? NotFound() : Ok(item);
}

Phase 5: Background Services & Scheduled Tasks

Estimated Effort: 2-3 weeks

Files to Update:

  • Emby.Server.Implementations\ScheduledTasks\*
  • Background services using repositories

Pattern:

// Ensure ExecuteAsync uses await for all database operations
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
    await _itemRepository.UpdateInheritedValuesAsync(cancellationToken);
}

🧪 Testing Strategy

1. Unit Tests

  • Update all repository unit tests to async
  • Add tests for cancellation token handling
  • Verify no synchronous database calls remain

2. Integration Tests

  • Test full request pipeline (API → Service → Repository → Database)
  • Verify async behavior under load
  • Test cancellation scenarios

3. Performance Testing

  • Compare performance before/after migration
  • Test with multiplexing enabled vs disabled
  • Monitor connection pool usage

📦 Migration Tools & Utilities

Automated Detection Script

# Find synchronous EF Core operations
Get-ChildItem -Recurse -Filter *.cs | 
    Select-String -Pattern "\.ToList\(\)|\.ToArray\(\)|\.FirstOrDefault\(\)|\.SaveChanges\(\)|\.ExecuteDelete\(\)|\.ExecuteUpdate\(\)|\.Any\(\)|\.Count\(\)" |
    Where-Object { $_.Line -notmatch "Async" } |
    Select-Object Path, LineNumber, Line

Regex Patterns for Find/Replace

# SaveChanges → SaveChangesAsync
Find: \.SaveChanges\(\);
Replace: await .SaveChangesAsync(cancellationToken);

# ToArray → ToArrayAsync
Find: \.ToArray\(\);
Replace: await .ToArrayAsync(cancellationToken);

# FirstOrDefault → FirstOrDefaultAsync
Find: \.FirstOrDefault\(
Replace: await .FirstOrDefaultAsync(

🚧 Breaking Changes & Compatibility

API Versioning

  • Consider maintaining v1 API with sync methods
  • Introduce v2 API with async methods
  • Deprecation timeline for v1

Plugin Compatibility

  • Plugins using sync methods will break
  • Provide migration guide for plugin developers
  • Consider compatibility shims for transition period

📊 Effort Estimation Summary

Phase Estimated Time Risk Level Priority
Phase 1: Interfaces 2-3 weeks High Critical
Phase 2: Repositories 3-4 weeks High Critical
Phase 3: Services 4-6 weeks Medium High
Phase 4: Controllers 3-4 weeks Low High
Phase 5: Background 2-3 weeks Medium Medium
Testing 2-3 weeks High Critical
Total 16-23 weeks - -

Success Criteria

  1. Zero synchronous database operations in hot paths
  2. All repository methods are async
  3. All API endpoints are async
  4. PostgreSQL multiplexing can be enabled without errors
  5. Performance improvements measurable
  6. All tests passing
  7. No increase in API latency

🎯 Quick Win: Start Small

Proof of Concept (1-2 weeks)

Convert a single, isolated feature to async:

Recommended Starting Point: Activity Log Repository

  • Relatively simple
  • Limited dependencies
  • Easy to test
  • Low risk
// Convert ActivityLogRepository
public interface IActivityLogRepository
{
    Task<ActivityLog?> GetAsync(int id, CancellationToken cancellationToken = default);
    Task<QueryResult<ActivityLog>> GetAllAsync(
        ActivityLogFilter filter, 
        CancellationToken cancellationToken = default);
    Task CreateAsync(ActivityLog entry, CancellationToken cancellationToken = default);
    Task DeleteAsync(int id, CancellationToken cancellationToken = default);
}

🔧 PostgreSQL Multiplexing Benefits

Once fully async, enable multiplexing:

{
  "Database": {
    "CustomProviderOptions": {
      "Options": [
        { "Key": "multiplexing", "Value": "true" },
        { "Key": "max-pool-size", "Value": "50" }
      ]
    }
  }
}

Expected Benefits:

  • 20-40% reduction in connection pool usage
  • Better throughput under high concurrency
  • Lower memory footprint
  • Improved scalability

📚 Resources

⚠️ Risks & Mitigation

Risk Impact Mitigation
Breaking plugin API High Version API, provide migration guide
Deadlocks from improper async High Code review, static analysis
Performance regression Medium Extensive performance testing
Testing gaps High Increase test coverage before migration
Team learning curve Medium Training sessions, pair programming

🎓 Team Onboarding

Required Knowledge

  1. C# async/await patterns
  2. Task-based asynchronous programming
  3. CancellationToken usage
  4. EF Core async methods
  5. Async enumeration (IAsyncEnumerable)

Training Resources

  • Microsoft Learn: Async Programming
  • Jellyfin async migration guide (to be created)
  • Code review checklist
  • Pair programming sessions

Document Version: 1.0
Last Updated: 2025-01-15
Owner: Database Team
Status: Planning