Files
zeekayandHanzo Dev 4c683f0929 brand: Hanzo Datastore, never ClickHouse
35 references across 11 files named the upstream store. Our column store is
hanzoai/datastore (a ClickHouse fork, same wire protocol, live at
datastore.hanzo.svc). Per brand policy the upstream name does not appear in our
surfaces; per the platform rule the analytics path targets OUR store.

The pending analytics no longer describe themselves as a 'ClickHouse shim' —
they are Datastore aggregations that are not wired yet. Naming them after a
store we do not run is how the wrong one gets dialed.

Schema regenerated (byte-identical structure); 22 tests + the scope invariant
still green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 13:52:07 -07:00

726 lines
24 KiB
Go

package server
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
zaplib "github.com/luxfi/zap"
zcap "github.com/zap-proto/go/cap"
gen "github.com/hanzoai/observability/gen"
)
// Client is an Observability ZAP capability-RPC client — what console's bridge
// substitutes for the in-process Server: a thin typed wrapper over a luxfi/zap
// connection that ships the verified capability with every call. One Go method
// per tRPC procedure, each shipping a typed params view and decoding a typed
// result view.
//
// Construct with Dial, then call the typed methods or Pipeline*.
type Client struct {
node *zaplib.Node
peerID string
capBuf []byte
promiseSeq uint32 // monotonic PromiseID allocator
// sendLog records, in order, every call shipped — used by the pipelining
// proof to show a dependent call ships before the target's answer resolves.
logMu sync.Mutex
sendLog *[]SendEvent
}
// SendEvent is one entry in the instrumentation log: a call left the client
// (send) or its answer arrived (recv), with a monotonic sequence number.
type SendEvent struct {
Seq uint64
Kind string // "send" or "recv"
Method uint32
PromiseID uint32
Target uint32
At time.Time
}
var sendEventSeq uint64
// pipelineIDSeq hands out process-unique promise ids for pipelined call groups,
// starting high so a pipeline id never collides with a plain per-call PromiseID.
var pipelineIDSeq uint32 = 1 << 20
func nextPipelineID() uint32 { return atomic.AddUint32(&pipelineIDSeq, 1) }
// Dial constructs a Client over an already-started local node, connecting to the
// service at addr (e.g. "127.0.0.1:9992"). capBuf is the caller's opaque
// capability buffer (a zcap.Cap.Bytes()). peerID is the service's ZAP node id.
func Dial(node *zaplib.Node, addr, peerID string, capBuf []byte) (*Client, error) {
if err := node.ConnectDirect(addr); err != nil {
return nil, fmt.Errorf("obs client: connect %s: %w", addr, err)
}
return &Client{node: node, peerID: peerID, capBuf: capBuf}, nil
}
// WithSendLog attaches an instrumentation slice the client appends send/recv
// events to. Returns the client for chaining.
func (c *Client) WithSendLog(log *[]SendEvent) *Client {
c.sendLog = log
return c
}
func (c *Client) record(kind string, method, promiseID, target uint32) {
if c.sendLog == nil {
return
}
c.logMu.Lock()
*c.sendLog = append(*c.sendLog, SendEvent{
Seq: atomic.AddUint64(&sendEventSeq, 1),
Kind: kind,
Method: method,
PromiseID: promiseID,
Target: target,
At: time.Now(),
})
c.logMu.Unlock()
}
func (c *Client) nextPromise() uint32 { return atomic.AddUint32(&c.promiseSeq, 1) }
// call ships one request (with payload) and blocks for its correlated response.
func (c *Client) call(ctx context.Context, method, promiseID, target uint32, payload []byte) (Response, error) {
msg, err := buildRequest(Call{
Method: method,
PromiseID: promiseID,
Target: target,
Cap: c.capBuf,
Payload: payload,
})
if err != nil {
return Response{}, err
}
c.record("send", method, promiseID, target)
resp, err := c.node.Call(ctx, c.peerID, msg)
if err != nil {
return Response{}, err
}
c.record("recv", method, promiseID, target)
return parseResponse(resp), nil
}
// do is the typed-call helper: ship method+payload, check status, return body.
func (c *Client) do(ctx context.Context, method uint32, payload []byte) ([]byte, error) {
resp, err := c.call(ctx, method, c.nextPromise(), NoTarget, payload)
if err != nil {
return nil, err
}
if resp.Status != StatusOK {
return nil, fmt.Errorf("method %d: status %d: %s", method, resp.Status, resp.Body)
}
return resp.Body, nil
}
// =============================== traces ====================================
func (c *Client) TraceById(ctx context.Context, p gen.TraceByIdParamsInput) (gen.Trace, error) {
b, err := c.do(ctx, MethodTraceById, gen.NewTraceByIdParams(p))
if err != nil {
return gen.Trace{}, err
}
return gen.WrapTrace(b)
}
func (c *Client) TraceWithDetail(ctx context.Context, p gen.TraceByIdParamsInput) (gen.TraceWithDetail, error) {
b, err := c.do(ctx, MethodTraceWithDetail, gen.NewTraceByIdParams(p))
if err != nil {
return gen.TraceWithDetail{}, err
}
return gen.WrapTraceWithDetail(b)
}
func (c *Client) TraceBookmark(ctx context.Context, p gen.TraceBookmarkParamsInput) (gen.Trace, error) {
b, err := c.do(ctx, MethodTraceBookmark, gen.NewTraceBookmarkParams(p))
if err != nil {
return gen.Trace{}, err
}
return gen.WrapTrace(b)
}
func (c *Client) TracePublish(ctx context.Context, p gen.TracePublishParamsInput) (gen.Trace, error) {
b, err := c.do(ctx, MethodTracePublish, gen.NewTracePublishParams(p))
if err != nil {
return gen.Trace{}, err
}
return gen.WrapTrace(b)
}
func (c *Client) TraceUpdateTags(ctx context.Context, p gen.TraceUpdateTagsParamsInput) (gen.Trace, error) {
b, err := c.do(ctx, MethodTraceUpdateTags, gen.NewTraceUpdateTagsParams(p))
if err != nil {
return gen.Trace{}, err
}
return gen.WrapTrace(b)
}
func (c *Client) TraceDeleteMany(ctx context.Context, p gen.TraceDeleteManyParamsInput) (gen.MutationCount, error) {
b, err := c.do(ctx, MethodTraceDeleteMany, gen.NewTraceDeleteManyParams(p))
if err != nil {
return gen.MutationCount{}, err
}
return gen.WrapMutationCount(b)
}
// ============================ observations =================================
func (c *Client) ObservationById(ctx context.Context, p gen.ObservationByIdParamsInput) (gen.Observation, error) {
b, err := c.do(ctx, MethodObservationById, gen.NewObservationByIdParams(p))
if err != nil {
return gen.Observation{}, err
}
return gen.WrapObservation(b)
}
func (c *Client) EventBatchIO(ctx context.Context, p gen.EventBatchIOParamsInput) (gen.ObservationIOList, error) {
b, err := c.do(ctx, MethodEventBatchIO, gen.NewEventBatchIOParams(p))
if err != nil {
return gen.ObservationIOList{}, err
}
return gen.WrapObservationIOList(b)
}
// ============================== sessions ===================================
func (c *Client) SessionHasAny(ctx context.Context, p gen.ProjectScopeInput) (gen.BoolResult, error) {
b, err := c.do(ctx, MethodSessionHasAny, gen.NewProjectScope(p))
if err != nil {
return gen.BoolResult{}, err
}
return gen.WrapBoolResult(b)
}
func (c *Client) SessionById(ctx context.Context, p gen.SessionByIdParamsInput) (gen.SessionWithScores, error) {
b, err := c.do(ctx, MethodSessionById, gen.NewSessionByIdParams(p))
if err != nil {
return gen.SessionWithScores{}, err
}
return gen.WrapSessionWithScores(b)
}
func (c *Client) SessionBookmark(ctx context.Context, p gen.SessionBookmarkParamsInput) (gen.Session, error) {
b, err := c.do(ctx, MethodSessionBookmark, gen.NewSessionBookmarkParams(p))
if err != nil {
return gen.Session{}, err
}
return gen.WrapSession(b)
}
func (c *Client) SessionPublish(ctx context.Context, p gen.SessionPublishParamsInput) (gen.Session, error) {
b, err := c.do(ctx, MethodSessionPublish, gen.NewSessionPublishParams(p))
if err != nil {
return gen.Session{}, err
}
return gen.WrapSession(b)
}
// =============================== scores ====================================
func (c *Client) ScoreById(ctx context.Context, p gen.ScoreByIdParamsInput) (gen.Score, error) {
b, err := c.do(ctx, MethodScoreById, gen.NewScoreByIdParams(p))
if err != nil {
return gen.Score{}, err
}
return gen.WrapScore(b)
}
func (c *Client) ScoreHasAny(ctx context.Context, p gen.ProjectScopeInput) (gen.BoolResult, error) {
b, err := c.do(ctx, MethodScoreHasAny, gen.NewProjectScope(p))
if err != nil {
return gen.BoolResult{}, err
}
return gen.WrapBoolResult(b)
}
func (c *Client) ScoreCreateAnnotation(ctx context.Context, p gen.AnnotationScoreParamsInput) (gen.Score, error) {
b, err := c.do(ctx, MethodScoreCreateAnnotation, gen.NewAnnotationScoreParams(p))
if err != nil {
return gen.Score{}, err
}
return gen.WrapScore(b)
}
func (c *Client) ScoreUpdateAnnotation(ctx context.Context, p gen.AnnotationScoreParamsInput) (gen.Score, error) {
b, err := c.do(ctx, MethodScoreUpdateAnnotation, gen.NewAnnotationScoreParams(p))
if err != nil {
return gen.Score{}, err
}
return gen.WrapScore(b)
}
func (c *Client) ScoreDeleteAnnotation(ctx context.Context, p gen.ScoreByIdParamsInput) (gen.MutationCount, error) {
b, err := c.do(ctx, MethodScoreDeleteAnnotation, gen.NewScoreByIdParams(p))
if err != nil {
return gen.MutationCount{}, err
}
return gen.WrapMutationCount(b)
}
func (c *Client) EventScoresForTrace(ctx context.Context, p gen.EventScoresParamsInput) (gen.ScoreList, error) {
b, err := c.do(ctx, MethodEventScoresForTrace, gen.NewEventScoresParams(p))
if err != nil {
return gen.ScoreList{}, err
}
return gen.WrapScoreList(b)
}
// ============================ score configs ================================
func (c *Client) ScoreConfigAll(ctx context.Context, p gen.ScoreConfigAllParamsInput) (gen.ScoreConfigList, error) {
b, err := c.do(ctx, MethodScoreConfigAll, gen.NewScoreConfigAllParams(p))
if err != nil {
return gen.ScoreConfigList{}, err
}
return gen.WrapScoreConfigList(b)
}
func (c *Client) ScoreConfigById(ctx context.Context, p gen.ScoreConfigByIdParamsInput) (gen.ScoreConfig, error) {
b, err := c.do(ctx, MethodScoreConfigById, gen.NewScoreConfigByIdParams(p))
if err != nil {
return gen.ScoreConfig{}, err
}
return gen.WrapScoreConfig(b)
}
func (c *Client) ScoreConfigCreate(ctx context.Context, p gen.ScoreConfigWriteParamsInput) (gen.ScoreConfig, error) {
b, err := c.do(ctx, MethodScoreConfigCreate, gen.NewScoreConfigWriteParams(p))
if err != nil {
return gen.ScoreConfig{}, err
}
return gen.WrapScoreConfig(b)
}
func (c *Client) ScoreConfigUpdate(ctx context.Context, p gen.ScoreConfigWriteParamsInput) (gen.ScoreConfig, error) {
b, err := c.do(ctx, MethodScoreConfigUpdate, gen.NewScoreConfigWriteParams(p))
if err != nil {
return gen.ScoreConfig{}, err
}
return gen.WrapScoreConfig(b)
}
// ====================== analytics (STUBBED with TODO) ======================
// AnalyticsStub calls any of the stubbed Datastore analytics methods and
// returns the Empty result (Stubbed=true until the shim lands). Exposed so a
// caller can probe whether the analytics path is wired without special-casing
// each method.
func (c *Client) AnalyticsStub(ctx context.Context, method uint32, p gen.TableQueryParamsInput) (gen.Empty, error) {
b, err := c.do(ctx, method, gen.NewTableQueryParams(p))
if err != nil {
return gen.Empty{}, err
}
return gen.WrapEmpty(b)
}
// ============================== pipelining =================================
// PipelineTraceObservation proves Cap'n Proto promise pipelining over the
// trace→observation dependency: traceById on connection c, then observationById
// on connection dep pipelined off the trace call's promise. The dependent
// observation call is shipped BEFORE the trace call's answer resolves; the
// server holds it on its promise table (Server.await) until the trace call
// resolves the org, then dispatches it — no intermediate round trip.
//
// Transport note (load-bearing): luxfi/zap processes a single connection's
// frames strictly FIFO — one handler runs to completion before the next frame
// is read. So genuine concurrent in-flight calls require the two calls on
// SEPARATE connections (dep is a second client). The shared send log both
// clients append to is the proof surface.
func (c *Client) PipelineTraceObservation(
ctx context.Context,
dep *Client,
trace gen.TraceByIdParamsInput,
obs gen.ObservationByIdParamsInput,
) (gen.Trace, gen.Observation, error) {
tracePromise := nextPipelineID()
obsPromise := nextPipelineID()
var (
gotTrace gen.Trace
gotObs gen.Observation
traceErr error
obsErr error
wg sync.WaitGroup
)
// barrier releases the dependent send only after the trace send is committed
// to its wire, so the server resolves the trace promise before (or
// concurrently with) the dependent call's await — never after a timeout.
barrier := make(chan struct{})
wg.Add(2)
// Call #1: traceById on connection c — the promise the dependent call targets.
go func() {
defer wg.Done()
close(barrier)
resp, err := c.call(ctx, MethodTraceById, tracePromise, NoTarget, gen.NewTraceByIdParams(trace))
if err != nil {
traceErr = err
return
}
if resp.Status != StatusOK {
traceErr = fmt.Errorf("traceById: status %d: %s", resp.Status, resp.Body)
return
}
gotTrace, traceErr = gen.WrapTrace(resp.Body)
}()
// Call #2: observationById on connection dep, pipelined off the trace promise.
go func() {
defer wg.Done()
<-barrier
resp, err := dep.call(ctx, MethodObservationById, obsPromise, tracePromise, gen.NewObservationByIdParams(obs))
if err != nil {
obsErr = err
return
}
if resp.Status != StatusOK {
obsErr = fmt.Errorf("observationById: status %d: %s", resp.Status, resp.Body)
return
}
gotObs, obsErr = gen.WrapObservation(resp.Body)
}()
wg.Wait()
if traceErr != nil {
return gen.Trace{}, gen.Observation{}, traceErr
}
if obsErr != nil {
return gen.Trace{}, gen.Observation{}, obsErr
}
return gotTrace, gotObs, nil
}
// SyntheticCap mints an in-memory CapKindIAMSession capability for tests and
// bootstrap: ed25519-signed (the SPEC bootstrap scheme), holding the given
// permission bits. The signature is real (ed25519); production wires an
// IAM-issued cap instead. Returns the opaque buffer to pass to Dial.
func SyntheticCap(perms uint64) ([]byte, error) {
signer, err := zcap.NewEd25519Signer()
if err != nil {
return nil, err
}
c, err := zcap.Issue(zcap.Issuance{
Kind: uint32(zcap.KindIAMSession),
Holder: signer.Public(),
Permissions: perms,
ExpiresAt: time.Now().Add(time.Hour).Unix(),
}, signer)
if err != nil {
return nil, err
}
return c.Bytes(), nil
}
// ─────────────────── dashboards surface ───────────────────
// Ported from the standalone dashboards binary when it was folded in. Same
// envelope, same capability, same connection — only the method ids differ.
func (c *Client) AllDashboards(ctx context.Context, in gen.ListReqInput) (gen.DashboardList, error) {
body, err := c.do(ctx, MethodAllDashboards, gen.NewListReq(in))
if err != nil {
return gen.DashboardList{}, err
}
return gen.WrapDashboardList(body)
}
func (c *Client) GetDashboard(ctx context.Context, in gen.IdReqInput) (gen.Dashboard, error) {
body, err := c.do(ctx, MethodGetDashboard, gen.NewIdReq(in))
if err != nil {
return gen.Dashboard{}, err
}
return gen.WrapDashboard(body)
}
func (c *Client) CreateDashboard(ctx context.Context, in gen.CreateDashReqInput) (gen.Dashboard, error) {
body, err := c.do(ctx, MethodCreateDashboard, gen.NewCreateDashReq(in))
if err != nil {
return gen.Dashboard{}, err
}
return gen.WrapDashboard(body)
}
func (c *Client) UpdateDashboardMetadata(ctx context.Context, in gen.UpdateDashReqInput) (gen.Dashboard, error) {
body, err := c.do(ctx, MethodUpdateDashboardMeta, gen.NewUpdateDashReq(in))
if err != nil {
return gen.Dashboard{}, err
}
return gen.WrapDashboard(body)
}
func (c *Client) UpdateDashboardDefinition(ctx context.Context, in gen.DashDefReqInput) (gen.Dashboard, error) {
body, err := c.do(ctx, MethodUpdateDashboardDef, gen.NewDashDefReq(in))
if err != nil {
return gen.Dashboard{}, err
}
return gen.WrapDashboard(body)
}
func (c *Client) UpdateDashboardFilters(ctx context.Context, in gen.DashFiltersReqInput) (gen.Dashboard, error) {
body, err := c.do(ctx, MethodUpdateDashboardFilters, gen.NewDashFiltersReq(in))
if err != nil {
return gen.Dashboard{}, err
}
return gen.WrapDashboard(body)
}
func (c *Client) CloneDashboard(ctx context.Context, in gen.IdReqInput) (gen.Dashboard, error) {
body, err := c.do(ctx, MethodCloneDashboard, gen.NewIdReq(in))
if err != nil {
return gen.Dashboard{}, err
}
return gen.WrapDashboard(body)
}
func (c *Client) DeleteDashboard(ctx context.Context, in gen.IdReqInput) (gen.Mutation, error) {
body, err := c.do(ctx, MethodDeleteDashboard, gen.NewIdReq(in))
if err != nil {
return gen.Mutation{}, err
}
return gen.WrapMutation(body)
}
func (c *Client) AllWidgets(ctx context.Context, in gen.ListReqInput) (gen.WidgetList, error) {
body, err := c.do(ctx, MethodAllWidgets, gen.NewListReq(in))
if err != nil {
return gen.WidgetList{}, err
}
return gen.WrapWidgetList(body)
}
func (c *Client) GetWidget(ctx context.Context, in gen.IdReqInput) (gen.Widget, error) {
body, err := c.do(ctx, MethodGetWidget, gen.NewIdReq(in))
if err != nil {
return gen.Widget{}, err
}
return gen.WrapWidget(body)
}
func (c *Client) CreateWidget(ctx context.Context, in gen.WidgetReqInput) (gen.Widget, error) {
body, err := c.do(ctx, MethodCreateWidget, gen.NewWidgetReq(in))
if err != nil {
return gen.Widget{}, err
}
return gen.WrapWidget(body)
}
func (c *Client) UpdateWidget(ctx context.Context, in gen.WidgetReqInput) (gen.Widget, error) {
body, err := c.do(ctx, MethodUpdateWidget, gen.NewWidgetReq(in))
if err != nil {
return gen.Widget{}, err
}
return gen.WrapWidget(body)
}
func (c *Client) CopyWidgetToProject(ctx context.Context, in gen.CopyWidgetReqInput) (gen.Mutation, error) {
body, err := c.do(ctx, MethodCopyWidgetToProject, gen.NewCopyWidgetReq(in))
if err != nil {
return gen.Mutation{}, err
}
return gen.WrapMutation(body)
}
func (c *Client) DeleteWidget(ctx context.Context, in gen.IdReqInput) (gen.Mutation, error) {
body, err := c.do(ctx, MethodDeleteWidget, gen.NewIdReq(in))
if err != nil {
return gen.Mutation{}, err
}
return gen.WrapMutation(body)
}
func (c *Client) GetPresetsByTableName(ctx context.Context, in gen.PresetListReqInput) (gen.PresetList, error) {
body, err := c.do(ctx, MethodGetPresetsByTableName, gen.NewPresetListReq(in))
if err != nil {
return gen.PresetList{}, err
}
return gen.WrapPresetList(body)
}
func (c *Client) GetPresetById(ctx context.Context, in gen.IdReqInput) (gen.Preset, error) {
body, err := c.do(ctx, MethodGetPresetById, gen.NewIdReq(in))
if err != nil {
return gen.Preset{}, err
}
return gen.WrapPreset(body)
}
func (c *Client) CreatePreset(ctx context.Context, in gen.PresetReqInput) (gen.Preset, error) {
body, err := c.do(ctx, MethodCreatePreset, gen.NewPresetReq(in))
if err != nil {
return gen.Preset{}, err
}
return gen.WrapPreset(body)
}
func (c *Client) UpdatePreset(ctx context.Context, in gen.PresetReqInput) (gen.Preset, error) {
body, err := c.do(ctx, MethodUpdatePreset, gen.NewPresetReq(in))
if err != nil {
return gen.Preset{}, err
}
return gen.WrapPreset(body)
}
func (c *Client) UpdatePresetName(ctx context.Context, in gen.PresetNameReqInput) (gen.Preset, error) {
body, err := c.do(ctx, MethodUpdatePresetName, gen.NewPresetNameReq(in))
if err != nil {
return gen.Preset{}, err
}
return gen.WrapPreset(body)
}
func (c *Client) DeletePreset(ctx context.Context, in gen.IdReqInput) (gen.Mutation, error) {
body, err := c.do(ctx, MethodDeletePreset, gen.NewIdReq(in))
if err != nil {
return gen.Mutation{}, err
}
return gen.WrapMutation(body)
}
func (c *Client) GeneratePermalink(ctx context.Context, in gen.PermalinkReqInput) (gen.StringResult, error) {
body, err := c.do(ctx, MethodGeneratePermalink, gen.NewPermalinkReq(in))
if err != nil {
return gen.StringResult{}, err
}
return gen.WrapStringResult(body)
}
func (c *Client) AllMonitors(ctx context.Context, in gen.ListReqInput) (gen.MonitorList, error) {
body, err := c.do(ctx, MethodAllMonitors, gen.NewListReq(in))
if err != nil {
return gen.MonitorList{}, err
}
return gen.WrapMonitorList(body)
}
func (c *Client) GetMonitor(ctx context.Context, in gen.IdReqInput) (gen.Monitor, error) {
body, err := c.do(ctx, MethodGetMonitor, gen.NewIdReq(in))
if err != nil {
return gen.Monitor{}, err
}
return gen.WrapMonitor(body)
}
func (c *Client) CreateMonitor(ctx context.Context, in gen.MonitorReqInput) (gen.Monitor, error) {
body, err := c.do(ctx, MethodCreateMonitor, gen.NewMonitorReq(in))
if err != nil {
return gen.Monitor{}, err
}
return gen.WrapMonitor(body)
}
func (c *Client) UpdateMonitor(ctx context.Context, in gen.MonitorReqInput) (gen.Monitor, error) {
body, err := c.do(ctx, MethodUpdateMonitor, gen.NewMonitorReq(in))
if err != nil {
return gen.Monitor{}, err
}
return gen.WrapMonitor(body)
}
func (c *Client) DeleteMonitor(ctx context.Context, in gen.IdReqInput) (gen.Mutation, error) {
body, err := c.do(ctx, MethodDeleteMonitor, gen.NewIdReq(in))
if err != nil {
return gen.Mutation{}, err
}
return gen.WrapMutation(body)
}
// PipelineCreateDashboardThenWidget creates a dashboard and, pipelined off that
// call's promise, creates a widget in the SAME project — Cap'n Proto promise
// pipelining. The widget call Targets the dashboard call's PromiseID; the server
// resolves the dashboard call's project scope and only then dispatches the
// promised widget call against it, eliding a client round trip. This is the
// canonical dashboard-builder flow: open a dashboard, drop a widget on it.
//
// Transport note (load-bearing): luxfi/zap processes a single connection's
// frames strictly FIFO — one handler runs to completion before the next frame is
// read (node.go dispatchLoop). Genuine concurrent in-flight calls therefore need
// SEPARATE connections, where the server runs two dispatch loops concurrently and
// its promise table (Server.await/resolve) coordinates them. `dep` is a SECOND
// client connection over which the dependent CreateWidget call is shipped; pass
// a Client dialed on its own *zaplib.Node. When dep == c (one connection) this
// still works but degrades to sequential (no overlap) because of FIFO.
//
// Proof (on the shared send log both clients append to): the widget send precedes
// the dashboard recv — the dependent call was on the wire before the call it
// depends on had answered.
func (c *Client) PipelineCreateDashboardThenWidget(
ctx context.Context, dep *Client, dash gen.CreateDashReqInput, widget gen.WidgetReqInput,
) (gen.Dashboard, gen.Widget, error) {
dashPromise := nextPipelineID()
widgetPromise := nextPipelineID()
var (
gotDash gen.Dashboard
gotWidget gen.Widget
dashErr error
widgetErr error
wg sync.WaitGroup
)
// barrier releases the dependent send only after the dashboard send is
// committed to its wire, so the server resolves the dashboard promise id
// before (or concurrently with) the dependent call's await.
barrier := make(chan struct{})
wg.Add(2)
// Call #1: createDashboard on connection c — the promise the widget targets.
go func() {
defer wg.Done()
close(barrier)
resp, err := c.call(ctx, MethodCreateDashboard, dashPromise, NoTarget, gen.NewCreateDashReq(dash))
if err != nil {
dashErr = err
return
}
if resp.Status != StatusOK {
dashErr = fmt.Errorf("createDashboard: status %d: %s", resp.Status, resp.Body)
return
}
gotDash, dashErr = gen.WrapDashboard(resp.Body)
}()
// Call #2: createWidget on connection dep, pipelined off the dashboard promise.
// Shipped without awaiting the dashboard answer; the server holds it until the
// dashboard call resolves the project scope.
go func() {
defer wg.Done()
<-barrier
resp, err := dep.call(ctx, MethodCreateWidget, widgetPromise, dashPromise, gen.NewWidgetReq(widget))
if err != nil {
widgetErr = err
return
}
if resp.Status != StatusOK {
widgetErr = fmt.Errorf("createWidget: status %d: %s", resp.Status, resp.Body)
return
}
gotWidget, widgetErr = gen.WrapWidget(resp.Body)
}()
wg.Wait()
if dashErr != nil {
return gen.Dashboard{}, gen.Widget{}, dashErr
}
if widgetErr != nil {
return gen.Dashboard{}, gen.Widget{}, widgetErr
}
return gotDash, gotWidget, nil
}
// Probe issues a raw call and reports only the response status. Tests use it to
// assert the gate and the stub contract on methods with no typed client method
// — the status is the whole assertion, so decoding a body would add nothing.
func (c *Client) Probe(ctx context.Context, method uint32, payload []byte) uint32 {
resp, err := c.call(ctx, method, c.nextPromise(), NoTarget, payload)
if err != nil {
return 0
}
return resp.Status
}