package grpcserver import ( "context" "fmt" "net" "time" pbv1 "git3.techno-world.net/lrosales/broad-announce/gen/go/broadannounce/v1" "git3.techno-world.net/lrosales/broad-announce/internal/observability" "git3.techno-world.net/lrosales/broad-announce/internal/pipeline" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" ) // Config for the gRPC server. type Config struct { Addr string // default ":9090" MaxInflight int // max in-flight per stream; default 256 } // IngestdDeps is what ingestd's main.go passes to NewForIngestd. // Using an interface avoids an import cycle (internal/grpcserver cannot import cmd/ingestd). type IngestdDeps interface { GetLogger() interface{ Info(msg string, args ...any) } GetMetrics() *observability.IngestdMetrics GetPipeline() pipeline.Deps } // NewForIngestd is the constructor ingestd's main.go calls. // It registers the Ingest service on a grpc.Server but does not start listening. func NewForIngestd(cfg Config, deps IngestdDeps) (*grpc.Server, error) { if cfg.Addr == "" { cfg.Addr = ":9090" } if cfg.MaxInflight == 0 { cfg.MaxInflight = 256 } logger := deps.GetLogger() m := deps.GetMetrics() // Prime the histogram buckets with a zero observation so Prometheus // registers them immediately (avoids missing bucket labels on first scrape). m.GRPCAckLatency.WithLabelValues("_init").Observe(0) ka := keepalive.ServerParameters{ Time: 30 * time.Second, Timeout: 10 * time.Second, } enforcement := keepalive.EnforcementPolicy{ MinTime: 5 * time.Second, PermitWithoutStream: false, } grpcOpts := []grpc.ServerOption{ grpc.KeepaliveParams(ka), grpc.KeepaliveEnforcementPolicy(enforcement), grpc.WriteBufferSize(256 * 1024), grpc.ReadBufferSize(32 * 1024), // TODO(m11): grpc.Creds(tlsCredentials()) — enable TLS once certificates are available } srv := grpc.NewServer(grpcOpts...) pbv1.RegisterIngestServer(srv, &Server{ Dep: deps.GetPipeline(), MaxInflight: cfg.MaxInflight, Logger: logger, }) logger.Info("grpc server configured", "addr", cfg.Addr, "max_inflight", cfg.MaxInflight) return srv, nil } // Serve starts a TCP listener on addr and blocks serving. // On ctx cancellation, it initiates a graceful shutdown (drains for up to 15s // then hard-stops). The caller should run Serve in a goroutine. func Serve(ctx context.Context, cfg Config, deps IngestdDeps) error { srv, err := NewForIngestd(cfg, deps) if err != nil { return fmt.Errorf("grpcserver.NewForIngestd: %w", err) } ln, err := net.Listen("tcp", cfg.Addr) if err != nil { return fmt.Errorf("grpc listen %s: %w", cfg.Addr, err) } errCh := make(chan error, 1) go func() { errCh <- srv.Serve(ln) }() select { case <-ctx.Done(): done := make(chan struct{}) go func() { srv.GracefulStop() close(done) }() select { case <-done: return nil case <-time.After(15 * time.Second): srv.Stop() return ctx.Err() } case err := <-errCh: return err } }