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.
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user