Skip to content
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

Merged
merged 2 commits into from
Nov 16, 2023
Merged

cmd/containerboot: fix unclean shutdown #10035

merged 2 commits into from
Nov 16, 2023

Conversation

irbekrm
Copy link
Contributor

@irbekrm irbekrm commented Oct 31, 2023

This PR fixes a bug where containerboot does not shut down cleanly when SIGTERM is received.

It ensures that:

  • containerboot's long running tailscaled watch shuts down when SIGTERM is received
  • the watch shuts down before tailscaled

I have verified that the fix works following the same steps used to reproduce the issue, see #10090

Updates #10090

@irbekrm irbekrm marked this pull request as draft November 1, 2023 05:59
@irbekrm irbekrm force-pushed the irbekrm/fixshutdown branch 4 times, most recently from ca71177 to bfc979c Compare November 2, 2023 18:58
@irbekrm irbekrm marked this pull request as ready for review November 2, 2023 18:59
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>
@irbekrm irbekrm changed the title WIP: cmd/containerboot: fix shutdown cmd/containerboot: fix shutdown Nov 2, 2023
@irbekrm irbekrm changed the title cmd/containerboot: fix shutdown cmd/containerboot: fix unclean shutdown Nov 2, 2023
// 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() {
Copy link
Contributor Author

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.

Comment on lines 313 to 411
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(&currentDeviceInfo, &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(&currentDeviceInfo, &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()
}
Copy link
Contributor Author

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

@irbekrm irbekrm requested review from maisem and knyar November 2, 2023 19:11
cmd/containerboot/main.go Outdated Show resolved Hide resolved
for {
n, err := w.Next()
if err != nil {
errChan <- err
Copy link
Contributor

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()?

Copy link
Contributor Author

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

// kill tailscaled and let reaper clean up child
// processes.
killTailscaled()
return
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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)

Copy link
Contributor

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!

cmd/containerboot/main.go Outdated Show resolved Hide resolved
@deandre
Copy link

deandre commented Nov 14, 2023

@irbekrm is there a timeline for when this will be merged and deployed? got a customer asking about it.

@DentonGentry
Copy link
Contributor

DentonGentry commented Nov 14, 2023

It is too late for 1.54.0, platform testing has already substantially completed.
This can go in after 1.54 branches, and be suitable for delivery in 1.56.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
@irbekrm
Copy link
Contributor Author

irbekrm commented Nov 15, 2023

This can go in after 1.54 branches, and be suitable for delivery in 1.56.

I think we should be able to cut a new tag once it merges though (so after the 1.54)

@irbekrm
Copy link
Contributor Author

irbekrm commented Nov 15, 2023

Thanks for the review @knyar ! I think I've addressed the comments, PTAL

@irbekrm irbekrm merged commit 664ebb1 into main Nov 16, 2023
45 checks passed
@irbekrm irbekrm deleted the irbekrm/fixshutdown branch November 16, 2023 19:23
@irbekrm
Copy link
Contributor Author

irbekrm commented Nov 20, 2023

@deandre the latest containerboot image tailscale/tailscale:unstable-v1.55.46 will have the fix

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants