Phase 2 of the deploy-hardening I master bundle. Closes the agent-side
race window where two concurrent renewals against the same target ID
(typical: two SAN entries renewing in the same window) would otherwise
collide on the connector's temp-file path or run the reload command
against itself.
The Agent struct grows a sync.Map of *sync.Mutex keyed on target ID;
targetDeployMutex(targetID) lazy-init's one on first acquisition.
executeDeploymentJob acquires the mutex before connector.DeployCertificate
and releases via defer at function exit — the lock spans the full
Deploy duration including PreCommit (validate), atomic-rename, PostCommit
(reload), and post-deploy verify (Phases 4-9).
Granularity per frozen decision 0.5: one mutex per target ID, NOT per
(target, cert) pair. Cert deploy throughput is operator-grade
tens-per-minute; coarse serialization simplifies reasoning about
reload-side race windows. Mutexes live for the agent's lifetime —
target IDs are bounded so no janitor needed (~16 bytes per entry).
Empty TargetID (defensive — should never happen for deploy jobs)
bypasses the lock to avoid a singleton serialization point pulling
all targetless work onto a shared mutex.
Tests (5 named cases in cmd/agent/deploy_mutex_test.go):
- TestAgent_ConcurrentDeploysToSameTarget_Serialize — race-detector
smoke; 10 goroutines acquire same target's mutex; max-in-flight
asserts == 1
- TestAgent_DifferentTargetIDs_ParallelizeIndependently — per-target
granularity proof
- TestAgent_EmptyTargetID_ReturnsNilMutex — defensive contract
- TestAgent_TargetMutex_IsStable — sync.Map LoadOrStore returns same
pointer across calls
- TestAgent_TargetMutex_RaceLookup — race-free under N=50 concurrent
lookups for same key
go test -race -count=1 green; gofmt + go vet + golangci-lint v2.11.4
all 0 issues against my new code (pre-existing import-grouping drift
in agent_test.go / main.go / verify*.go is unrelated to this change
and not caught by `go fmt ./...` which CI uses).
Phase 3 next: ValidateOnly method on target.Connector interface;
default impl returns ErrValidateOnlyNotSupported across all 13
connectors.