{"record":{"id":"d82d7ec4326e97dc","repo":"larksuite/cli","slug":"w-s","errorCode":null,"errorMessage":"%w: %s","messagePattern":"%w: %s","errorType":"exception","errorClass":"ErrHeld","httpStatus":null,"severity":"error","filePath":"internal/lockfile/lockfile.go","lineNumber":50,"sourceCode":"// ForSubscribe sanitises appID against path traversal before forming the lock filename.\nfunc ForSubscribe(appID string) (*LockFile, error) {\n\tif appID == \"\" {\n\t\treturn nil, fmt.Errorf(\"app ID must not be empty\")\n\t}\n\tdir := filepath.Join(core.GetConfigDir(), \"locks\")\n\tif err := vfs.MkdirAll(dir, 0700); err != nil {\n\t\treturn nil, fmt.Errorf(\"create lock dir: %w\", err)\n\t}\n\tsafe := safeIDChars.ReplaceAllString(appID, \"_\")\n\tname := filepath.Base(fmt.Sprintf(\"subscribe_%s.lock\", safe))\n\tpath := filepath.Join(dir, name)\n\treturn New(path), nil\n}\n\n// TryLock acquires an exclusive non-blocking lock; auto-released on process exit.\nfunc (l *LockFile) TryLock() error {\n\tif l.file != nil {\n\t\treturn fmt.Errorf(\"%w: %s\", ErrHeld, l.path)\n\t}\n\tf, err := vfs.OpenFile(l.path, os.O_CREATE|os.O_RDWR, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open lock file: %w\", err)\n\t}\n\tif err := tryLockFile(f); err != nil {\n\t\tf.Close()\n\t\treturn err\n\t}\n\tl.file = f\n\treturn nil\n}\n\n// Unlock keeps the file on disk to avoid inode-reuse races between unlock and competing open+flock.\nfunc (l *LockFile) Unlock() error {\n\tif l.file == nil {\n\t\treturn nil\n\t}","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/lockfile/lockfile.go#L32-L68","documentation":"TryLock refuses to re-lock a LockFile instance that already holds a lock, wrapping the ErrHeld sentinel with the lock path. ErrHeld is the package's retryable-contention marker; callers use errors.Is to distinguish contention (lock held) from real failures.","triggerScenarios":"Calling TryLock() twice on the same *LockFile without an intervening Unlock(), or reusing a LockFile struct whose lock was acquired in a prior iteration.","commonSituations":"Retry loops that forget to Unlock before the next TryLock; shared LockFile handed to two code paths; forgot release after a long-lived subscribe loop restart.","solutions":["Call Unlock() before attempting TryLock again on the same instance.","Create a fresh LockFile via lockfile.New/ForSubscribe for each acquisition scope.","Structure code as acquire/deferred-Unlock so re-entry cannot happen.","If you need contention detection against other processes, note TryLock already returns ErrHeld from tryLockFile; branch on errors.Is(err, ErrHeld)."],"exampleFix":"// before\nif err := lf.TryLock(); err != nil { ... }\nif err := lf.TryLock(); err != nil { ... } // second call errors\n// after\nif err := lf.TryLock(); err != nil { ... }\nlf.Unlock()\nif err := lf.TryLock(); err != nil { ... }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := lf.TryLock(); err != nil {\n    if errors.Is(err, lockfile.ErrHeld) {\n        lf.Unlock() // or skip: already locked by this instance\n    }\n}","preventionTips":["Pair every TryLock with a deferred Unlock","Never reuse a LockFile across acquisition scopes; create a new one","Wrap lock/unlock in acquire/release helpers to prevent double TryLock"],"tags":["file-locking","concurrency","go"],"backgroundTag":"file-lock-contention","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}