grpc: Automatically detect if a target is the current server itself.

This allows configuring the same list of targets for all instances without
having to setup the "own" address differently for each server.
This commit is contained in:
Joachim Bauch 2022-06-29 13:33:02 +02:00
parent 5a242b2570
commit 20cc51c2fe
No known key found for this signature in database
GPG key ID: 77C1D22D53E15F02
11 changed files with 140 additions and 70 deletions

View file

@ -119,6 +119,7 @@ common_easyjson: \
api_signaling_easyjson.go api_signaling_easyjson.go
common_proto: \ common_proto: \
grpc_internal.pb.go \
grpc_mcu.pb.go \ grpc_mcu.pb.go \
grpc_sessions.pb.go grpc_sessions.pb.go

View file

@ -88,7 +88,7 @@ func CreateBackendServerForTestFromConfig(t *testing.T, config *goconf.ConfigFil
config.AddOption("clients", "internalsecret", string(testInternalSecret)) config.AddOption("clients", "internalsecret", string(testInternalSecret))
config.AddOption("geoip", "url", "none") config.AddOption("geoip", "url", "none")
events := getAsyncEventsForTest(t) events := getAsyncEventsForTest(t)
hub, err := NewHub(config, events, nil, r, "no-version") hub, err := NewHub(config, events, nil, nil, r, "no-version")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -162,7 +162,7 @@ func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *g
events1.Close() events1.Close()
}) })
client1 := NewGrpcClientsForTest(t, addr2) client1 := NewGrpcClientsForTest(t, addr2)
hub1, err := NewHub(config1, events1, client1, r1, "no-version") hub1, err := NewHub(config1, events1, grpcServer1, client1, r1, "no-version")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -191,7 +191,7 @@ func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *g
events2.Close() events2.Close()
}) })
client2 := NewGrpcClientsForTest(t, addr1) client2 := NewGrpcClientsForTest(t, addr1)
hub2, err := NewHub(config2, events2, client2, r2, "no-version") hub2, err := NewHub(config2, events2, grpcServer2, client2, r2, "no-version")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -211,20 +211,6 @@ func CreateBackendServerWithClusteringForTestFromConfig(t *testing.T, config1 *g
t.Fatal(err) t.Fatal(err)
} }
grpcServer1.hub = hub1
grpcServer2.hub = hub2
go func() {
if err := grpcServer1.Run(); err != nil {
t.Errorf("Could not start RPC server on %s: %s", addr1, err)
}
}()
go func() {
if err := grpcServer2.Run(); err != nil {
t.Errorf("Could not start RPC server on %s: %s", addr2, err)
}
}()
go hub1.Run() go hub1.Run()
go hub2.Run() go hub2.Run()

View file

@ -53,12 +53,14 @@ func init() {
} }
type grpcClientImpl struct { type grpcClientImpl struct {
RpcInternalClient
RpcMcuClient RpcMcuClient
RpcSessionsClient RpcSessionsClient
} }
func newGrpcClientImpl(conn grpc.ClientConnInterface) *grpcClientImpl { func newGrpcClientImpl(conn grpc.ClientConnInterface) *grpcClientImpl {
return &grpcClientImpl{ return &grpcClientImpl{
RpcInternalClient: NewRpcInternalClient(conn),
RpcMcuClient: NewRpcMcuClient(conn), RpcMcuClient: NewRpcMcuClient(conn),
RpcSessionsClient: NewRpcSessionsClient(conn), RpcSessionsClient: NewRpcSessionsClient(conn),
} }
@ -90,6 +92,16 @@ func (c *GrpcClient) Close() error {
return c.conn.Close() return c.conn.Close()
} }
func (c *GrpcClient) GetServerId(ctx context.Context) (string, error) {
statsGrpcClientCalls.WithLabelValues("GetServerId").Inc()
response, err := c.impl.GetServerId(ctx, &GetServerIdRequest{}, grpc.WaitForReady(true))
if err != nil {
return "", err
}
return response.GetServerId(), nil
}
func (c *GrpcClient) LookupSessionId(ctx context.Context, roomSessionId string) (string, error) { func (c *GrpcClient) LookupSessionId(ctx context.Context, roomSessionId string) (string, error) {
statsGrpcClientCalls.WithLabelValues("LookupSessionId").Inc() statsGrpcClientCalls.WithLabelValues("LookupSessionId").Inc()
// TODO: Remove debug logging // TODO: Remove debug logging
@ -154,7 +166,6 @@ type GrpcClients struct {
etcdClient *EtcdClient etcdClient *EtcdClient
targetPrefix string targetPrefix string
targetSelf string
targetInformation map[string]*GrpcTargetInformationEtcd targetInformation map[string]*GrpcTargetInformationEtcd
dialOptions atomic.Value // []grpc.DialOption dialOptions atomic.Value // []grpc.DialOption
@ -241,6 +252,19 @@ func (c *GrpcClients) loadTargetsStatic(config *goconf.ConfigFile, opts ...grpc.
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if id, err := client.GetServerId(ctx); err != nil {
log.Printf("Error checking server id of %s: %s", client.Target(), err)
} else if id == GrpcServerId {
log.Printf("GRPC target %s is this server, ignoring", client.Target())
if err := client.Close(); err != nil {
log.Printf("Error closing client to %s: %s", client.Target(), err)
}
continue
}
log.Printf("Adding %s as GRPC target", target) log.Printf("Adding %s as GRPC target", target)
clientsMap[target] = client clientsMap[target] = client
clients = append(clients, client) clients = append(clients, client)
@ -277,9 +301,6 @@ func (c *GrpcClients) loadTargetsEtcd(config *goconf.ConfigFile, opts ...grpc.Di
c.targetInformation = make(map[string]*GrpcTargetInformationEtcd) c.targetInformation = make(map[string]*GrpcTargetInformationEtcd)
} }
targetSelf, _ := config.GetString("grpc", "targetself")
c.targetSelf = targetSelf
if opts == nil { if opts == nil {
opts = make([]grpc.DialOption, 0) opts = make([]grpc.DialOption, 0)
} }
@ -353,12 +374,6 @@ func (c *GrpcClients) EtcdKeyUpdated(client *EtcdClient, key string, data []byte
c.removeEtcdClientLocked(key) c.removeEtcdClientLocked(key)
} }
if c.targetSelf != "" && info.Address == c.targetSelf {
log.Printf("GRPC target %s is this server, ignoring %s", info.Address, key)
c.wakeupForTesting()
return
}
if _, found := c.clientsMap[info.Address]; found { if _, found := c.clientsMap[info.Address]; found {
log.Printf("GRPC target %s already exists, ignoring %s", info.Address, key) log.Printf("GRPC target %s already exists, ignoring %s", info.Address, key)
return return
@ -371,6 +386,20 @@ func (c *GrpcClients) EtcdKeyUpdated(client *EtcdClient, key string, data []byte
return return
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if id, err := cl.GetServerId(ctx); err != nil {
log.Printf("Error checking server id of %s: %s", cl.Target(), err)
} else if id == GrpcServerId {
log.Printf("GRPC target %s is this server, ignoring %s", cl.Target(), key)
if err := cl.Close(); err != nil {
log.Printf("Error closing client to %s: %s", cl.Target(), err)
}
c.wakeupForTesting()
return
}
log.Printf("Adding %s as GRPC target", info.Address) log.Printf("Adding %s as GRPC target", info.Address)
if c.clientsMap == nil { if c.clientsMap == nil {

View file

@ -30,10 +30,6 @@ import (
"go.etcd.io/etcd/server/v3/embed" "go.etcd.io/etcd/server/v3/embed"
) )
const (
GrpcSelfTargetForTesting = "testing.grpc.target"
)
func NewGrpcClientsForTest(t *testing.T, addr string) *GrpcClients { func NewGrpcClientsForTest(t *testing.T, addr string) *GrpcClients {
config := goconf.NewConfigFile() config := goconf.NewConfigFile()
config.AddOption("grpc", "targets", addr) config.AddOption("grpc", "targets", addr)
@ -55,7 +51,6 @@ func NewGrpcClientsWithEtcdForTest(t *testing.T, etcd *embed.Etcd) *GrpcClients
config.AddOption("grpc", "targettype", "etcd") config.AddOption("grpc", "targettype", "etcd")
config.AddOption("grpc", "targetprefix", "/grpctargets") config.AddOption("grpc", "targetprefix", "/grpctargets")
config.AddOption("grpc", "targetself", GrpcSelfTargetForTesting)
etcdClient, err := NewEtcdClient(config, "") etcdClient, err := NewEtcdClient(config, "")
if err != nil { if err != nil {
@ -89,11 +84,12 @@ func drainWakeupChannel(ch chan bool) {
} }
func Test_GrpcClients_EtcdInitial(t *testing.T) { func Test_GrpcClients_EtcdInitial(t *testing.T) {
_, addr1 := NewGrpcServerForTest(t)
_, addr2 := NewGrpcServerForTest(t)
etcd := NewEtcdForTest(t) etcd := NewEtcdForTest(t)
_, addr1 := NewGrpcServerForTest(t)
SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}")) SetEtcdValue(etcd, "/grpctargets/one", []byte("{\"address\":\""+addr1+"\"}"))
_, addr2 := NewGrpcServerForTest(t)
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}")) SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}"))
client := NewGrpcClientsWithEtcdForTest(t, etcd) client := NewGrpcClientsWithEtcdForTest(t, etcd)
@ -181,7 +177,9 @@ func Test_GrpcClients_EtcdIgnoreSelf(t *testing.T) {
} }
drainWakeupChannel(ch) drainWakeupChannel(ch)
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+GrpcSelfTargetForTesting+"\"}")) server2, addr2 := NewGrpcServerForTest(t)
server2.serverId = GrpcServerId
SetEtcdValue(etcd, "/grpctargets/two", []byte("{\"address\":\""+addr2+"\"}"))
<-ch <-ch
if clients := client.GetClients(); len(clients) != 1 { if clients := client.GetClients(); len(clients) != 1 {
t.Errorf("Expected one client, got %+v", clients) t.Errorf("Expected one client, got %+v", clients)

37
grpc_internal.proto Normal file
View file

@ -0,0 +1,37 @@
/**
* Standalone signaling server for the Nextcloud Spreed app.
* Copyright (C) 2022 struktur AG
*
* @author Joachim Bauch <bauch@struktur.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
syntax = "proto3";
option go_package = "github.com/strukturag/nextcloud-spreed-signaling;signaling";
package signaling;
service RpcInternal {
rpc GetServerId(GetServerIdRequest) returns (GetServerIdReply) {}
}
message GetServerIdRequest {
}
message GetServerIdReply {
string serverId = 1;
}

View file

@ -23,10 +23,13 @@ package signaling
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net" "net"
"os"
"github.com/dlintw/goconf" "github.com/dlintw/goconf"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -35,16 +38,30 @@ import (
status "google.golang.org/grpc/status" status "google.golang.org/grpc/status"
) )
var (
GrpcServerId string
)
func init() { func init() {
RegisterGrpcServerStats() RegisterGrpcServerStats()
hostname, err := os.Hostname()
if err != nil {
hostname = newRandomString(8)
}
md := sha256.New()
md.Write([]byte(fmt.Sprintf("%s-%s-%d", newRandomString(32), hostname, os.Getpid())))
GrpcServerId = hex.EncodeToString(md.Sum(nil))
} }
type GrpcServer struct { type GrpcServer struct {
UnimplementedRpcInternalServer
UnimplementedRpcMcuServer UnimplementedRpcMcuServer
UnimplementedRpcSessionsServer UnimplementedRpcSessionsServer
conn *grpc.Server conn *grpc.Server
listener net.Listener listener net.Listener
serverId string // can be overwritten from tests
hub *Hub hub *Hub
} }
@ -77,7 +94,9 @@ func NewGrpcServer(config *goconf.ConfigFile) (*GrpcServer, error) {
result := &GrpcServer{ result := &GrpcServer{
conn: conn, conn: conn,
listener: listener, listener: listener,
serverId: GrpcServerId,
} }
RegisterRpcInternalServer(conn, result)
RegisterRpcSessionsServer(conn, result) RegisterRpcSessionsServer(conn, result)
RegisterRpcMcuServer(conn, result) RegisterRpcMcuServer(conn, result)
return result, nil return result, nil
@ -160,3 +179,10 @@ func (s *GrpcServer) GetPublisherId(ctx context.Context, request *GetPublisherId
return nil, status.Error(codes.NotFound, "no such publisher") return nil, status.Error(codes.NotFound, "no such publisher")
} }
func (s *GrpcServer) GetServerId(ctx context.Context, request *GetServerIdRequest) (*GetServerIdReply, error) {
statsGrpcServerCalls.WithLabelValues("GetServerId").Inc()
return &GetServerIdReply{
ServerId: s.serverId,
}, nil
}

View file

@ -48,6 +48,15 @@ func NewGrpcServerForTest(t *testing.T) (server *GrpcServer, addr string) {
t.Fatal("could not find free port") t.Fatal("could not find free port")
} }
// Don't match with own server id by default.
server.serverId = "dont-match"
go func() {
if err := server.Run(); err != nil {
t.Errorf("could not start GRPC server: %s", err)
}
}()
t.Cleanup(func() { t.Cleanup(func() {
server.Close() server.Close()
}) })

17
hub.go
View file

@ -154,7 +154,7 @@ type Hub struct {
rpcClients *GrpcClients rpcClients *GrpcClients
} }
func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcClients *GrpcClients, r *mux.Router, version string) (*Hub, error) { func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer, rpcClients *GrpcClients, r *mux.Router, version string) (*Hub, error) {
hashKey, _ := config.GetString("sessions", "hashkey") hashKey, _ := config.GetString("sessions", "hashkey")
switch len(hashKey) { switch len(hashKey) {
case 32: case 32:
@ -214,11 +214,6 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcClients *GrpcClien
decodeCaches = append(decodeCaches, NewLruCache(decodeCacheSize)) decodeCaches = append(decodeCaches, NewLruCache(decodeCacheSize))
} }
rpcServer, err := NewGrpcServer(config)
if err != nil {
return nil, err
}
roomSessions, err := NewBuiltinRoomSessions(rpcClients) roomSessions, err := NewBuiltinRoomSessions(rpcClients)
if err != nil { if err != nil {
return nil, err return nil, err
@ -352,7 +347,9 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcClients *GrpcClien
rpcClients: rpcClients, rpcClients: rpcClients,
} }
backend.hub = hub backend.hub = hub
rpcServer.hub = hub if rpcServer != nil {
rpcServer.hub = hub
}
hub.upgrader.CheckOrigin = hub.checkOrigin hub.upgrader.CheckOrigin = hub.checkOrigin
r.HandleFunc("/spreed", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/spreed", func(w http.ResponseWriter, r *http.Request) {
hub.serveWs(w, r) hub.serveWs(w, r)
@ -450,12 +447,6 @@ func (h *Hub) Run() {
go h.updateGeoDatabase() go h.updateGeoDatabase()
h.roomPing.Start() h.roomPing.Start()
defer h.roomPing.Stop() defer h.roomPing.Stop()
go func() {
if err := h.rpcServer.Run(); err != nil {
log.Fatalf("Could not start RPC server: %s", err)
}
}()
defer h.rpcServer.Close()
housekeeping := time.NewTicker(housekeepingInterval) housekeeping := time.NewTicker(housekeepingInterval)
geoipUpdater := time.NewTicker(24 * time.Hour) geoipUpdater := time.NewTicker(24 * time.Hour)

View file

@ -122,7 +122,7 @@ func CreateHubForTestWithConfig(t *testing.T, getConfigFunc func(*httptest.Serve
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
h, err := NewHub(config, events, nil, r, "no-version") h, err := NewHub(config, events, nil, nil, r, "no-version")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -190,7 +190,7 @@ func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*http
t.Fatal(err) t.Fatal(err)
} }
client1 := NewGrpcClientsForTest(t, addr2) client1 := NewGrpcClientsForTest(t, addr2)
h1, err := NewHub(config1, events1, client1, r1, "no-version") h1, err := NewHub(config1, events1, grpcServer1, client1, r1, "no-version")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -210,7 +210,7 @@ func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*http
t.Fatal(err) t.Fatal(err)
} }
client2 := NewGrpcClientsForTest(t, addr1) client2 := NewGrpcClientsForTest(t, addr1)
h2, err := NewHub(config2, events2, client2, r2, "no-version") h2, err := NewHub(config2, events2, grpcServer2, client2, r2, "no-version")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -225,20 +225,6 @@ func CreateClusteredHubsForTestWithConfig(t *testing.T, getConfigFunc func(*http
t.Fatal(err) t.Fatal(err)
} }
grpcServer1.hub = h1
grpcServer2.hub = h2
go func() {
if err := grpcServer1.Run(); err != nil {
t.Errorf("Could not start RPC server on %s: %s", addr1, err)
}
}()
go func() {
if err := grpcServer2.Run(); err != nil {
t.Errorf("Could not start RPC server on %s: %s", addr2, err)
}
}()
go h1.Run() go h1.Run()
go h2.Run() go h2.Run()

View file

@ -270,7 +270,3 @@ connectionsperhost = 8
# "/signaling/cluster/grpc/one" -> {"address": "192.168.0.1:9090"} # "/signaling/cluster/grpc/one" -> {"address": "192.168.0.1:9090"}
# "/signaling/cluster/grpc/two" -> {"address": "192.168.0.2:9090"} # "/signaling/cluster/grpc/two" -> {"address": "192.168.0.2:9090"}
#targetprefix = /signaling/cluster/grpc #targetprefix = /signaling/cluster/grpc
# For target type "etcd": Address of this signaling server instance. Will be
# ignored when retrieved from the etcd cluster to avoid loopback connections.
#targetself = 192.168.0.1:9090

View file

@ -164,6 +164,17 @@ func main() {
} }
}() }()
rpcServer, err := signaling.NewGrpcServer(config)
if err != nil {
log.Fatalf("Could not create RPC server: %s", err)
}
go func() {
if err := rpcServer.Run(); err != nil {
log.Fatalf("Could not start RPC server: %s", err)
}
}()
defer rpcServer.Close()
rpcClients, err := signaling.NewGrpcClients(config, etcdClient) rpcClients, err := signaling.NewGrpcClients(config, etcdClient)
if err != nil { if err != nil {
log.Fatalf("Could not create RPC clients: %s", err) log.Fatalf("Could not create RPC clients: %s", err)
@ -171,7 +182,7 @@ func main() {
defer rpcClients.Close() defer rpcClients.Close()
r := mux.NewRouter() r := mux.NewRouter()
hub, err := signaling.NewHub(config, events, rpcClients, r, version) hub, err := signaling.NewHub(config, events, rpcServer, rpcClients, r, version)
if err != nil { if err != nil {
log.Fatal("Could not create hub: ", err) log.Fatal("Could not create hub: ", err)
} }