Skip to content

DePasqualeOrg/swift-filelock

Repository files navigation

Swift FileLock

A Swift port of Python's filelock library, providing file-based locking using flock(2).

Features

  • Process-safe file locking via flock(2)
  • Safe concurrent access from multiple instances - all FileLock instances for the same path share state
  • Reentrant locking within async task hierarchies
  • Automatic lock release on scope exit or error
  • Configurable retry behavior, timeouts, and blocking mode
  • Cross-platform: macOS, iOS, and Linux

Motivation

A naive flock(2) implementation has a subtle but critical bug: flock() locks are per file descriptor, not per file. If multiple parts of your code each create their own lock instance for the same path, they each open their own file descriptor, and each can acquire the "lock" simultaneously—completely defeating the purpose.

This library solves this by using a singleton pattern with shared context: all FileLock instances pointing to the same path share a single FileLockContext that manages the underlying file descriptor. This ensures true mutual exclusion even when locks are created independently across your codebase.

// These two locks coordinate correctly, even though they're separate instances
let lockPath = URL(filePath: "/tmp/resource.lock")

// Task 1
Task {
    let lock1 = await FileLock(lockPath: lockPath)
    try await lock1.withLock {
        // Has exclusive access
    }
}

// Task 2 - will wait for Task 1 to release
Task {
    let lock2 = await FileLock(lockPath: lockPath)
    try await lock2.withLock {
        // Properly waits, then gets exclusive access
    }
}

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/DePasqualeOrg/swift-filelock", from: "0.1.0"),
]

Usage

Basic locking

import FileLock

let lock = await FileLock(lockPath: URL(filePath: "/tmp/myfile.lock"))

try await lock.withLock {
    // Exclusive access to the protected resource
    try data.write(to: targetPath)
}

Custom timeout

// Wait up to 5 minutes (300 retries × 1 second)
let lock = await FileLock(lockPath: lockPath, maxRetries: 300, retryDelay: 1.0)

// Wait indefinitely
let lock = await FileLock(lockPath: lockPath, maxRetries: nil)

Non-blocking mode

// Fail immediately if lock is held
let lock = await FileLock(lockPath: lockPath, blocking: false)

do {
    try await lock.withLock {
        // Exclusive access
    }
} catch FileLockError.acquisitionFailed {
    // Lock was held by another process
}

// Or override per-call
let lock = await FileLock(lockPath: lockPath) // blocking by default
try await lock.withLock(blocking: false) {
    // Fails immediately if unavailable
}

Nested/reentrant locking

Locks are reentrant within the same async task hierarchy:

let lock = await FileLock(lockPath: lockPath)

try await lock.withLock {
    // Outer lock acquired
    try await lock.withLock {
        // Inner lock succeeds (same task owns outer lock)
    }
}