Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

276 lines
7.5 KiB
Go

// Copyright 2019 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remotesrv
import (
"context"
"crypto/tls"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"google.golang.org/grpc"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
)
// disabledFeaturesEnvVar is an undocumented internal test hook. If
// set, its value is parsed as a comma-separated list of
// remotesapi.Feature enum names; each resolved feature is appended to
// ServerArgs.DisabledFeatures before the server starts. Used by
// integration tests to force clients down legacy-RPC fallback paths
// against a server that actually supports the new RPC. Unknown names
// cause NewServer to log.Fatalln — loud failure on typos is the right
// default for a test hook.
const disabledFeaturesEnvVar = "DOLT_REMOTESAPI_DISABLED_FEATURES"
// MaxGRPCMessageSize is the maximum gRPC message size used for chunk store
// operations. This applies to both send and receive directions.
const MaxGRPCMessageSize = 128 * 1024 * 1024
type Server struct {
wg sync.WaitGroup
stopChan chan struct{}
grpcListenAddr string
httpListenAddr string
grpcSrv *grpc.Server
httpSrv http.Server
grpcHttpReqsWG sync.WaitGroup
tlsConfig *tls.Config
}
func (s *Server) GracefulStop() {
close(s.stopChan)
s.wg.Wait()
}
type ServerArgs struct {
Logger *logrus.Entry
HttpHost string
HttpListenAddr string
GrpcListenAddr string
FS filesys.Filesys
DBCache DBCache
ReadOnly bool
Options []grpc.ServerOption
ConcurrencyControl remotesapi.PushConcurrencyControl
// Feature flags this server implements but will not advertise in
// GetRepoMetadataResponse.features. Internal test hook — used by
// integration tests to force clients down the older-RPC fallback
// against a server that actually supports the new RPC. Go-level
// tests set this directly; process-level tests set it indirectly
// via the DOLT_REMOTESAPI_DISABLED_FEATURES env var, which
// NewServer appends to this field at startup.
DisabledFeatures []remotesapi.Feature
HttpInterceptor func(http.Handler) http.Handler
// If supplied, the listener(s) returned from Listeners() will be TLS
// listeners. The scheme used in the URLs returned from the gRPC server
// will be https.
TLSConfig *tls.Config
}
func NewServer(args ServerArgs) (*Server, error) {
if v := os.Getenv(disabledFeaturesEnvVar); v != "" {
for _, name := range strings.Split(v, ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
id, ok := remotesapi.Feature_value[name]
if !ok {
log.Fatalln(disabledFeaturesEnvVar + ": unknown feature name: " + name)
}
args.DisabledFeatures = append(args.DisabledFeatures, remotesapi.Feature(id))
}
}
if args.Logger == nil {
args.Logger = logrus.NewEntry(logrus.StandardLogger())
}
s := new(Server)
s.stopChan = make(chan struct{})
sealer, err := NewSingleSymmetricKeySealer()
if err != nil {
return nil, err
}
scheme := "http"
if args.TLSConfig != nil {
scheme = "https"
}
s.tlsConfig = args.TLSConfig
s.wg.Add(2)
s.grpcListenAddr = args.GrpcListenAddr
s.grpcSrv = grpc.NewServer(append([]grpc.ServerOption{grpc.MaxRecvMsgSize(MaxGRPCMessageSize)}, args.Options...)...)
var chnkSt remotesapi.ChunkStoreServiceServer = NewHttpFSBackedChunkStore(args.Logger, args.HttpHost, args.DBCache, args.FS, scheme, args.ConcurrencyControl, sealer, args.DisabledFeatures)
if args.ReadOnly {
chnkSt = ReadOnlyChunkStore{chnkSt}
}
remotesapi.RegisterChunkStoreServiceServer(s.grpcSrv, chnkSt)
var handler http.Handler = newFileHandler(args.Logger, args.DBCache, args.FS, args.ReadOnly, sealer)
if args.HttpInterceptor != nil {
handler = args.HttpInterceptor(handler)
}
if args.HttpListenAddr == args.GrpcListenAddr {
handler = s.grpcMultiplexHandler(s.grpcSrv, handler)
} else {
s.wg.Add(2)
}
s.httpListenAddr = args.HttpListenAddr
s.httpSrv = http.Server{
Addr: args.HttpListenAddr,
Handler: handler,
}
return s, nil
}
func (s *Server) grpcMultiplexHandler(grpcSrv *grpc.Server, handler http.Handler) http.Handler {
h2s := &http2.Server{}
newHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
s.grpcHttpReqsWG.Add(1)
defer s.grpcHttpReqsWG.Done()
grpcSrv.ServeHTTP(w, r)
} else {
handler.ServeHTTP(w, r)
}
})
return h2c.NewHandler(newHandler, h2s)
}
type Listeners struct {
http net.Listener
grpc net.Listener
}
func (l Listeners) Close() error {
if l.http != nil {
err := l.http.Close()
if err != nil {
if l.grpc != nil {
l.grpc.Close()
}
return err
}
}
if l.grpc != nil {
return l.grpc.Close()
}
return nil
}
func (s *Server) Listeners() (Listeners, error) {
var httpListener net.Listener
var grpcListener net.Listener
var err error
if s.tlsConfig != nil {
httpListener, err = tls.Listen("tcp", s.httpListenAddr, s.tlsConfig)
} else {
httpListener, err = net.Listen("tcp", s.httpListenAddr)
}
if err != nil {
return Listeners{}, err
}
if s.httpListenAddr == s.grpcListenAddr {
return Listeners{http: httpListener}, nil
}
if s.tlsConfig != nil {
grpcListener, err = tls.Listen("tcp", s.grpcListenAddr, s.tlsConfig)
} else {
grpcListener, err = net.Listen("tcp", s.grpcListenAddr)
}
if err != nil {
httpListener.Close()
return Listeners{}, err
}
return Listeners{http: httpListener, grpc: grpcListener}, nil
}
// Can be used to register more services on the server.
// Should only be accessed before `Serve` is called.
func (s *Server) GrpcServer() *grpc.Server {
return s.grpcSrv
}
func (s *Server) Serve(listeners Listeners) {
if listeners.grpc != nil {
go func() {
defer s.wg.Done()
logrus.Println("Starting grpc server on", s.grpcListenAddr)
err := s.grpcSrv.Serve(listeners.grpc)
logrus.Println("grpc server exited. error:", err)
}()
go func() {
defer s.wg.Done()
<-s.stopChan
logrus.Traceln("Calling grpcSrv.GracefulStop")
s.grpcSrv.GracefulStop()
logrus.Traceln("Finished calling grpcSrv.GracefulStop")
}()
}
go func() {
defer s.wg.Done()
logrus.Println("Starting http server on", s.httpListenAddr)
err := s.httpSrv.Serve(listeners.http)
logrus.Println("http server exited. exit error:", err)
}()
go func() {
defer s.wg.Done()
<-s.stopChan
logrus.Traceln("Calling httpSrv.Shutdown")
s.httpSrv.Shutdown(context.Background())
logrus.Traceln("Finished calling httpSrv.Shutdown")
// If we are multiplexing HTTP and gRPC requests on the same
// listener, we need to stop the gRPC server here as well. We
// cannot stop it gracefully, but if we stop it forcefully
// here, we guarantee all the handler threads are cleaned up
// before we return.
if listeners.grpc == nil {
logrus.Traceln("Calling grpcSrv.Stop")
s.grpcSrv.Stop()
s.grpcHttpReqsWG.Wait()
logrus.Traceln("Finished calling grpcSrv.Stop")
}
}()
s.wg.Wait()
}