larksuite/cli · error · ErrHeld

%w: %s

Error message

%w: %s

What it means

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.

Source

Thrown at internal/lockfile/lockfile.go:50

// ForSubscribe sanitises appID against path traversal before forming the lock filename.
func ForSubscribe(appID string) (*LockFile, error) {
	if appID == "" {
		return nil, fmt.Errorf("app ID must not be empty")
	}
	dir := filepath.Join(core.GetConfigDir(), "locks")
	if err := vfs.MkdirAll(dir, 0700); err != nil {
		return nil, fmt.Errorf("create lock dir: %w", err)
	}
	safe := safeIDChars.ReplaceAllString(appID, "_")
	name := filepath.Base(fmt.Sprintf("subscribe_%s.lock", safe))
	path := filepath.Join(dir, name)
	return New(path), nil
}

// TryLock acquires an exclusive non-blocking lock; auto-released on process exit.
func (l *LockFile) TryLock() error {
	if l.file != nil {
		return fmt.Errorf("%w: %s", ErrHeld, l.path)
	}
	f, err := vfs.OpenFile(l.path, os.O_CREATE|os.O_RDWR, 0600)
	if err != nil {
		return fmt.Errorf("open lock file: %w", err)
	}
	if err := tryLockFile(f); err != nil {
		f.Close()
		return err
	}
	l.file = f
	return nil
}

// Unlock keeps the file on disk to avoid inode-reuse races between unlock and competing open+flock.
func (l *LockFile) Unlock() error {
	if l.file == nil {
		return nil
	}

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Call Unlock() before attempting TryLock again on the same instance.
  2. Create a fresh LockFile via lockfile.New/ForSubscribe for each acquisition scope.
  3. Structure code as acquire/deferred-Unlock so re-entry cannot happen.
  4. If you need contention detection against other processes, note TryLock already returns ErrHeld from tryLockFile; branch on errors.Is(err, ErrHeld).

Example fix

// before
if err := lf.TryLock(); err != nil { ... }
if err := lf.TryLock(); err != nil { ... } // second call errors
// after
if err := lf.TryLock(); err != nil { ... }
lf.Unlock()
if err := lf.TryLock(); err != nil { ... }
Defensive patterns

Strategy: try-catch

Try / catch

if err := lf.TryLock(); err != nil {
    if errors.Is(err, lockfile.ErrHeld) {
        lf.Unlock() // or skip: already locked by this instance
    }
}

Prevention

When it happens

Trigger: Calling TryLock() twice on the same *LockFile without an intervening Unlock(), or reusing a LockFile struct whose lock was acquired in a prior iteration.

Common situations: 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.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/d82d7ec4326e97dc. Report an issue: GitHub.