Skip to content

Commit c4e9bd1

Browse files
committed
feat: improve documentation, tests, and examples
This commit significantly enhances the go-pglock package with comprehensive documentation, improved test coverage, and practical examples for common distributed locking patterns. Documentation improvements: - Rewrite README.md with 10 detailed examples covering real-world use cases including leader election, task processing, resource pools, and migrations - Add QUICKREF.md with concise API reference, patterns, and troubleshooting - Add examples/README.md with detailed guide for running example programs - Enhance all Go doc strings in lock.go following best practices with comprehensive descriptions of behavior, parameters, and edge cases - Add package-level documentation explaining PostgreSQL advisory locks Test improvements: - Add 12 comprehensive test cases covering: * Basic lock acquisition and release * Lock stacking behavior (multiple acquisitions) * Context cancellation and timeout handling * Concurrent lock attempts with race detection * Blocking vs non-blocking behavior * Lock cleanup via Close() * Error cases and edge conditions - Improve test helpers with t.Helper(), require assertions, and better error messages - Add proper test skipping when DATABASE_URL is not set - All tests pass with race detector enabled Example programs: - Add 5 complete, runnable example programs demonstrating: * basic/ - Simple lock acquire/release pattern * workers/ - Concurrent workers coordinating access * leader-election/ - Cluster leadership election * timeout/ - Context timeout and cancellation * task-processing/ - Distributed task coordination - All examples compile successfully and include proper error handling Development tooling: - Add docker-compose.yml for easy PostgreSQL test database setup - Enhance Makefile with test-race, test-coverage, docker-up, docker-down, and test-local targets for improved developer experience The documentation now provides clear guidance from basic usage to advanced patterns, with 10 real-world examples, troubleshooting guides, and best practices. Test coverage includes comprehensive scenarios with proper concurrency testing and edge case handling.
1 parent acbfc79 commit c4e9bd1

File tree

12 files changed

+2393
-158
lines changed

12 files changed

+2393
-158
lines changed

Makefile

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,29 @@ lint:
88
test:
99
go test -covermode=count -coverprofile=count.out -v ./...
1010

11+
test-race:
12+
go test -race -covermode=atomic -coverprofile=count.out -v ./...
13+
14+
test-coverage:
15+
go test -covermode=count -coverprofile=count.out -v ./...
16+
go tool cover -html=count.out -o coverage.html
17+
@echo "Coverage report generated: coverage.html"
18+
19+
docker-up:
20+
docker-compose up -d
21+
@echo "Waiting for PostgreSQL to be ready..."
22+
@sleep 3
23+
24+
docker-down:
25+
docker-compose down -v
26+
27+
test-local: docker-up
28+
@echo "Running tests with local PostgreSQL..."
29+
@DATABASE_URL='postgres://test:test@localhost:5432/pglock?sslmode=disable' go test -v ./...
30+
@$(MAKE) docker-down
31+
1132
mock:
1233
@rm -rf mocks
1334
mockery --name Locker
1435

15-
.PHONY: lint test mock
36+
.PHONY: lint test test-race test-coverage docker-up docker-down test-local mock

QUICKREF.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# go-pglock Quick Reference
2+
3+
## Installation
4+
5+
```bash
6+
go get github.com/allisson/go-pglock/v3
7+
```
8+
9+
## Basic Usage
10+
11+
```go
12+
import "github.com/allisson/go-pglock/v3"
13+
14+
// Connect to database
15+
db, _ := sql.Open("postgres", "postgres://user:pass@host/db?sslmode=disable")
16+
17+
// Create lock
18+
ctx := context.Background()
19+
lock, err := pglock.NewLock(ctx, lockID, db)
20+
defer lock.Close()
21+
22+
// Acquire lock (non-blocking)
23+
acquired, err := lock.Lock(ctx)
24+
25+
// Wait for lock (blocking)
26+
err := lock.WaitAndLock(ctx)
27+
28+
// Release lock
29+
err := lock.Unlock(ctx)
30+
```
31+
32+
## API Quick Reference
33+
34+
| Method | Behavior | Returns | Use Case |
35+
|--------|----------|---------|----------|
36+
| `NewLock(ctx, id, db)` | Create lock instance | `Lock, error` | Initialize lock |
37+
| `Lock(ctx)` | Try acquire (non-blocking) | `bool, error` | Skip if busy |
38+
| `WaitAndLock(ctx)` | Wait for lock (blocking) | `error` | Must execute |
39+
| `Unlock(ctx)` | Release one lock level | `error` | After work |
40+
| `Close()` | Release all & cleanup | `error` | Shutdown |
41+
42+
## Common Patterns
43+
44+
### Pattern: Try Lock
45+
46+
```go
47+
acquired, _ := lock.Lock(ctx)
48+
if !acquired {
49+
return // Skip work
50+
}
51+
defer lock.Unlock(ctx)
52+
// Do work
53+
```
54+
55+
### Pattern: Wait for Lock
56+
57+
```go
58+
if err := lock.WaitAndLock(ctx); err != nil {
59+
return err
60+
}
61+
defer lock.Unlock(ctx)
62+
// Do work
63+
```
64+
65+
### Pattern: Lock with Timeout
66+
67+
```go
68+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
69+
defer cancel()
70+
71+
if err := lock.WaitAndLock(ctx); err != nil {
72+
return err // Timeout or other error
73+
}
74+
defer lock.Unlock(context.Background())
75+
// Do work
76+
```
77+
78+
### Pattern: Unique Lock per Resource
79+
80+
```go
81+
import "hash/fnv"
82+
83+
func lockIDFromString(s string) int64 {
84+
h := fnv.New64a()
85+
h.Write([]byte(s))
86+
return int64(h.Sum64())
87+
}
88+
89+
lockID := lockIDFromString("user-" + userID)
90+
```
91+
92+
## Lock Behavior
93+
94+
### Blocking vs Non-blocking
95+
96+
| Method | Blocks? | Use When |
97+
|--------|---------|----------|
98+
| `Lock()` | No | Can skip if locked |
99+
| `WaitAndLock()` | Yes | Must execute eventually |
100+
101+
### Lock Stacking
102+
103+
Locks **stack** within the same session:
104+
105+
```go
106+
lock.Lock(ctx) // Acquired (count: 1)
107+
lock.Lock(ctx) // Acquired (count: 2)
108+
lock.Unlock(ctx) // Released (count: 1) - still locked!
109+
lock.Unlock(ctx) // Released (count: 0) - now free
110+
```
111+
112+
### Lock Release
113+
114+
Locks are released when:
115+
-`Unlock()` called (decrements count)
116+
-`Close()` called (releases all)
117+
- ✅ Connection closes
118+
- ✅ Database session ends
119+
120+
## Error Handling
121+
122+
```go
123+
acquired, err := lock.Lock(ctx)
124+
if err != nil {
125+
// Database or connection error
126+
}
127+
if !acquired {
128+
// Lock held by another process
129+
}
130+
```
131+
132+
```go
133+
err := lock.WaitAndLock(ctx)
134+
if errors.Is(err, context.DeadlineExceeded) {
135+
// Timeout occurred
136+
}
137+
if errors.Is(err, context.Canceled) {
138+
// Context was cancelled
139+
}
140+
```
141+
142+
## Best Practices
143+
144+
### ✅ DO
145+
146+
- Always `defer lock.Close()`
147+
- Use context with timeouts for `WaitAndLock()`
148+
- Match lock and unlock calls (stacking)
149+
- Use deterministic lock IDs
150+
- Check `acquired` return value
151+
152+
### ❌ DON'T
153+
154+
- Don't use random lock IDs
155+
- Don't forget to unlock
156+
- Don't acquire in inconsistent order (deadlock)
157+
- Don't share Lock instances across goroutines
158+
- Don't rely on lock after `Close()`
159+
160+
## Testing
161+
162+
```bash
163+
# Start PostgreSQL
164+
docker-compose up -d
165+
166+
# Run tests
167+
make test-local
168+
169+
# With race detector
170+
make test-race
171+
```
172+
173+
## Use Cases at a Glance
174+
175+
| Use Case | Pattern | Lock Type |
176+
|----------|---------|-----------|
177+
| Scheduled jobs | Try Lock | Non-blocking |
178+
| Database migrations | Wait + Timeout | Blocking |
179+
| Leader election | Try Lock | Non-blocking |
180+
| Task processing | Try Lock | Non-blocking |
181+
| Resource pools | Try each slot | Non-blocking |
182+
| Critical sections | Wait Lock | Blocking |
183+
184+
## Connection Management
185+
186+
```go
187+
// Configure pool for locks
188+
db.SetMaxOpenConns(50) // Each lock uses one connection
189+
db.SetMaxIdleConns(10)
190+
db.SetConnMaxLifetime(time.Hour)
191+
```
192+
193+
## Lock ID Strategies
194+
195+
### Strategy 1: Sequential IDs
196+
197+
```go
198+
const (
199+
LockIDMigration = 1
200+
LockIDBackup = 2
201+
LockIDCleanup = 3
202+
)
203+
```
204+
205+
### Strategy 2: Hash-based
206+
207+
```go
208+
lockID := hashToInt64("resource-name")
209+
```
210+
211+
### Strategy 3: Composite
212+
213+
```go
214+
lockID := (resourceType << 32) | resourceID
215+
```
216+
217+
## Troubleshooting
218+
219+
| Problem | Solution |
220+
|---------|----------|
221+
| Lock never released | Add `defer lock.Close()` |
222+
| Too many connections | Reduce pool size or close locks |
223+
| Deadlocks | Acquire locks in consistent order |
224+
| Timeouts | Increase timeout or investigate blocking |
225+
| Tests skip | Set `DATABASE_URL` environment variable |
226+
227+
## Resources
228+
229+
- [Full Documentation](README.md)
230+
- [Examples](examples/)
231+
- [API Reference](https://pkg.go.dev/github.com/allisson/go-pglock/v3)
232+
- [PostgreSQL Advisory Locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS)

0 commit comments

Comments
 (0)