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
19 changes: 10 additions & 9 deletions cmd/anfra/commands.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -74,7 +75,7 @@ func buildCobraCommand(c app.Command) *cobra.Command {
}
}
}
cmd.RunE = func(_ *cobra.Command, posArgs []string) error {
cmd.RunE = func(runCmd *cobra.Command, posArgs []string) error {
args := map[string]any{}
for k, v := range strVals {
args[k] = *v
Expand All @@ -91,7 +92,7 @@ func buildCobraCommand(c app.Command) *cobra.Command {
if err := applyStdin(c, args); err != nil {
return err
}
return runCommand(c, args)
return runCommand(runCmd.Context(), c, args)
}
return cmd
}
Expand Down Expand Up @@ -121,7 +122,7 @@ func applyStdin(c app.Command, args map[string]any) error {

// runCommand routes a command to the warm server when one is running for this
// repo, otherwise runs it one-shot (spawning only the sidecars it needs).
func runCommand(c app.Command, args map[string]any) error {
func runCommand(ctx context.Context, c app.Command, args map[string]any) error {
Comment thread
datbth marked this conversation as resolved.
repoDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("resolve repo dir: %w", err)
Expand All @@ -137,14 +138,14 @@ func runCommand(c app.Command, args map[string]any) error {
return present(body, contentType)
}

return withRepo(func(h hostContext) error {
clients, closeSidecars, err := startNeededSidecars(h, c, args)
return withRepo(ctx, func(ctx context.Context, h hostContext) error {
clients, closeSidecars, err := startNeededSidecars(ctx, h, c, args)
if err != nil {
return err
}
defer closeSidecars()

resp, err := app.Dispatch(h.ctx, clients, h.repo, req)
resp, err := app.Dispatch(ctx, clients, h.repo, req)
if err != nil {
return err
}
Expand Down Expand Up @@ -183,7 +184,7 @@ func present(body []byte, contentType string) error {

// startNeededSidecars spawns just the sidecars the command declares it needs
// for these args, returning the clients and a single close func (LIFO).
func startNeededSidecars(h hostContext, c app.Command, args map[string]any) (app.Clients, func(), error) {
func startNeededSidecars(ctx context.Context, h hostContext, c app.Command, args map[string]any) (app.Clients, func(), error) {
var need app.Sidecars
if c.Needs != nil {
need = c.Needs(args)
Expand All @@ -198,7 +199,7 @@ func startNeededSidecars(h hostContext, c app.Command, args map[string]any) (app

if need.Node {
node := sidecar.NewAnfraNode(h.cfg)
if err := node.Start(h.ctx); err != nil {
if err := node.Start(ctx); err != nil {
closeAll()
return app.Clients{}, nil, fmt.Errorf("start anfra-node sidecar: %w", err)
}
Expand All @@ -207,7 +208,7 @@ func startNeededSidecars(h hostContext, c app.Command, args map[string]any) (app
}
if need.CanalQuery {
canal := sidecar.NewCanalQuery(h.cfg)
if err := canal.Start(h.ctx); err != nil {
if err := canal.Start(ctx); err != nil {
closeAll()
return app.Clients{}, nil, fmt.Errorf("start canal-query sidecar: %w", err)
}
Expand Down
15 changes: 8 additions & 7 deletions cmd/anfra/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ import (

// hostContext carries the per-invocation repo + the sidecar Config (with the
// host-aggregated log sink) so commands can spawn whichever sidecars they need.
// The context is passed to fn as a parameter (not stored here) so it stays a
// properly-inherited, cancelable value.
type hostContext struct {
ctx context.Context
repo repo.Repo
cfg sidecar.Config
}

// withRepo resolves the repo and sets up host-aggregated logging, then
// runs fn. Sidecar lifecycle is the command's choice (some need only anfra-node,
// query execution also needs canal-query).
func withRepo(fn func(h hostContext) error) error {
// withRepo resolves the repo and sets up host-aggregated logging, then runs fn
// with the caller's (signal-cancelable) context and the host context. Sidecar
// lifecycle is the command's choice (some need only anfra-node, query execution
// also needs canal-query).
func withRepo(ctx context.Context, fn func(ctx context.Context, h hostContext) error) error {
repoDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("resolve repo dir: %w", err)
Expand All @@ -35,8 +37,7 @@ func withRepo(fn func(h hostContext) error) error {
}
defer lg.Close()

return fn(hostContext{
ctx: context.Background(),
return fn(ctx, hostContext{
repo: repo,
cfg: sidecar.Config{
RepoID: repo.ID,
Expand Down
15 changes: 12 additions & 3 deletions cmd/anfra/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package main

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/holistics/anfra/internal/meta"
"github.com/spf13/cobra"
Expand All @@ -16,9 +19,15 @@ type exitCodeError struct{ code int }
func (e *exitCodeError) Error() string { return fmt.Sprintf("exit code %d", e.code) }

func main() {
// ExecuteC returns the command that actually ran, so we get the real
// subcommand name (handles flags/args/aliases) rather than parsing os.Args.
executed, err := newRootCmd().ExecuteC()
// A single signal-cancelable root context, threaded down through cobra so
// Ctrl-C (SIGINT/SIGTERM) cancels in-flight work — an update download, a
// query, or the serve loop.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

// ExecuteContextC sets that root context and returns the command that actually
// ran, so we get the real subcommand name (handles flags/args/aliases).
executed, err := newRootCmd().ExecuteContextC(ctx)
// After the command runs, surface a cached "update available" notice (and, if
// opted in, kick a background update). Best-effort; never affects exit status.
if executed != nil {
Expand Down
31 changes: 14 additions & 17 deletions cmd/anfra/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import (
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"

"github.com/holistics/anfra/internal/app"
Expand All @@ -31,14 +29,14 @@ func newServeCmd() *cobra.Command {
return &cobra.Command{
Use: "serve",
Short: "Run the anfra server: keep sidecars warm and expose POST /call for agents and subsequent CLI calls",
RunE: func(_ *cobra.Command, _ []string) error {
return runServe()
RunE: func(cmd *cobra.Command, _ []string) error {
return runServe(cmd.Context())
},
}
}

func runServe() error {
return withRepo(func(h hostContext) error {
func runServe(ctx context.Context) error {
return withRepo(ctx, func(ctx context.Context, h hostContext) error {
if isServeRunning(h.repo) {
return fmt.Errorf("anfra serve already running for this repo (socket %s)", serveSocketPath(h.repo))
}
Expand All @@ -49,12 +47,12 @@ func runServe() error {
cfg.EnablePooling = true

node := sidecar.NewAnfraNode(cfg)
if err := node.Start(h.ctx); err != nil {
if err := node.Start(ctx); err != nil {
return fmt.Errorf("start anfra-node sidecar: %w", err)
}
defer node.Close()
canal := sidecar.NewCanalQuery(cfg)
if err := canal.Start(h.ctx); err != nil {
if err := canal.Start(ctx); err != nil {
return fmt.Errorf("start canal-query sidecar: %w", err)
}
defer canal.Close()
Expand All @@ -72,12 +70,12 @@ func runServe() error {
srv := &http.Server{Handler: serveMux(h, clients), ReadHeaderTimeout: 10 * time.Second}

go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
<-ctx.Done() // cancelled on SIGINT/SIGTERM by the root context in main
// Drain with a fresh deadline: WithoutCancel keeps ctx's values but drops
// its (already-fired) cancellation, so Shutdown gets the full 5s.
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
_ = srv.Shutdown(shutdownCtx)
}()

h.cfg.Logger.Info("serve.listening", "socket", sockPath)
Expand Down Expand Up @@ -111,11 +109,10 @@ func serveMux(h hostContext, clients app.Clients) http.Handler {

// `help` (truthy) → the command's cobra help text, identical to `anfra <cmd> --help`.
if app.IsTruthy(req.Args["help"]) {
// commandHelp only renders help text; it builds the command tree but never
// executes a RunE, so there is no request context to thread into the update
// command's HTTP client (which is why contextcheck is suppressed here).
// commandHelp only builds the command tree to render help text; it never
// executes a RunE, so there's no request context to thread into a command
// body. False positive — the rest of the tree is properly ctx-threaded.
text, err := commandHelp(req.Command) //nolint:contextcheck

if err != nil {
writeCallError(w, http.StatusNotFound, err.Error())
return
Expand Down
17 changes: 8 additions & 9 deletions cmd/anfra/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,18 @@ func newUpdateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update",
Short: "Update anfra to the latest release (use --check to only report)",
RunE: func(_ *cobra.Command, _ []string) error {
return runUpdate(checkOnly)
RunE: func(cmd *cobra.Command, _ []string) error {
return runUpdate(cmd.Context(), checkOnly)
},
}
cmd.Flags().BoolVar(&checkOnly, "check", false, "only check for an update; do not install")
return cmd
}

func runUpdate(checkOnly bool) error {
// Root at Background (repo convention, see host.go/runServe); timeouts are
// bounded per-request inside the update package (a quick lookup, then a large
// download).
ctx := context.Background()
func runUpdate(ctx context.Context, checkOnly bool) error {
// ctx is the signal-cancelable root from main, so Ctrl-C aborts the download
// (its request is context-aware). Per-request timeouts are bounded inside the
// update package (a quick lookup, then a large download).
rel, err := update.Latest(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -68,8 +67,8 @@ func newUpdateCheckCmd() *cobra.Command {
return &cobra.Command{
Use: "__update-check",
Hidden: true,
RunE: func(_ *cobra.Command, _ []string) error {
return update.Refresh(context.Background())
RunE: func(cmd *cobra.Command, _ []string) error {
return update.Refresh(cmd.Context())
},
}
}
Expand Down
Loading