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

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