fix(run): scope micro run as a dev tool and harden the dev loop - #4875
Conversation
micro run is the local development inner loop, not a production runtime. Make that explicit and fix real dev-UX bugs: - Clean shutdown: the new-service scanner shared the signal channel with the main wait; a signal goes to one receiver, so it could steal Ctrl-C and hang teardown. Give background goroutines a dedicated shutdown channel and guard the services slice with a mutex (also fixes a data race on that slice during shutdown). - Hot reload no longer takes a service offline on a compile error: build into a temp binary first and only swap+restart if the build succeeds; on failure the previous version keeps serving and the error is printed. - Raise the log scanner buffer so long lines (JSON logs, stack traces) no longer overflow it and silently drop a services logs. - Help text and the micro run guide now state plainly it is a dev tool with no daemon, and point to systemd/Docker/Kubernetes for production. Remove the guides fictional micro logs/status/stop commands (no such commands exist) and describe the real lifecycle (Ctrl-C, log files).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 218bb4f488
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // caller reports the error and the previous version keeps serving. | ||
| func (s *serviceProcess) reload(logDir string) error { | ||
| newBin := s.binPath + ".new" | ||
| if err := s.build(newBin); err != nil { |
There was a problem hiding this comment.
Prevent reload from launching after shutdown
When Ctrl-C or SIGTERM arrives while this out-of-lock build is running, teardown can stop the old process and finish before the build completes. The reload goroutine then resumes, sees nothing running in s.stop(), swaps the binary, and calls s.launch, leaving a service process alive after micro run exits. watch.Stop() does not join the event-handler goroutine, so reloads need to be coordinated with shutdown or awaited before teardown returns.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR reframes micro run explicitly as a local development inner-loop tool and hardens its hot-reload supervision behavior to improve dev UX and shutdown correctness.
Changes:
- Updates
micro runservice supervision to avoid signal-stealing during shutdown, reduce races around service discovery, and improve hot reload behavior (build-then-swap so compile errors don’t take services offline). - Increases per-service log scan buffer limits to better handle long log lines without dropping output.
- Revises the
micro rundocumentation/help text to reflect the real lifecycle (no daemon) and provide production guidance.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| internal/website/content/en/docs/guides/micro-run.md | Updates the guide to clearly position micro run as a dev tool (no daemon), removes fictional commands, and adds a production-oriented section. |
| cmd/micro/run/run.go | Hardens the dev loop: build-then-swap reload behavior, dedicated shutdown signaling, mutex-guarded service tracking, and larger log scanner buffer. |
Suppressed comments (2)
cmd/micro/run/run.go:474
- The new-service scanner can still start services after shutdown begins: if the ticker.C case is already selected when
shutdownis closed, the goroutine will append toservicesand thensp.start()even during teardown. In the worst case this can leave a newly started process running aftermicro runexits (e.g., if it starts after the stop-snapshot loop has already run). Add ashutdowncheck before starting each discovered service to ensure no new processes are launched once teardown begins.
for _, sp := range newSvcs {
watch.AddDir(sp.dir)
if err := sp.start(logsDir); err != nil {
fmt.Fprintf(os.Stderr, "[%s] %v\n", sp.name, err)
continue
}
fmt.Printf("\n \033[32m●\033[0m %s \033[2m(new)\033[0m\n", sp.name)
cmd/micro/run/run.go:121
- The log streaming goroutine never checks
scanner.Err(). If scanning stops due to an error (e.g., a line still exceeds the max token size), logs will stop without any indication even though this change is explicitly trying to avoid “silent” log loss. Emit the scan error so users know why logs stopped for a service.
for scanner.Scan() {
line := scanner.Text()
fmt.Printf("%s[%s]%s %s\n", color, name, colorReset, line)
_, _ = logFile.WriteString("[" + name + "] " + line + "\n")
}
}(s.name, s.color, pr, logFile)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| s.stop() | ||
| return s.start(logDir) | ||
| if err := os.Rename(newBin, s.binPath); err != nil { | ||
| return fmt.Errorf("swap binary: %w", err) | ||
| } | ||
| return s.launch(logDir) |
Why
micro runis the local development inner loop — build, run, hot-reload — not a production runtime. There's no daemon; processes stop when it exits. This makes that explicit and fixes real dev-UX bugs in the loop.Changes
Correctness / UX
Ctrl-Cand hang teardown. Background goroutines now use a dedicatedshutdownchannel, and theservices/servicesByDirstate is guarded by a mutex (also fixing a data race on the slice during shutdown).Honest framing (dev tool, no daemon)
micro run --helpnow states plainly it's a local dev tool with no daemon, and points to systemd / Docker / Kubernetes for production.micro runguide removes the fictionalmicro logs/micro status/micro stopcommands (no such commands exist), describes the real lifecycle (Ctrl-C,~/micro/logs/*.log), and adds a "Going to production" section.Verification
go build ./cmd/micro/...,go vet ./cmd/micro/run/, andgo test -race ./cmd/micro/run/...all pass.micro run --helpreflects the dev-tool framing.Not included (pending a separate decision)
We're still deciding whether
micro servershould exist as a distinct command or be folded into a single deployable "gateway." The docs here are intentionally neutral on that naming — this PR doesn't entrenchmicro server. The gateway/command-surface change will be its own PR.🤖 Generated with Claude Code
Generated by Claude Code