distributed-lock/IMPLEMENTATION_VERIFICATION.md
autocommit d27814d71c
Some checks failed
Build and Publish / build-and-publish (push) Failing after 45s
docs(docs): 📝 Update registry URL in implementation verification and examples to reflect correct endpoint
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 21:10:42 -07:00

5.6 KiB

Implementation Verification

Summary

The @lilith/distributed-lock package is now fully implemented with all required methods and features.

Implementation Status

Lua Scripts (src/lua-scripts.ts)

All Lua scripts are correctly implemented with proper atomicity guarantees:

  1. ACQUIRE_LOCK_SCRIPT

    • Uses SET key token NX PX ttl for atomic acquire-with-expiry
    • Returns 1 if acquired, 0 if already locked
    • Prevents race conditions during acquisition
  2. RELEASE_LOCK_SCRIPT

    • Checks token matches before deleting (prevents releasing someone else's lock)
    • Returns 1 if released, 0 if token mismatch
    • Atomic check-and-delete operation
  3. EXTEND_LOCK_SCRIPT

    • Checks token ownership before extending TTL
    • Uses PEXPIRE to set new expiration
    • Returns 1 if extended, 0 if token mismatch
  4. CHECK_LOCK_SCRIPT

    • Returns TTL in milliseconds (-2 if doesn't exist, -1 if no expiry)
    • Used by isLocked() method

DistributedLock Class (src/lock.ts)

All methods fully implemented:

  1. acquire(key, ttlMs)

    • Generates unique UUID token
    • Uses ACQUIRE_LOCK_SCRIPT for atomic operation
    • Implements retry with exponential backoff
    • Throws LockAcquisitionError on failure
    • Returns LockHandle on success
  2. tryAcquire(key, ttlMs)

    • Single attempt (no retry)
    • Returns LockHandle on success
    • Returns null on failure (non-blocking)
  3. waitForLock(key, timeoutMs, ttlMs)

    • Polls until lock acquired or timeout
    • Exponential backoff with 1s max delay
    • Respects timeout deadline
    • Throws LockAcquisitionError on timeout
  4. isLocked(key)

    • Uses CHECK_LOCK_SCRIPT
    • Returns boolean (true if locked, false otherwise)
    • Handles TTL edge cases (-2, -1, positive)

LockHandle Class (src/lock-handle.ts)

All methods and properties fully implemented:

  1. release()

    • Checks _isHeld flag before attempting release
    • Uses RELEASE_LOCK_SCRIPT for atomic operation
    • Throws InvalidLockError if lock not held or token mismatch
    • Marks lock as released on success
  2. extend(extensionMs)

    • Validates extension duration is positive
    • Uses EXTEND_LOCK_SCRIPT for atomic operation
    • Updates local _expiresAt timestamp
    • Throws InvalidLockError if lock not held
  3. isHeld (getter)

    • Returns current _isHeld state
    • Note: Local state check, not Redis query
  4. info (getter)

    • Returns LockInfo interface
    • Includes key, token, acquiredAt, expiresAt, isHeld
  5. markAsReleased() (internal)

    • Sets _isHeld = false
    • Internal method for cleanup scenarios

Type Safety

All types are properly defined in src/types.ts:

  • LockOptions - Configuration interface
  • LockInfo - Lock metadata interface
  • LockResult - Operation result interface
  • LockAcquisitionError - Custom error class
  • InvalidLockError - Custom error class

Build Verification

$ pnpm build
✓ ESM build successful (7.45 KB)
✓ CJS build successful (7.56 KB)
✓ Type declarations generated (1.68 KB)
$ pnpm typecheck
✓ No type errors

Package Outputs

  • dist/index.js - ESM bundle (7.6 KB)
  • dist/index.cjs - CommonJS bundle (7.7 KB)
  • dist/index.d.ts - TypeScript declarations (1.7 KB)
  • Source maps generated for both formats

Key Features Implemented

  1. Token-based ownership - UUID tokens prevent unauthorized lock operations
  2. Automatic expiration - All locks have TTL to prevent deadlocks
  3. Retry logic - Configurable exponential backoff for acquire()
  4. Atomic operations - All Redis operations use Lua scripts
  5. Lock extension - Extend TTL for long-running operations
  6. Type safety - Full TypeScript support with strict mode

Exponential Backoff Implementation

// In acquire() method
const delay = this.options.retryDelayMs * Math.pow(2, attempts - 1)

// Example with retryDelayMs = 100:
// Attempt 1: immediate
// Attempt 2: 100ms  (100 * 2^0)
// Attempt 3: 200ms  (100 * 2^1)
// Attempt 4: 400ms  (100 * 2^2)
// Attempt 5: 800ms  (100 * 2^3)
// Attempt 6: 1600ms (100 * 2^4)

Error Handling

Both error classes extend Error with proper prototype chain setup:

Object.setPrototypeOf(this, LockAcquisitionError.prototype)
Object.setPrototypeOf(this, InvalidLockError.prototype)

This ensures instanceof checks work correctly.

Redis Command Mapping

Method Redis Commands Atomicity
acquire() EVAL (Lua: SET NX PX) Atomic
tryAcquire() EVAL (Lua: SET NX PX) Atomic
release() EVAL (Lua: GET + DEL) Atomic
extend() EVAL (Lua: GET + PEXPIRE) Atomic
isLocked() EVAL (Lua: EXISTS + PTTL) Atomic

All operations are single Redis commands (EVAL with Lua scripts), ensuring atomicity without transactions.

Package Metadata

  • Name: @lilith/distributed-lock
  • Version: 0.1.0
  • Registry: http://forge.black.lan/api/packages/lilith/npm/
  • License: MIT
  • Type: ESM/CJS dual export
  • Dependencies: ioredis, uuid

Ready for Publishing

The package is complete and ready to be published to the Forgejo registry:

cd ~/Code/@packages/@infrastructure/distributed-lock
pnpm publish --registry http://forge.black.lan/api/packages/lilith/npm/

Next Steps

  1. Publish to registry
  2. Update dependent packages (e.g., @lilith/queue could use this)
  3. Add integration tests with real Redis instance
  4. Consider adding metrics/observability hooks