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.
342 lines
9.4 KiB
Markdown
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
|