A distributed lock for your scheduled tasks in Go, inspired by ShedLock.
ShedLock makes sure that your scheduled tasks are executed at most once at the same time. If a task is being executed on one node, it acquires a lock which prevents execution of the same task from another node (or thread). Please note, that if one task is already being executed on one node, execution on other nodes does not wait, it is simply skipped.
- đź”’ Distributed Locking: Ensure scheduled tasks run on only one node at a time
- 🚀 Simple API: Easy to use and integrate into your Go applications
- 🔌 Multiple Storage Backends: PostgreSQL, Redis (more coming soon)
- ⚡ Lock Extension: Extend lock duration for long-running tasks
- 🎯 Configurable: Set minimum and maximum lock durations
- 📊 Logging Support: Built-in logging interface for monitoring
ShedLock uses an external store (database, Redis, etc.) for coordination. When a scheduled task is about to execute:
- It tries to acquire a lock with a unique name
- If successful, the task executes
- If the lock is already held by another node, execution is skipped (not queued)
- The lock automatically expires after
lockAtMostForduration
This ensures that even if a node crashes, the lock will eventually be released and other nodes can acquire it.
go get github.com/adityajoshi12/shedlock-goFor PostgreSQL support:
go get github.com/adityajoshi12/shedlock-go/providers/postgresFor Redis support:
go get github.com/adityajoshi12/shedlock-go/providers/redispackage main
import (
"context"
"database/sql"
"log"
"time"
"github.com/adityajoshi12/shedlock-go"
"github.com/adityajoshi12/shedlock-go/providers/postgres"
_ "github.com/lib/pq"
)
func main() {
// Connect to PostgreSQL
db, err := sql.Open("postgres", "postgres://user:password@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create lock provider
provider, err := postgres.NewPostgresLockProvider(postgres.Config{
DB: db,
TableName: "shedlock",
})
if err != nil {
log.Fatal(err)
}
// Create the shedlock table (run once)
if err := provider.CreateTable(context.Background()); err != nil {
log.Fatal(err)
}
// Create task executor
executor := shedlock.NewDefaultLockingTaskExecutor(provider)
// Define your task
task := func(ctx context.Context) error {
log.Println("Executing scheduled task...")
// Your task logic here
return nil
}
// Execute with lock
err = executor.ExecuteWithLock(context.Background(), task, shedlock.LockConfiguration{
Name: "myScheduledTask",
LockAtMostFor: 10 * time.Minute,
LockAtLeastFor: 0,
})
if err != nil {
log.Printf("Error: %v", err)
}
}package main
import (
"context"
"log"
"time"
"github.com/adityajoshi12/shedlock-go"
"github.com/adityajoshi12/shedlock-go/providers/redis"
goredis "github.com/redis/go-redis/v9"
)
func main() {
// Connect to Redis
client := goredis.NewClient(&goredis.Options{
Addr: "localhost:6379",
})
defer client.Close()
// Create lock provider
provider, err := redis.NewRedisLockProvider(redis.Config{
Client: client,
Prefix: "shedlock:",
})
if err != nil {
log.Fatal(err)
}
// Create task executor
executor := shedlock.NewDefaultLockingTaskExecutor(provider)
// Define and execute your task
task := func(ctx context.Context) error {
log.Println("Executing scheduled task...")
// Your task logic here
return nil
}
err = executor.ExecuteWithLock(context.Background(), task, shedlock.LockConfiguration{
Name: "myScheduledTask",
LockAtMostFor: 10 * time.Minute,
LockAtLeastFor: 0,
})
if err != nil {
log.Printf("Error: %v", err)
}
}| Field | Type | Description |
|---|---|---|
Name |
string |
Unique name of the lock (required) |
LockAtMostFor |
time.Duration |
Maximum duration the lock will be held. Prevents deadlock if node crashes (required) |
LockAtLeastFor |
time.Duration |
Minimum duration the lock will be held. Prevents too frequent execution (optional) |
The LockAtMostFor parameter specifies how long the lock will be held in case the executing node dies. This is a safety mechanism to prevent deadlocks. Set it to a value significantly longer than your task's expected execution time.
LockAtMostFor: 10 * time.Minute // Lock expires after 10 minutesThe LockAtLeastFor parameter specifies the minimum time the lock should be held. This is useful for preventing a task from executing too frequently, even if it completes quickly.
LockAtLeastFor: 5 * time.Second // Lock held for at least 5 secondsThe PostgreSQL provider stores locks in a dedicated table:
CREATE TABLE shedlock (
name VARCHAR(64) PRIMARY KEY,
lock_until TIMESTAMP NOT NULL,
locked_at TIMESTAMP NOT NULL,
locked_by VARCHAR(255) NOT NULL
);Configuration:
provider, err := postgres.NewPostgresLockProvider(postgres.Config{
DB: db, // *sql.DB instance
TableName: "shedlock", // Optional, defaults to "shedlock"
})The Redis provider uses Redis keys with TTL for locking.
Configuration:
provider, err := redis.NewRedisLockProvider(redis.Config{
Client: client, // *redis.Client instance
Prefix: "shedlock:", // Optional, defaults to "shedlock:"
})ShedLock works great with Go scheduling libraries. Here's an example with a simple ticker:
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
err := executor.ExecuteWithLock(ctx, task, lockConfig)
if err != nil {
log.Printf("Error: %v", err)
}
}You can also use it with popular cron libraries like:
Implement the Logger interface for custom logging:
type CustomLogger struct {
// your logger implementation
}
func (l *CustomLogger) Debug(msg string, keysAndValues ...interface{}) { /* ... */ }
func (l *CustomLogger) Info(msg string, keysAndValues ...interface{}) { /* ... */ }
func (l *CustomLogger) Error(msg string, keysAndValues ...interface{}) { /* ... */ }
// Use custom logger
executor := shedlock.NewDefaultLockingTaskExecutorWithLogger(provider, &CustomLogger{})For long-running tasks, you can extend the lock duration:
// This requires direct access to the lock, which is managed internally
// Future versions may expose this functionality more directlyYou can also manually manage locks without the executor:
ctx := context.Background()
config := shedlock.LockConfiguration{
Name: "myLock",
LockAtMostFor: 5 * time.Minute,
LockAtLeastFor: 0,
}
lock, err := provider.Lock(ctx, config)
if err != nil {
log.Fatal(err)
}
if lock == nil {
log.Println("Lock is held by another process")
return
}
defer lock.Unlock(ctx)
// Do your work here
log.Println("Lock acquired, executing task...")Check out the examples directory for complete working examples:
- PostgreSQL Example
- Redis Example
- Scheduler Example - Run multiple instances to see distributed locking in action!
- Set appropriate timeouts:
LockAtMostForshould be significantly longer than your task's expected execution time - Use unique lock names: Each scheduled task should have a unique lock name
- Handle errors: Always check and log errors from
ExecuteWithLock - Use
LockAtLeastForfor short tasks: Prevents too frequent execution - Monitor your locks: Use the built-in logging to monitor lock acquisition and release
Locks in ShedLock have an expiration time which leads to the following possible issues:
- If the task runs longer than
lockAtMostFor, the task can be executed more than once - Clock skew: If the clock difference between nodes is significant, it may affect lock behavior
- Network issues: Network partitions or delays can affect lock acquisition
This Go implementation maintains the core concepts of the original Java ShedLock while following Go idioms:
- Uses Go's
context.Contextfor cancellation and timeouts - Uses Go's
time.Durationinstead of Java's duration strings - Follows Go error handling patterns
- Uses Go interfaces for extensibility
Contributions are welcome! Please feel free to submit a Pull Request.
Apache License 2.0
Inspired by the excellent ShedLock library for Java by Lukas Krecan.
- MySQL provider
- MongoDB provider
- DynamoDB provider
- Metrics and monitoring support
- More comprehensive testing
- Benchmarking and performance optimization