Files
pgsql-jellyfin/docs/LIBRARYMONITOR_DISPOSAL_FIX.md
T
wjones 7eb2b445cb Postgres perf, path migration, and shutdown race fixes
- 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.
2026-03-05 08:56:42 -05:00

220 lines
6.9 KiB
Markdown

# 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:
1. ✅ The `IServiceProvider` and `DbContextFactory` are disposed
2. ⏱️ A `FileRefresher` timer callback fires **after** disposal has started
3. ❌ The callback tries to access the database through `LibraryManager.FindByPath`
4. ❌ The disposed `ServiceProvider` throws `ObjectDisposedException`
### 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:
```csharp
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:
```csharp
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:
```csharp
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
1. **Pooled DbContext Factory**: The application uses `AddPooledDbContextFactory<JellyfinDbContext>()` which creates contexts from a pooled factory
2. **Service Lifetime**: The factory depends on the `IServiceProvider` which is disposed during shutdown
3. **Async Timer Callbacks**: Timer callbacks execute on thread pool threads and may outlive the disposing object
4. **Long-Lived Service**: `LibraryMonitor` is 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:**
1. Added disposal check at start of `OnTimerCallback` (line ~114)
2. Added disposal check before `ProcessPathChanges` (line ~128)
3. Added disposal check in `ProcessPathChanges` (line ~140)
4. Added `try-catch` for `ObjectDisposedException` in `ProcessPathChanges` (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
1. Start Jellyfin
2. Modify files in a monitored library folder
3. Immediately shut down Jellyfin before the file refresh timer fires
4. **Expected Before Fix:** `ObjectDisposedException` in logs
5. **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
1. **Early Exit Pattern**: Check disposal state as early as possible
2. **Double-Check Locking**: Verify disposal state after lock release
3. **Defensive Error Handling**: Catch and log disposal exceptions gracefully
4. **Lightweight Guards**: Use boolean flags instead of expensive checks
### Alternative Solutions Considered
1. **❌ CancellationToken in Timer**: Timers don't support cancellation tokens natively
2. **❌ Await Timer Completion**: Would delay shutdown unnecessarily
3. **✅ 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