itsfatduck/optimizerDuck · error · TimeoutException

Timed out waiting for revert file lock

Error message

Timed out waiting for revert file lock ({0}).

What it means

Concurrency guard fired in RevertManager.AcquireFileLockAsync: the per-revert-file SemaphoreSlim (keyed by the revert-data Guid) could not be acquired within FileLockTimeoutSeconds (30s). This means another thread/process path — an apply appending revert steps, a concurrent revert, or a crashed holder that never released — is holding the lock for the same {optimizationId}.json. It is a sentinel for lock contention or a leaked lock, not corruption of the file itself.

Solutions

  1. Retry the operation after a short delay — transient contention (concurrent apply + revert of the same optimization) usually clears once the holder finishes
  2. Check for concurrent calls targeting the same optimization id (e.g., double-clicked revert or apply racing a background retry) and serialize them at the call site
  3. If it persists, a lock holder likely deadlocked or the process is hung; restarting the app releases all in-process SemaphoreSlim locks
  4. Review any recent code path that acquires the file lock without a matching finally-release
Defensive patterns

Strategy: retry

When it happens

Trigger: Thrown at optimizerDuck/Services/Revert/RevertManager.cs:589 when the library encounters an invalid state.

Common situations: See trigger scenarios.

Understand the failure class


AI-assisted analysis of itsfatduck/optimizerDuck@36acf585ae (2026-09-13). Data as JSON: /api/errors/3a9572bbf79651c6. Report an issue: GitHub.

Appendix: source

Thrown at optimizerDuck/Services/Revert/RevertManager.cs:589

                File.Delete(tmp);
                logger?.LogInformation("Removed orphaned revert temp file {Path}", tmp);
            }
            catch (Exception ex)
            {
                logger?.LogWarning(ex, "Failed to remove orphaned temp file {Path}", tmp);
            }
        }
    }

    private static async Task<SemaphoreSlim> AcquireFileLockAsync(Guid id)
    {
        var lockObj = _fileLocks.GetOrAdd(id, _ => new SemaphoreSlim(1, 1));
        if (
            !await lockObj
                .WaitAsync(TimeSpan.FromSeconds(FileLockTimeoutSeconds))
                .ConfigureAwait(false)
        )
            throw new TimeoutException(
                string.Format("Timed out waiting for revert file lock ({0}).", id)
            );

        return lockObj;
    }

    private async Task<List<(int Index, IRevertStep Step)>> LoadStepsAsync(Guid id)
    {
        var data = await LoadAsync(GetFilePath(id), _logger).ConfigureAwait(false);
        if (data == null || data.Steps.Length == 0)
            return [];

        var seenIndexes = new HashSet<int>();
        var result = new List<(int, IRevertStep)>();

        foreach (var stepData in data.Steps.Where(s => s != null))
        {
            if (stepData!.Index <= 0)

View on GitHub (pinned to 36acf585ae)