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
- Retry the operation after a short delay — transient contention (concurrent apply + revert of the same optimization) usually clears once the holder finishes
- 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
- If it persists, a lock holder likely deadlocked or the process is hung; restarting the app releases all in-process SemaphoreSlim locks
- 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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
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)