# 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
