|
4 | 4 | "context" |
5 | 5 | "fmt" |
6 | 6 | "io" |
| 7 | + "net" |
7 | 8 | "net/http" |
| 9 | + "net/url" |
8 | 10 | "os" |
9 | 11 | "strings" |
10 | 12 | "time" |
@@ -124,10 +126,7 @@ func New(in *Input) (*Output, error) { |
124 | 126 | ).WaitForService(DEFAULT_RED_PANDA_SERVICE_NAME, |
125 | 127 | wait.ForAll( |
126 | 128 | wait.ForListeningPort(DEFAULT_RED_PANDA_SCHEMA_REGISTRY_PORT).WithPollInterval(100*time.Millisecond), |
127 | | - wait.ForListeningPort(DEFAULT_RED_PANDA_KAFKA_PORT).WithPollInterval(100*time.Millisecond), |
128 | 129 | wait.NewHostPortStrategy(DEFAULT_RED_PANDA_SCHEMA_REGISTRY_PORT).WithPollInterval(100*time.Millisecond), |
129 | | - wait.NewHostPortStrategy(DEFAULT_RED_PANDA_KAFKA_PORT).WithPollInterval(100*time.Millisecond), |
130 | | - wait.ForHTTP("/v1/status/ready").WithPort("9644"), // admin API port |
131 | 130 | wait.ForHTTP("/status/ready").WithPort(DEFAULT_RED_PANDA_SCHEMA_REGISTRY_PORT).WithPollInterval(100*time.Millisecond), |
132 | 131 | ).WithDeadline(2*time.Minute), |
133 | 132 | ).WaitForService(DEFAULT_RED_PANDA_CONSOLE_SERVICE_NAME, |
@@ -221,7 +220,7 @@ func New(in *Input) (*Output, error) { |
221 | 220 | in.UseCache = true |
222 | 221 | framework.L.Info().Msg("Chip Ingress stack started") |
223 | 222 |
|
224 | | - return output, nil |
| 223 | + return output, checkSchemaRegistryReadiness(2*time.Minute, 300*time.Millisecond, output.RedPanda.SchemaRegistryExternalURL, 3) |
225 | 224 | } |
226 | 225 |
|
227 | 226 | func composeFilePath(rawFilePath string) (string, error) { |
@@ -282,3 +281,86 @@ func connectNetwork(connCtx context.Context, timeout time.Duration, dockerClient |
282 | 281 | } |
283 | 282 | } |
284 | 283 | } |
| 284 | + |
| 285 | +// checkSchemaRegistryReadiness verifies that the Schema Registry answers 2xx on GET /subjects |
| 286 | +// for minSuccessCount *consecutive* attempts, polling every `interval`, with an overall `timeout`. |
| 287 | +func checkSchemaRegistryReadiness(timeout, interval time.Duration, registryURL string, minSuccessCount int) error { |
| 288 | + if minSuccessCount < 1 { |
| 289 | + minSuccessCount = 1 |
| 290 | + } |
| 291 | + u, uErr := url.Parse(registryURL) |
| 292 | + if uErr != nil { |
| 293 | + return fmt.Errorf("parse registry URL: %w", uErr) |
| 294 | + } |
| 295 | + var pErr error |
| 296 | + u.Path, pErr = url.JoinPath(u.Path, "/subjects") // keeps existing base path, adds /subjects |
| 297 | + if pErr != nil { |
| 298 | + return fmt.Errorf("join /subjects path: %w", pErr) |
| 299 | + } |
| 300 | + |
| 301 | + // Fresh connection per request; prefer IPv4 to avoid ::1 races. |
| 302 | + tr := &http.Transport{ |
| 303 | + DisableKeepAlives: true, |
| 304 | + DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { |
| 305 | + d := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second} |
| 306 | + return d.DialContext(ctx, "tcp4", addr) |
| 307 | + }, |
| 308 | + ForceAttemptHTTP2: false, // optional; stick to HTTP/1.1 for simplicity |
| 309 | + } |
| 310 | + client := &http.Client{ |
| 311 | + Transport: tr, |
| 312 | + Timeout: 10 * time.Second, // per-request timeout |
| 313 | + } |
| 314 | + |
| 315 | + ctx, cancel := context.WithTimeout(context.Background(), timeout) |
| 316 | + defer cancel() |
| 317 | + |
| 318 | + t := time.NewTicker(interval) |
| 319 | + defer t.Stop() |
| 320 | + |
| 321 | + consecutive := 0 |
| 322 | + var lastErr error |
| 323 | + |
| 324 | + for { |
| 325 | + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) |
| 326 | + // small belt-and-suspenders to ensure no reuse even if transport changes |
| 327 | + req.Close = true |
| 328 | + |
| 329 | + resp, err := client.Do(req) |
| 330 | + if err == nil { |
| 331 | + // Always drain & close. |
| 332 | + io.Copy(io.Discard, resp.Body) |
| 333 | + resp.Body.Close() |
| 334 | + } |
| 335 | + |
| 336 | + if err == nil && resp.StatusCode/100 == 2 { |
| 337 | + framework.L.Debug().Msgf("schema registry ready check succeeded with status %d (%d/%d)", resp.StatusCode, consecutive+1, minSuccessCount) |
| 338 | + consecutive++ |
| 339 | + if consecutive >= minSuccessCount { |
| 340 | + framework.L.Debug().Msg("schema registry is ready") |
| 341 | + return nil |
| 342 | + } |
| 343 | + } else { |
| 344 | + consecutive = 0 |
| 345 | + if err != nil { |
| 346 | + framework.L.Debug().Msgf("schema registry ready check failed with error %v (need %d/%d consecutive successes)", err, consecutive, minSuccessCount) |
| 347 | + lastErr = fmt.Errorf("GET /subjects failed: %w", err) |
| 348 | + } else { |
| 349 | + framework.L.Debug().Msgf("schema registry ready check failed with error %v and status code %d (need %d/%d consecutive successes)", err, resp.StatusCode, consecutive, minSuccessCount) |
| 350 | + lastErr = fmt.Errorf("GET /subjects status %d", resp.StatusCode) |
| 351 | + } |
| 352 | + } |
| 353 | + |
| 354 | + select { |
| 355 | + case <-ctx.Done(): |
| 356 | + if lastErr == nil { |
| 357 | + lastErr = ctx.Err() |
| 358 | + } |
| 359 | + return fmt.Errorf("schema registry not ready after %s; needed %d consecutive successes (got %d): %w", |
| 360 | + timeout, minSuccessCount, consecutive, lastErr) |
| 361 | + case <-t.C: |
| 362 | + framework.L.Debug().Msg("schema registry not ready yet, retrying...") |
| 363 | + // poll again |
| 364 | + } |
| 365 | + } |
| 366 | +} |
0 commit comments