diff --git a/docs/embedding.md b/docs/embedding.md index 4228841..ad057b6 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -32,6 +32,7 @@ the socket is created owner-only (`0600`), so only the embedding program's user ## lifecycle +- ignore SIGPIPE before `hydrant_start`. rust binaries do that at startup but a library can't, and on linux hydrant writing to a socket whose client already left would otherwise kill the whole process. the go wrapper does it for you. - `hydrant_start` returns once the database is open and hydrant is starting. the api binds in the background, so poll it (eg. `GET /stats`) until it answers. - `hydrant_wait` blocks until hydrant stops: when it fails (for example when its socket is already in use) it gives the reason, and after `hydrant_shutdown` it returns 0. - `hydrant_shutdown` stops hydrant and waits for its database to close, which gives its memory back. anything it was in the middle of is cut off like a crash, and the next start picks up from its saved cursors. a hydrant that failed still holds its database until it's shut down. diff --git a/ffi/go/hydrant.go b/ffi/go/hydrant.go index 8490813..4cca35a 100644 --- a/ffi/go/hydrant.go +++ b/ffi/go/hydrant.go @@ -21,6 +21,8 @@ import ( "encoding/json" "errors" "fmt" + "os/signal" + "syscall" "time" "unsafe" ) @@ -38,7 +40,8 @@ type Hydrant struct { // Start runs hydrant with the settings its binary reads from the environment, like // HYDRANT_DATABASE_PATH and RUST_LOG, given as a map instead. the process environment is // ignored. HYDRANT_API_BIND is required, and "none" runs without an api. hydrant binds it in -// the background, so the api may need a moment before it answers. +// the background, so the api may need a moment before it answers. it also ignores SIGPIPE +// for the whole process, so writing to a closed pipe returns EPIPE instead of exiting. func Start(settings map[string]string) (*Hydrant, error) { raw, err := json.Marshal(settings) if err != nil { @@ -47,6 +50,11 @@ func Start(settings map[string]string) (*Hydrant, error) { cSettings := C.CString(string(raw)) defer C.free(unsafe.Pointer(cSettings)) + // hydrant writes to sockets whose peer may be gone, and on linux that raises + // SIGPIPE on a rust thread, which go doesn't handle and so the default kills + // the whole process. rust binaries ignore it at startup, and this does the same + signal.Ignore(syscall.SIGPIPE) + var cErr *C.char handle := C.hydrant_start(cSettings, &cErr) if handle == nil { diff --git a/ffi/go/hydrant_test.go b/ffi/go/hydrant_test.go index 4f6a79b..9dc1645 100644 --- a/ffi/go/hydrant_test.go +++ b/ffi/go/hydrant_test.go @@ -109,7 +109,7 @@ func request(t *testing.T, client *http.Client, method, path, body string) strin } // opens a /stream websocket and leaves it open until the test ends -func subscribe(t *testing.T, socket string) { +func subscribe(t *testing.T, socket string) net.Conn { t.Helper() conn, err := net.Dial("unix", socket) if err != nil { @@ -126,6 +126,7 @@ func subscribe(t *testing.T, socket string) { if !strings.Contains(status, "101") { t.Errorf("/stream didn't upgrade over the socket: %q", status) } + return conn } func TestEmbeddedHydrantServesOverUnixSocket(t *testing.T) { @@ -231,6 +232,26 @@ func TestShutdownLetsTheSameDatabaseStartAgain(t *testing.T) { } } +func TestShutdownAfterTheSubscriberHungUp(t *testing.T) { + dir := shortTempDir(t) + settings := offlineSettings(dir) + socket := strings.TrimPrefix(settings["HYDRANT_API_BIND"], "unix:") + h, err := Start(settings) + if err != nil { + t.Fatalf("start: %v", err) + } + waitReady(t, h, unixClient(socket)) + + // hydrant's goodbye to this socket is a write into a closed pipe, and on + // linux that raises SIGPIPE on a rust thread, which go doesn't handle. the + // sleep lets it get there before the shutdown cuts that task off + subscribe(t, socket).Close() + time.Sleep(200 * time.Millisecond) + if err := h.Shutdown(10 * time.Second); err != nil { + t.Fatalf("shutdown: %v", err) + } +} + func TestSocketModeIsConfigurable(t *testing.T) { dir := shortTempDir(t) settings := offlineSettings(dir) diff --git a/ffi/include/hydrant.h b/ffi/include/hydrant.h index 3106618..4e55035 100644 --- a/ffi/include/hydrant.h +++ b/ffi/include/hydrant.h @@ -14,7 +14,8 @@ typedef struct HydrantHandle hydrant_t; /* starts hydrant from a json object of the HYDRANT_* settings the binary reads from its * environment, plus RUST_LOG. HYDRANT_API_BIND is required, and `none` runs without an api. - * returns NULL and sets *err on failure. */ + * returns NULL and sets *err on failure. ignore SIGPIPE before calling this, the way + * rust binaries do at startup, or a write to a socket whose peer left kills the process. */ hydrant_t *hydrant_start(const char *settings_json, char **err); /* blocks until hydrant stops. returns -1 with *err set when it failed, or 0 once hydrant_shutdown