withExclusiveLock<T> function
Run body with nothing else holding lockPath.
Implemented as an atomic symlink creation rather than with
RandomAccessFile.lock. POSIX record locks are owned by the process, so
a second request from the same process succeeds — which would silently
break whenever dart test runs suites as isolates in one process.
symlink(2) fails when the name exists, whoever asks, so the filesystem
itself provides the exclusion.
Implementation
Future<T> withExclusiveLock<T>(
String lockPath,
Future<T> Function() body, {
Duration staleAfter = defaultLockStaleAfter,
Duration timeout = _defaultTimeout,
Duration retryInterval = _defaultRetryInterval,
DateTime Function() now = DateTime.now,
Future<void> Function(Duration) sleep = _delay,
}) async {
final link = Link(lockPath);
link.parent.createSync(recursive: true);
final deadline = now().add(timeout);
while (true) {
if (_tryCreate(link, now())) {
try {
return await body();
} finally {
_release(link);
}
}
// Checked on every turn of the loop, not only on the waiting branch.
// The stale branch retries immediately so a crashed holder is recovered
// from fast — which means that without this it would spin without end
// whenever the stale lock cannot actually be deleted. Hanging is the one
// failure this library exists to remove, so no path may be unbounded.
if (!now().isBefore(deadline)) {
throw LockTimeout(lockPath: lockPath, waited: timeout);
}
final held = _readTarget(link);
if (held == null || _isStale(held, now(), staleAfter)) {
breakStaleLockIfUnchanged(link, held);
} else {
await sleep(retryInterval);
}
}
}