-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cmd/containerboot: fix unclean shutdown #10035
Conversation
6898c75
to
cb9e41c
Compare
ca71177
to
bfc979c
Compare
Make sure that tailscaled watcher returns when SIGTERM is received and also that it shuts down before tailscaled exits. Updates #10090 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
bfc979c
to
88f32ee
Compare
// only start doing this once we've stopped shelling out to things | ||
// `tailscale up`, otherwise this goroutine can reap the CLI subprocesses | ||
// and wedge bringup. | ||
reaper := func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gave this function a name so I can refer to it in a code comment above- I haven't changed the contents.
cmd/containerboot/main.go
Outdated
if n.NetMap != nil { | ||
addrs := n.NetMap.SelfNode.Addresses().AsSlice() | ||
newCurrentIPs := deephash.Hash(&addrs) | ||
ipsHaveChanged := newCurrentIPs != currentIPs | ||
if cfg.ProxyTo != "" && len(addrs) > 0 && ipsHaveChanged { | ||
log.Printf("Installing proxy rules") | ||
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs, nfr); err != nil { | ||
log.Fatalf("installing ingress proxy rules: %v", err) | ||
} | ||
} | ||
} | ||
if cfg.TailnetTargetIP != "" && ipsHaveChanged && len(addrs) > 0 { | ||
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs, nfr); err != nil { | ||
log.Fatalf("installing egress proxy rules: %v", err) | ||
if cfg.ServeConfigPath != "" && len(n.NetMap.DNS.CertDomains) > 0 { | ||
cd := n.NetMap.DNS.CertDomains[0] | ||
prev := certDomain.Swap(ptr.To(cd)) | ||
if prev == nil || *prev != cd { | ||
select { | ||
case certDomainChanged <- true: | ||
default: | ||
} | ||
} | ||
} | ||
} | ||
currentIPs = newCurrentIPs | ||
if cfg.TailnetTargetIP != "" && ipsHaveChanged && len(addrs) > 0 { | ||
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs, nfr); err != nil { | ||
log.Fatalf("installing egress proxy rules: %v", err) | ||
} | ||
} | ||
currentIPs = newCurrentIPs | ||
|
||
deviceInfo := []any{n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name()} | ||
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(¤tDeviceInfo, &deviceInfo) { | ||
if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name(), n.NetMap.SelfNode.Addresses().AsSlice()); err != nil { | ||
log.Fatalf("storing device ID in kube secret: %v", err) | ||
deviceInfo := []any{n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name()} | ||
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(¤tDeviceInfo, &deviceInfo) { | ||
if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID(), n.NetMap.SelfNode.Name(), n.NetMap.SelfNode.Addresses().AsSlice()); err != nil { | ||
log.Fatalf("storing device ID in kube secret: %v", err) | ||
} | ||
} | ||
} | ||
} | ||
if !startupTasksDone { | ||
if (!wantProxy || currentIPs != deephash.Sum{}) && (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) { | ||
// This log message is used in tests to detect when all | ||
// post-auth configuration is done. | ||
log.Println("Startup complete, waiting for shutdown signal") | ||
startupTasksDone = true | ||
|
||
// Reap all processes, since we are PID1 and need to collect zombies. We can | ||
// only start doing this once we've stopped shelling out to things | ||
// `tailscale up`, otherwise this goroutine can reap the CLI subprocesses | ||
// and wedge bringup. | ||
go func() { | ||
for { | ||
var status unix.WaitStatus | ||
pid, err := unix.Wait4(-1, &status, 0, nil) | ||
if errors.Is(err, unix.EINTR) { | ||
continue | ||
} | ||
if err != nil { | ||
log.Fatalf("Waiting for exited processes: %v", err) | ||
} | ||
if pid == daemonPid { | ||
log.Printf("Tailscaled exited") | ||
os.Exit(0) | ||
if !startupTasksDone { | ||
if (!wantProxy || currentIPs != deephash.Sum{}) && (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) { | ||
// This log message is used in tests to detect when all | ||
// post-auth configuration is done. | ||
log.Println("Startup complete, waiting for shutdown signal") | ||
startupTasksDone = true | ||
|
||
// Reap all processes, since we are PID1 and need to collect zombies. We can | ||
// only start doing this once we've stopped shelling out to things | ||
// `tailscale up`, otherwise this goroutine can reap the CLI subprocesses | ||
// and wedge bringup. | ||
reaper := func() { | ||
for { | ||
var status unix.WaitStatus | ||
pid, err := unix.Wait4(-1, &status, 0, nil) | ||
if errors.Is(err, unix.EINTR) { | ||
continue | ||
} | ||
if err != nil { | ||
log.Fatalf("Waiting for exited processes: %v", err) | ||
} | ||
if pid == daemonProcess.Pid { | ||
log.Printf("Tailscaled exited") | ||
os.Exit(0) | ||
} | ||
} | ||
|
||
} | ||
}() | ||
go reaper() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a code change, just the indentation
for { | ||
n, err := w.Next() | ||
if err != nil { | ||
errChan <- err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know currently it does not matter (since we are exiting the process anyway), but perhaps we should return from this loop on error, and on ctx.Done()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a break for the error case. That should cover ctx.Done()
too since we pass the same context when creating the watcher which should then return an error when context is done
https://github.com/tailscale/tailscale/blob/v1.52.1/client/tailscale/localclient.go#L1350-L1351
cmd/containerboot/main.go
Outdated
// kill tailscaled and let reaper clean up child | ||
// processes. | ||
killTailscaled() | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment above says that we need to let the reaper clean up child processes, but I don't think we have any synchronization in place that would actually guarantee this. As I understand, return
here will race with the reaper, and there's a chance that the containerboot
process will terminate before it gets the SIGCHLD about tailscaled
.
I suspect we don't actually need to reap tailscaled after it's done, since we are exiting anyway, and the whole container will terminate imminently. So perhaps simply return
here will be sufficient, letting killTailscaled
run as part of defer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've swapped the return with a break + wait for the reaper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read around a bit and my understanding is that the container orchestrator won't necessarily reap zombies
krallin/tini#8 (comment) (couldn't find a more authoritative source)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the container orchestrator won't necessarily reap zombies
I believe if containerboot
runs as PID 1 in a given PID namespace, and exits, then the kernel will destroy the PID namespace alongside any unreaped zombies. If we share the PID namespace with other processes, then it will be init
's job to reap it, so it's good to try to do that ourselves. Adding a wg to wait for the reaper makes sense, thanks!
@irbekrm is there a timeline for when this will be merged and deployed? got a customer asking about it. |
It is too late for 1.54.0, platform testing has already substantially completed. |
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
492ec4b
to
e911c1c
Compare
I think we should be able to cut a new tag once it merges though (so after the 1.54) |
Thanks for the review @knyar ! I think I've addressed the comments, PTAL |
@deandre the latest containerboot image |
This PR fixes a bug where containerboot does not shut down cleanly when SIGTERM is received.
It ensures that:
I have verified that the fix works following the same steps used to reproduce the issue, see #10090
Updates #10090