@@ -402,9 +402,10 @@ func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
404 if needsStart {
404 if needsStart {
405− if err := startDetachedServer(cmd); err != nil {
405+ if err := spawnAndWaitReady(cmd, hostURL); err != nil {
406− return err
406+ return fmt.Errorf("failed to initialize crush server: %v", err)
410 if err := waitForServerReady(cmd.Context(), hostURL); err != nil {
411 if err := waitForServerReady(cmd.Context(), hostURL); err != nil {
@@ -415,6 +416,70 @@ func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
415 return nil
416 return nil
419+// spawnAndWaitReady serializes the spawn-and-wait-for-readiness sequence
420+// across concurrent clients via an exclusive flock on
421+// $XDG_CACHE_HOME/crush/server-<safeHost>/start.lock.
423+// After acquiring the lock it re-probes readiness so that a client that
424+// blocked while another client was spawning can skip its own spawn and
425+// just use the now-running server. The lock is held only for the
426+// duration of "spawn + readiness probe" and released before the caller
427+// resumes its normal lifetime.
428+func spawnAndWaitReady(cmd *cobra.Command, hostURL *url.URL) error {
429+ chDir, err := perHostServerDir()
433+ release, err := acquireSpawnLock(filepath.Join(chDir, "start.lock"))
435+ // If the lock itself is unavailable, fall back to the
436+ // unsynchronized path rather than blocking the user.
437+ slog.Warn("Failed to acquire spawn lock, proceeding without single-flight", "error", err)
438+ if err := startDetachedServer(cmd); err != nil {
441+ return waitForServerReady(cmd.Context(), hostURL)
445+ // Another client may have just finished spawning while we were
446+ // waiting on the lock; if the server is already responsive, skip
447+ // the spawn entirely.
448+ probeCtx, cancel := context.WithTimeout(cmd.Context(), 200*time.Millisecond)
449+ probeErr := quickHealthProbe(probeCtx, hostURL)
451+ if probeErr == nil {
455+ if err := startDetachedServer(cmd); err != nil {
458+ return waitForServerReady(cmd.Context(), hostURL)
461+// quickHealthProbe issues a single readiness request with the caller's
462+// context and returns nil iff the server is responsive right now.
463+func quickHealthProbe(ctx context.Context, hostURL *url.URL) error {
464+ httpClient, reqURL, err := readinessHTTPClient(hostURL)
468+ return probeHealth(ctx, httpClient, reqURL, hostURL)
471+// perHostServerDir returns (and creates) the cache directory used for
472+// per-host server state (logs, start.lock, etc.). It mirrors the path
473+// computed in startDetachedServer so both code paths stay in sync.
474+func perHostServerDir() (string, error) {
475+ safeClientHost := safeNameRegexp.ReplaceAllString(clientHost, "_")
476+ chDir := filepath.Join(config.GlobalCacheDir(), "server-"+safeClientHost)
477+ if err := os.MkdirAll(chDir, 0o700); err != nil {
478+ return "", fmt.Errorf("failed to create server working directory: %v", err)
418// serverReadyTimeout returns the total budget for the readiness probe.
483// serverReadyTimeout returns the total budget for the readiness probe.
419// Overridable via CRUSH_SERVER_READY_TIMEOUT (parsed as a Go duration).
484// Overridable via CRUSH_SERVER_READY_TIMEOUT (parsed as a Go duration).
420func serverReadyTimeout() time.Duration {
485func serverReadyTimeout() time.Duration {
@@ -572,10 +637,9 @@ func startDetachedServer(cmd *cobra.Command) error {
572 return fmt.Errorf("failed to get executable path: %v", err)
637 return fmt.Errorf("failed to get executable path: %v", err)
575− safeClientHost := safeNameRegexp.ReplaceAllString(clientHost, "_")
640+ chDir, err := perHostServerDir()
576− chDir := filepath.Join(config.GlobalCacheDir(), "server-"+safeClientHost)
577− if err := os.MkdirAll(chDir, 0o700); err != nil {
641+ if err != nil {
578− return fmt.Errorf("failed to create server working directory: %v", err)
642+ return err
581 cmdArgs := []string{"server"}
645 cmdArgs := []string{"server"}