timedlock

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 28, 2025 License: MIT Imports: 4 Imported by: 0

README

TimedLock

Go Reference Go Report Card

A production-ready Go synchronization library providing mutex-like locking with timeout, context, and auto-release support.

Why TimedLock?

While sync.Mutex is perfect for simple mutual exclusion, real-world applications often need:

  • Timeouts to prevent indefinite blocking
  • Context integration for cancellation and deadlines
  • Auto-release to prevent deadlocks
  • Non-blocking attempts to check lock availability
  • Rich error information for debugging

TimedLock provides all of these while maintaining the simplicity of a mutex.

How It Works

TimedLock uses a buffered channel (capacity 1) as a semaphore:

┌─────────────────────────────────────────────────────────────┐
│                       TimedLock                             │
│                                                             │
│  ┌───────────────────────────────────────────┐             │
│  │   semaphore: chan struct{} (capacity: 1)  │             │
│  └───────────────────────────────────────────┘             │
│                                                             │
│  Empty channel = Lock available  ✓                         │
│  Full channel  = Lock held        ✗                        │
└─────────────────────────────────────────────────────────────┘

Lock Acquisition Flow:
─────────────────────

Goroutine A                  Goroutine B
    │                            │
    ├─── Lock(ctx, 5s) ─────────┼─── Lock(ctx, 5s)
    │                            │
    ▼                            ▼
┌────────┐                  ┌────────┐
│ select │                  │ select │
│  case  │                  │  case  │
└────┬───┘                  └────┬───┘
     │                            │
     ├─→ semaphore ← struct{}     │
     │   (Success! ✓)             │
     │                            │
     ├─→ Auto-release timer       ├─→ Waiting... ⏳
     │   starts (if enabled)      │
     │                            │
     ├─→ Critical Section         │
     │                            ├─→ Timeout after 5s?
     │                            ├─→ Context cancelled?
     └─→ Unlock() ────────────────┼─→ Lock acquired! ✓
                                  │
                                  └─→ Critical Section

Installation

go get github.com/piku98/timedlock

Quick Start

package main

import (
    "context"
    "fmt"
    "time"
    
    "github.com/piku98/timedlock"
)

func main() {
    lock := timedlock.New()
    ctx := context.Background()
    
    // Acquire lock with 5 second timeout
    if err := lock.Lock(ctx, 5*time.Second); err != nil {
        fmt.Printf("Failed to acquire lock: %v\n", err)
        return
    }
    defer lock.Unlock()
    
    // Critical section - only one goroutine at a time
    fmt.Println("Processing shared resource...")
    processSharedResource()
}

Core API

Creating a Lock
lock := timedlock.New()
Lock Acquisition
Lock(ctx context.Context, timeout time.Duration) error

Attempts to acquire the lock within the specified timeout.

Parameters:

  • ctx - Context for cancellation support
  • timeout - Maximum time to wait for lock acquisition

Returns:

  • nil - Lock acquired successfully
  • ErrLockTimeout - Timeout expired before acquiring lock
  • ErrContextCancelled - Context was cancelled
  • ErrInvalidTimeout - Timeout is negative
if err := lock.Lock(ctx, 5*time.Second); err != nil {
    switch {
    case errors.Is(err, timedlock.ErrLockTimeout):
        fmt.Println("Timeout: lock is held by another goroutine")
    case errors.Is(err, timedlock.ErrContextCancelled):
        fmt.Println("Operation cancelled")
    default:
        fmt.Printf("Error: %v\n", err)
    }
    return
}
defer lock.Unlock()
LockWithAutoRelease(ctx context.Context, acquireTimeout time.Duration, releaseTimeout *time.Duration) error

Acquires lock with optional automatic release after a duration. Perfect for preventing deadlocks!

// Lock will automatically release after 10 seconds
releaseTimeout := 10 * time.Second
if err := lock.LockWithAutoRelease(ctx, 5*time.Second, &releaseTimeout); err != nil {
    return err
}
// Even if we forget to unlock, it releases automatically after 10s
doWork()
TryLock() bool

Non-blocking lock attempt. Returns immediately.

if lock.TryLock() {
    defer lock.Unlock()
    // Got the lock, proceed
    processResource()
} else {
    // Lock is busy, do something else
    fmt.Println("Resource busy, skipping...")
}
Lock Release
Unlock()

Releases the lock. Safe to call multiple times (subsequent calls are no-ops).

lock.Unlock()  // Releases the lock
lock.Unlock()  // Safe - no panic

Real-World Examples

Example 1: Rate Limiting

Limit operations to once per second:

type RateLimiter struct {
    lock *timedlock.TimedLock
}

func NewRateLimiter() *RateLimiter {
    return &RateLimiter{lock: timedlock.New()}
}

func (rl *RateLimiter) AllowRequest(ctx context.Context) error {
    // Auto-release after 1 second = 1 request per second max
    interval := 1 * time.Second
    
    if err := rl.lock.LockWithAutoRelease(ctx, 100*time.Millisecond, &interval); err != nil {
        return fmt.Errorf("rate limit exceeded: %w", err)
    }
    
    return nil // Request allowed
}

// Usage
limiter := NewRateLimiter()

for i := 0; i < 5; i++ {
    if err := limiter.AllowRequest(ctx); err != nil {
        fmt.Printf("Request %d: DENIED (rate limited)\n", i+1)
    } else {
        fmt.Printf("Request %d: ALLOWED\n", i+1)
        processRequest()
    }
    time.Sleep(400 * time.Millisecond)
}
Example 2: Resource Pool

Manage exclusive access to pooled resources:

type ResourcePool struct {
    resources []Resource
    locks     []*timedlock.TimedLock
}

func NewResourcePool(size int) *ResourcePool {
    pool := &ResourcePool{
        resources: make([]Resource, size),
        locks:     make([]*timedlock.TimedLock, size),
    }
    for i := 0; i < size; i++ {
        pool.locks[i] = timedlock.New()
    }
    return pool
}

func (p *ResourcePool) Acquire(ctx context.Context) (*Resource, func(), error) {
    // Try each resource
    for i, lock := range p.locks {
        if err := lock.Lock(ctx, 2*time.Second); err == nil {
            resource := &p.resources[i]
            release := func() { lock.Unlock() }
            return resource, release, nil
        }
    }
    return nil, nil, errors.New("no resources available")
}

// Usage
pool := NewResourcePool(3)

resource, release, err := pool.Acquire(ctx)
if err != nil {
    log.Fatal(err)
}
defer release()

resource.Use()
Example 3: Graceful Shutdown

Respect context cancellation for clean shutdowns:

func worker(ctx context.Context, lock *timedlock.TimedLock, id int) {
    for {
        // Try to acquire lock, respecting context cancellation
        if err := lock.Lock(ctx, 5*time.Second); err != nil {
            if errors.Is(err, timedlock.ErrContextCancelled) {
                fmt.Printf("Worker %d: Shutdown signal received\n", id)
                return
            }
            time.Sleep(100 * time.Millisecond)
            continue
        }
        
        // Do work
        fmt.Printf("Worker %d: Processing...\n", id)
        time.Sleep(500 * time.Millisecond)
        lock.Unlock()
        
        // Check for shutdown between iterations
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d: Graceful shutdown\n", id)
            return
        default:
            // Continue working
        }
    }
}

// Usage
ctx, cancel := context.WithCancel(context.Background())
lock := timedlock.New()

// Start workers
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        worker(ctx, lock, id)
    }(i)
}

// Wait for interrupt signal
<-sigterm
cancel() // Signal all workers to stop

wg.Wait() // Wait for clean shutdown
fmt.Println("All workers stopped gracefully")
Example 4: Deadlock Prevention

Use auto-release to prevent deadlocks:

func processWithTimeout(lock *timedlock.TimedLock, data Data) error {
    ctx := context.Background()
    
    // Auto-release after 30 seconds as a safety mechanism
    maxProcessTime := 30 * time.Second
    
    if err := lock.LockWithAutoRelease(ctx, 5*time.Second, &maxProcessTime); err != nil {
        return err
    }
    
    // Even if processing hangs, lock releases after 30s
    // preventing system-wide deadlock
    return processData(data)
}

Error Handling

Standard Errors
var (
    ErrLockTimeout      = errors.New("failed to acquire lock within timeout")
    ErrContextCancelled = errors.New("context cancelled during lock acquisition")
    ErrInvalidTimeout   = errors.New("invalid timeout duration")
)
Custom Error Types
LockTimeoutError

Provides detailed timeout information:

type LockTimeoutError struct {
    Timeout     time.Duration  // Requested timeout
    ElapsedTime time.Duration  // Actual time elapsed
    Message     string         // Descriptive message
}

Usage:

err := lock.Lock(ctx, 100*time.Millisecond)
if err != nil {
    var timeoutErr *timedlock.LockTimeoutError
    if errors.As(err, &timeoutErr) {
        fmt.Printf("Timed out after %v (requested %v)\n", 
            timeoutErr.ElapsedTime, timeoutErr.Timeout)
    }
}
ContextError

Wraps context-related errors:

type ContextError struct {
    Operation string  // Operation that was attempted
    Cause     error   // Underlying cause
}

Performance

Benchmarks
$ go test -bench=. ./tests/...

BenchmarkTimedLock_Lock-8           1000000    1204 ns/op    0 B/op   0 allocs/op
BenchmarkTimedLock_TryLock-8       50000000      32 ns/op    0 B/op   0 allocs/op
BenchmarkTimedLock_Contention-8      500000    3512 ns/op    0 B/op   0 allocs/op
Performance Characteristics
  • Lock/Unlock: ~1200 ns per operation (no contention)
  • TryLock: ~32 ns per operation
  • Memory: Zero allocations for basic operations
  • Contention: Gracefully handles high contention
Comparison with sync.Mutex
Feature sync.Mutex TimedLock Use Case
Performance ~20 ns ~1200 ns sync.Mutex 60x faster
Timeout support Network operations
Context cancellation Graceful shutdown
Non-blocking try Opportunistic locking
Auto-release Deadlock prevention
Custom errors Error debugging
Safe multiple unlock Defensive programming

When to use what:

  • sync.Mutex: High-frequency, low-latency critical sections
  • TimedLock: Network I/O, distributed systems, user-facing operations

Testing

# Run all tests
go test ./tests/...

# Run with race detector
go test -race ./tests/...

# Run benchmarks
go test -bench=. -benchmem ./tests/...

# Check coverage
go test -cover ./tests/...
Test Coverage
  • ✅ 20 comprehensive unit tests
  • ✅ Race condition detection
  • ✅ Timeout and context cancellation
  • ✅ Concurrent access patterns
  • ✅ Edge cases and error conditions
  • ✅ Auto-release timing correctness

Best Practices

1. Always Use defer for Unlock
if err := lock.Lock(ctx, timeout); err != nil {
    return err
}
defer lock.Unlock()  // Ensures unlock even if panic occurs
// Critical section
2. Choose Appropriate Timeouts
// Fast operation
lock.Lock(ctx, 100*time.Millisecond)

// I/O operation  
lock.Lock(ctx, 5*time.Second)

// Background job
lock.Lock(ctx, 30*time.Second)
3. Handle All Error Cases
err := lock.Lock(ctx, timeout)
if err != nil {
    switch {
    case errors.Is(err, timedlock.ErrLockTimeout):
        // Retry or return busy error
    case errors.Is(err, timedlock.ErrContextCancelled):
        // Clean shutdown
    default:
        // Unexpected error
    }
}
4. Use TryLock for Optional Operations
if lock.TryLock() {
    defer lock.Unlock()
    updateCache()  // Nice to have, not critical
} else {
    // Skip if busy
    metrics.IncrementSkipped()
}

Design Principles

  1. Simplicity - Clean API that feels natural to Go developers
  2. Safety - Thread-safe, panic-free, well-tested
  3. Performance - Minimal overhead, zero allocations where possible
  4. Observability - Rich error information for debugging
  5. Flexibility - Multiple acquisition patterns for different use cases

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

MIT License - see LICENSE file for details.

Author

Sourav Das

Created for the Go community


Need help? Open an issue or check the documentation

Documentation

Overview

Package timedlock provides synchronization primitives for concurrent programming.

The main type is TimedLock, which provides a mutex-like lock with timeout support for both acquisition and automatic release.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrLockTimeout      = errors.New("failed to acquire lock within timeout")
	ErrLockNotHeld      = errors.New("lock is not currently held")
	ErrLockAlreadyHeld  = errors.New("lock is already held")
	ErrInvalidTimeout   = errors.New("invalid timeout duration")
	ErrContextCancelled = errors.New("context cancelled during lock acquisition")
)

Functions

This section is empty.

Types

type ContextError

type ContextError struct {
	Operation string
	Cause     error
}

func NewContextError

func NewContextError(operation string, cause error) *ContextError

func (*ContextError) Error

func (e *ContextError) Error() string

func (*ContextError) Unwrap

func (e *ContextError) Unwrap() error

type LockTimeoutError

type LockTimeoutError struct {
	Timeout     time.Duration
	ElapsedTime time.Duration
	Message     string
}

func NewLockTimeoutError

func NewLockTimeoutError(timeout, elapsed time.Duration, message string) *LockTimeoutError

func (*LockTimeoutError) Error

func (e *LockTimeoutError) Error() string

func (*LockTimeoutError) Is

func (e *LockTimeoutError) Is(target error) bool

func (*LockTimeoutError) Unwrap

func (e *LockTimeoutError) Unwrap() error

type TimedLock

type TimedLock struct {
	// contains filtered or unexported fields
}

TimedLock is a synchronization primitive that provides mutual exclusion with timeout support. It allows setting timeouts for both lock acquisition and automatic release.

Unlike sync.Mutex, TimedLock:

  • Supports timeout-based lock acquisition
  • Supports automatic lock release after a duration
  • Is context-aware for cancellation support
  • Provides non-blocking TryLock operations

TimedLock is safe for concurrent use by multiple goroutines.

func New

func New() *TimedLock

New creates and returns a new TimedLock instance.

Example:

lock := timedlock.New()
defer lock.Unlock()

if err := lock.Lock(ctx, 5*time.Second); err != nil {
    log.Printf("Failed to acquire lock: %v", err)
    return err
}
// critical section

func (*TimedLock) Lock

func (t *TimedLock) Lock(ctx context.Context, acquireTimeout time.Duration) error

Lock attempts to acquire the lock within the specified timeout. It blocks until either the lock is acquired or the timeout expires.

Parameters:

  • ctx: Context for cancellation support
  • acquireTimeout: Maximum duration to wait for lock acquisition

Returns:

  • nil if lock is successfully acquired
  • ErrLockTimeout if timeout expires before acquiring lock
  • ErrContextCancelled if context is cancelled
  • ErrInvalidTimeout if timeout is negative

Example:

if err := lock.Lock(ctx, 5*time.Second); err != nil {
    if errors.Is(err, timedlock.ErrLockTimeout) {
        // Handle timeout
    }
    return err
}
defer lock.Unlock()

func (*TimedLock) LockWithAutoRelease

func (t *TimedLock) LockWithAutoRelease(
	ctx context.Context,
	acquireTimeout time.Duration,
	releaseTimeout *time.Duration,
) error

LockWithAutoRelease attempts to acquire the lock with optional automatic release. This is useful for scenarios where you want to ensure the lock is released even if the holder crashes or forgets to unlock.

Parameters:

  • ctx: Context for cancellation support
  • acquireTimeout: Maximum duration to wait for lock acquisition
  • releaseTimeout: Optional duration after which lock is automatically released

Returns:

  • nil if lock is successfully acquired
  • ErrLockTimeout if timeout expires before acquiring lock
  • ErrContextCancelled if context is cancelled
  • ErrInvalidTimeout if any timeout is negative

Example:

// Acquire lock with auto-release after 10 seconds
releaseTimeout := 10 * time.Second
if err := lock.LockWithAutoRelease(ctx, 5*time.Second, &releaseTimeout); err != nil {
    return err
}
// Lock will be automatically released after 10 seconds

func (*TimedLock) TryLock

func (t *TimedLock) TryLock() bool

TryLock attempts to acquire the lock without blocking. It returns immediately whether or not the lock was acquired.

Returns:

  • true if lock was successfully acquired
  • false if lock is already held

Example:

if lock.TryLock() {
    defer lock.Unlock()
    // critical section
} else {
    // Lock is busy, do something else
}

func (*TimedLock) Unlock

func (t *TimedLock) Unlock()

Unlock releases the lock, allowing other goroutines to acquire it. It is safe to call Unlock on an already unlocked lock (it will be a no-op).

Best practice is to use defer:

if err := lock.Lock(ctx, timeout); err != nil {
    return err
}
defer lock.Unlock()

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL