Skip to content

Commit e4df4fe

Browse files
authored
Merge pull request #455 from Caesarsage/fix/start-readiness-wait
fix(start): wait for Microcks server readiness before returning
2 parents 814e7cc + abdf25b commit e4df4fe

2 files changed

Lines changed: 96 additions & 5 deletions

File tree

cmd/start.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cmd
33
import (
44
"fmt"
55
"log"
6+
"net/http"
7+
"time"
68

79
"github.com/microcks/microcks-cli/pkg/config"
810
"github.com/microcks/microcks-cli/pkg/connectors"
@@ -12,11 +14,13 @@ import (
1214

1315
func NewStartCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command {
1416
var (
15-
name string
16-
hostPort string
17-
imageName string
18-
autoRemove bool
19-
driver string
17+
name string
18+
hostPort string
19+
imageName string
20+
autoRemove bool
21+
driver string
22+
readyTimeout time.Duration
23+
noWait bool
2024
)
2125
var startCmd = &cobra.Command{
2226
Use: "start",
@@ -140,6 +144,17 @@ microcks start --name [name of you container/instance]`,
140144
err = config.WriteLocalConfig(*localConfig, configFile)
141145
errors.CheckError(err)
142146

147+
// The container being up doesn't mean the Microcks server inside
148+
// is serving traffic yet: wait until HTTP is actually answering
149+
// so chained commands (import, test) don't race the boot.
150+
if !noWait {
151+
fmt.Printf("Waiting for Microcks to be ready at %s ...\n", server)
152+
if err := waitForReady(server, readyTimeout); err != nil {
153+
log.Fatalf("Microcks container is started but the server is not ready: %v. "+
154+
"It may still be booting — retry shortly or raise --ready-timeout.", err)
155+
}
156+
}
157+
143158
fmt.Printf("Microcks started successfully at %s\n", server)
144159
},
145160
}
@@ -148,5 +163,28 @@ microcks start --name [name of you container/instance]`,
148163
startCmd.Flags().StringVar(&imageName, "image", "quay.io/microcks/microcks-uber:latest-native", "image which will be used to create a container")
149164
startCmd.Flags().BoolVar(&autoRemove, "rm", false, "mimic of '--rm' flag of Docker to automatically remove the container when it exits")
150165
startCmd.Flags().StringVar(&driver, "driver", "docker", "use --driver to change driver from docker to podman")
166+
startCmd.Flags().DurationVar(&readyTimeout, "ready-timeout", 60*time.Second, "how long to wait for the Microcks server to be ready before failing")
167+
startCmd.Flags().BoolVar(&noWait, "no-wait", false, "return as soon as the container is started, without waiting for the Microcks server to be ready")
151168
return startCmd
152169
}
170+
171+
// waitForReady polls the Microcks API until it answers with 200 or the
172+
// timeout elapses. HTTP being up is the signal users care about — the
173+
// Spring Boot app inside the container takes a while after the container
174+
// process itself is running.
175+
func waitForReady(serverURL string, timeout time.Duration) error {
176+
url := serverURL + "/api/keycloak/config"
177+
httpClient := &http.Client{Timeout: 2 * time.Second}
178+
deadline := time.Now().Add(timeout)
179+
for time.Now().Before(deadline) {
180+
resp, err := httpClient.Get(url)
181+
if err == nil {
182+
resp.Body.Close()
183+
if resp.StatusCode == http.StatusOK {
184+
return nil
185+
}
186+
}
187+
time.Sleep(500 * time.Millisecond)
188+
}
189+
return fmt.Errorf("not ready after %s", timeout)
190+
}

cmd/start_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cmd
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"sync/atomic"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestWaitForReadyImmediate(t *testing.T) {
12+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
if r.URL.Path != "/api/keycloak/config" {
14+
t.Errorf("unexpected path %s", r.URL.Path)
15+
}
16+
w.WriteHeader(http.StatusOK)
17+
}))
18+
defer server.Close()
19+
20+
if err := waitForReady(server.URL, 5*time.Second); err != nil {
21+
t.Errorf("expected ready, got error: %v", err)
22+
}
23+
}
24+
25+
func TestWaitForReadyAfterRetries(t *testing.T) {
26+
var calls atomic.Int32
27+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
if calls.Add(1) < 3 {
29+
w.WriteHeader(http.StatusServiceUnavailable)
30+
return
31+
}
32+
w.WriteHeader(http.StatusOK)
33+
}))
34+
defer server.Close()
35+
36+
if err := waitForReady(server.URL, 10*time.Second); err != nil {
37+
t.Errorf("expected ready after retries, got error: %v", err)
38+
}
39+
if calls.Load() < 3 {
40+
t.Errorf("expected at least 3 polls, got %d", calls.Load())
41+
}
42+
}
43+
44+
func TestWaitForReadyTimeout(t *testing.T) {
45+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46+
w.WriteHeader(http.StatusServiceUnavailable)
47+
}))
48+
defer server.Close()
49+
50+
if err := waitForReady(server.URL, 1*time.Second); err == nil {
51+
t.Error("expected timeout error, got nil")
52+
}
53+
}

0 commit comments

Comments
 (0)