Back to Beads

Exclusive Lock Protocol

docs/EXCLUSIVE_LOCK.md

1.0.36.7 KB
Original Source

Exclusive Lock Protocol

The exclusive lock protocol allows external tools to claim exclusive management of a beads database, preventing the Dolt server from interfering with their operations.

Use Cases

  • Deterministic execution systems (e.g., VibeCoder) that need full control over database state
  • CI/CD pipelines that perform atomic issue updates without server interference
  • Custom automation tools that manage their own git sync workflow

How It Works

Lock File Format

The lock file is located at .beads/.exclusive-lock and contains JSON:

json
{
  "holder": "vc-executor",
  "pid": 12345,
  "hostname": "dev-machine",
  "started_at": "2025-10-25T12:00:00Z",
  "version": "1.0.0"
}

Fields:

  • holder (string, required): Name of the tool holding the lock (e.g., "vc-executor", "ci-runner")
  • pid (int, required): Process ID of the lock holder
  • hostname (string, required): Hostname where the process is running
  • started_at (RFC3339 timestamp, required): When the lock was acquired
  • version (string, optional): Version of the lock holder

Server Behavior

The Dolt server checks for exclusive locks at the start of each sync cycle:

  1. No lock file: Server proceeds normally with sync operations
  2. Valid lock (process alive): Server skips all operations for this database
  3. Stale lock (process dead): Server removes the lock and proceeds
  4. Malformed lock: Server fails safe and skips the database

Stale Lock Detection

A lock is considered stale if:

  • The hostname matches the current machine (case-insensitive) AND
  • The PID does not exist on the local system (returns ESRCH)

Important: The server only removes locks when it can definitively determine the process is dead (ESRCH error). If the server lacks permission to signal a PID (EPERM), it treats the lock as valid and skips the database. This fail-safe approach prevents accidentally removing locks owned by other users.

Remote locks (different hostname) are always assumed to be valid since the server cannot verify remote processes.

When a stale lock is successfully removed, the server logs: Removed stale lock (holder-name), proceeding with sync

Usage Examples

Creating a Lock (Go)

go
import (
    "encoding/json"
    "os"
    "path/filepath"
    "github.com/steveyegge/beads/internal/types"
)

func acquireLock(beadsDir, holder, version string) error {
    lock, err := types.NewExclusiveLock(holder, version)
    if err != nil {
        return err
    }

    data, err := json.MarshalIndent(lock, "", "  ")
    if err != nil {
        return err
    }

    lockPath := filepath.Join(beadsDir, ".exclusive-lock")
    return os.WriteFile(lockPath, data, 0644)
}

Releasing a Lock (Go)

go
func releaseLock(beadsDir string) error {
    lockPath := filepath.Join(beadsDir, ".exclusive-lock")
    return os.Remove(lockPath)
}

Creating a Lock (Shell)

bash
#!/bin/bash
BEADS_DIR=".beads"
LOCK_FILE="$BEADS_DIR/.exclusive-lock"

# Create lock
cat > "$LOCK_FILE" <<EOF
{
  "holder": "my-tool",
  "pid": $$,
  "hostname": "$(hostname)",
  "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "version": "1.0.0"
}
EOF

# Do work...
bd create "My issue" -p 1
bd update bd-42 --claim

# Release lock
rm "$LOCK_FILE"

Always use cleanup handlers to ensure locks are released:

go
func main() {
    beadsDir := ".beads"
    
    // Acquire lock
    if err := acquireLock(beadsDir, "my-tool", "1.0.0"); err != nil {
        log.Fatal(err)
    }
    
    // Ensure lock is released on exit
    defer func() {
        if err := releaseLock(beadsDir); err != nil {
            log.Printf("Warning: failed to release lock: %v", err)
        }
    }()
    
    // Do work with beads database...
}

Edge Cases and Limitations

Multiple Writers Without Server

The exclusive lock protocol only prevents Dolt server interference. It does NOT provide:

  • ❌ Mutual exclusion between multiple external tools
  • ❌ Transaction isolation or ACID guarantees
  • ❌ Protection against direct file system manipulation

If you need coordination between multiple tools, implement your own locking mechanism.

Git Worktrees

Dolt handles git worktrees natively. The exclusive lock protocol is separate from worktree support.

Remote Hosts

Locks from remote hosts are always assumed valid because the server cannot verify remote PIDs. This means:

  • Stale locks from remote hosts will not be automatically cleaned up
  • You must manually remove stale remote locks

Lock File Corruption

If the lock file becomes corrupted (invalid JSON), the server fails safe and skips the database. You must manually fix or remove the lock file.

Server Logging

The Dolt server logs lock-related events:

Skipping database (locked by vc-executor)
Removed stale lock (vc-executor), proceeding with sync
Skipping database (lock check failed: malformed lock file: unexpected EOF)

Check server logs (.beads/dolt/sql-server.log) to troubleshoot lock issues.

Note: The server checks for locks at the start of each sync cycle. If a lock is created during a sync cycle, that cycle will complete, but subsequent cycles will skip the database.

Testing Your Integration

  1. Start the Dolt server: bd dolt start
  2. Create a lock: Use your tool to create .beads/.exclusive-lock
  3. Verify server skips: Check server logs for "Skipping database" message
  4. Release lock: Remove .beads/.exclusive-lock
  5. Verify server resumes: Check server logs for normal sync cycle

Security Considerations

  • Lock files are not secure. Any process can create, modify, or delete them.
  • PID reuse could theoretically cause issues (very rare, especially with hostname check)
  • This is a cooperative protocol, not a security mechanism

API Reference

Go Types

go
// ExclusiveLock represents the lock file format
type ExclusiveLock struct {
    Holder    string    `json:"holder"`
    PID       int       `json:"pid"`
    Hostname  string    `json:"hostname"`
    StartedAt time.Time `json:"started_at"`
    Version   string    `json:"version"`
}

// NewExclusiveLock creates a lock for the current process
func NewExclusiveLock(holder, version string) (*ExclusiveLock, error)

// Validate checks if the lock has valid field values
func (e *ExclusiveLock) Validate() error

// ShouldSkipDatabase checks if database should be skipped due to lock
func ShouldSkipDatabase(beadsDir string) (skip bool, holder string, err error)

// IsProcessAlive checks if a process is running
func IsProcessAlive(pid int, hostname string) bool

Questions?

For integration help, see:

  • AGENTS.md - General workflow guidance
  • README.md - Server configuration
  • examples/ - Sample integrations

File issues at: https://github.com/gastownhall/beads/issues