Files
go/rpc/pipeline_test.go
zeekay 9f22e43b50 rpc: real Target-based promise pipelining (Session + Pipeliner)
The call envelope has always carried Target @8, but zapgen hardcoded
Target: rpc.NoTarget in every generated method, so Go could DECODE a
pipelined envelope yet never ORIGINATE or RESOLVE one — pipelining was a
no-op. Make the canonical Target model real on both sides.

Client (origination): rpc.Session allocates unique non-zero PromiseIDs and
builds Calls — Origin (Target=NoTarget) and Pipeline (Target=prior.ID).
zapgen now emits a *rpc.Session-backed client: M(...) originates, MOn(on
rpc.Promise) pipelines on a prior answer (payload supplied server-side).
NoTarget is no longer hardcoded — it flows through one shared invoke<M>
path. Both forms return the call's own rpc.Promise so chains compose.

Server (resolution): rpc.Pipeliner wraps a generated Dispatch<Iface> and is
the per-connection promise table: records each OK answer under its
PromiseID, substitutes a resolved Target's Body for the dependent's Payload
before dispatch, QUEUES a dependent whose Target hasn't resolved yet until
it does, and REFUSES (StatusBadRequest, never hangs) a dependent whose
Target answered non-OK or was Finished. Finish(id) bounds the table.

Wire-compatible: Target=NoTarget is the non-pipelined call, so a Pipeliner
and a plain dispatcher are byte-identical on the wire; a non-pipelining
peer (any other runtime) interoperates trivially.

Proven end-to-end in rpc/pipeline_test.go (auth->getResource): single-pass
resolution, server-side queuing when the dependent arrives first, a 3-deep
A->B->C chain, non-OK/Finished targets refused, Finish waking a parked
dependent, and Target riding on the wire byte-for-byte. Full suite green
under -race. Docs: README + LLM.md describe the one canonical model and the
Rust capnp PromisedAnswer boundary honestly.
2026-06-21 01:49:48 -07:00

401 lines
13 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"bytes"
"encoding/binary"
"sync"
"testing"
"time"
)
// The pipelining tests model the canonical use case: call A authenticates and
// returns an opaque org token; call B (getResource) needs that token as its
// input. Pipelined, B sets Target = A's PromiseID and ships immediately — the
// server substitutes A's resolved token for B's payload before dispatching B,
// so B never waits for A's answer to round-trip back to the client.
const (
mAuthenticate uint32 = 1 // () -> token
mGetResource uint32 = 2 // (token) -> "resource@<token>"
)
// authDispatch is a DispatchFunc: authenticate returns a fixed token; get
// returns a resource string keyed by the token it receives as payload. It
// records, in order, the payload every getResource call was dispatched with —
// the proof that the server fed A's result into B.
type authServer struct {
mu sync.Mutex
token string
gotInput [][]byte // payloads getResource was dispatched with, in order
}
func (s *authServer) dispatch(envelope []byte) ([]byte, error) {
call, err := ParseRequest(envelope)
if err != nil {
return nil, err
}
switch call.Method {
case mAuthenticate:
return BuildResponse(StatusOK, call.PromiseID, []byte(s.token)), nil
case mGetResource:
s.mu.Lock()
s.gotInput = append(s.gotInput, append([]byte(nil), call.Payload...))
s.mu.Unlock()
return BuildResponse(StatusOK, call.PromiseID, []byte("resource@"+string(call.Payload))), nil
default:
return BuildResponse(StatusNotFound, call.PromiseID, nil), nil
}
}
// TestPipelineResolvesTarget is the core end-to-end proof: B pipelines on A's
// answer via Target, the Pipeliner substitutes A's resolved token for B's
// payload before dispatch, and B's result reflects it — no round trip threads
// A's body back through the client.
func TestPipelineResolvesTarget(t *testing.T) {
srv := &authServer{token: "org-7"}
p := NewPipeliner(srv.dispatch)
sess := NewSession()
// A: authenticate, fresh PromiseID, Target = NoTarget.
a := sess.Next()
aResp, err := handle(t, p, sess.Origin(a, mAuthenticate, nil, nil))
if err != nil {
t.Fatalf("A handle: %v", err)
}
if got := string(aResp.Body); got != "org-7" {
t.Fatalf("A body = %q, want org-7", got)
}
// B: getResource pipelined on A. Its payload is supplied server-side by
// A's resolved token — the client sends nil.
b := sess.Next()
bResp, err := handle(t, p, sess.Pipeline(b, a, mGetResource, nil, nil))
if err != nil {
t.Fatalf("B handle: %v", err)
}
if got := string(bResp.Body); got != "resource@org-7" {
t.Errorf("B body = %q, want resource@org-7 (A's token fed into B)", got)
}
// The server must have dispatched B with A's token as the payload.
srv.mu.Lock()
defer srv.mu.Unlock()
if len(srv.gotInput) != 1 || !bytes.Equal(srv.gotInput[0], []byte("org-7")) {
t.Errorf("getResource dispatched with %q, want [org-7]", srv.gotInput)
}
}
// TestPipelineQueuesUntilResolved proves server-side queuing: B arrives BEFORE
// A and must park inside the Pipeliner until A resolves, then complete with
// A's result. We launch B first on its own goroutine, confirm it is still
// blocked, then handle A — which releases B.
func TestPipelineQueuesUntilResolved(t *testing.T) {
srv := &authServer{token: "org-42"}
p := NewPipeliner(srv.dispatch)
sess := NewSession()
a := sess.Next()
b := sess.Next()
bDone := make(chan Response, 1)
go func() {
// B references A's PromiseID, but A has NOT been handled yet — B must
// block inside Handle until A resolves.
resp, err := handle(t, p, sess.Pipeline(b, a, mGetResource, nil, nil))
if err != nil {
t.Errorf("B handle: %v", err)
close(bDone)
return
}
bDone <- resp
}()
// Give B a chance to park. It must NOT complete before A is handled.
select {
case <-bDone:
t.Fatal("B completed before A resolved — it was not queued on its Target")
case <-time.After(50 * time.Millisecond):
// expected: B is parked.
}
// Now resolve A; B must unblock with A's token fed in.
if _, err := handle(t, p, sess.Origin(a, mAuthenticate, nil, nil)); err != nil {
t.Fatalf("A handle: %v", err)
}
select {
case resp, ok := <-bDone:
if !ok {
t.Fatal("B errored (see above)")
}
if got := string(resp.Body); got != "resource@org-42" {
t.Errorf("B body = %q, want resource@org-42", got)
}
case <-time.After(time.Second):
t.Fatal("B never resolved after A was handled")
}
}
// TestPipelineTargetFailurePropagates proves a dependent whose Target answered
// non-OK is refused (StatusBadRequest), not hung: there is no result to
// pipeline on. Here authenticate is mapped to an ordinal the server rejects.
func TestPipelineTargetFailurePropagates(t *testing.T) {
// A server that fails authentication with StatusUnauthorized.
dispatch := func(envelope []byte) ([]byte, error) {
call, err := ParseRequest(envelope)
if err != nil {
return nil, err
}
if call.Method == mAuthenticate {
return BuildResponse(StatusUnauthorized, call.PromiseID, nil), nil
}
return BuildResponse(StatusOK, call.PromiseID, []byte("resource@"+string(call.Payload))), nil
}
p := NewPipeliner(dispatch)
sess := NewSession()
a := sess.Next()
aResp, err := handle(t, p, sess.Origin(a, mAuthenticate, nil, nil))
if err != nil {
t.Fatalf("A handle: %v", err)
}
if aResp.Status != StatusUnauthorized {
t.Fatalf("A status = %d, want Unauthorized", aResp.Status)
}
b := sess.Next()
bResp, err := handle(t, p, sess.Pipeline(b, a, mGetResource, nil, nil))
if err != nil {
t.Fatalf("B handle: %v", err)
}
if bResp.Status != StatusBadRequest {
t.Errorf("B status = %d, want BadRequest (target never resolves)", bResp.Status)
}
}
// TestPipelineQueuedFailurePropagates is the queued twin of the above: B parks
// before A, then A fails — B must wake with StatusBadRequest, not hang.
func TestPipelineQueuedFailurePropagates(t *testing.T) {
dispatch := func(envelope []byte) ([]byte, error) {
call, _ := ParseRequest(envelope)
if call.Method == mAuthenticate {
return BuildResponse(StatusForbidden, call.PromiseID, nil), nil
}
return BuildResponse(StatusOK, call.PromiseID, nil), nil
}
p := NewPipeliner(dispatch)
sess := NewSession()
a := sess.Next()
b := sess.Next()
bDone := make(chan Response, 1)
go func() {
resp, err := handle(t, p, sess.Pipeline(b, a, mGetResource, nil, nil))
if err != nil {
t.Errorf("B handle: %v", err)
}
bDone <- resp
}()
time.Sleep(20 * time.Millisecond) // let B park
if _, err := handle(t, p, sess.Origin(a, mAuthenticate, nil, nil)); err != nil {
t.Fatalf("A handle: %v", err)
}
select {
case resp := <-bDone:
if resp.Status != StatusBadRequest {
t.Errorf("queued B status = %d, want BadRequest after A failed", resp.Status)
}
case <-time.After(time.Second):
t.Fatal("queued B never woke after A failed")
}
}
// TestPipelineFinishDropsAnswer proves Finish bounds the table: once a promise
// is Finished, a later dependent targeting it is refused (BadRequest), exactly
// like a never-resolved target, instead of finding a stale cached answer.
func TestPipelineFinishDropsAnswer(t *testing.T) {
srv := &authServer{token: "org-9"}
p := NewPipeliner(srv.dispatch)
sess := NewSession()
a := sess.Next()
if _, err := handle(t, p, sess.Origin(a, mAuthenticate, nil, nil)); err != nil {
t.Fatalf("A handle: %v", err)
}
// Before Finish: the dependent resolves.
b := sess.Next()
if resp, err := handle(t, p, sess.Pipeline(b, a, mGetResource, nil, nil)); err != nil {
t.Fatalf("B handle: %v", err)
} else if string(resp.Body) != "resource@org-9" {
t.Fatalf("B body = %q, want resource@org-9", resp.Body)
}
// After Finish: A's answer is gone; a new dependent on A is refused.
p.Finish(a.ID)
c := sess.Next()
resp, err := handle(t, p, sess.Pipeline(c, a, mGetResource, nil, nil))
if err != nil {
t.Fatalf("C handle: %v", err)
}
if resp.Status != StatusBadRequest {
t.Errorf("C status = %d, want BadRequest after A Finished", resp.Status)
}
}
// TestPipelineChainOfThree proves a chain deeper than two resolves: C
// pipelines on B which pipelines on A, all queued before A resolves. The
// server feeds A's answer into B and B's answer into C, each in turn. The
// chain server prefixes "->" so the depth is visible in the final result.
func TestPipelineChainOfThree(t *testing.T) {
// method 1 seeds "a"; method 2 appends ">" + its payload-derived step.
dispatch := func(envelope []byte) ([]byte, error) {
call, _ := ParseRequest(envelope)
switch call.Method {
case 1:
return BuildResponse(StatusOK, call.PromiseID, []byte("a")), nil
case 2:
return BuildResponse(StatusOK, call.PromiseID, append(append([]byte(nil), call.Payload...), '>')), nil
default:
return BuildResponse(StatusNotFound, call.PromiseID, nil), nil
}
}
p := NewPipeliner(dispatch)
sess := NewSession()
a, b, c := sess.Next(), sess.Next(), sess.Next()
// Queue C (on B) and B (on A) BEFORE A — both must park, then cascade.
cDone := make(chan Response, 1)
bDone := make(chan Response, 1)
go func() { r, _ := handle(t, p, sess.Pipeline(c, b, 2, nil, nil)); cDone <- r }()
go func() { r, _ := handle(t, p, sess.Pipeline(b, a, 2, nil, nil)); bDone <- r }()
time.Sleep(30 * time.Millisecond) // let B and C park
if _, err := handle(t, p, sess.Origin(a, 1, nil, nil)); err != nil {
t.Fatalf("A handle: %v", err)
}
bResp := <-bDone
cResp := <-cDone
if got := string(bResp.Body); got != "a>" {
t.Errorf("B body = %q, want a>", got)
}
if got := string(cResp.Body); got != "a>>" {
t.Errorf("C body = %q, want a>> (chained A->B->C)", got)
}
}
// TestPipelineFinishWakesParkedDependent proves that Finishing a Target with a
// dependent already parked on it wakes that dependent with a refusal instead
// of hanging it forever (the answer is gone and will never be re-produced).
func TestPipelineFinishWakesParkedDependent(t *testing.T) {
srv := &authServer{token: "org-3"}
p := NewPipeliner(srv.dispatch)
sess := NewSession()
a := sess.Next()
b := sess.Next()
bDone := make(chan Response, 1)
go func() {
resp, err := handle(t, p, sess.Pipeline(b, a, mGetResource, nil, nil))
if err != nil {
t.Errorf("B handle: %v", err)
}
bDone <- resp
}()
time.Sleep(20 * time.Millisecond) // let B park on A (A never originated)
p.Finish(a.ID) // A will never produce an answer — refuse B.
select {
case resp := <-bDone:
if resp.Status != StatusBadRequest {
t.Errorf("parked B status = %d, want BadRequest after A Finished", resp.Status)
}
case <-time.After(time.Second):
t.Fatal("parked B never woke after A Finished")
}
}
// TestPipelineWireEncoding proves the dependent call's Target rides on the
// wire as the prior call's PromiseID (byte-level), and a non-pipelined call
// carries NoTarget — the encoding is unchanged and byte-compatible across
// runtimes.
func TestPipelineWireEncoding(t *testing.T) {
sess := NewSession()
a := sess.Next()
b := sess.Next()
if a.ID == NoTarget || b.ID == NoTarget || a.ID == b.ID {
t.Fatalf("PromiseIDs must be unique and non-zero: a=%d b=%d", a.ID, b.ID)
}
// Originating call A: Target field (@8) must be NoTarget on the wire.
aEnv := BuildRequest(sess.Origin(a, mAuthenticate, nil, nil))
if got := wireTarget(t, aEnv); got != NoTarget {
t.Errorf("A wire Target = %d, want NoTarget(0)", got)
}
if got := wirePromiseID(t, aEnv); got != a.ID {
t.Errorf("A wire PromiseID = %d, want %d", got, a.ID)
}
// Dependent call B: Target field (@8) must equal A's PromiseID on the wire.
bEnv := BuildRequest(sess.Pipeline(b, a, mGetResource, nil, nil))
if got := wireTarget(t, bEnv); got != a.ID {
t.Errorf("B wire Target = %d, want A.ID %d", got, a.ID)
}
if got := wirePromiseID(t, bEnv); got != b.ID {
t.Errorf("B wire PromiseID = %d, want %d", got, b.ID)
}
// And it round-trips through the decoder unchanged.
call, err := ParseRequest(bEnv)
if err != nil {
t.Fatalf("ParseRequest: %v", err)
}
if call.Target != a.ID || call.PromiseID != b.ID || call.Method != mGetResource {
t.Errorf("decoded B = %+v, want Target=%d PromiseID=%d Method=%d", call, a.ID, b.ID, mGetResource)
}
}
// --- helpers ----------------------------------------------------------------
// handle ships one Call through the Pipeliner and returns the parsed Response.
func handle(t *testing.T, p *Pipeliner, c Call) (Response, error) {
t.Helper()
respBytes, err := p.Handle(BuildRequest(c))
if err != nil {
return Response{}, err
}
return ParseResponse(respBytes)
}
// wireTarget / wirePromiseID read the request struct's Target(@8) /
// PromiseID(@4) directly from the encoded message, proving the on-wire bytes
// (not just the in-memory Call) carry the pipelining reference.
func wireTarget(t *testing.T, env []byte) uint32 {
t.Helper()
return wireField(t, env, reqTargetOff)
}
func wirePromiseID(t *testing.T, env []byte) uint32 {
t.Helper()
return wireField(t, env, reqPromiseIDOff)
}
func wireField(t *testing.T, env []byte, off int) uint32 {
t.Helper()
// Validate framing the same way a peer would before trusting offsets.
if _, err := ParseRequest(env); err != nil {
t.Fatalf("parse envelope: %v", err)
}
// Root object byte offset lives in the header at [8:12]; a field at struct
// offset `off` is at byte rootOffset+off. Read the u32 there directly so we
// assert the on-wire bytes, not an in-memory struct.
root := int(binary.LittleEndian.Uint32(env[8:12]))
pos := root + off
if pos+4 > len(env) {
t.Fatalf("field offset %d out of range (msg len %d)", pos, len(env))
}
return binary.LittleEndian.Uint32(env[pos : pos+4])
}