runner: bound dead ssh connections !373

merged merged by cmc on 2026-09-11 02:08 UTC · krz/gitbay:runner-keepalive into main

3 files changed, +29 −1

Layout: unified · split

cmd/gitbay-runner/config.go +11
@@ -96,3 +96,14 @@ func identityOpts(path string) []string {
96 } 96 }
97 return []string{"-F", "/dev/null", "-i", path, "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=accept-new"} 97 return []string{"-F", "/dev/null", "-i", path, "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=accept-new"}
98} 98}
99
100// sshOptions is every ssh invocation's option list: the identity, the
101// operator's -ssh-opts, then a bound on dead connections. Without one a
102// claim whose TCP session died under it (a laptop's network dropping)
103// blocks the poll loop indefinitely; ssh took thirteen hours on one
104// before a restart. ssh honours the first value of an option, so the
105// operator's come first and override these.
106func sshOptions(identity string, extra []string) []string {
107 opts := append(identityOpts(identity), extra...)
108 return append(opts, "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3")
109}
cmd/gitbay-runner/config_test.go +17
@@ -84,3 +84,20 @@ func TestIdentityOpts(t *testing.T) {
84 t.Fatalf("got %q, want %q", got, want) 84 t.Fatalf("got %q, want %q", got, want)
85 } 85 }
86} 86}
87
88func TestSSHOptionsBoundDeadConnections(t *testing.T) {
89 got := strings.Join(sshOptions("/k", []string{"-o", "ServerAliveInterval=60"}), " ")
90 want := "-F /dev/null -i /k -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" +
91 " -o ServerAliveInterval=60" +
92 " -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
93 if got != want {
94 t.Fatalf("got %q, want %q", got, want)
95 }
96 // No identity: the keepalive still applies, so a runner on its
97 // default key cannot hang on a dead connection either.
98 got = strings.Join(sshOptions("", nil), " ")
99 want = "-o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
100 if got != want {
101 t.Fatalf("got %q, want %q", got, want)
102 }
103}
cmd/gitbay-runner/main.go +1 −1
@@ -152,7 +152,7 @@ func main() {
152 } 152 }
153 // Clipped: the later appends run from concurrent workers, and spare 153 // Clipped: the later appends run from concurrent workers, and spare
154 // capacity here would have them writing the same backing array. 154 // capacity here would have them writing the same backing array.
155 r.sshOpts = slices.Clip(append(identityOpts(*identity), r.sshOpts...)) 155 r.sshOpts = slices.Clip(sshOptions(*identity, r.sshOpts))
156 r.untrusted = *untrusted 156 r.untrusted = *untrusted
157 for _, name := range strings.Split(*repos, ",") { 157 for _, name := range strings.Split(*repos, ",") {
158 if name = strings.TrimSpace(name); name != "" { 158 if name = strings.TrimSpace(name); name != "" {