// Package hydrant runs hydrant inside a go program through its C api.
//
// hydrant keeps serving its usual http api, so after Start you talk to it over the bind you
// gave it, ideally a unix socket. link against libhydrant_ffi.a by putting its directory in
// CGO_LDFLAGS, eg. CGO_LDFLAGS=-L/path/to/target/release.
package hydrant
/*
#cgo CFLAGS: -I${SRCDIR}/../include
#cgo LDFLAGS: -lhydrant_ffi
#cgo darwin LDFLAGS: -framework SystemConfiguration -framework Security -framework CoreFoundation -liconv -lm
// no -lgcc_s because musl has no shared one, and gcc already pulls in an unwinder
// either way (libgcc_s when dynamic, libgcc_eh when static)
#cgo linux LDFLAGS: -lutil -lrt -lpthread -lm -ldl
#include <stdlib.h>
#include "hydrant.h"
*/
import "C"
import (
"encoding/json"
"errors"
"fmt"
"os/signal"
"syscall"
"time"
"unsafe"
)
// ErrShutdown is what Err reports after Shutdown stopped hydrant.
var ErrShutdown = errors.New("hydrant was shut down")
// Hydrant is a hydrant running in this process, until Shutdown.
type Hydrant struct {
handle *C.hydrant_t
done chan struct{}
err error
}
// 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. 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 {
return nil, fmt.Errorf("encode hydrant settings: %w", err)
}
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 {
return nil, takeErr(cErr)
}
h := &Hydrant{handle: handle, done: make(chan struct{})}
go func() {
var cErr *C.char
if C.hydrant_wait(handle, &cErr) != 0 {
h.err = takeErr(cErr)
} else {
h.err = ErrShutdown
}
close(h.done)
}()
return h, nil
}
// Done is closed once hydrant stops, because it failed or Shutdown stopped it. a failed
// hydrant still holds its database until Shutdown.
func (h *Hydrant) Done() <-chan struct{} {
return h.done
}
// Shutdown stops hydrant and waits up to timeout for its database to close, so its memory
// is given back and Start can open the same database again. whatever hydrant was in the
// middle of is cut off like a crash, which it recovers from on the next start. call it
// once, from one goroutine, also after a failure. it errors if the database is still open
// after timeout, and the Hydrant can't be used again either way.
func (h *Hydrant) Shutdown(timeout time.Duration) error {
if h.handle == nil {
return errors.New("hydrant was already shut down")
}
var cErr *C.char
var err error
if C.hydrant_shutdown(h.handle, C.uint64_t(timeout.Milliseconds()), &cErr) != 0 {
err = takeErr(cErr)
}
// the wait goroutine still uses the handle until it sees the stop
<-h.done
C.hydrant_free(h.handle)
h.handle = nil
return err
}
// Err says why hydrant stopped. it's nil while hydrant is running.
func (h *Hydrant) Err() error {
select {
case <-h.done:
return h.err
default:
return nil
}
}
func takeErr(cErr *C.char) error {
if cErr == nil {
return errors.New("hydrant failed without saying why")
}
defer C.hydrant_free_string(cErr)
return errors.New(C.GoString(cErr))
}