mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
go.mod still declared `module github.com/hanzoai/deploy` after the repo was renamed. A Go module path is not cosmetic — it is the resolvable location. So `go get github.com/hanzoai/cd` could not work, and anything importing this module had to spell a name that no longer exists at that address. The build log of v3.6.0 is where it surfaced: the compiler was still emitting github.com/hanzoai/deploy/cmd/cd/commands. 3541 import paths across 690 .go, 19 .proto, 4 .yaml, 3 .sh, 2 .mod, Procfile and Makefile. Plus the 62 ghcr.io/hanzoai/deploy image references, moved to ghcr.io/hanzoai/cd to match hanzo.yml's build target — a distinct string from the module path, replaced separately so neither could clobber the other. go mod tidy clean, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./... clean. Co-authored-by: Hanzo Dev <[email protected]>
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"k8s.io/client-go/util/workqueue"
|
|
|
|
hydratortypes "github.com/hanzoai/cd/controller/hydrator/types"
|
|
"github.com/hanzoai/cd/pkg/ratelimiter"
|
|
)
|
|
|
|
// TestNormalizeHydrationProcessors verifies that the hydration worker count is clamped to a safe minimum.
|
|
// The --hydration-processors CLI flag can be set to 0 or a negative value (the env default is clamped, but
|
|
// an explicit flag value is not), which would otherwise start zero workers and silently stall hydration.
|
|
// See https://github.com/argoproj/argo-cd/issues/27926.
|
|
func TestNormalizeHydrationProcessors(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
configured int
|
|
want int
|
|
}{
|
|
{"zero clamps to one", 0, 1},
|
|
{"negative clamps to one", -3, 1},
|
|
{"one stays one", 1, 1},
|
|
{"default preserved", 5, 5},
|
|
{"large value preserved", 50, 50},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, tc.want, normalizeHydrationProcessors(tc.configured))
|
|
})
|
|
}
|
|
}
|
|
|
|
func newTestHydrationQueue() workqueue.TypedRateLimitingInterface[hydratortypes.HydrationQueueKey] {
|
|
return workqueue.NewTypedRateLimitingQueue(
|
|
ratelimiter.NewCustomAppControllerRateLimiter[hydratortypes.HydrationQueueKey](ratelimiter.GetDefaultAppRateLimiterConfig()),
|
|
)
|
|
}
|
|
|
|
// TestHydrationQueue_DistinctKeysProcessConcurrently asserts the core feature claim of #27926: when the
|
|
// hydration queue is drained by multiple workers, distinct hydration keys are processed concurrently, while
|
|
// no single key is ever processed by two workers at the same time. The worker loop mirrors the one started
|
|
// in ApplicationController.Run.
|
|
func TestHydrationQueue_DistinctKeysProcessConcurrently(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
queue := newTestHydrationQueue()
|
|
t.Cleanup(queue.ShutDown)
|
|
|
|
const numKeys = 12
|
|
const numWorkers = 4
|
|
for i := range numKeys {
|
|
queue.Add(hydratortypes.HydrationQueueKey{SourceRepoURL: fmt.Sprintf("https://example.com/repo-%d", i)})
|
|
}
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
inFlight int
|
|
maxConcurrent int
|
|
processed int
|
|
perKeyActive = map[hydratortypes.HydrationQueueKey]bool{}
|
|
sameKeyOverlap bool
|
|
)
|
|
|
|
processNext := func() bool {
|
|
key, shutdown := queue.Get()
|
|
if shutdown {
|
|
return false
|
|
}
|
|
defer queue.Done(key)
|
|
|
|
mu.Lock()
|
|
if perKeyActive[key] {
|
|
sameKeyOverlap = true
|
|
}
|
|
perKeyActive[key] = true
|
|
inFlight++
|
|
if inFlight > maxConcurrent {
|
|
maxConcurrent = inFlight
|
|
}
|
|
mu.Unlock()
|
|
|
|
// Hold the key briefly so that concurrent processing of distinct keys is observable.
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
inFlight--
|
|
perKeyActive[key] = false
|
|
processed++
|
|
allDone := processed == numKeys
|
|
mu.Unlock()
|
|
|
|
if allDone {
|
|
queue.ShutDown()
|
|
}
|
|
return true
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for range numWorkers {
|
|
wg.Go(func() {
|
|
for processNext() {
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
assert.Equal(t, numKeys, processed, "every distinct key should be processed exactly once")
|
|
assert.False(t, sameKeyOverlap, "the same hydration key must never be processed by two workers at once")
|
|
assert.GreaterOrEqual(t, maxConcurrent, 2, "distinct hydration keys should be processed concurrently with multiple workers")
|
|
}
|
|
|
|
// TestHydrationQueue_SameKeyNotProcessedConcurrently asserts the dedup guarantee the feature relies on: a
|
|
// hydration key that is re-enqueued while it is still in-flight is withheld until the in-flight processing
|
|
// calls Done, so the same key is never handed to two workers simultaneously. See #27926.
|
|
func TestHydrationQueue_SameKeyNotProcessedConcurrently(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
queue := newTestHydrationQueue()
|
|
t.Cleanup(queue.ShutDown)
|
|
|
|
key := hydratortypes.HydrationQueueKey{SourceRepoURL: "https://example.com/repo", DestinationBranch: "env/dev"}
|
|
queue.Add(key)
|
|
|
|
started := make(chan struct{})
|
|
release := make(chan struct{})
|
|
secondPickup := make(chan hydratortypes.HydrationQueueKey, 1)
|
|
|
|
// Worker 1 takes the key and holds it (does not call Done) until released.
|
|
go func() {
|
|
k, shutdown := queue.Get()
|
|
if shutdown {
|
|
return
|
|
}
|
|
close(started)
|
|
<-release
|
|
queue.Done(k)
|
|
}()
|
|
|
|
<-started
|
|
// Re-enqueue the same key while it is still in-flight. The workqueue must not hand it to another
|
|
// worker until the first one calls Done.
|
|
queue.Add(key)
|
|
|
|
// Worker 2 attempts to take work; it must block while the key is in-flight.
|
|
go func() {
|
|
k, shutdown := queue.Get()
|
|
if !shutdown {
|
|
secondPickup <- k
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-secondPickup:
|
|
t.Fatal("the same hydration key was delivered to a second worker while still in-flight")
|
|
case <-time.After(150 * time.Millisecond):
|
|
// Expected: the re-added key is withheld until the first worker calls Done.
|
|
}
|
|
|
|
// Release the first worker; the re-added key should now become available to worker 2.
|
|
close(release)
|
|
select {
|
|
case got := <-secondPickup:
|
|
assert.Equal(t, key, got)
|
|
queue.Done(got)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("re-added key was not delivered after the first worker finished")
|
|
}
|
|
}
|