Skip to content

Commit 4bd8353

Browse files
committed
Add build tag system with synctest compatibility and disable mode
+ sync_disable.go for zero-overhead mode (-tags=deadlock_disable) + synctest compatibility mode (-tags=goexperiment.synctest) + buildtag_test.go and scripts/test-buildtags.sh
1 parent 8a3723e commit 4bd8353

22 files changed

+1010
-10
lines changed

.envrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
# Automatically sets up your devbox environment whenever you cd into this
4+
# directory via our direnv integration:
5+
6+
eval "$(devbox generate direnv --print-envrc)"
7+
8+
# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/
9+
# for more details

.github/workflows/go.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,15 @@ jobs:
3333
run: go test -v -bench=. -coverprofile=coverage.txt ./...
3434
env:
3535
GODEBUG: asynctimerchan=0
36+
37+
- name: Test with deadlock_disable tag
38+
run: go test -v -tags=deadlock_disable ./...
39+
40+
- name: Test with synctest tag (Go 1.25+)
41+
if: matrix.go == '1.25'
42+
run: go test -v -tags=goexperiment.synctest ./...
43+
env:
44+
GODEBUG: asynctimerchan=0
45+
46+
- name: Test all build tag combinations
47+
run: ./scripts/test-buildtags.sh

BUILD_TAGS.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Build Tags and Compatibility Modes
2+
3+
This document describes the build tag system in go-deadlock and how to use different compatibility modes.
4+
5+
## Overview
6+
7+
go-deadlock provides multiple build configurations to handle different runtime environments and performance requirements:
8+
9+
1. **Normal mode** (default) - Full deadlock detection with timer pooling
10+
2. **Synctest compatibility mode** - Channel-based mutexes for Go's `testing/synctest`
11+
3. **Disabled mode** - Zero overhead, no deadlock detection
12+
13+
## Build Tags
14+
15+
### `goexperiment.synctest`
16+
17+
**When to use:** When using Go's `testing/synctest` package for deterministic testing.
18+
19+
**What it does:**
20+
- Switches from `sync.Mutex` to channel-based mutex implementations
21+
- Disables timer pooling (timers across synctest bubbles are incompatible)
22+
- Maintains full deadlock detection functionality
23+
- Required for Go 1.25+ when using `testing/synctest`
24+
25+
**Example:**
26+
```bash
27+
GODEBUG=asynctimerchan=0 go test -tags=goexperiment.synctest ./...
28+
```
29+
30+
**Important:** Always set `GODEBUG=asynctimerchan=0` when using synctest in Go 1.25+.
31+
32+
### `deadlock_disable`
33+
34+
**When to use:** Production builds where you want zero overhead.
35+
36+
**What it does:**
37+
- Completely disables deadlock detection
38+
- Uses standard `sync.Mutex` and `sync.RWMutex` directly
39+
- Zero performance overhead
40+
- Can be combined with `goexperiment.synctest`
41+
42+
**Example:**
43+
```bash
44+
go build -tags=deadlock_disable ./...
45+
```
46+
47+
## Build Configurations Matrix
48+
49+
| Configuration | Build Command | Use Case |
50+
|--------------|---------------|----------|
51+
| Normal (default) | `go test ./...` | Development & testing with deadlock detection |
52+
| Synctest mode | `GODEBUG=asynctimerchan=0 go test -tags=goexperiment.synctest` | Testing with `testing/synctest` |
53+
| Disabled | `go build -tags=deadlock_disable` | Production builds (zero overhead) |
54+
| Synctest + Disabled | `go test -tags="goexperiment.synctest,deadlock_disable"` | Synctest without deadlock detection |
55+
56+
## Technical Details
57+
58+
### Normal Mode (Default)
59+
60+
- Uses `sync.Mutex` and `sync.RWMutex` wrappers
61+
- Timer pooling enabled for performance
62+
- Full deadlock detection:
63+
- Lock order detection
64+
- Timeout-based detection (default: 30 seconds)
65+
- Recursive locking detection
66+
67+
**Implementation files:**
68+
- `sync_mutex_go118.go` (Go 1.18+)
69+
- `sync_mutex_legacy.go` (Go < 1.18)
70+
- `timerpool_default.go` or `timerpool_go125.go`
71+
72+
### Synctest Compatibility Mode
73+
74+
**Why it's needed:**
75+
76+
Go's `testing/synctest` package virtualizes time and goroutine scheduling. It has two main incompatibilities with standard deadlock detection:
77+
78+
1. **Timer incompatibility:** Timers created inside synctest bubbles cannot be mixed with timers created outside. A shared timer pool causes runtime panics.
79+
80+
2. **Non-durable blocking:** `sync.Mutex` is not "durably blocking" in synctest, meaning the virtual scheduler might not properly detect blocking. Channel operations ARE durably blocking.
81+
82+
**Solution:**
83+
84+
When `goexperiment.synctest` is set:
85+
- Mutexes use channel-based implementations (`ChannelMutex`, `ChannelRWMutex`)
86+
- Timer pooling is disabled
87+
- Deadlock detection still works via channel operations
88+
89+
**Implementation files:**
90+
- `synctest_mutex.go` - Channel-based mutex implementations
91+
- `timerpool_synctest.go` - Disables timer pooling
92+
93+
### Disabled Mode
94+
95+
When `deadlock_disable` is set:
96+
- Direct `sync.Mutex` wrappers with no detection logic
97+
- All deadlock detection code is compiled out
98+
- Identical performance to standard library mutexes
99+
100+
**Implementation files:**
101+
- `sync_disable.go` - Zero-overhead wrappers
102+
- `timerpool_disable.go` - Stub implementation
103+
104+
## Testing with Different Configurations
105+
106+
### Run all build tag tests:
107+
```bash
108+
./scripts/test-buildtags.sh
109+
```
110+
111+
### Manual testing:
112+
```bash
113+
# Normal mode
114+
go test -v ./...
115+
116+
# Synctest mode
117+
GODEBUG=asynctimerchan=0 go test -v -tags=goexperiment.synctest ./...
118+
119+
# Disabled mode
120+
go test -v -tags=deadlock_disable ./...
121+
122+
# Synctest with disabled detection
123+
go test -v -tags="goexperiment.synctest,deadlock_disable" ./...
124+
```
125+
126+
## Integration Testing with Mercure
127+
128+
The project includes Docker-based integration tests using the [Mercure](https://github.com/dunglas/mercure) project:
129+
130+
```bash
131+
# Build the test image
132+
docker build -f Dockerfile.mercure-test -t go-deadlock-mercure-test .
133+
134+
# Run Mercure tests with synctest compatibility
135+
docker run --rm -v $(pwd):/src/go-deadlock go-deadlock-mercure-test
136+
```
137+
138+
This validates that go-deadlock works correctly with a real-world project using synctest.
139+
140+
## Migration Guide
141+
142+
### From Standard Library to go-deadlock
143+
144+
1. Import go-deadlock:
145+
```go
146+
import (
147+
"sync"
148+
deadlock "github.com/sasha-s/go-deadlock"
149+
)
150+
```
151+
152+
2. Replace mutex types:
153+
```go
154+
// Before
155+
var mu sync.Mutex
156+
var rwmu sync.RWMutex
157+
158+
// After
159+
var mu deadlock.Mutex
160+
var rwmu deadlock.RWMutex
161+
```
162+
163+
3. For automated replacement, use:
164+
```bash
165+
./mercure/tests/use-go-deadlock.sh
166+
```
167+
168+
### Adding Synctest to Existing Tests
169+
170+
If you have tests using go-deadlock and want to add synctest:
171+
172+
1. Ensure Go 1.25+
173+
2. Run tests with synctest tag:
174+
```bash
175+
GODEBUG=asynctimerchan=0 go test -tags=goexperiment.synctest -run TestYourTest
176+
```
177+
178+
3. Wrap test code in `synctest.Run()`:
179+
```go
180+
func TestExample(t *testing.T) {
181+
synctest.Run(func() {
182+
// Test code here
183+
})
184+
}
185+
```
186+
187+
### Production Deployment
188+
189+
For production builds where you want to disable deadlock detection:
190+
191+
```bash
192+
go build -tags=deadlock_disable -o myapp ./cmd/myapp
193+
```
194+
195+
This compiles out all deadlock detection code for zero overhead.
196+
197+
## Compatibility
198+
199+
- **Go 1.18+**: Full support including TryLock
200+
- **Go 1.25+**: Native synctest support
201+
- **Go < 1.18**: Basic support without TryLock
202+
203+
## Performance Characteristics
204+
205+
| Mode | Lock/Unlock Overhead | Memory Overhead | Timer Pool |
206+
|------|---------------------|-----------------|------------|
207+
| Normal | Medium (tracking + timers) | Medium (lock order map) | Enabled |
208+
| Synctest | Low-Medium (channels only) | Medium (lock order map) | Disabled |
209+
| Disabled | None (direct passthrough) | None | N/A |
210+
211+
## Troubleshooting
212+
213+
### "stop of synctest timer from outside bubble"
214+
215+
**Cause:** Mixing timers created inside and outside synctest bubbles.
216+
217+
**Solution:** Use `-tags=goexperiment.synctest` to enable compatibility mode.
218+
219+
### "synctest.Run not supported with asynctimerchan!=0"
220+
221+
**Cause:** Missing GODEBUG environment variable in Go 1.25+.
222+
223+
**Solution:** Set `GODEBUG=asynctimerchan=0` before running tests.
224+
225+
### Deadlock detection not triggering in synctest
226+
227+
**Cause:** Using standard mode instead of synctest compatibility mode.
228+
229+
**Solution:** Use `-tags=goexperiment.synctest` to enable channel-based mutexes.
230+
231+
## References
232+
233+
- [Go synctest documentation](https://pkg.go.dev/testing/synctest)
234+
- [Durable blocking in synctest](https://github.com/golang/go/issues/67434)
235+
- [Timer channel incompatibility](https://github.com/golang/go/issues/68686)

Dockerfile.mercure-test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM golang:1.25-alpine
2+
3+
# Install build dependencies
4+
RUN apk add --no-cache git bash
5+
6+
# Setup mercure in container
7+
WORKDIR /app
8+
9+
# Copy mercure source
10+
COPY mercure/ .
11+
12+
# Default command: Run tests with synctest compatibility mode
13+
# The go-deadlock directory should be mounted at /src/go-deadlock at runtime
14+
CMD ["sh", "-c", "\
15+
echo '=== Setting up go-deadlock replacement ===' && \
16+
go mod edit -replace=github.com/sasha-s/go-deadlock=/src/go-deadlock && \
17+
cd /app/caddy && go mod edit -replace=github.com/sasha-s/go-deadlock=/src/go-deadlock && cd /app && \
18+
echo '=== Replacing sync.Mutex with go-deadlock ===' && \
19+
bash /src/go-deadlock/mercure/tests/use-go-deadlock.sh && \
20+
echo '=== Running tests with synctest compatibility mode ===' && \
21+
GODEBUG=asynctimerchan=0 go test -tags goexperiment.synctest -timeout 30s -v . \
22+
"]

0 commit comments

Comments
 (0)