Skip to content

Commit 25dabf9

Browse files
committed
Allow configuring GRPC targets through etcd.
1 parent b6e419f commit 25dabf9

File tree

7 files changed

+534
-8
lines changed

7 files changed

+534
-8
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ common: common_easyjson common_proto
114114
common_easyjson: \
115115
api_async_easyjson.go \
116116
api_backend_easyjson.go \
117+
api_grpc_easyjson.go \
117118
api_proxy_easyjson.go \
118119
api_signaling_easyjson.go
119120

api_grpc.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Standalone signaling server for the Nextcloud Spreed app.
3+
* Copyright (C) 2022 struktur AG
4+
*
5+
* @author Joachim Bauch <[email protected]>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as published by
11+
* the Free Software Foundation, either version 3 of the License, or
12+
* (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*/
22+
package signaling
23+
24+
import (
25+
"fmt"
26+
)
27+
28+
// Information on a GRPC target in the etcd cluster.
29+
30+
type GrpcTargetInformationEtcd struct {
31+
Address string `json:"address"`
32+
}
33+
34+
func (p *GrpcTargetInformationEtcd) CheckValid() error {
35+
if l := len(p.Address); l == 0 {
36+
return fmt.Errorf("address missing")
37+
} else if p.Address[l-1] == '/' {
38+
p.Address = p.Address[:l-1]
39+
}
40+
return nil
41+
}

grpc_client.go

Lines changed: 226 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,31 @@ package signaling
2323

2424
import (
2525
"context"
26+
"encoding/json"
2627
"fmt"
2728
"log"
2829
"net"
2930
"strings"
3031
"sync"
32+
"sync/atomic"
33+
"time"
3134

3235
"github.com/dlintw/goconf"
36+
clientv3 "go.etcd.io/etcd/client/v3"
3337
"google.golang.org/grpc"
3438
codes "google.golang.org/grpc/codes"
3539
"google.golang.org/grpc/credentials"
3640
"google.golang.org/grpc/credentials/insecure"
3741
status "google.golang.org/grpc/status"
3842
)
3943

44+
const (
45+
GrpcTargetTypeStatic = "static"
46+
GrpcTargetTypeEtcd = "etcd"
47+
48+
DefaultGrpcTargetType = GrpcTargetTypeStatic
49+
)
50+
4051
func init() {
4152
RegisterGrpcClientStats()
4253
}
@@ -140,20 +151,32 @@ type GrpcClients struct {
140151

141152
clientsMap map[string]*GrpcClient
142153
clients []*GrpcClient
154+
155+
etcdClient *EtcdClient
156+
targetPrefix string
157+
targetSelf string
158+
targetInformation map[string]*GrpcTargetInformationEtcd
159+
dialOptions atomic.Value // []grpc.DialOption
160+
161+
initializedCtx context.Context
162+
initializedFunc context.CancelFunc
163+
wakeupChanForTesting chan bool
143164
}
144165

145-
func NewGrpcClients(config *goconf.ConfigFile) (*GrpcClients, error) {
146-
result := &GrpcClients{}
166+
func NewGrpcClients(config *goconf.ConfigFile, etcdClient *EtcdClient) (*GrpcClients, error) {
167+
initializedCtx, initializedFunc := context.WithCancel(context.Background())
168+
result := &GrpcClients{
169+
etcdClient: etcdClient,
170+
initializedCtx: initializedCtx,
171+
initializedFunc: initializedFunc,
172+
}
147173
if err := result.load(config); err != nil {
148174
return nil, err
149175
}
150176
return result, nil
151177
}
152178

153179
func (c *GrpcClients) load(config *goconf.ConfigFile) error {
154-
c.mu.Lock()
155-
defer c.mu.Unlock()
156-
157180
var opts []grpc.DialOption
158181
caFile, _ := config.GetString("grpc", "ca")
159182
if caFile != "" {
@@ -168,6 +191,25 @@ func (c *GrpcClients) load(config *goconf.ConfigFile) error {
168191
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
169192
}
170193

194+
targetType, _ := config.GetString("grpc", "targettype")
195+
if targetType == "" {
196+
targetType = DefaultGrpcTargetType
197+
}
198+
199+
switch targetType {
200+
case GrpcTargetTypeStatic:
201+
return c.loadTargetsStatic(config, opts...)
202+
case GrpcTargetTypeEtcd:
203+
return c.loadTargetsEtcd(config, opts...)
204+
default:
205+
return fmt.Errorf("unknown GRPC target type: %s", targetType)
206+
}
207+
}
208+
209+
func (c *GrpcClients) loadTargetsStatic(config *goconf.ConfigFile, opts ...grpc.DialOption) error {
210+
c.mu.Lock()
211+
defer c.mu.Unlock()
212+
171213
clientsMap := make(map[string]*GrpcClient)
172214
var clients []*GrpcClient
173215
removeTargets := make(map[string]bool, len(c.clientsMap))
@@ -216,10 +258,185 @@ func (c *GrpcClients) load(config *goconf.ConfigFile) error {
216258

217259
c.clients = clients
218260
c.clientsMap = clientsMap
261+
c.initializedFunc()
219262
statsGrpcClients.Set(float64(len(clients)))
220263
return nil
221264
}
222265

266+
func (c *GrpcClients) loadTargetsEtcd(config *goconf.ConfigFile, opts ...grpc.DialOption) error {
267+
if !c.etcdClient.IsConfigured() {
268+
return fmt.Errorf("No etcd endpoints configured")
269+
}
270+
271+
targetPrefix, _ := config.GetString("grpc", "targetprefix")
272+
if targetPrefix == "" {
273+
return fmt.Errorf("No GRPC target prefix configured")
274+
}
275+
c.targetPrefix = targetPrefix
276+
if c.targetInformation == nil {
277+
c.targetInformation = make(map[string]*GrpcTargetInformationEtcd)
278+
}
279+
280+
targetSelf, _ := config.GetString("grpc", "targetself")
281+
c.targetSelf = targetSelf
282+
283+
if opts == nil {
284+
opts = make([]grpc.DialOption, 0)
285+
}
286+
c.dialOptions.Store(opts)
287+
288+
c.etcdClient.AddListener(c)
289+
return nil
290+
}
291+
292+
func (c *GrpcClients) EtcdClientCreated(client *EtcdClient) {
293+
go func() {
294+
if err := client.Watch(context.Background(), c.targetPrefix, c, clientv3.WithPrefix()); err != nil {
295+
log.Printf("Error processing watch for %s: %s", c.targetPrefix, err)
296+
}
297+
}()
298+
299+
go func() {
300+
client.WaitForConnection()
301+
302+
waitDelay := initialWaitDelay
303+
for {
304+
response, err := c.getGrpcTargets(client, c.targetPrefix)
305+
if err != nil {
306+
if err == context.DeadlineExceeded {
307+
log.Printf("Timeout getting initial list of GRPC targets, retry in %s", waitDelay)
308+
} else {
309+
log.Printf("Could not get initial list of GRPC targets, retry in %s: %s", waitDelay, err)
310+
}
311+
312+
time.Sleep(waitDelay)
313+
waitDelay = waitDelay * 2
314+
if waitDelay > maxWaitDelay {
315+
waitDelay = maxWaitDelay
316+
}
317+
continue
318+
}
319+
320+
for _, ev := range response.Kvs {
321+
c.EtcdKeyUpdated(client, string(ev.Key), ev.Value)
322+
}
323+
c.initializedFunc()
324+
return
325+
}
326+
}()
327+
}
328+
329+
func (c *GrpcClients) getGrpcTargets(client *EtcdClient, targetPrefix string) (*clientv3.GetResponse, error) {
330+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
331+
defer cancel()
332+
333+
return client.Get(ctx, targetPrefix, clientv3.WithPrefix())
334+
}
335+
336+
func (c *GrpcClients) EtcdKeyUpdated(client *EtcdClient, key string, data []byte) {
337+
var info GrpcTargetInformationEtcd
338+
if err := json.Unmarshal(data, &info); err != nil {
339+
log.Printf("Could not decode GRPC target %s=%s: %s", key, string(data), err)
340+
return
341+
}
342+
if err := info.CheckValid(); err != nil {
343+
log.Printf("Received invalid GRPC target %s=%s: %s", key, string(data), err)
344+
return
345+
}
346+
347+
c.mu.Lock()
348+
defer c.mu.Unlock()
349+
350+
prev, found := c.targetInformation[key]
351+
if found && prev.Address != info.Address {
352+
// Address of endpoint has changed, remove old one.
353+
c.removeEtcdClientLocked(key)
354+
}
355+
356+
if c.targetSelf != "" && info.Address == c.targetSelf {
357+
log.Printf("GRPC target %s is this server, ignoring %s", info.Address, key)
358+
c.wakeupForTesting()
359+
return
360+
}
361+
362+
if _, found := c.clientsMap[info.Address]; found {
363+
log.Printf("GRPC target %s already exists, ignoring %s", info.Address, key)
364+
return
365+
}
366+
367+
opts := c.dialOptions.Load().([]grpc.DialOption)
368+
cl, err := NewGrpcClient(info.Address, opts...)
369+
if err != nil {
370+
log.Printf("Could not create GRPC client for target %s: %s", info.Address, err)
371+
return
372+
}
373+
374+
log.Printf("Adding %s as GRPC target", info.Address)
375+
376+
if c.clientsMap == nil {
377+
c.clientsMap = make(map[string]*GrpcClient)
378+
}
379+
c.clientsMap[info.Address] = cl
380+
c.clients = append(c.clients, cl)
381+
c.targetInformation[key] = &info
382+
statsGrpcClients.Inc()
383+
c.wakeupForTesting()
384+
}
385+
386+
func (c *GrpcClients) EtcdKeyDeleted(client *EtcdClient, key string) {
387+
c.mu.Lock()
388+
defer c.mu.Unlock()
389+
390+
c.removeEtcdClientLocked(key)
391+
}
392+
393+
func (c *GrpcClients) removeEtcdClientLocked(key string) {
394+
info, found := c.targetInformation[key]
395+
if !found {
396+
log.Printf("No connection found for %s, ignoring", key)
397+
c.wakeupForTesting()
398+
return
399+
}
400+
401+
delete(c.targetInformation, key)
402+
client, found := c.clientsMap[info.Address]
403+
if !found {
404+
return
405+
}
406+
407+
log.Printf("Removing connection to %s (from %s)", info.Address, key)
408+
if err := client.Close(); err != nil {
409+
log.Printf("Error closing client to %s: %s", client.Target(), err)
410+
}
411+
delete(c.clientsMap, info.Address)
412+
c.clients = make([]*GrpcClient, 0, len(c.clientsMap))
413+
for _, client := range c.clientsMap {
414+
c.clients = append(c.clients, client)
415+
}
416+
statsGrpcClients.Dec()
417+
c.wakeupForTesting()
418+
}
419+
420+
func (c *GrpcClients) WaitForInitialized(ctx context.Context) error {
421+
select {
422+
case <-ctx.Done():
423+
return ctx.Err()
424+
case <-c.initializedCtx.Done():
425+
return nil
426+
}
427+
}
428+
429+
func (c *GrpcClients) wakeupForTesting() {
430+
if c.wakeupChanForTesting == nil {
431+
return
432+
}
433+
434+
select {
435+
case c.wakeupChanForTesting <- true:
436+
default:
437+
}
438+
}
439+
223440
func (c *GrpcClients) Reload(config *goconf.ConfigFile) {
224441
if err := c.load(config); err != nil {
225442
log.Printf("Could not reload RPC clients: %s", err)
@@ -238,6 +455,10 @@ func (c *GrpcClients) Close() {
238455

239456
c.clients = nil
240457
c.clientsMap = nil
458+
459+
if c.etcdClient != nil {
460+
c.etcdClient.RemoveListener(c)
461+
}
241462
}
242463

243464
func (c *GrpcClients) GetClients() []*GrpcClient {

0 commit comments

Comments
 (0)