--- name: pglock-integration description: Use when building or modifying a Go application that uses cirello.io/pglock for PostgreSQL-backed distributed locks, per-resource mutual exclusion, leader election, leases, heartbeats, lock metadata, or lock-aware workflows. Guide dependency and driver selection, schema provisioning, client configuration, acquisition and release, context cancellation, error handling, and integration tests. Do not use for changing pglock's own internals or for generic PostgreSQL administration. license: Apache-2.0 compatibility: Requires a Go module compatible with the selected pglock version and PostgreSQL 15 or newer. The current package declares Go 1.25.0. Integration tests need a reachable PostgreSQL primary and permission to create or migrate the lock table and sequence. metadata: repository: cirello.io/pglock purpose: third-party-application-integration version: "1" --- # pglock Integration Use this skill for application code that consumes `cirello.io/pglock`. Do not modify the library's implementation or tests unless the user explicitly asks for an upstream change. First identify the application operation that must be exclusive, the stable lock key, the maximum hold time, the behavior on contention, and what should happen if the database or lease becomes unhealthy. ## Integration Sequence 1. Add `cirello.io/pglock` to the application's Go module at the version the application supports. Use a PostgreSQL `*sql.DB`; keep one shared pool open for the application's clients. 2. Choose the driver constructor deliberately. `pglock.New` validates the connection as the `lib/pq` driver. If the application uses `pgx` through `github.com/jackc/pgx/v5/stdlib`, use `pglock.UnsafeNew` after verifying that the `*sql.DB` really targets PostgreSQL. Do not use `UnsafeNew` to bypass an unknown or non-PostgreSQL driver. 3. Provision the lock table and its `_rvn` sequence once. Prefer a migration or `TryCreateTable` during controlled startup; use `CreateTable` when a duplicate table should be an error. Never drop the shared table from normal application startup. 4. Configure a single shared table and a stable owner identity for all contenders. Use `WithCustomTable` only with a trusted, validated identifier; the package interpolates that table name into SQL. Never derive it directly from user input. 5. Select lease and heartbeat values from the workload. A positive heartbeat must be no more than half the lease, and a lease at least four times the heartbeat is the safer starting point. The defaults are a 20-second lease and a 5-second heartbeat. Set the lease above normal critical-section time plus expected database latency, not merely above average execution time. 6. Use `AcquireContext` or `Do` with an application context, and always release an acquired lock. Make long-running callbacks stop when their callback context is canceled because that context is also canceled when heartbeat loss is detected. 7. Test contention and lease behavior against PostgreSQL. SQLite or an in-memory substitute cannot validate this package's PostgreSQL upsert, sequence, locking, and serialization behavior. ## Basic Bootstrap For `lib/pq`, the safe constructor is: ```go import ( "context" "database/sql" "log" "time" "cirello.io/pglock" _ "github.com/lib/pq" ) ctx := context.Background() db, err := sql.Open("postgres", dsn) if err != nil { log.Fatal(err) } if err := db.PingContext(ctx); err != nil { log.Fatal(err) } client, err := pglock.New(db, pglock.WithOwner(instanceID), pglock.WithLeaseDuration(20*time.Second), pglock.WithHeartbeatFrequency(5*time.Second), ) if err != nil { log.Fatal(err) } if err := client.TryCreateTable(); err != nil { log.Fatal(err) } ``` For `pgx` stdlib, open with the `pgx` driver and call `UnsafeNew`; `New` will return `ErrNotPostgreSQLDriver` because it only recognizes `lib/pq`: ```go import _ "github.com/jackc/pgx/v5/stdlib" db, err := sql.Open("pgx", dsn) client, err := pglock.UnsafeNew(db) ``` Check `err` after every setup call and close `db` only after all locks using it have been released. ## Lock Patterns Use a stable, bounded key shared by every process that must contend, such as `campaign:` or `leader:`. The schema stores names and owners in `VARCHAR(255)`, so bound or hash longer application identifiers before passing them to pglock. For a bounded critical section: ```go import ( "errors" "log" ) lock, err := client.AcquireContext(ctx, lockKey) if err != nil { if errors.Is(err, pglock.ErrNotAcquired) { // The context ended before acquisition, or use FailIfLocked below. } return err } defer func() { if releaseErr := lock.Close(); releaseErr != nil && !errors.Is(releaseErr, pglock.ErrLockAlreadyReleased) { log.Printf("release %q: %v", lockKey, releaseErr) } }() // Do the exclusive work while the lock is held. ``` - `Acquire` waits until the key is available. Use `FailIfLocked()` when a caller should get `ErrNotAcquired` immediately instead of waiting. - `AcquireContext` returns `ErrNotAcquired` when its context is already done or ends before acquisition. Give it a deadline when a request cannot wait indefinitely. - `Do` acquires, invokes `func(context.Context, *pglock.Lock) error`, cancels that callback context on heartbeat loss, and releases on return. Callback code must select on `ctx.Done()` around long work and must not continue externally visible side effects after losing the lock. - `KeepOnRelease()` retains the row after release. Combine it with `WithData` when lock metadata should survive ownership changes. Use `ReplaceData()` on a later acquisition when retained data must be replaced; otherwise existing data is reused. - `Get`, `GetData`, and `GetAllLocks` are inspection APIs. They do not acquire ownership and must not be used as an authorization check for protected work. - `WithOwner` makes the current process or instance visible to `Get` and `GetAllLocks`. Use a stable, non-secret identifier, not a password or token. ## Errors And Operations Use `errors.Is` and `errors.As`, never error-string matching: - `ErrNotAcquired`: contention or acquisition context ended. - `ErrLockAlreadyReleased`: the lock was already released or lost; a deferred close may treat this as an expected cleanup result. - `ErrLockNotFound` and `NotExistError`: an inspection target is absent. - `ErrDurationTooSmall`: the heartbeat is too slow relative to the lease. - `ErrNotPostgreSQLDriver`: `New` received a driver other than `lib/pq`. - `UnavailableError`, `FailedPreconditionError`, and `OtherError` classify wrapped database failures. The client retries PostgreSQL serialization failures (`SQLSTATE 40001`) internally, but application work must still be idempotent if the caller chooses to retry after a returned error. Point the client at the PostgreSQL primary, not a read replica. Every contender must reach the same database and lock table. Grant the application the DDL permissions needed for one-time setup, or run the equivalent `schema.sql` migration under a database owner and grant runtime DML/sequence access. Keep credentials in the application's normal secret/configuration path. ## Integration Tests At minimum, run these scenarios against a real PostgreSQL service: 1. Two clients acquire the same key; the first owns it and a second client with `FailIfLocked()` receives `ErrNotAcquired`. 2. Release the first lock and verify a second client can acquire the same key. 3. Cancel or time out a waiting `AcquireContext` and verify it does not enter the critical section. 4. Hold a lock longer than one heartbeat interval and verify the owner remains valid. Use a lease/heartbeat pair that keeps the test deterministic. 5. For `Do`, cancel the callback context and verify the callback exits and the lock becomes available to another client. 6. If using lock data or custom owners, read them with `Get`/`GetData` and test `KeepOnRelease` and `ReplaceData` explicitly. Use unique trusted test table names or an isolated database, clean up with `DropTable` only in test teardown, and do not let parallel tests share a fixed table/key unless the contention is intentional. Run the application race detector as well, but do not treat `-race` without PostgreSQL as proof of distributed-lock correctness. ## Bundled Resources - Read `references/integration-guide.md` for the API matrix, schema shape, and failure-mode details when the integration spans multiple components. - Run `python3 scripts/validate_skill.py --help` for the dependency-free skill validator. It emits structured JSON and never prompts. - Use `evals/evals.json` for application-level output evals and `evals/trigger_queries.json` for description-trigger evaluation. Keep the trigger train/validation split fixed when measuring a compatible agent client's activation rate.