gofrs / flock

Thread-safe file locking library in Go
https://pkg.go.dev/github.com/gofrs/flock
BSD 3-Clause "New" or "Revised" License
578 stars 66 forks source link

Unexpected behaviour: file created when acquiring lock #59

Closed Integralist closed 4 months ago

Integralist commented 2 years ago

👋🏻

I'm not sure if this is expected behaviour for this package or not, it was unexpected to me at least, but if I pass a file path that doesn't exist, I will successfully be able to acquire a lock and then when reading the contents of the file it returns empty content.

The reason this was unexpected was that in my project I have a test that validates an error is returned if a path fails to be read, but it was failing because it expected an error and now (since implementing this package) it was no longer getting an error because the file did exist (and for my use case that was unexpected).

An example of what I'm describing is:

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/gofrs/flock"
)

const (
    // FileLockTimeout is the amount of time to wait trying to acquire a lock.
    FileLockTimeout = 10 * time.Second

    // FileLockRetryDelay is the mount of time to wait before attempting a retry.
    FileLockRetryDelay = 500 * time.Millisecond
)

func main() {
    filename := "does_not_exist"

    fileLock := flock.New(filename)
    lockCtx, cancel := context.WithTimeout(context.Background(), FileLockTimeout)
    defer cancel()

    locked, err := fileLock.TryLockContext(lockCtx, FileLockRetryDelay)
    if err != nil {
        fmt.Printf("error acquiring file lock for '%s': %v", fileLock.Path(), err)
        return
    }

    if locked {
        fmt.Println("got a lock", fileLock.Path())

        fi, err := os.Stat(fileLock.Path())
        if err != nil {
            fmt.Printf("error stating file '%s': %v", fileLock.Path(), err)
            return
        }
        fmt.Printf("%+v\n", fi) // seems the file now exists

        data, err := os.ReadFile(fileLock.Path())
        if err != nil {
            fmt.Printf("error reading file '%s': %v", fileLock.Path(), err)
            return
        }
        fmt.Printf("file content: %+v\n", string(data)) // empty file content

        if err := fileLock.Unlock(); err != nil {
            fmt.Printf("error releasing file lock for '%s': %v", fileLock.Path(), err)
        }
    }
}

The solution in my case was to add a .Stat() call up front before calling flock.New() but I wanted to be sure this wasn't a bug. Maybe it's I just don't understand how file locks are supposed to work (very likely).

Any feedback/guidance appreciated.

Thanks!

jnoxon commented 1 year ago

I came here because of this. This behavior has been causing corruption in a production app I maintain.

I came across this fork which corrects the behavior: https://github.com/AODocs-Dev/flock/commit/3c69484ba0dfa6a36bffb67e762ebc2dec1e0d7d

I don't think this project is maintained, but if this affects you, you can try this:

go mod edit -replace github.com/gofrs/flock@latest=github.com/AODocs-Dev/flock@3c69484
go mod tidy
ldez commented 4 months ago

I extended the constructor, now you can set the flag with an option:

_ = flock.New("example", flock.SetFlag(os.O_CREATE|os.O_RDWR)))