fix: make internals yaegi-interpretable for traefik plugin consumers

Swap atomic.Pointer[string] for a mutex-guarded string, atomic.Bool for the function-based sync/atomic int32 API, two range-over-int loops for classic index loops, and drop the init() that seeded the endpoint pointer. No public API or runtime behaviour change; go test, go vet, and golangci-lint all pass.

Rationale: the traefikoidc Traefik plugin vendors and interprets dependency source via Yaegi, which on several Traefik runtimes cannot interpret generic stdlib types or range-over-int. This keeps the package importable from that plugin without altering what it does.
This commit is contained in:
2026-05-30 12:35:42 +01:00
parent 1fb9f14efa
commit f06f5561d4
2 changed files with 36 additions and 23 deletions
+28 -14
View File
@@ -49,21 +49,35 @@ const (
maxVersionLen = 32 maxVersionLen = 32
) )
// endpoint holds the ingest URL. Production code never mutates it; it is // Yaegi note: this package is consumed by the traefikoidc Traefik plugin, which
// atomic only so the package's own test suite can safely retarget it at // Traefik interprets with Yaegi (it vendors and interprets dependency source).
// httptest servers while goroutines started by Send are still in flight. // It therefore avoids generic stdlib types (atomic.Pointer[T], atomic.Bool) and
var endpoint atomic.Pointer[string] // range-over-int (Go 1.22), which some Traefik/Yaegi runtimes cannot interpret.
// Endpoint mutation uses a mutex-guarded string; the disabled flag uses the
// function-based sync/atomic int32 API (atomic.LoadInt32/StoreInt32).
var ( var (
disabled atomic.Bool // endpointURL holds the ingest URL. Production code never mutates it; the
// setter exists only so the test suite can retarget it at httptest servers
// while goroutines started by Send are still in flight.
endpointMu sync.RWMutex
endpointURL = defaultEndpoint
disabled int32 // 0 = enabled, 1 = disabled; accessed via sync/atomic only
inflight sync.WaitGroup inflight sync.WaitGroup
client = &http.Client{Timeout: httpTimeout} client = &http.Client{Timeout: httpTimeout}
) )
func init() { func currentEndpoint() string {
s := defaultEndpoint endpointMu.RLock()
endpoint.Store(&s) defer endpointMu.RUnlock()
return endpointURL
}
func setEndpointURL(u string) {
endpointMu.Lock()
endpointURL = u
endpointMu.Unlock()
} }
// Send fires a single anonymous telemetry ping in the background and returns // Send fires a single anonymous telemetry ping in the background and returns
@@ -78,7 +92,7 @@ func init() {
// Call once at program startup. Calling repeatedly will send repeated pings; // Call once at program startup. Calling repeatedly will send repeated pings;
// the server is responsible for deduplication. // the server is responsible for deduplication.
func Send(project, version string) { func Send(project, version string) {
if disabled.Load() { if atomic.LoadInt32(&disabled) != 0 {
return return
} }
if isDisabledByEnv(project) { if isDisabledByEnv(project) {
@@ -144,7 +158,7 @@ func isUsableBuildVersion(v string) bool {
// Disable suppresses all subsequent Send calls in this process. // Disable suppresses all subsequent Send calls in this process.
// Idempotent and safe to call from any goroutine. // Idempotent and safe to call from any goroutine.
func Disable() { func Disable() {
disabled.Store(true) atomic.StoreInt32(&disabled, 1)
} }
// Wait blocks until all in-flight pings have completed, or until timeout // Wait blocks until all in-flight pings have completed, or until timeout
@@ -173,7 +187,7 @@ func dispatch(project, version string) {
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, *endpoint.Load(), bytes.NewReader(body)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, currentEndpoint(), bytes.NewReader(body))
if err != nil { if err != nil {
return return
} }
@@ -208,7 +222,7 @@ func validProject(p string) bool {
if n == 0 || n > maxProjectLen { if n == 0 || n > maxProjectLen {
return false return false
} }
for i := range n { for i := 0; i < n; i++ {
c := p[i] c := p[i]
switch { switch {
case c >= 'a' && c <= 'z', case c >= 'a' && c <= 'z',
@@ -330,7 +344,7 @@ func isDisabledByEnv(project string) bool {
func projectEnvKey(project string) string { func projectEnvKey(project string) string {
const suffix = "_DISABLE_TELEMETRY" const suffix = "_DISABLE_TELEMETRY"
buf := make([]byte, 0, len(project)+len(suffix)) buf := make([]byte, 0, len(project)+len(suffix))
for i := range len(project) { for i := 0; i < len(project); i++ {
c := project[i] c := project[i]
switch { switch {
case c == '-': case c == '-':
+8 -9
View File
@@ -25,22 +25,21 @@ type pingPayload struct {
// each other. Tests share `disabled`, `endpoint`, and `inflight`. // each other. Tests share `disabled`, `endpoint`, and `inflight`.
func resetState(t *testing.T) { func resetState(t *testing.T) {
t.Helper() t.Helper()
disabled.Store(false) atomic.StoreInt32(&disabled, 0)
t.Cleanup(func() { t.Cleanup(func() {
disabled.Store(false) atomic.StoreInt32(&disabled, 0)
}) })
} }
// setEndpoint atomically retargets the ingest URL for the duration of a test // setEndpoint retargets the ingest URL for the duration of a test and restores
// and restores it on cleanup. Safe even if Send goroutines outlive the test. // it on cleanup. Safe even if Send goroutines outlive the test (the setter is
// mutex-guarded).
func setEndpoint(t *testing.T, url string) { func setEndpoint(t *testing.T, url string) {
t.Helper() t.Helper()
prev := *endpoint.Load() prev := currentEndpoint()
s := url setEndpointURL(url)
endpoint.Store(&s)
t.Cleanup(func() { t.Cleanup(func() {
p := prev setEndpointURL(prev)
endpoint.Store(&p)
}) })
} }