Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 82 additions & 21 deletions cmd/micro/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,27 @@ type serviceProcess struct {
running bool
}

func (s *serviceProcess) start(logDir string) error {
// build compiles the service to the given output path. It is slow (invokes the
// Go toolchain) and must be called WITHOUT holding s.mu, so the running process
// keeps serving while a rebuild is in flight.
func (s *serviceProcess) build(out string) error {
buildCmd := exec.Command("go", "build", "-o", out, ".")
buildCmd.Dir = s.dir
if buildOut, err := buildCmd.CombinedOutput(); err != nil {
return fmt.Errorf("build failed: %s\n%s", err, string(buildOut))
}
return nil
}

// launch starts the already-built binary at s.binPath and streams its output.
func (s *serviceProcess) launch(logDir string) error {
s.mu.Lock()
defer s.mu.Unlock()

if s.running {
return nil
}

// Build
buildCmd := exec.Command("go", "build", "-o", s.binPath, ".")
buildCmd.Dir = s.dir
buildOut, buildErr := buildCmd.CombinedOutput()
if buildErr != nil {
return fmt.Errorf("build failed: %s\n%s", buildErr, string(buildOut))
}

// Open log file
logFile, err := os.OpenFile(s.logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
Expand All @@ -101,10 +106,13 @@ func (s *serviceProcess) start(logDir string) error {
s.cmd.Stdout = pw
s.cmd.Stderr = pw

// Stream output
// Stream output. The larger buffer keeps long lines (JSON logs, stack
// traces) from overflowing the scanner and silently dropping a service's
// logs from that point on.
go func(name string, color string, pr *io.PipeReader, logFile *os.File) {
defer logFile.Close()
scanner := bufio.NewScanner(pr)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
fmt.Printf("%s[%s]%s %s\n", color, name, colorReset, line)
Expand All @@ -127,6 +135,14 @@ func (s *serviceProcess) start(logDir string) error {
return nil
}

// start builds the service and launches it (initial start).
func (s *serviceProcess) start(logDir string) error {
if err := s.build(s.binPath); err != nil {
return err
}
return s.launch(logDir)
}

func (s *serviceProcess) stop() {
s.mu.Lock()
defer s.mu.Unlock()
Expand Down Expand Up @@ -161,9 +177,21 @@ func (s *serviceProcess) stop() {
s.running = false
}

func (s *serviceProcess) restart(logDir string) error {
// reload rebuilds the service and swaps in the new binary ONLY if the build
// succeeds. A failing build (a typo, a broken import) leaves the running
// process untouched, so a compile error never takes the service offline — the
// 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

_ = os.Remove(newBin)
return err
}
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)
Comment on lines 190 to +194
}

// waitForHealth waits for a service's health endpoint to respond
Expand Down Expand Up @@ -381,6 +409,16 @@ func Run(c *cli.Context) error {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)

// shutdown is closed once when teardown begins. Background goroutines watch
// it instead of sigCh — a signal is delivered to only one channel receiver,
// so sharing sigCh would let a goroutine steal the interrupt and hang the
// main wait below.
shutdown := make(chan struct{})

// svcMu guards services/servicesByDir, which the new-service scanner mutates
// concurrently with the shutdown loop.
var svcMu sync.Mutex

// Watch mode
watchEnabled := !c.Bool("no-watch")
var watch *watcher.Watcher
Expand All @@ -396,11 +434,17 @@ func Run(c *cli.Context) error {

go func() {
for event := range watch.Events() {
if svc, ok := servicesByDir[event.Dir]; ok {
fmt.Printf("%s[%s]%s rebuilding...\n", svc.color, svc.name, colorReset)
if err := svc.restart(logsDir); err != nil {
fmt.Fprintf(os.Stderr, "%s[%s]%s restart failed: %v\n", svc.color, svc.name, colorReset, err)
}
svcMu.Lock()
svc, ok := servicesByDir[event.Dir]
svcMu.Unlock()
if !ok {
continue
}
fmt.Printf("%s[%s]%s rebuilding...\n", svc.color, svc.name, colorReset)
if err := svc.reload(logsDir); err != nil {
// Build failed — the previous version is still serving.
fmt.Fprintf(os.Stderr, "%s[%s]%s build failed, keeping previous version running:\n%v\n",
svc.color, svc.name, colorReset, err)
}
}
}()
Expand All @@ -411,13 +455,17 @@ func Run(c *cli.Context) error {
defer ticker.Stop()
for {
select {
case <-sigCh:
case <-shutdown:
return
case <-ticker.C:
svcMu.Lock()
newSvcs := discoverNewServices(absDir, servicesByDir, binDir, runDir, logsDir, envVars, len(services))
for _, sp := range newSvcs {
services = append(services, sp)
servicesByDir[sp.dir] = sp
}
svcMu.Unlock()
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)
Expand All @@ -438,6 +486,8 @@ func Run(c *cli.Context) error {
}
fmt.Println("\nShutting down...")

close(shutdown)

if watch != nil {
watch.Stop()
}
Expand All @@ -446,9 +496,14 @@ func Run(c *cli.Context) error {
_ = gw.Stop()
}

// Stop services in reverse order
for i := len(services) - 1; i >= 0; i-- {
services[i].stop()
// Stop services in reverse order. Snapshot under the lock — the scanner
// goroutine may still be appending as teardown begins.
svcMu.Lock()
all := make([]*serviceProcess, len(services))
copy(all, services)
svcMu.Unlock()
for i := len(all) - 1; i >= 0; i-- {
all[i].stop()
}

return nil
Expand Down Expand Up @@ -688,6 +743,12 @@ Starts an HTTP gateway on :8080 providing:
With a micro.mu or micro.json config file, services start in dependency order.
Without config, all main.go files are discovered and run.

micro run is a local development tool — it builds and supervises service
processes with hot reload. It is not a production runtime: there is no daemon,
and processes stop when micro run exits. For production, build each service
(go build) and run it under a process manager or scheduler — systemd,
Docker/Compose, or Kubernetes (see deploy/kubernetes).

Examples:
micro run # Run with gateway on :8080
micro run --address :3000 # Gateway on custom port
Expand Down
42 changes: 31 additions & 11 deletions internal/website/content/en/docs/guides/micro-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ title: "micro run - Local Development"

> **Note**: This guide focuses on `micro run` features. For a comparison with `micro server` and gateway architecture details, see the [CLI & Gateway Guide](cli-gateway.md).

> **`micro run` is a development tool.** It builds and supervises your service processes locally with hot reload. There is no daemon — everything stops when `micro run` exits. For running services in production, see [Going to production](#going-to-production).

## Quick Start

```bash
Expand Down Expand Up @@ -191,22 +193,24 @@ If no `micro.mu` or `micro.json` exists:

## Logs

Service logs are written to:
- Terminal: Colorized with service name prefix
- File: `~/micro/logs/{service}-{hash}.log`
Every service streams to the terminal running `micro run`, colorized and
prefixed with the service name. The same output is also written to a file:

View logs:
```bash
micro logs # List available logs
micro logs users # Show logs for 'users' service
tail -f ~/micro/logs/users-*.log # one file per service: {service}-{hash}.log
```

## Process Management
## Lifecycle

```bash
micro status # Show running services
micro stop users # Stop a specific service
```
`micro run` is itself the process manager for as long as it runs — there is no
daemon and no `micro status`/`micro stop` command. Stop everything with
`Ctrl-C`; services are shut down in reverse dependency order.

On a `.go` change a service is rebuilt in place. If the rebuild fails to
compile, the **previous version keeps running** and the build error is printed —
a typo never takes your service offline. New service directories added while
`micro run` is up (e.g. by `micro new` or `micro chat`) are picked up and started
automatically.

## Example: a multi-service app

Expand Down Expand Up @@ -251,6 +255,22 @@ micro run --env production # Use production environment
micro run --mcp-address :3000 # Enable MCP protocol gateway for AI clients
```

## Going to production

`micro run` has no production mode by design — it's the dev inner loop. In
development it also hands you a gateway for free (`--no-gateway` to skip); in
production you don't run `micro run` at all. To ship:

1. **Build each service**: `go build` produces a static binary.
2. **Run it under a process manager or scheduler** — systemd, Docker/Compose, or
Kubernetes (see the Kubernetes deploy assets). That is your daemon: restarts,
log capture, and boot persistence come from there, not from Go Micro.
3. **Point them at a shared registry** (Consul, etcd, or NATS) so they discover
each other.
4. **Front them with the gateway** — the API/MCP gateway that turns your services
into an HTTP API and AI-callable MCP tools, with a dashboard and auth (see the
MCP gateway deploy assets).

## Tips

1. **Browse First**: Open http://localhost:8080 to explore your services
Expand Down
Loading