+141
cmd/goat/relay.go
+141
cmd/goat/relay.go
···
1
1
package main
2
2
3
3
import (
4
+
"context"
4
5
"encoding/json"
5
6
"fmt"
7
+
"sort"
6
8
7
9
comatproto "github.com/bluesky-social/indigo/api/atproto"
8
10
"github.com/bluesky-social/indigo/atproto/syntax"
···
92
94
},
93
95
},
94
96
Action: runRelayHostStatus,
97
+
},
98
+
&cli.Command{
99
+
Name: "diff",
100
+
Usage: "compare host set (and seq) between two relay instances",
101
+
ArgsUsage: `<relay-one> <relay-two>`,
102
+
Flags: []cli.Flag{
103
+
&cli.BoolFlag{
104
+
Name: "verbose",
105
+
Usage: "print all hosts",
106
+
},
107
+
&cli.IntFlag{
108
+
Name: "seq-slop",
109
+
Value: 100,
110
+
Usage: "sequence delta allowed as close enough",
111
+
},
112
+
},
113
+
Action: runRelayHostDiff,
95
114
},
96
115
},
97
116
},
···
326
345
327
346
return nil
328
347
}
348
+
349
+
type hostInfo struct {
350
+
Hostname string
351
+
Status string
352
+
Seq int64
353
+
}
354
+
355
+
func fetchHosts(ctx context.Context, relayHost string) ([]hostInfo, error) {
356
+
357
+
client := xrpc.Client{
358
+
Host: relayHost,
359
+
}
360
+
361
+
hosts := []hostInfo{}
362
+
cursor := ""
363
+
var size int64 = 500
364
+
for {
365
+
resp, err := comatproto.SyncListHosts(ctx, &client, cursor, size)
366
+
if err != nil {
367
+
return nil, err
368
+
}
369
+
370
+
for _, h := range resp.Hosts {
371
+
if h.Status == nil || h.Seq == nil || *h.Seq <= 0 {
372
+
continue
373
+
}
374
+
375
+
// TODO: only active or idle hosts?
376
+
info := hostInfo{
377
+
Hostname: h.Hostname,
378
+
Status: *h.Status,
379
+
Seq: *h.Seq,
380
+
}
381
+
hosts = append(hosts, info)
382
+
}
383
+
384
+
if resp.Cursor == nil || *resp.Cursor == "" {
385
+
break
386
+
}
387
+
cursor = *resp.Cursor
388
+
}
389
+
return hosts, nil
390
+
}
391
+
392
+
func runRelayHostDiff(cctx *cli.Context) error {
393
+
ctx := cctx.Context
394
+
verbose := cctx.Bool("verbose")
395
+
seqSlop := cctx.Int64("seq-slop")
396
+
397
+
if cctx.Args().Len() != 2 {
398
+
return fmt.Errorf("expected two relay URLs are args")
399
+
}
400
+
401
+
urlOne := cctx.Args().Get(0)
402
+
urlTwo := cctx.Args().Get(1)
403
+
404
+
listOne, err := fetchHosts(ctx, urlOne)
405
+
if err != nil {
406
+
return err
407
+
}
408
+
listTwo, err := fetchHosts(ctx, urlTwo)
409
+
if err != nil {
410
+
return err
411
+
}
412
+
413
+
allHosts := make(map[string]bool)
414
+
mapOne := make(map[string]hostInfo)
415
+
for _, val := range listOne {
416
+
allHosts[val.Hostname] = true
417
+
mapOne[val.Hostname] = val
418
+
}
419
+
mapTwo := make(map[string]hostInfo)
420
+
for _, val := range listTwo {
421
+
allHosts[val.Hostname] = true
422
+
mapTwo[val.Hostname] = val
423
+
}
424
+
425
+
names := []string{}
426
+
for k, _ := range allHosts {
427
+
names = append(names, k)
428
+
}
429
+
sort.Strings(names)
430
+
431
+
for _, k := range names {
432
+
one, okOne := mapOne[k]
433
+
two, okTwo := mapTwo[k]
434
+
if !okOne {
435
+
if !verbose && two.Status != "active" {
436
+
continue
437
+
}
438
+
fmt.Printf("%s\t\t%s/%d\tone-missing\n", k, two.Status, two.Seq)
439
+
} else if !okTwo {
440
+
if !verbose && one.Status != "active" {
441
+
continue
442
+
}
443
+
fmt.Printf("%s\t%s/%d\t\ttwo-missing\n", k, one.Status, one.Seq)
444
+
} else {
445
+
status := ""
446
+
if one.Status != two.Status {
447
+
status = "diff"
448
+
} else {
449
+
delta := max(one.Seq, two.Seq) - min(one.Seq, two.Seq)
450
+
if delta == 0 {
451
+
status = "exact"
452
+
if !verbose {
453
+
continue
454
+
}
455
+
} else if delta < seqSlop {
456
+
status = "close"
457
+
if !verbose {
458
+
continue
459
+
}
460
+
} else {
461
+
status = fmt.Sprintf("delta=%d", delta)
462
+
}
463
+
}
464
+
fmt.Printf("%s\t%s/%d\t%s/%d\t%s\n", k, one.Status, one.Seq, two.Status, two.Seq, status)
465
+
}
466
+
}
467
+
468
+
return nil
469
+
}