naco-api/internal/server/server.go
Nacorid b27108467e
fix: harden main entrypoint — graceful shutdown, context handling, readyz binding
- Add Server.Shutdown() method delegating to X402Server
- Wire main-server graceful shutdown with 15s bounded timeout
- Derive long-lived clients (bluesky, ollama) from backgroundCtx instead of
  signal context to prevent poisoned sessions on SIGTERM during startup
- Remove empty errgroup goroutine that only logged on cancellation
- Bind readyz sidecar to configurable READYZ_ADDRESS (default 127.0.0.1)
  instead of 0.0.0.0
- Validate port strings (ReadyzPort, MetricsPort, ListenPort) at config load
- Fix ready-check Ollama probe: use defer for body drain, guard nil resp
2026-09-09 15:07:27 +00:00

120 lines
3.6 KiB
Go

package server
import (
"context"
"fmt"
"os"
"strings"
"time"
log "github.com/nacorid/logger"
"github.com/nacorid/naco-api/internal/activitypub"
"github.com/nacorid/naco-api/internal/bluesky"
"github.com/nacorid/naco-api/internal/config"
"github.com/nacorid/naco-api/internal/nostr"
"github.com/nacorid/naco-api/internal/ollama"
"github.com/nacorid/naco-api/internal/store"
"github.com/nacorid/naco-api/internal/twitter"
"github.com/nacorid/naco-api/internal/x402"
)
type Server struct {
// Transport & middleware
listenAddress string
permissions os.FileMode
mcpHandler *x402.X402Server
metrics *MetricsMiddleware
// Dependencies
db Store
apClient apBackend
bskyClient blueskyBackend
nostrClient nostrBackend
twitterClient twitterBackend
ollama ollamaBackend
// Cache & background work
postCache *ttlCache
enrichSem chan struct{}
prewarmAuthors []string
prewarmFeedURI string
enrichEnabled bool
// Payments & analytics tuning
x402Wallet string
activeNetworks []NetworkInfo
analyticsTTLEngagement time.Duration
analyticsTTLPatterns time.Duration
analyticsTTLTopics time.Duration
analyticsTTLSentiment time.Duration
analyticsTTLDrivers time.Duration
// twitterEnabled reports whether the Twitter API is available (a bearer
// token is configured). It flips the Twitter-capable tools to upto
// variable pricing so the realized Twitter API costs are passed on to
// callers (see registerTools).
twitterEnabled bool
}
func New(ctx context.Context, db *store.Queries, ap *activitypub.APClient, bsky *bluesky.BlueskyClient, n *nostr.Client, t *twitter.Client, o *ollama.OllamaClient, cfg *config.Config) *Server {
toolchain, metrics := buildMiddleware()
srv := newX402Server(ctx, db, cfg, toolchain, metrics)
activeNetworks := resolveActiveNetworks(ctx, cfg.ActiveNetworks)
s := &Server{
listenAddress: cfg.ListenAddress,
permissions: cfg.UnixSocketPermissions,
mcpHandler: srv,
db: db,
apClient: ap,
bskyClient: bsky,
nostrClient: n,
twitterClient: t,
ollama: o,
metrics: metrics,
postCache: newTTLCache(postCacheTTL),
prewarmAuthors: cfg.PrewarmAuthors,
prewarmFeedURI: cfg.PrewarmFeedURI,
enrichEnabled: cfg.EnrichCrossPlatform,
x402Wallet: cfg.X402WalletAddress,
activeNetworks: activeNetworks,
enrichSem: make(chan struct{}, 4),
analyticsTTLEngagement: cfg.AnalyticsTTLEngagement,
analyticsTTLPatterns: cfg.AnalyticsTTLPatterns,
analyticsTTLTopics: cfg.AnalyticsTTLTopics,
analyticsTTLSentiment: cfg.AnalyticsTTLSentiment,
analyticsTTLDrivers: cfg.AnalyticsTTLDrivers,
twitterEnabled: cfg.TwitterBearerToken != "",
}
// Combine address and port for TCP (not unix socket) if port not already present
if !strings.HasPrefix(cfg.ListenAddress, "unix:") && !strings.Contains(cfg.ListenAddress, ":") {
s.listenAddress = fmt.Sprintf("%s:%s", cfg.ListenAddress, cfg.ListenPort)
}
log.DebugWithContext(ctx, "server initialized",
"networks", len(activeNetworks),
"enrich", s.enrichEnabled,
"prewarm_authors", len(s.prewarmAuthors),
)
s.registerTools()
return s
}
func (s *Server) MetricsText() string {
if s.metrics == nil {
return ""
}
return s.metrics.MetricsText()
}
func (s *Server) Start() error {
//go s.startPostPruner()
return s.mcpHandler.Start(s.listenAddress, s.permissions)
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.mcpHandler.Shutdown(ctx)
}