mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
sun_path is a fixed field holding a terminated string, so a unix address has 103 usable bytes on Darwin and 107 on Linux. Over that the kernel refuses the bind with EINVAL, which surfaces as "invalid argument" and names neither the length nor the path — the plugin child exits 1 and the host reports "exited before listening", which reads like a timing bug and is not. The limit now lives in internal/sock beside the path shape, since both are properties of the same address: minting one is not enough, it also has to be bindable. start() checks the longest address in play — the child's .sock.http upgrade listener — and refuses to launch rather than watching it die. 103 is measured, not read off a header: on Darwin a 103-byte path binds and a 104-byte one does not. The first cut of this used 104 and let through a path that was exactly one byte too long. The private instance directory also stops repeating the plugin's name. The binary and the socket inside it are already named for the plugin, so the copy in the directory spent len(name)+1 of a 103-byte budget saying it twice, and that was what pushed a long name over. Four idle tests and the status test built their Plugin.Dir from t.TempDir, which spends 89 bytes on the test's own name; sockDir has existed for exactly this reason. shortDir was a third copy of sockDir in the same test package, written from a comment that blamed sockDir for t.TempDir's length. Co-authored-by: Hanzo Dev <[email protected]>
129 lines
4.2 KiB
Go
129 lines
4.2 KiB
Go
package zip_test
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// A lazy plugin has exactly ONE trigger today: a request reaching one of its
|
|
// prefixes. A host that reaches its plugins another way — Hanzo's fleet calls
|
|
// app-to-app over each app's own unix socket, which never touches the router —
|
|
// has no way to say "bring this one up". Start is that door, and these prove
|
|
// it behaves like a door and not like Reload.
|
|
|
|
func TestStart_BringsUpALazyPluginThatNoRequestHasReached(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
|
app.Use(must(zip.Load(
|
|
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: sockDir(t), Lazy: true}, "/v1/demo",
|
|
)))
|
|
t.Cleanup(func() { _ = app.Shutdown() })
|
|
|
|
if st := app.Plugins(); st[0].Running {
|
|
t.Fatal("a lazy plugin must not be running before anything asks for it")
|
|
}
|
|
addr, err := app.Start("demo")
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if addr == "" {
|
|
t.Fatal("Start returned no address")
|
|
}
|
|
if st := app.Plugins(); !st[0].Running || st[0].PID == 0 {
|
|
t.Fatalf("Start did not bring the plugin up: %+v", st[0])
|
|
}
|
|
}
|
|
|
|
// Idempotence is the whole difference from Reload. Reload on a running plugin
|
|
// starts a SECOND child and retires the first; a caller that only wants the
|
|
// plugin to exist must not pay a process swap — and must not tear down the
|
|
// child that is answering its own in-flight calls.
|
|
func TestStart_OnARunningPluginIsANoOp(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
|
app.Use(must(zip.Load(
|
|
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: sockDir(t), Lazy: true}, "/v1/demo",
|
|
)))
|
|
t.Cleanup(func() { _ = app.Shutdown() })
|
|
|
|
if _, err := app.Start("demo"); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
first := app.Plugins()[0].PID
|
|
for i := 0; i < 5; i++ {
|
|
if _, err := app.Start("demo"); err != nil {
|
|
t.Fatalf("Start #%d: %v", i+2, err)
|
|
}
|
|
}
|
|
st := app.Plugins()[0]
|
|
if st.PID != first {
|
|
t.Fatalf("Start replaced the running child: pid %d → %d", first, st.PID)
|
|
}
|
|
if st.Reloads != 0 {
|
|
t.Fatalf("Start counted %d reloads — it is not a reload", st.Reloads)
|
|
}
|
|
}
|
|
|
|
// Start goes through target(), the same single-flighted path a prefix request
|
|
// takes, so a burst of first callers arriving by EITHER door produces exactly
|
|
// one child rather than one per caller.
|
|
func TestStart_ConcurrentFirstCallersProduceOneChild(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
|
app.Use(must(zip.Load(
|
|
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: sockDir(t), Lazy: true}, "/v1/demo",
|
|
)))
|
|
t.Cleanup(func() { _ = app.Shutdown() })
|
|
|
|
const n = 12
|
|
var wg sync.WaitGroup
|
|
addrs := make([]string, n)
|
|
errs := make([]error, n)
|
|
wg.Add(n)
|
|
for i := range n {
|
|
go func() { defer wg.Done(); addrs[i], errs[i] = app.Start("demo") }()
|
|
}
|
|
wg.Wait()
|
|
for i, err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("caller %d: %v", i, err)
|
|
}
|
|
if addrs[i] != addrs[0] {
|
|
t.Fatalf("caller %d got %q, caller 0 got %q — more than one child", i, addrs[i], addrs[0])
|
|
}
|
|
}
|
|
if st := app.Plugins()[0]; st.Reloads != 0 {
|
|
t.Fatalf("%d concurrent Starts caused %d reloads", n, st.Reloads)
|
|
}
|
|
}
|
|
|
|
// Unload must stick. A deliberate stop that the next caller silently undoes is
|
|
// not a stop, and for a LAZY plugin — the affordable default — every caller is
|
|
// a potential resurrection.
|
|
func TestStart_WillNotResurrectAnUnloadedPlugin(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
|
app.Use(must(zip.Load(
|
|
zip.Plugin{Name: "demo", Bin: buildPlugin(t, "v1"), Dir: sockDir(t), Lazy: true}, "/v1/demo",
|
|
)))
|
|
t.Cleanup(func() { _ = app.Shutdown() })
|
|
|
|
if _, err := app.Start("demo"); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if err := app.Unload("demo"); err != nil {
|
|
t.Fatalf("Unload: %v", err)
|
|
}
|
|
if _, err := app.Start("demo"); err == nil {
|
|
t.Fatal("Start resurrected an unloaded plugin")
|
|
}
|
|
if st := app.Plugins()[0]; st.Running {
|
|
t.Fatal("unloaded plugin is running again")
|
|
}
|
|
}
|
|
|
|
func TestStart_UnknownPluginIsAnError(t *testing.T) {
|
|
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
|
if _, err := app.Start("nope"); err == nil {
|
|
t.Fatal("Start on an unknown plugin must fail")
|
|
}
|
|
}
|