snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

445
internal/app/run.go Normal file
View File

@@ -0,0 +1,445 @@
package app
import (
"context"
"crypto/fips140"
"errors"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/centrifugal/centrifugo/v6/internal/api"
"github.com/centrifugal/centrifugo/v6/internal/build"
"github.com/centrifugal/centrifugo/v6/internal/client"
"github.com/centrifugal/centrifugo/v6/internal/config"
"github.com/centrifugal/centrifugo/v6/internal/confighelpers"
"github.com/centrifugal/centrifugo/v6/internal/consuming"
"github.com/centrifugal/centrifugo/v6/internal/jwtverify"
"github.com/centrifugal/centrifugo/v6/internal/logging"
"github.com/centrifugal/centrifugo/v6/internal/metrics"
"github.com/centrifugal/centrifugo/v6/internal/notify"
"github.com/centrifugal/centrifugo/v6/internal/service"
"github.com/centrifugal/centrifugo/v6/internal/survey"
"github.com/centrifugal/centrifugo/v6/internal/telemetry"
"github.com/centrifugal/centrifugo/v6/internal/tools"
"github.com/centrifugal/centrifugo/v6/internal/usage"
"github.com/centrifugal/centrifuge"
"github.com/joho/godotenv"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"go.uber.org/automaxprocs/maxprocs"
"google.golang.org/grpc"
_ "google.golang.org/grpc/encoding/gzip"
)
const edition = "oss"
func Run(cmd *cobra.Command, configFile string) {
dotEnvUsed := false
if tools.FileExists(".env") {
err := godotenv.Load()
if err != nil {
log.Fatal().Err(err).Msg("error loading .env file")
}
dotEnvUsed = true
}
cfg, cfgMeta, err := config.GetConfig(cmd, configFile)
if err != nil {
log.Fatal().Err(err).Msg("error getting config")
}
ctx, serviceCancel := context.WithCancel(context.Background())
defer serviceCancel()
// Ingest services (async consumers) have a separate lifecycle from the rest of
// services – see the shutdown sequence in handleSignals.
ingestCtx, ingestCancel := context.WithCancel(context.Background())
defer ingestCancel()
centrifugeLogHandler, logCloseFn := logging.Setup(cfg)
if logCloseFn != nil {
defer logCloseFn()
}
if cfgMeta.FileNotFound {
log.Warn().Msg("config file not found, continue using environment and flag options")
} else {
absConfPath, _ := filepath.Abs(configFile)
log.Info().Str("path", absConfPath).Msg("using config file")
if dotEnvUsed {
log.Info().Msg("environment variables have been loaded from .env file")
}
}
err = tools.WritePidFile(cfg.PidFile)
if err != nil {
log.Fatal().Err(err).Msg("error writing PID")
}
_, _ = maxprocs.Set(maxprocs.Logger(func(s string, i ...interface{}) {
log.Info().Msgf(strings.ToLower(s), i...)
}))
// Registered services will be run after node.Run() but before HTTP/GRPC servers start.
// Registered services will be stopped after node's shutdown and HTTP/GRPC servers shutdown.
serviceManager := service.NewManager()
entry := log.Info().
Str("version", build.Version).
Str("runtime", runtime.Version()).
Int("pid", os.Getpid()).
Int("gomaxprocs", runtime.GOMAXPROCS(0))
if fips140.Enabled() { // See https://go.dev/doc/security/fips140.
entry = entry.Bool("fips", true)
}
if cfg.Broker.Enabled {
entry = entry.Str("broker", cfg.Broker.Type)
}
if cfg.PresenceManager.Enabled {
entry = entry.Str("presence_manager", cfg.PresenceManager.Type)
}
if !cfg.Broker.Enabled || !cfg.PresenceManager.Enabled {
entry = entry.Str("engine", cfg.Engine.Type)
}
entry.Msg("starting Centrifugo")
if build.Version == "0.0.0" {
log.Warn().Msg("running a development build of Centrifugo (version 0.0.0), ensure to use release build in production")
}
err = cfg.Validate()
if err != nil {
log.Fatal().Err(err).Msg("error validating config")
}
cfgContainer, err := config.NewContainer(cfg)
if err != nil {
log.Fatal().Err(err).Msg("error creating config")
}
cfgContainer.ChannelOptionsCacheTTL = 200 * time.Millisecond
// Initialize centralized metrics registry.
err = metrics.Init(metrics.Config{
Namespace: "", // Use default "centrifugo" namespace.
ConstLabels: nil, // Can be populated from config in the future.
Registerer: nil, // Use prometheus.DefaultRegisterer.
NativeHistograms: cfg.Prometheus.NativeHistograms,
})
if err != nil {
log.Fatal().Err(err).Msg("error initializing metrics")
}
proxyMap, keepHeadersInContext, err := buildProxyMap(cfg)
if err != nil {
log.Fatal().Err(err).Msg("error building proxy map")
}
nodeCfg := centrifugeNodeConfig(build.Version, edition, cfgContainer, centrifugeLogHandler)
node, err := centrifuge.New(nodeCfg)
if err != nil {
log.Fatal().Err(err).Msg("error creating Centrifuge Node")
}
if cfg.OpenTelemetry.Enabled {
_, err := telemetry.SetupTracing(context.Background(), cfg.OpenTelemetry, node.ID())
if err != nil {
log.Fatal().Err(err).Msg("error setting up opentelemetry tracing")
}
}
err = configureEngines(node, cfgContainer)
if err != nil {
log.Fatal().Err(err).Msg("configure engines error")
}
err = configureMapBroker(node, cfgContainer)
if err != nil {
log.Fatal().Err(err).Msg("configure map broker error")
}
verifierConfig, err := confighelpers.MakeVerifierConfig(cfg.Client.Token)
if err != nil {
log.Fatal().Err(err).Msg("error creating JWT verifier config")
}
tokenVerifier, err := jwtverify.NewTokenVerifierJWT(verifierConfig, cfgContainer)
if err != nil {
log.Fatal().Err(err).Msg("error creating token verifier")
}
var subTokenVerifier *jwtverify.VerifierJWT
if cfg.Client.SubscriptionToken.Enabled {
log.Info().Msg("initializing separate verifier for subscription tokens")
subVerifier, err := confighelpers.MakeVerifierConfig(cfg.Client.SubscriptionToken.Token)
if err != nil {
log.Fatal().Err(err).Msg("error creating subscription JWT verifier config")
}
subTokenVerifier, err = jwtverify.NewTokenVerifierJWT(subVerifier, cfgContainer)
if err != nil {
log.Fatal().Err(err).Msg("error creating token verifier")
}
}
clientHandler := client.NewHandler(node, cfgContainer, tokenVerifier, subTokenVerifier, proxyMap)
err = clientHandler.Setup()
if err != nil {
log.Fatal().Err(err).Msg("error setting up client handler")
}
if cfg.RPC.Ping.Enabled {
log.Info().Str("method", cfg.RPC.Ping.Method).Msg("RPC ping extension enabled")
clientHandler.SetRPCExtension(cfg.RPC.Ping.Method, func(c client.Client, e centrifuge.RPCEvent) (centrifuge.RPCReply, error) {
return centrifuge.RPCReply{}, nil
})
}
surveyCaller := survey.NewCaller(node)
useAPIOpentelemetry := cfg.OpenTelemetry.Enabled && cfg.OpenTelemetry.API
useConsumingOpentelemetry := cfg.OpenTelemetry.Enabled && cfg.OpenTelemetry.Consuming
httpAPIExecutor := api.NewExecutor(node, cfgContainer, surveyCaller, api.ExecutorConfig{
Protocol: "http",
UseOpenTelemetry: useAPIOpentelemetry,
})
grpcAPIExecutor := api.NewExecutor(node, cfgContainer, surveyCaller, api.ExecutorConfig{
Protocol: "grpc",
UseOpenTelemetry: useAPIOpentelemetry,
})
consumingAPIExecutor := api.NewExecutor(node, cfgContainer, surveyCaller, api.ExecutorConfig{
Protocol: "consuming",
UseOpenTelemetry: useConsumingOpentelemetry,
})
consumingHandler := api.NewConsumingHandler(node, consumingAPIExecutor, api.ConsumingHandlerConfig{
UseOpenTelemetry: useConsumingOpentelemetry,
})
consumingServices, err := consuming.New(node.ID(), consumingHandler, cfg.Consumers)
if err != nil {
log.Fatal().Err(err).Msg("error initializing consumers")
}
// Consumers are registered in a separate manager: unlike other services they
// must be stopped before Node shutdown, so that no consumed message is
// acknowledged after this node lost the ability to deliver it.
ingestManager := service.NewManager()
ingestManager.Register(consumingServices...)
if cfg.Graphite.Enabled {
serviceManager.Register(graphiteExporter(cfg, nodeCfg))
}
var statsSender *usage.Sender
if !cfg.UsageStats.Disabled {
statsSender = usage.NewSender(node, cfgContainer, usage.Features{
Edition: edition,
Version: build.Version,
EngineEnabled: !cfg.Broker.Enabled || !cfg.PresenceManager.Enabled,
EngineType: cfg.Engine.Type,
BrokerEnabled: cfg.Broker.Enabled,
BrokerType: cfg.Broker.Type,
PresenceManagerEnabled: cfg.PresenceManager.Enabled,
PresenceManagerType: cfg.PresenceManager.Type,
Websocket: !cfg.WebSocket.Disabled,
HTTPStream: cfg.HTTPStream.Enabled,
SSE: cfg.SSE.Enabled,
UniWebsocket: cfg.UniWS.Enabled,
UniHTTPStream: cfg.UniHTTPStream.Enabled,
UniSSE: cfg.UniSSE.Enabled,
UniGRPC: cfg.UniGRPC.Enabled,
WebTransport: cfg.WebTransport.Enabled,
EnabledConsumers: usage.GetEnabledConsumers(cfg.Consumers),
GrpcAPI: cfg.GrpcAPI.Enabled,
SubscribeToPersonal: cfg.Client.SubscribeToUserPersonalChannel.Enabled,
Admin: cfg.Admin.Enabled,
ConnectProxy: proxyMap.ConnectProxy != nil,
RefreshProxy: proxyMap.RefreshProxy != nil,
SubscribeProxy: len(proxyMap.SubscribeProxies) > 0,
PublishProxy: len(proxyMap.PublishProxies) > 0,
RPCProxy: len(proxyMap.RpcProxies) > 0,
SubRefreshProxy: len(proxyMap.SubRefreshProxies) > 0,
SubscribeStreamProxy: len(proxyMap.SubscribeStreamProxies) > 0,
Swagger: cfg.Swagger.Enabled,
})
serviceManager.Register(statsSender)
}
notify.RegisterHandlers(node, statsSender)
if err = node.Run(); err != nil {
log.Fatal().Err(err).Msg("error running node")
}
serviceDone := runServices(ctx, serviceManager)
ingestDone := runServices(ingestCtx, ingestManager)
var grpcAPIServer *grpc.Server
if cfg.GrpcAPI.Enabled {
var err error
grpcAPIServer, err = runGRPCAPIServer(cfg, node, useAPIOpentelemetry, grpcAPIExecutor)
if err != nil {
log.Fatal().Err(err).Msg("error creating GRPC API server")
}
}
var grpcUniServer *grpc.Server
if cfg.UniGRPC.Enabled {
var err error
grpcUniServer, err = runGRPCUniServer(cfg, node)
if err != nil {
log.Fatal().Err(err).Msg("error creating uni GRPC server")
}
}
httpServers, err := runHTTPServers(node, cfgContainer, httpAPIExecutor, keepHeadersInContext)
if err != nil {
log.Fatal().Err(err).Msg("error running HTTP server")
}
logStartWarnings(cfg, cfgMeta)
handleSignals(
cmd, configFile, node, cfgContainer, tokenVerifier, subTokenVerifier,
httpServers, grpcAPIServer, grpcUniServer,
serviceDone, serviceCancel, ingestDone, ingestCancel,
)
}
// runServices runs all services registered in the manager and returns a channel
// closed when all of them stopped.
func runServices(ctx context.Context, manager *service.Manager) chan struct{} {
done := make(chan struct{})
manager.Run(ctx)
go func() {
defer close(done)
err := manager.Wait()
if err != nil && !errors.Is(err, context.Canceled) {
log.Error().Err(err).Msg("error running service")
os.Exit(1)
}
}()
return done
}
func handleSignals(
cmd *cobra.Command, configFile string, n *centrifuge.Node, cfgContainer *config.Container,
tokenVerifier *jwtverify.VerifierJWT, subTokenVerifier *jwtverify.VerifierJWT, httpServers []*http.Server,
grpcAPIServer *grpc.Server, grpcUniServer *grpc.Server, serviceDone chan struct{},
serviceCancel context.CancelFunc, ingestDone chan struct{}, ingestCancel context.CancelFunc,
) {
cfg := cfgContainer.Config()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, os.Interrupt, syscall.SIGTERM)
for {
sig := <-sigCh
log.Info().Msgf("signal received: %v", sig)
switch sig {
case syscall.SIGHUP:
// Reload application configuration on SIGHUP.
// Note that Centrifugo can't reload config for everything – just best effort to reload what's possible.
// We can now reload channel options and token verifiers.
log.Info().Msg("reloading configuration")
newCfg, _, err := config.GetConfig(cmd, configFile)
if err != nil {
log.Err(err).Msg("error reading config")
continue
}
if err = newCfg.Validate(); err != nil {
log.Error().Msgf("error validating config: %v", err)
continue
}
verifierConfig, err := confighelpers.MakeVerifierConfig(newCfg.Client.Token)
if err != nil {
log.Error().Msgf("error reloading: %v", err)
continue
}
if err = tokenVerifier.Reload(verifierConfig); err != nil {
log.Error().Msgf("error reloading: %v", err)
continue
}
if subTokenVerifier != nil {
subVerifierConfig, err := confighelpers.MakeVerifierConfig(newCfg.Client.SubscriptionToken.Token)
if err != nil {
log.Error().Msgf("error reloading: %v", err)
continue
}
if err := subTokenVerifier.Reload(subVerifierConfig); err != nil {
log.Error().Msgf("error reloading: %v", err)
continue
}
}
if err = cfgContainer.Reload(newCfg); err != nil {
log.Error().Msgf("error reloading: %v", err)
continue
}
log.Info().Msg("configuration successfully reloaded")
case syscall.SIGINT, os.Interrupt, syscall.SIGTERM:
log.Info().Msg("shutting down ...")
pidFile := cfg.PidFile
shutdownTimeout := cfg.Shutdown.Timeout
go time.AfterFunc(shutdownTimeout.ToDuration(), func() {
if pidFile != "" {
_ = os.Remove(pidFile)
}
log.Fatal().Msg("shutdown timeout reached")
})
var wg sync.WaitGroup
if grpcAPIServer != nil {
wg.Add(1)
go func() {
defer wg.Done()
grpcAPIServer.GracefulStop()
}()
}
if grpcUniServer != nil {
wg.Add(1)
go func() {
defer wg.Done()
grpcUniServer.GracefulStop()
}()
}
for _, srv := range httpServers {
wg.Add(1)
go func(srv *http.Server) {
defer wg.Done()
_ = srv.Shutdown(context.Background()) // We have a separate timeout goroutine.
}(srv)
}
// Stop consuming messages before Node shutdown and wait until consumers
// stopped. Otherwise a message consumed here may be acknowledged while
// this node is already unable to deliver it – such message must be left
// unacknowledged instead, to be redelivered upon restart. Note that we
// don't wait for HTTP/GRPC servers here: their shutdown only completes
// after Node disconnected clients, since long-lived streaming requests
// (SSE, HTTP-streaming, unidirectional GRPC) are in-flight until then.
ingestCancel()
<-ingestDone
_ = n.Shutdown(context.Background()) // We have a separate timeout goroutine.
wg.Wait()
serviceCancel()
<-serviceDone
if pidFile != "" {
_ = os.Remove(pidFile)
}
os.Exit(0)
}
}
}