{"id":35560641,"url":"https://github.com/trviph/redlock","last_synced_at":"2026-01-25T10:01:27.669Z","repository":{"id":332013546,"uuid":"1127571185","full_name":"trviph/redlock","owner":"trviph","description":"Yet another Redis lock implementation","archived":false,"fork":false,"pushed_at":"2026-01-16T18:37:15.000Z","size":63,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-17T07:59:06.034Z","etag":null,"topics":["distributed-lock","lock","redis"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/trviph.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-04T06:44:15.000Z","updated_at":"2026-01-16T18:37:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/trviph/redlock","commit_stats":null,"previous_names":["trviph/redlock"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/trviph/redlock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trviph%2Fredlock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trviph%2Fredlock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trviph%2Fredlock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trviph%2Fredlock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/trviph","download_url":"https://codeload.github.com/trviph/redlock/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trviph%2Fredlock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28751049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T09:58:17.166Z","status":"ssl_error","status_checked_at":"2026-01-25T09:55:56.104Z","response_time":113,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distributed-lock","lock","redis"],"created_at":"2026-01-04T11:14:10.658Z","updated_at":"2026-01-25T10:01:27.664Z","avatar_url":"https://github.com/trviph.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Redlock\n\n[![Go Test](https://github.com/trviph/redlock/actions/workflows/test.yml/badge.svg)](https://github.com/trviph/redlock/actions/workflows/test.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/trviph/redlock)](https://goreportcard.com/report/github.com/trviph/redlock)\n[![Go Reference](https://pkg.go.dev/badge/github.com/trviph/redlock.svg)](https://pkg.go.dev/github.com/trviph/redlock)\n[![codecov](https://codecov.io/gh/trviph/redlock/graph/badge.svg?token=CODECOV_TOKEN)](https://codecov.io/gh/trviph/redlock)\n\nA distributed lock implementation in Go backed by Redis, supporting both single-instance locks and quorum-based multi-instance locks via the [Redlock algorithm](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/).\n\n## Table of Contents\n\n- [Redlock](#redlock)\n  - [Table of Contents](#table-of-contents)\n  - [Installation](#installation)\n  - [Usage](#usage)\n    - [Single Instance](#single-instance)\n      - [Key Methods](#key-methods)\n    - [Multi-Instance (Redlock Algorithm)](#multi-instance-redlock-algorithm)\n    - [Watchdog Pattern (Auto-Renewal)](#watchdog-pattern-auto-renewal)\n  - [Error Handling](#error-handling)\n    - [Unwrapping Joined Errors](#unwrapping-joined-errors)\n  - [Testing](#testing)\n  - [License](#license)\n\n## Installation\n\n```bash\ngo get github.com/trviph/redlock\n```\n\n## Usage\n\n### Single Instance\n\n```go\nimport (\n    \"context\"\n    \"time\"\n    \"github.com/redis/go-redis/v9\"\n    \"github.com/trviph/redlock\"\n)\n\nrdb := redis.NewClient(\u0026redis.Options{Addr: \"localhost:6379\"})\nlock := redlock.NewLock(rdb,\n    redlock.WithMaxRetry(-1),                         // Default: -1 (infinite)\n    redlock.WithMinRetryDelay(0),                     // Default: 0\n    redlock.WithJitterDuration(300*time.Millisecond), // Default: 300ms\n)\n\nctx := context.Background()\nkey := \"my-resource\"\nttl := 10 * time.Second\n\n// Acquire lock (retries until success, context cancellation, or max retries)\nfencing, err := lock.Acquire(ctx, key, ttl)\nif err != nil {\n    panic(err)\n}\ndefer lock.Release(ctx, key, fencing)\n\n// Do work...\n```\n\n#### Key Methods\n\n| Method            | Description                                                    |\n| ----------------- | -------------------------------------------------------------- |\n| `Acquire`         | Acquires lock with retry, returns fencing token                |\n| `TryAcquire`      | Single attempt, no retry; returns `ErrLockAlreadyHeld` if held |\n| `Extend`          | Extends TTL with retry if fencing token matches                |\n| `TryExtend`       | Single extend attempt; returns `ErrLockNotHeld` on failure     |\n| `AcquireOrExtend` | Extends if held, otherwise acquires (with retry)               |\n| `Release`         | Atomically releases lock if fencing token matches              |\n\n\u003e [!NOTE]\n\u003e The `fencing` token returned by `Acquire` is a random UUID used solely to identify the lock owner and prevent race conditions when extending or releasing the lock. It is **not** a monotonically increasing number and cannot be used for external shielding (e.g., preventing split-brain writes in storage systems) as described in [Martin Kleppmann's critique](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html).\n\u003e\n\u003e If you require strict monotonic fencing tokens for external shielding, you can generate them yourself (e.g., using a separate counter) and pass them to the `AcquireWithFencing` or `TryAcquireWithFencing` methods. However, if strong consistency is a strict requirement, it is recommended to consider systems designed for it, such as **etcd** or **Zookeeper**, instead of Redis.\n\n---\n\n### Multi-Instance (Redlock Algorithm)\n\n`DistributedLock` implements the Redlock algorithm for higher availability. It acquires locks across multiple independent Redis instances and requires a quorum (N/2 + 1) to succeed.\n\n```go\nredis1 := redis.NewClient(\u0026redis.Options{Addr: \"redis1:6379\"})\nredis2 := redis.NewClient(\u0026redis.Options{Addr: \"redis2:6379\"})\nredis3 := redis.NewClient(\u0026redis.Options{Addr: \"redis3:6379\"})\n\nlocks := []*redlock.Lock{\n    redlock.NewLock(redis1),\n    redlock.NewLock(redis2),\n    redlock.NewLock(redis3),\n}\n\ndl := redlock.NewDistributedLock(locks,\n    redlock.WithClockDriftFactor(0.01),                // Default: 1%\n    redlock.WithClockDriftBuffer(2*time.Millisecond),  // Default: 2ms\n    redlock.WithReleaseTimeout(5*time.Second),       // Default: 5s\n    redlock.WithDistMaxRetry(-1),                    // Default: -1 (infinite)\n    redlock.WithDistMinRetryDelay(0),                // Default: 0\n    redlock.WithDistMaxJitterDuration(300*time.Millisecond), // Default: 300ms\n)\n\nfencing, err := dl.Acquire(ctx, \"my-resource\", 30*time.Second)\nif err != nil {\n    panic(err)\n}\ndefer dl.Release(ctx, \"my-resource\", fencing)\n```\n\nThe API mirrors `Lock` for consistency (`Acquire`, `TryAcquire`, `Extend`, `TryExtend`, `AcquireOrExtend`, `Release`).\n\n\u003e **Note:** Use an odd number of instances (3, 5, 7) for optimal fault tolerance.\n\n---\n\n### Watchdog Pattern (Auto-Renewal)\n\nFor long-running operations where duration is unknown, use a watchdog goroutine to periodically extend the lock. This pattern works with both `Lock` and `DistributedLock`:\n\n```go\nfencing, _ := lock.Acquire(ctx, key, 10*time.Second)\n\nwatchdogCtx, stop := context.WithCancel(ctx)\ndefer stop()\n\ngo func() {\n    ticker := time.NewTicker(5 * time.Second) // Extend at ~half TTL\n    defer ticker.Stop()\n    for {\n        select {\n        case \u003c-watchdogCtx.Done():\n            return\n        case \u003c-ticker.C:\n            lock.TryExtend(watchdogCtx, key, fencing, 10*time.Second)\n        }\n    }\n}()\n\n// Do long-running work...\nlock.Release(ctx, key, fencing)\n```\n\nAlternatively, you can use the built-in `Watch` helper which simplifies this pattern:\n\n```go\nfencing, err := lock.Acquire(ctx, key, ttl)\nif err != nil {\n    // Handle error\n}\n\nwatchCtx, watchCancel := context.WithCancel(ctx)\ndefer watchCancel()\n\nredlock.Watch(watchCtx, lock, key, fencing, ttl)\n\n// Do long-running work...\n\nwatchCancel() // Stop the watchdog\nlock.Release(ctx, key, fencing)\n```\n\nYou can also customize the extension interval using `WatchWithInterval`:\n\n```go\n// Check every 1 second instead of default ttl/2\nredlock.WatchWithInterval(watchCtx, lock, key, fencing, ttl, 1*time.Second)\n```\n\nFor more control on handling errors (logging, early stopping), use `WatchDog`:\n\n```go\n// Define a callback to handle errors\nerrHandler := func(ctx context.Context, item *redlock.WatchItem, err error) {\n    if err == context.Canceled {\n        // Context cancellation is always the last error received\n        log.Printf(\"WatchDog stopped for key %s\", item.Key)\n        return\n    }\n    log.Printf(\"WatchDog error for key %s: %v\", item.Key, err)\n}\n\n// Start WatchDog with the callback\nwd := redlock.NewWatchDog(locker,\n    redlock.WithCallbacks(cbCtx, errHandler),\n    // Watch item with specific interval (pass 0 for default ttl/2)\n    redlock.WithItem(\"resource-1\", \"token-1\", 10*time.Second, 2*time.Second),\n)\ngo wd.Run(ctx)\n```\n\n\u003e [!WARNING]\n\u003e The watchdog goroutine (`Watch` or `WatchWithInterval`) will **not** stop automatically if the lock is lost or fails to extend. It will continue attempting to extend the lock indefinitely until the provided `context` is canceled.\n\u003e\n\u003e **Design Rationale:** This behavior is intentional to handle cases where the watchdog is started before the lock is successfully acquired (e.g., during a retry loop) or to survive transient network failures. It avoids prematurely killing the watchdog due to temporary errors.\n\u003e\n\u003e Always ensure you cancel the context when the operation is finished or if you detect that the lock has been lost.\n\n## Error Handling\n\nThe package provides sentinel errors for reliable error checking:\n\n| Error                 | Description                                                                    |\n| --------------------- | ------------------------------------------------------------------------------ |\n| `ErrLockAlreadyHeld`  | Lock is held by another client (from `TryAcquire`)                             |\n| `ErrLockNotHeld`      | Lock doesn't exist or fencing token mismatch (from `TryExtend`)                |\n| `ErrMaxRetryExceeded` | Maximum retry attempts exhausted                                               |\n| `ErrValidityExpired`  | Lock acquired but validity expired due to clock drift (`DistributedLock` only) |\n\n```go\nfencing, err := lock.Acquire(ctx, key, ttl)\nif err != nil {\n    switch {\n    case errors.Is(err, redlock.ErrLockAlreadyHeld):\n        log.Println(\"Resource busy\")\n    case errors.Is(err, redlock.ErrMaxRetryExceeded):\n        log.Println(\"Max retries reached\")\n    case errors.Is(err, redlock.ErrValidityExpired):\n        log.Println(\"Lock validity expired\")\n    case errors.Is(err, redlock.ErrLockNotHeld):\n        log.Println(\"Cannot extend: lock not held\")\n    case errors.Is(err, context.DeadlineExceeded):\n        log.Println(\"Timeout\")\n    default:\n        log.Printf(\"Error: %v\", err)\n    }\n}\n```\n\n### Unwrapping Joined Errors\n\n`DistributedLock` operations may join errors from multiple instances using `errors.Join()`:\n\n```go\nif unwrapper, ok := err.(interface{ Unwrap() []error }); ok {\n    for _, e := range unwrapper.Unwrap() {\n        log.Printf(\"Instance error: %v\", e)\n    }\n}\n```\n\n## Testing\n\nThis project uses Docker Compose for integration testing:\n\n```bash\n# Start Redis instances\ndocker compose up -d\n\n# Run tests\ngo test -v ./...\n\n# Cleanup\ndocker compose down\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrviph%2Fredlock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftrviph%2Fredlock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrviph%2Fredlock/lists"}