diff --git a/config/flipt.schema.cue b/config/flipt.schema.cue index ca0a7cf727..ce1f3b0bd2 100644 --- a/config/flipt.schema.cue +++ b/config/flipt.schema.cue @@ -247,6 +247,9 @@ import "strings" grpc_port?: int | *9000 cert_file?: string cert_key?: string + grpc_conn_max_idle_time?: =~#duration + grpc_conn_max_age?: =~#duration + grpc_conn_max_age_grace?: =~#duration } #tracing: { diff --git a/config/flipt.schema.json b/config/flipt.schema.json index 0bf0de25f4..416c067fe7 100644 --- a/config/flipt.schema.json +++ b/config/flipt.schema.json @@ -856,6 +856,18 @@ }, "cert_key": { "type": "string" + }, + "grpc_conn_max_idle_time": { + "type": "string", + "pattern": "^([0-9]+(ns|us|µs|ms|s|m|h))+$" + }, + "grpc_conn_max_age": { + "type": "string", + "pattern": "^([0-9]+(ns|us|µs|ms|s|m|h))+$" + }, + "grpc_conn_max_age_grace": { + "type": "string", + "pattern": "^([0-9]+(ns|us|µs|ms|s|m|h))+$" } }, "required": [], diff --git a/internal/cmd/grpc.go b/internal/cmd/grpc.go index 265404c722..017c5382c9 100644 --- a/internal/cmd/grpc.go +++ b/internal/cmd/grpc.go @@ -53,6 +53,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/health" + "google.golang.org/grpc/keepalive" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" @@ -358,7 +359,14 @@ func NewGRPCServer( otel.SetTracerProvider(tracingProvider) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - grpcOpts := []grpc.ServerOption{grpc.ChainUnaryInterceptor(interceptors...)} + grpcOpts := []grpc.ServerOption{ + grpc.ChainUnaryInterceptor(interceptors...), + grpc.KeepaliveParams(keepalive.ServerParameters{ + MaxConnectionIdle: cfg.Server.GRPCConnectionMaxIdleTime, + MaxConnectionAge: cfg.Server.GRPCConnectionMaxAge, + MaxConnectionAgeGrace: cfg.Server.GRPCConnectionMaxAgeGrace, + }), + } if cfg.Server.Protocol == config.HTTPS { creds, err := credentials.NewServerTLSFromFile(cfg.Server.CertFile, cfg.Server.CertKey) diff --git a/internal/cmd/grpc_test.go b/internal/cmd/grpc_test.go index 0e5d0c444e..4dc416fa52 100644 --- a/internal/cmd/grpc_test.go +++ b/internal/cmd/grpc_test.go @@ -3,11 +3,15 @@ package cmd import ( "context" "errors" + "fmt" + "path/filepath" "sync" "testing" "github.com/stretchr/testify/assert" "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/internal/info" + "go.uber.org/zap/zaptest" ) func TestGetTraceExporter(t *testing.T) { @@ -104,9 +108,29 @@ func TestGetTraceExporter(t *testing.T) { assert.EqualError(t, err, tt.wantErr.Error()) return } + t.Cleanup(func() { + err := expFunc(context.Background()) + assert.NoError(t, err) + }) assert.NoError(t, err) assert.NotNil(t, exp) assert.NotNil(t, expFunc) + }) } } + +func TestNewGRPCServer(t *testing.T) { + tmp := t.TempDir() + cfg := &config.Config{} + cfg.Database.URL = fmt.Sprintf("file:%s", filepath.Join(tmp, "flipt.db")) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + s, err := NewGRPCServer(ctx, zaptest.NewLogger(t), cfg, info.Flipt{}, false) + assert.NoError(t, err) + t.Cleanup(func() { + err := s.Shutdown(ctx) + assert.NoError(t, err) + }) + assert.NotEmpty(t, s.Server.GetServiceInfo()) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bfc964b5ed..1aa42b3fef 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -873,6 +873,17 @@ func TestLoad(t *testing.T) { return cfg }, }, + { + name: "grpc keepalive config provided", + path: "./testdata/server/grpc_keepalive.yml", + expected: func() *Config { + cfg := Default() + cfg.Server.GRPCConnectionMaxIdleTime = 1 * time.Hour + cfg.Server.GRPCConnectionMaxAge = 30 * time.Second + cfg.Server.GRPCConnectionMaxAgeGrace = 10 * time.Second + return cfg + }, + }, } for _, tt := range tests { diff --git a/internal/config/server.go b/internal/config/server.go index c57ba0abbe..aa8fb46783 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "os" + "time" "github.com/spf13/viper" ) @@ -13,13 +14,16 @@ var _ defaulter = (*ServerConfig)(nil) // ServerConfig contains fields, which configure both HTTP and gRPC // API serving. type ServerConfig struct { - Host string `json:"host,omitempty" mapstructure:"host" yaml:"host,omitempty"` - Protocol Scheme `json:"protocol,omitempty" mapstructure:"protocol" yaml:"protocol,omitempty"` - HTTPPort int `json:"httpPort,omitempty" mapstructure:"http_port" yaml:"http_port,omitempty"` - HTTPSPort int `json:"httpsPort,omitempty" mapstructure:"https_port" yaml:"https_port,omitempty"` - GRPCPort int `json:"grpcPort,omitempty" mapstructure:"grpc_port" yaml:"grpc_port,omitempty"` - CertFile string `json:"-" mapstructure:"cert_file" yaml:"-"` - CertKey string `json:"-" mapstructure:"cert_key" yaml:"-"` + Host string `json:"host,omitempty" mapstructure:"host" yaml:"host,omitempty"` + Protocol Scheme `json:"protocol,omitempty" mapstructure:"protocol" yaml:"protocol,omitempty"` + HTTPPort int `json:"httpPort,omitempty" mapstructure:"http_port" yaml:"http_port,omitempty"` + HTTPSPort int `json:"httpsPort,omitempty" mapstructure:"https_port" yaml:"https_port,omitempty"` + GRPCPort int `json:"grpcPort,omitempty" mapstructure:"grpc_port" yaml:"grpc_port,omitempty"` + CertFile string `json:"-" mapstructure:"cert_file" yaml:"-"` + CertKey string `json:"-" mapstructure:"cert_key" yaml:"-"` + GRPCConnectionMaxIdleTime time.Duration `json:"-" mapstructure:"grpc_conn_max_idle_time" yaml:"-"` + GRPCConnectionMaxAge time.Duration `json:"-" mapstructure:"grpc_conn_max_age" yaml:"-"` + GRPCConnectionMaxAgeGrace time.Duration `json:"-" mapstructure:"grpc_conn_max_age_grace" yaml:"-"` } func (c *ServerConfig) setDefaults(v *viper.Viper) error { diff --git a/internal/config/testdata/server/grpc_keepalive.yml b/internal/config/testdata/server/grpc_keepalive.yml new file mode 100644 index 0000000000..682826345f --- /dev/null +++ b/internal/config/testdata/server/grpc_keepalive.yml @@ -0,0 +1,30 @@ +# log: +# level: INFO +# grpc_level: ERROR + +# ui: +# enabled: true + +# cors: +# enabled: false +# allowed_origins: "*" + +# cache: +# enabled: false +# backend: memory +# ttl: 60s +# memory: +# eviction_interval: 5m # Evict Expired Items Every 5m + +server: +# protocol: https +# host: 0.0.0.0 +# https_port: 443 +# http_port: 8080 +# grpc_port: 9000 + grpc_conn_max_idle_time: 1h + grpc_conn_max_age: 30s + grpc_conn_max_age_grace: 10s + +# db: +# url: file:/var/opt/flipt/flipt.db