Skip to content

Feature Request: Implement File Locking Mechanism #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ThisaraWeerakoon opened this issue Apr 17, 2025 · 3 comments · May be fixed by #255
Open

Feature Request: Implement File Locking Mechanism #254

ThisaraWeerakoon opened this issue Apr 17, 2025 · 3 comments · May be fixed by #255
Assignees

Comments

@ThisaraWeerakoon
Copy link
Contributor

Feature Request: Implement File Locking Mechanism

Problem

Currently, when multiple processes or application instances interact concurrently with the same file resource via vfs (especially on shared backends like OS, SFTP, or potentially cloud storage depending on usage patterns), there's no standard mechanism provided by the vfs interfaces to prevent race conditions or ensure exclusive access during critical operations (e.g., read-modify-write cycles). This can lead to data corruption or unpredictable application behavior.

Proposal

I propose adding a file locking mechanism to the vfs.File interface. This could take the form of methods like:

  • Lock() error: Acquires an exclusive lock (blocking).
  • TryLock() (bool, error): Attempts to acquire an exclusive lock (non-blocking).
  • RLock() error: Acquires a shared/read lock (blocking).
  • TryRLock() (bool, error): Attempts to acquire a shared/read lock (non-blocking).
  • Unlock() error: Releases the lock held by the file handle.

Considerations

  1. Backend Support: File locking semantics and capabilities vary significantly across backends (OS, SFTP, S3, GCS, Azure, FTP, mem).
    • How should backends that don't natively support locking handle these methods? (e.g., return a specific ErrNotSupported?)
    • OS/SFTP might map reasonably well to flock or fcntl.
    • Cloud storage might require different strategies (e.g., using leases/metadata, or potentially being unsupported for standard locking).
    • The mem backend could implement simple mutex-based locking.
  2. Lock Type: Primarily advisory locks seem most feasible across different systems. Mandatory locking is often OS-specific and harder to abstract.
  3. Scope: Locks would typically be associated with the vfs.File handle/instance.

Question

Is there any existing functionality or recommended pattern within c2fo/vfs to handle file locking or achieve exclusive access currently? I couldn't find explicit mentions of a locking API in the documentation or recent issues.

Implementing such a feature, even if only for a subset of backends initially, would significantly enhance the robustness of applications using c2fo/vfs in concurrent environments.

Thanks for considering!

@funkyshu
Copy link
Member

Can you provide a real-world example of when this might occur? In the 8yrs of this project, I've honestly never run into this issue. My initial gut check says this might be beyond the scope of this project. Are there other ways to address with without adding on an already bloated interface?

@ThisaraWeerakoon
Copy link
Contributor Author

Thanks for your response and for maintaining this project for so long!

To provide a concrete example, we're currently building a mediation engine in Go. A key component of this involves implementing a file listener. This listener is designed to monitor a specific location within a file system at a defined polling interval and perform basic file operations like moving or deleting files based on certain conditions.

Within a single instance of our mediation engine, even with its multi-threaded nature, we could potentially encounter scenarios where concurrent operations attempt to access the same file simultaneously while it's being processed. While we might be able to implement concurrency control mechanisms within a single process (e.g., using Go's sync package), the challenge becomes more significant in a distributed environment.

Imagine a deployment scenario where we have multiple instances of this file listener running across several machines, all monitoring the same file location. In this case, the different processes are entirely unaware of any concurrency controls implemented within a single instance. Without a file locking mechanism at the vfs level, we could easily run into race conditions where multiple listeners try to process the same file concurrently, leading to potential data corruption or unexpected behavior during file operations like move or delete.

Therefore, while a single process might be manageable with internal concurrency controls, the need for a locking mechanism becomes crucial when dealing with distributed systems where multiple independent processes are interacting with the same file resources managed by vfs. This is where the proposed Lock and RLock methods on the vfs.File interface would provide a standardized way to ensure exclusive or shared access across these distributed instances, regardless of the underlying backend.

@funkyshu
Copy link
Member

Thank you for the detailed example. I understand the distributed file processing scenario you're describing. While we could add locking to the core vfs interface, I'd like to propose an alternative approach that might better suit your needs.

I've made a first-pass at a lockfile utility in the contrib/lockfile package in the following branch that implements advisory file locking using companion .lock files. This approach:

  1. Works across all vfs backends (local, S3, GCS, SFTP, etc.)
  2. Is portable and backend-agnostic
  3. Includes features like TTL, stale lock detection, and owner identification

I chose this approach over adding native locking to the vfs.File interface for several reasons:

  1. Backend Compatibility: Native file locking varies significantly across backends. While OS and SFTP might support flock/fcntl, cloud storage backends (S3, GCS, Azure) have no native locking mechanism. Adding locking to the interface would either:

    • Force backends to return ErrNotSupported (poor user experience)
    • Require complex emulation in cloud backends
    • Create inconsistent behavior across backends
  2. Interface Bloat: The vfs.File interface is already quite large. Adding multiple locking methods (Lock, TryLock, RLock, TryRLock) would further complicate it and increase the implementation burden on all backends.

  3. Implementation Consistency: By keeping locking as a separate utility, we ensure consistent behavior across all backends and avoid the complexity of each backend implementing its own locking strategy.

However, for your specific distributed scenario, I'd recommend considering Redis or another distributed locking service instead, such as redsync. The lockfile utility has some important caveats:

  • It's advisory locking (requires all processes to respect the lock)
  • Network partitions or process crashes may leave stale locks
  • Not ideal for high-contention scenarios
  • S3's eventual consistency may affect lock visibility
  • You'll need to filter out .lock and .tmp files in your file listener to avoid processing lock files

If you still want to use the lockfile approach, here's a quick example:

// In your file listener
files, err := location.List()
if err != nil {
    log.Fatal(err)
}

for _, f := range files {
    // Skip lock files
    if strings.HasSuffix(f.Name(), ".lock") || strings.HasSuffix(f.Name(), ".tmp") {
        continue
    }

    // Process each file with locking
    err := lockfile.WithLock(f, func(f vfs.File) error {
        // Process file with 5-minute timeout
        return processFile(f)
    }, lockfile.WithTTL(5*time.Minute))

    if err != nil {
        if errors.Is(err, lockfile.ErrLockAlreadyHeld) {
            log.Printf("File %s is already being processed", f.Name())
            continue
        }
        log.Printf("Error processing %s: %v", f.Name(), err)
    }
}

You can find the complete documentation and more examples in the README of the lockfile branch.

Would you like me to elaborate on any of these points or discuss alternative approaches for your specific use case?

@funkyshu funkyshu linked a pull request Apr 21, 2025 that will close this issue
@funkyshu funkyshu self-assigned this Apr 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants