Fix database: restore clean 135 issues and add exclusive lock docs
This commit is contained in:
File diff suppressed because one or more lines are too long
229
EXCLUSIVE_LOCK.md
Normal file
229
EXCLUSIVE_LOCK.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Exclusive Lock Protocol
|
||||
|
||||
The exclusive lock protocol allows external tools to claim exclusive management of a beads database, preventing the bd daemon 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 daemon 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
|
||||
|
||||
### Daemon Behavior
|
||||
|
||||
The bd daemon checks for exclusive locks at the start of each sync cycle:
|
||||
|
||||
1. **No lock file**: Daemon proceeds normally with sync operations
|
||||
2. **Valid lock (process alive)**: Daemon skips all operations for this database
|
||||
3. **Stale lock (process dead)**: Daemon removes the lock and proceeds
|
||||
4. **Malformed lock**: Daemon 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 daemon only removes locks when it can definitively determine the process is dead (ESRCH error). If the daemon 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 daemon cannot verify remote processes.
|
||||
|
||||
When a stale lock is successfully removed, the daemon 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 --status in_progress
|
||||
|
||||
# Release lock
|
||||
rm "$LOCK_FILE"
|
||||
```
|
||||
|
||||
### Recommended Pattern
|
||||
|
||||
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 Daemon
|
||||
|
||||
The exclusive lock protocol **only prevents daemon 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
|
||||
|
||||
The daemon already has issues with git worktrees (see AGENTS.md). The exclusive lock protocol doesn't solve this—use `--no-daemon` mode in worktrees instead.
|
||||
|
||||
### Remote Hosts
|
||||
|
||||
Locks from remote hosts are always assumed valid because the daemon 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 daemon **fails safe** and skips the database. You must manually fix or remove the lock file.
|
||||
|
||||
## Daemon Logging
|
||||
|
||||
The daemon 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 daemon logs (default: `.beads/daemon.log`) to troubleshoot lock issues.
|
||||
|
||||
**Note:** The daemon 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 daemon**: `bd daemon --interval 1m`
|
||||
2. **Create a lock**: Use your tool to create `.beads/.exclusive-lock`
|
||||
3. **Verify daemon skips**: Check daemon logs for "Skipping database" message
|
||||
4. **Release lock**: Remove `.beads/.exclusive-lock`
|
||||
5. **Verify daemon resumes**: Check daemon 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** - Daemon configuration
|
||||
- **examples/** - Sample integrations
|
||||
|
||||
File issues at: https://github.com/steveyegge/beads/issues
|
||||
Reference in New Issue
Block a user