Files
pgsql-jellyfin/docs/ASYNC_MIGRATION_PLAN.md
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

342 lines
9.4 KiB
Markdown

# 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:
```csharp
// 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
```csharp
// BEFORE
context.SaveChanges();
transaction.Commit();
// AFTER
await context.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
```
##### 2. ExecuteDelete → ExecuteDeleteAsync
```csharp
// BEFORE
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
// AFTER
await context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken);
```
##### 3. ExecuteUpdate → ExecuteUpdateAsync
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
// BEFORE
if (context.BaseItems.Any(x => x.Id == id))
// AFTER
if (await context.BaseItems.AnyAsync(x => x.Id == id, cancellationToken))
```
##### 7. Count → CountAsync
```csharp
// 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:
```csharp
// 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:
```csharp
// 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:
```csharp
// 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
```powershell
# 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
```regex
# 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
```csharp
// 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:
```json
{
"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
- [EF Core Async Guidance](https://learn.microsoft.com/en-us/ef/core/miscellaneous/async)
- [Npgsql Multiplexing](https://www.npgsql.org/doc/connection-string-parameters.html#multiplexing)
- [ASP.NET Core Async Best Practices](https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices)
## ⚠️ 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