Skip to content

Commit 0fa5fbe

Browse files
committed
Add a new test for our SIGPIPE handling in the common code
This test makes sure that we ignore SIGPIPE. Instead, writes to a broken pipe will cause the write call to return an error. The tools should always check errors on write calls, so these will be surfaced as normal "failure to write to an fd" errors, as opposed to the signal aborting the process.
1 parent aede66b commit 0fa5fbe

6 files changed

Lines changed: 91 additions & 157 deletions

File tree

common/signals/signals.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,23 @@ func Handle() chan struct{} {
2121
return HandleWithInterrupt(nil)
2222
}
2323

24-
// HandleWithInterrupt starts a goroutine which listens for SIGTERM, SIGINT, and
25-
// SIGKILL and explicitly ignores SIGPIPE. It calls the finalizer function when
26-
// the first signal is received and forcibly terminates the program after the
27-
// second. If a nil function is provided, the program will exit after the first
28-
// signal.
24+
// HandleWithInterrupt starts a goroutine which listens for SIGTERM, SIGINT, and SIGKILL. It also
25+
// calles signal.Ignore to explicitly ignore SIGPIPE. It calls the finalizer function when the first
26+
// signal is received and forcibly terminates the program after the second. If a nil function is
27+
// provided, the program will exit after the first signal.
2928
func HandleWithInterrupt(finalizer func()) chan struct{} {
29+
// Ignore SIGPIPE synchronously so there is no race between this call and the first write: a
30+
// broken pipe must surface as an EPIPE write error, not kill the process.
31+
signal.Ignore(syscall.SIGPIPE)
32+
3033
finishedChan := make(chan struct{})
3134
go handleSignals(finalizer, finishedChan)
3235
return finishedChan
3336
}
3437

3538
func handleSignals(finalizer func(), finishedChan chan struct{}) {
36-
// explicitly ignore SIGPIPE; the tools should deal with write errors
37-
noopChan := make(chan os.Signal)
38-
signal.Notify(noopChan, syscall.SIGPIPE)
39-
4039
log.Logv(log.DebugLow, "will listen for SIGTERM, SIGINT, and SIGKILL")
41-
sigChan := make(chan os.Signal, 2)
40+
sigChan := make(chan os.Signal, 1)
4241
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
4342
defer signal.Stop(sigChan)
4443
if finalizer != nil {

common/signals/signals_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (C) MongoDB, Inc. 2014-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package signals
8+
9+
import (
10+
"errors"
11+
"fmt"
12+
"os"
13+
"os/exec"
14+
"syscall"
15+
"testing"
16+
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
// TestMain handles the subprocess mode used by TestBrokenPipeHandledAsWriteError.
22+
func TestMain(m *testing.M) {
23+
if os.Getenv("TEST_BROKEN_PIPE_SUBPROCESS") == "1" {
24+
// Set up SIGPIPE handling as our tools do.
25+
done := HandleWithInterrupt(nil)
26+
defer close(done)
27+
28+
// Write repeatedly to stdout. When the reader closes the pipe,
29+
// the write will return an EPIPE error. We exit 0 to signal that
30+
// the error was surfaced and handled — not a SIGPIPE death.
31+
for {
32+
_, err := fmt.Println("data")
33+
if err != nil {
34+
os.Exit(0)
35+
}
36+
}
37+
}
38+
os.Exit(m.Run())
39+
}
40+
41+
// TestBrokenPipeHandledAsWriteError verifies that when the read end of a pipe
42+
// is closed, our signal handler causes the write error to surface as an EPIPE
43+
// (allowing the tool to exit cleanly) rather than the process being killed by
44+
// SIGPIPE (from all the '*_broken_pipe.js' JS tests).
45+
func TestBrokenPipeHandledAsWriteError(t *testing.T) {
46+
cmd := exec.Command(os.Args[0], "-test.run=TestBrokenPipeHandledAsWriteError")
47+
cmd.Env = append(os.Environ(), "TEST_BROKEN_PIPE_SUBPROCESS=1")
48+
49+
pr, pw, err := os.Pipe()
50+
require.NoError(t, err)
51+
cmd.Stdout = pw
52+
53+
require.NoError(t, cmd.Start())
54+
require.NoError(t, pw.Close()) // parent closes its copy so reads see EOF
55+
56+
// Let the subprocess write a little before breaking the pipe.
57+
buf := make([]byte, 64)
58+
_, err = pr.Read(buf)
59+
require.NoError(t, err)
60+
require.NoError(t, pr.Close()) // break the pipe
61+
62+
err = cmd.Wait()
63+
64+
// Exit 0 means the subprocess caught EPIPE and handled it cleanly.
65+
if err == nil {
66+
return
67+
}
68+
69+
var exitErr *exec.ExitError
70+
if errors.As(err, &exitErr) {
71+
status, ok := exitErr.Sys().(syscall.WaitStatus)
72+
require.True(t, ok)
73+
if status.Signaled() {
74+
assert.NotEqual(
75+
t,
76+
syscall.SIGPIPE,
77+
status.Signal(),
78+
"process should not be killed by SIGPIPE: broken pipe should surface as a write error",
79+
)
80+
}
81+
}
82+
}

test/qa-tests/jstests/bson/bsondump_broken_pipe.js

Lines changed: 0 additions & 18 deletions
This file was deleted.

test/qa-tests/jstests/dump/dump_broken_pipe.js

Lines changed: 0 additions & 49 deletions
This file was deleted.

test/qa-tests/jstests/export/export_broken_pipe.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

test/qa-tests/jstests/stat/stat_broken_pipe.js

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)