Repository navigation
Expand file tree
/
Copy pathcluster.go
More file actions
242 lines (227 loc) · 7.58 KB
/
Copy pathcluster.go
File metadata and controls
242 lines (227 loc) · 7.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package main
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/Agent-Field/codeaf/internal/clusterctl"
cs "github.com/Agent-Field/codeaf/internal/clusterstate"
"github.com/Agent-Field/codeaf/internal/devname"
"github.com/Agent-Field/codeaf/internal/guard"
"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/syncsetup"
"github.com/Agent-Field/codeaf/internal/tui2/tokens"
"io"
"os"
"slices"
"strings"
"time"
)
const clusterVerboseFlag = "verbose"
const clusterHelp = `codeaf cluster status [--` + clusterVerboseFlag + `]
Show the computers coordinating, deciding changes and keeping copies.
codeaf cluster lead [--` + clusterVerboseFlag + `]
Ask this joined computer to coordinate through the sync service.
Persistent storage is required; keep codeaf open while this computer leads.
Three deciding computers tolerate one unavailable computer.
`
type clusterDoor interface {
ClusterStatus(context.Context) (clusterctl.Status, error)
LeadCluster(context.Context, uint64) (cs.LeadershipReceipt, error)
}
const clusterStatusTimeout = 5 * time.Second
func runCluster(args []string) error {
if askedForHelp(args) {
fmt.Fprint(usageOut, clusterHelp)
return exitHelped
}
flags := commandFlags("cluster")
verbose := flags.Bool(clusterVerboseFlag, false, "show coordination details for debugging")
if err := parseCommandFlags(flags, reorder(flags, args)); err != nil {
return err
}
args = flags.Args()
if len(args) != 1 || args[0] != "status" && args[0] != "lead" {
return errors.New("usage: codeaf cluster status | lead")
}
if syncsetup.Quiet(home.Dir()) {
return syncsetup.ErrQuiet
}
s, ok, err := syncsetup.Open(home.Dir())
if err != nil {
return err
}
if !ok {
return errors.New("sync is off on this computer")
}
ctx, cancel := context.WithTimeout(context.Background(), clusterctl.ForwardTimeout)
defer cancel()
defer terminalSyncProgress(s.Relay)()
return clusterCommandMode(ctx, s, args[0], os.Stdout, *verbose, s.Relay)
}
// clusterCommandOptions carries the terminal's explicit mode and selected
// service. Calls without options keep the existing detailed output.
type clusterCommandOptions struct {
verbose bool
service string
}
func clusterCommandMode(ctx context.Context, c clusterDoor, verb string, out io.Writer, verbose bool, service ...string) error {
options := clusterCommandOptions{verbose: verbose}
if len(service) > 0 {
options.service = service[0]
}
return clusterCommand(ctx, c, verb, out, options)
}
// clusterCommand answers `codeaf cluster status` and `codeaf cluster lead`. The
// whole answer is written into a buffer by work bounded by one ceiling, so a
// store open that ignores its context cannot hold the terminal past it, and an
// answer that arrives late never reaches the terminal at all. Errors are explained
// once, with the service this command used, after either work or its ceiling ends.
func clusterCommand(ctx context.Context, c clusterDoor, verb string, out io.Writer, options ...clusterCommandOptions) error {
selected := clusterCommandOptions{verbose: true}
if len(options) > 0 {
selected = options[0]
}
verbose, address := selected.verbose, selected.service
if verb == "status" {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, clusterStatusTimeout)
defer cancel()
}
err := boundedClusterOutput(ctx, out, func(ctx context.Context, out io.Writer) error {
if verb == "lead" {
r, err := c.LeadCluster(ctx, 0)
if err != nil {
return explainRemovedDevice(err)
}
if !verbose {
names := clusterDeviceNames(ctx, c, clusterctl.Status{Self: r.Node})
_, err = fmt.Fprintln(out, names[r.Node]+" now coordinates your devices")
return err
}
_, err = fmt.Fprintf(out, "computer %d is coordinating · term %d · revision %d · read barrier %d\n", r.Node, r.Term, r.Revision, r.Barrier)
return err
}
s, err := c.ClusterStatus(ctx)
if err != nil {
return explainRemovedDevice(err)
}
if !verbose {
return printClusterStatus(out, s, clusterDeviceNames(ctx, c, s))
}
if s.Leader != 0 {
fmt.Fprintf(out, "coordinator %d\n", s.Leader)
}
if len(s.Configuration.Voters) > 0 {
fmt.Fprintf(out, "voting computers %v\n", s.Configuration.Voters)
}
if s.Self != 0 {
fmt.Fprintf(out, "this computer %d\n", s.Self)
}
if len(s.Configuration.Learners) > 0 {
fmt.Fprintf(out, "learning computers %v\n", s.Configuration.Learners)
}
if len(s.Ready) > 0 {
fmt.Fprintf(out, "ready computers %v\n", s.Ready)
}
if len(s.Configuration.Outgoing) > 0 {
fmt.Fprintf(out, "finishing the change from %v\n", s.Configuration.Outgoing)
}
return nil
})
return explainSyncUnreachable(err, address)
}
// boundedClusterOutput runs one cluster answer under the forwarding ceiling and
// copies its text to out only when it finished in time. A deadline is reported
// as the sync service not answering, never as an unreachable network.
func boundedClusterOutput(ctx context.Context, out io.Writer, work func(context.Context, io.Writer) error) error {
ctx, cancel := context.WithTimeout(ctx, clusterctl.ForwardTimeout)
defer cancel()
type result struct {
text string
err error
}
done := make(chan result, 1)
guard.Go("cluster/answer", func() {
var output bytes.Buffer
err := work(ctx, &output)
done <- result{output.String(), err}
})
select {
case got := <-done:
if got.err != nil {
return got.err
}
_, err := io.WriteString(out, got.text)
return err
case <-ctx.Done():
return ctx.Err()
}
}
// clusterNamer resolves protocol membership through the encrypted device roster.
type clusterNamer interface {
ClusterNames(context.Context, clusterctl.Status) map[uint64]string
}
func clusterDeviceNames(ctx context.Context, c clusterDoor, status clusterctl.Status) map[uint64]string {
names := map[uint64]string{}
if n, ok := c.(clusterNamer); ok {
names = n.ClusterNames(ctx, status)
}
if names == nil {
names = map[uint64]string{}
}
if names[status.Self] == "" && status.Self != 0 {
names[status.Self] = devname.Name(home.Dir())
}
return names
}
const clusterOtherComputer = "another computer"
func clusterName(names map[uint64]string, node uint64, status clusterctl.Status) string {
if name := names[node]; name != "" {
return name
}
if _, absent := names[node]; absent && slices.ContainsFunc(status.Members, func(member cs.MemberLabel) bool {
return member.Node == node && member.Device != "" && !member.Anchor
}) {
return "a computer no longer in your devices"
}
return clusterOtherComputer
}
func printClusterStatus(out io.Writer, s clusterctl.Status, names map[uint64]string) error {
if s.Leader != 0 {
leader := clusterName(names, s.Leader, s)
if s.Leader == s.Self {
leader += " (this computer)"
}
fmt.Fprintf(out, "coordinating %s\n", leader)
}
join := func(nodes []uint64) string {
var shown []string
unknown := 0
for _, node := range nodes {
name := clusterName(names, node, s)
if name == clusterOtherComputer && names[node] == "" {
unknown++
continue
}
if !slices.Contains(shown, name) {
shown = append(shown, name)
}
}
switch unknown {
case 0:
case 1:
shown = append(shown, clusterOtherComputer)
default:
shown = append(shown, fmt.Sprintf("%d other computers", unknown))
}
return strings.Join(shown, " "+tokens.Plain.Glyph(tokens.GSeparator)+" ")
}
if len(s.Configuration.Voters) > 0 {
fmt.Fprintf(out, "deciding %s %d of %d needed\n", join(s.Configuration.Voters), len(s.Configuration.Voters)/2+1, len(s.Configuration.Voters))
}
if len(s.Configuration.Learners) > 0 {
fmt.Fprintf(out, "keeping copies %s\n", join(s.Configuration.Learners))
}
return nil
}