- Major query performance boost: replaced correlated subqueries with DistinctBy() in BaseItemRepository, added episode deduplication index, and increased default command timeout to 120s. - Fixed LibraryMonitor shutdown race: added disposal checks and exception handling in FileRefresher to prevent ObjectDisposedException. - Added PowerShell and SQL utilities for fixing Linux paths in config files and database; new scripts for WebDir and path migration. - Auto-fix for WebDir in startup.json on load; new helper scripts and docs. - Added automated weekly DB performance monitoring scripts and reporting. - Expanded documentation and README for all fixes, scripts, and migration steps. - Updated SQL scripts for index creation and diagnostics; improved output handling. - Updated db-config defaults and added new diagnostic/report files.
6.9 KiB
ObjectDisposedException in LibraryMonitor Fix
Issue
Error: System.ObjectDisposedException: Cannot access a disposed object. Object name: 'IServiceProvider'
Location: Emby.Server.Implementations.IO.LibraryMonitor → FileRefresher → OnTimerCallback
Stack Trace:
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ThrowHelper.ThrowObjectDisposedException()
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at Microsoft.EntityFrameworkCore.Internal.DbContextPool
at Jellyfin.Server.Implementations.Item.BaseItemRepository.GetItemListAsync
at Emby.Server.Implementations.Library.LibraryManager.FindByPath
at Emby.Server.Implementations.IO.FileRefresher.GetAffectedBaseItem
at Emby.Server.Implementations.IO.FileRefresher.ProcessPathChanges
Root Cause
The FileRefresher class uses a timer to batch file system change notifications. During application shutdown:
- ✅ The
IServiceProviderandDbContextFactoryare disposed - ⏱️ A
FileRefreshertimer callback fires after disposal has started - ❌ The callback tries to access the database through
LibraryManager.FindByPath - ❌ The disposed
ServiceProviderthrowsObjectDisposedException
Race Condition
Application Shutdown Timeline:
┌─────────────────────────────────────────────────────┐
│ T0: ServiceProvider.Dispose() called │
│ T1: LibraryMonitor.Dispose() called │
│ T2: FileRefresher.Dispose() called │
│ T3: Timer.Dispose() called │
│ T4: [RACE] Timer callback fires before cancellation │ ❌
│ T5: Callback tries to access DbContext │ ❌
│ T6: ObjectDisposedException thrown │ ❌
└─────────────────────────────────────────────────────┘
The timer callback can fire in the window between FileRefresher.Dispose() being called and the timer actually being disposed/cancelled.
Solution
Fix 1: Early Disposal Check in Timer Callback
Added disposal check at the start of OnTimerCallback to exit immediately if disposed:
private void OnTimerCallback(object? state)
{
// Check if disposed before processing
if (_disposed)
{
return;
}
// ... rest of the method
}
Fix 2: Double-Check After Timer Disposal
Added a second check after the timer is disposed but before accessing services:
DisposeTimer();
Completed?.Invoke(this, EventArgs.Empty);
// Double-check disposal after timer is disposed
if (_disposed)
{
return;
}
try
{
ProcessPathChanges(paths);
}
Fix 3: Catch ObjectDisposedException in ProcessPathChanges
Added explicit handling for ObjectDisposedException when accessing services:
private void ProcessPathChanges(List<string> paths)
{
if (_disposed)
{
return;
}
IEnumerable<BaseItem> itemsToRefresh;
try
{
itemsToRefresh = paths
.Distinct()
.Select(GetAffectedBaseItem) // ← May throw ObjectDisposedException
.Where(item => item is not null)
.DistinctBy(x => x!.Id)!;
}
catch (ObjectDisposedException)
{
// Service provider disposed during shutdown, skip processing
_logger.LogDebug("Service provider disposed during path change processing, skipping refresh");
return;
}
// ... process items
}
Technical Details
Why This Happens
- Pooled DbContext Factory: The application uses
AddPooledDbContextFactory<JellyfinDbContext>()which creates contexts from a pooled factory - Service Lifetime: The factory depends on the
IServiceProviderwhich is disposed during shutdown - Async Timer Callbacks: Timer callbacks execute on thread pool threads and may outlive the disposing object
- Long-Lived Service:
LibraryMonitoris a singleton that monitors file system changes throughout the application lifetime
Service Dependency Chain
FileRefresher (IDisposable)
↓ uses
LibraryManager (Singleton)
↓ uses
BaseItemRepository (Transient)
↓ uses
IDbContextFactory<JellyfinDbContext> (Singleton)
↓ creates
JellyfinDbContext (Pooled)
↓ created from
IServiceProvider (Disposed during shutdown)
Files Modified
Emby.Server.Implementations\IO\FileRefresher.cs
Changes:
- Added disposal check at start of
OnTimerCallback(line ~114) - Added disposal check before
ProcessPathChanges(line ~128) - Added disposal check in
ProcessPathChanges(line ~140) - Added
try-catchforObjectDisposedExceptioninProcessPathChanges(line ~145-153)
Impact:
- ✅ Prevents timer callbacks from executing after disposal
- ✅ Gracefully handles race conditions during shutdown
- ✅ Reduces error noise in logs during application shutdown
- ✅ No impact on normal operation (disposal only happens during shutdown)
Testing
Reproduction Steps
- Start Jellyfin
- Modify files in a monitored library folder
- Immediately shut down Jellyfin before the file refresh timer fires
- Expected Before Fix:
ObjectDisposedExceptionin logs - Expected After Fix: Clean shutdown, no exception
Verification
- ✅ Build successful
- ✅ No breaking changes
- ✅ Disposal checks are lightweight (simple boolean check)
- ✅ Exception handling only activates during shutdown race condition
Related Issues
- Issue Type: Race condition during application shutdown
- Severity: Low (cosmetic error during shutdown, no impact on operation)
- Frequency: Intermittent (depends on timing of file changes vs shutdown)
- Impact: Error logs during shutdown, no functional impact
Prevention Strategy
Best Practices Implemented
- Early Exit Pattern: Check disposal state as early as possible
- Double-Check Locking: Verify disposal state after lock release
- Defensive Error Handling: Catch and log disposal exceptions gracefully
- Lightweight Guards: Use boolean flags instead of expensive checks
Alternative Solutions Considered
- ❌ CancellationToken in Timer: Timers don't support cancellation tokens natively
- ❌ Await Timer Completion: Would delay shutdown unnecessarily
- ✅ Disposal Flags + Try-Catch: Chosen for simplicity and effectiveness
Summary
This fix addresses a race condition during application shutdown where file system monitor timer callbacks can execute after the service provider has been disposed. The solution uses multiple layers of disposal checks and defensive exception handling to ensure clean shutdowns without error noise.
Status: ✅ Fixed
Risk: Low (shutdown-only scenario)
Testing: Build verified successful
Documentation: Complete