|
| 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) |
0 commit comments