modcall

package
v1.76.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package modcall is how one module calls another (docs/decisions/0071 D93 a): a typed request and response over one of three transports, behind one Go interface chosen by configuration.

  • Local calls a Server in the same process: tests, and every deployment that has not split the callee out.
  • HTTPCaller posts to the callee's Service on Kubernetes, with a bearer the callee verifies (Server.Handler).
  • internal/modcall/lambdacall invokes the callee's function through one alias per caller class (`live-issuer`, `live-console`, ...) on Lambda; it is a package of its own so that a Kubernetes build does not carry the Lambda client.

A call is a module name, a method name and a JSON payload. The callee answers a JSON result or a coded Error; anything else a handler returns is reported as an internal error with no detail, since the caller is another module's trust domain.

Who is calling is the transport's to say, never the payload's: Local knows it from its constructor (Local.As), HTTP from the verified token, Lambda from the alias the function was invoked through. A method that registers Allow answers only those caller classes and refuses the rest with CodeForbidden.

The envelope also carries the caller's trace (`traceparent`, `tracestate`), its deadline, and is bounded in size; a callee answers a request of the previous version in that version. See Request and [Dispatch].

Index

Constants

View Source
const (
	DefaultMaxBytes = 256 << 10
	HardMaxBytes    = 5 << 20
)

The size bounds of a call. A method answers requests and responses of at most DefaultMaxBytes unless it registers MaxBytes; no method may exceed HardMaxBytes, which keeps a call under Lambda's 6 MB synchronous payload.

View Source
const (
	CodeInternal     = "internal"
	CodeBadRequest   = "bad_request"
	CodeNoSuchMethod = "no_such_method"
	CodeUnavailable  = "unavailable"
	CodeForbidden    = "forbidden"
	// CodeDeadlineExceeded is a call whose deadline had passed when it arrived,
	// or passed while the method ran.
	CodeDeadlineExceeded = "deadline_exceeded"
)

Codes of an Error. A module may define more for its own methods.

View Source
const Kind = "rpc"

Kind is the `kind` of a Lambda event that carries a module call, beside the scheduler's ticks.

View Source
const MinVersion = Version - 1

MinVersion is the oldest version a callee still answers (N-1).

View Source
const RPCPath = "/rpc"

RPCPath is where a module's Service answers calls.

View Source
const Version = 2

Version is the protocol version a caller writes in Request.V. Version 1, which wrote none, is read as 1. A callee answers versions MinVersion to Version, each in the caller's own version: a release is deployed one function at a time, so for a while the two sides differ by one.

Variables

View Source
var (
	// ErrNoRoute is a call to a module no transport is configured for.
	ErrNoRoute = errors.New("module call: no route")
	// ErrTransport is a call that did not get an answer from the callee.
	ErrTransport = errors.New("module call: transport failed")
)

Functions

func Begin

func Begin(ctx context.Context, module, method string, payload []byte, maxBytes int) (context.Context, Request, *Pending, error)

Begin prepares an outgoing call for a transport: it refuses a payload over maxBytes (DefaultMaxBytes when 0) and a context already done, starts the client span, and builds the envelope with the trace and the deadline of the returned context. Every transport calls it, so that they carry the same fields; the caller class is never one of them.

func CallerOf

func CallerOf(ctx context.Context) string

CallerOf is the caller class the transport authenticated the call as; empty for a call the transport cannot name.

func Coded

func Coded(code, message string) error

Coded is an error a handler returns to answer with code and message.

func CodedCause added in v1.76.0

func CodedCause(code, message string, cause error) error

CodedCause is Coded that keeps the error behind it for the callee's log. The cause is not sent: a caller is another module's trust domain.

func DefaultTokenFile added in v1.76.0

func DefaultTokenFile(module string) string

DefaultTokenFile is where the projected token for a module's Service is mounted when the configuration does not say.

func Do

func Do[Req, Resp any](ctx context.Context, c Caller, module, method string, req Req) (Resp, error)

Do is a typed call.

func Handle

func Handle[Req, Resp any](s *Server, method string, fn func(ctx context.Context, req Req) (Resp, error), opts ...Option)

Handle registers a typed method. A method registered twice is a programming error and panics.

func NoRetry added in v1.76.0

func NoRetry(ctx context.Context) bool

NoRetry reports whether ctx was marked by WithoutRetry.

func Result

func Result(r Response) ([]byte, error)

Result is a response as a caller sees it.

func WithCaller

func WithCaller(ctx context.Context, class string) context.Context

WithCaller records the caller class the transport authenticated the call as. Only a transport calls it; a request's payload cannot.

func WithCallerClasses added in v1.76.0

func WithCallerClasses(ctx context.Context, classes ...string) context.Context

WithCallerClasses records that the caller the transport authenticated holds every one of classes. On Kubernetes one ServiceAccount is the issuer's pod, which is both the issuer and the console; on Lambda those are two aliases. Only a transport calls it. CallerOf is the first class.

func WithRequestID added in v1.76.0

func WithRequestID(ctx context.Context, id string) context.Context

WithRequestID marks ctx with the transport's id of the request (a Lambda request id), for the callee's log.

func WithoutRetry added in v1.76.0

func WithoutRetry(ctx context.Context) context.Context

WithoutRetry marks a call that must not be sent twice: a transport that retries a failed send on its own (the AWS client does, for a throttled or reset invoke) does not, for a call with this context. A method that spends something single-use, such as an OAuth code, is called this way, because a retry after a lost answer would find the code spent.

Types

type Caller

type Caller interface {
	Call(ctx context.Context, module, method string, payload []byte) ([]byte, error)
}

Caller sends one call to a module and returns its raw result.

type Config

type Config struct {
	Modules map[string]Target `json:"modules,omitempty"`
}

Config says how each module is reached.

func (Config) Remote

func (c Config) Remote(module string) bool

Remote reports whether the module has a target of its own.

type Error

type Error struct {
	Code    string `json:"code"`
	Message string `json:"message,omitempty"`
	// contains filtered or unexported fields
}

Error is a failure the callee chose to tell: a code a caller can switch on and a message that is safe to show another module.

func (*Error) Error

func (e *Error) Error() string

Error implements error.

func (*Error) Is

func (e *Error) Is(target error) bool

Is makes two errors with one code equal.

func (*Error) Unwrap added in v1.76.0

func (e *Error) Unwrap() error

Unwrap is the cause the callee kept for its log, if any.

type HTTPCaller

type HTTPCaller struct {
	// URLs maps a module to its Service's base URL; Audiences to the audience of
	// the bearer it verifies (the module's name when absent).
	URLs      map[string]string
	Audiences map[string]string
	Token     TokenSource
	Client    *http.Client
	// MaxBytes bounds a request payload and a result ([DefaultMaxBytes] when 0);
	// raise it only for a callee that registered a larger [MaxBytes].
	MaxBytes int
}

HTTPCaller calls modules' Services.

func (*HTTPCaller) Call

func (c *HTTPCaller) Call(ctx context.Context, module, method string, payload []byte) ([]byte, error)

Call implements Caller.

type HandlerOption

type HandlerOption func(*handlerConfig)

HandlerOption configures Server.Handler.

func WithClasses

func WithClasses(classes map[string]string) HandlerOption

WithClasses maps verified subjects to caller classes. A subject it does not map is its own class, as a test transport names it; on Kubernetes use WithSubjectClasses, which refuses what it does not list.

func WithSubjectClasses added in v1.76.0

func WithSubjectClasses(holds map[string][]string) HandlerOption

WithSubjectClasses maps verified subjects to the caller classes each holds, for a subject that is several (the issuer's pod is the issuer and the console). A subject it does not list is refused with a 401 before it reaches a method: the class is never guessed from a ServiceAccount's name, which any namespace may reuse.

type Local

type Local map[string]*Server

Local calls servers in this process, through the same JSON the other transports carry, so what works here works across a boundary. Called as itself it has no caller class, so a method with Allow refuses it; Local.As names the class the caller is.

func (Local) As

func (l Local) As(class string) Caller

As is a Caller whose calls arrive as the caller class.

func (Local) Call

func (l Local) Call(ctx context.Context, module, method string, payload []byte) ([]byte, error)

Call implements Caller.

type Method added in v1.76.0

type Method struct {
	Name string
	// Allow is the caller classes the method answers, sorted. Nil is any
	// caller the transport let in: no [Allow] was registered.
	Allow []string
	// MaxBytes is the bound of the request payload and of the result.
	MaxBytes int
}

Method describes one registered method, for a lint over what a module registered: it carries no handler.

type Option

type Option func(*handler)

Option configures a method at registration.

func Allow

func Allow(callers ...string) Option

Allow limits a method to the named caller classes. A call from any other class, or from one the transport cannot name, is refused with CodeForbidden before the method runs.

func MaxBytes

func MaxBytes(n int) Option

MaxBytes sets the bound, in bytes, of a method's request payload and of its result, in place of DefaultMaxBytes. A value over HardMaxBytes is a programming error and panics at registration. A caller of such a method sets the same bound on its transport.

type Pending

type Pending struct {
	// contains filtered or unexported fields
}

Pending is one outgoing call between Begin and Pending.Finish.

func (*Pending) Finish

func (p *Pending) Finish(result []byte, err error) ([]byte, error)

Finish ends the call: it refuses a result over the bound, with no echo of it, and closes the client span.

func (*Pending) Oversize

func (p *Pending) Oversize(answer []byte) bool

Oversize reports whether a whole answer, envelope included, is larger than any result within the bound could be.

type Request

type Request struct {
	// V is the protocol version; absent is 1.
	V       int             `json:"v,omitempty"`
	Kind    string          `json:"kind"`
	Module  string          `json:"module"`
	Method  string          `json:"method"`
	Payload json.RawMessage `json:"payload,omitempty"`

	// Version 2 and later. A version 1 request has none of them, and a callee
	// ignores them on one.
	//
	// Traceparent and Tracestate are the caller's W3C trace context; the callee
	// continues the trace with a span of its own.
	Traceparent string `json:"traceparent,omitempty"`
	Tracestate  string `json:"tracestate,omitempty"`
	// Deadline is the caller's deadline as Unix milliseconds. The callee bounds
	// its work by it, and refuses a call that arrives after it. It is read
	// against the callee's clock, so hosts need ordinary time sync.
	Deadline int64 `json:"deadline,omitempty"`
}

Request is one call on the wire. A reader ignores fields it does not know, so a later version can add some without breaking this one.

type Response

type Response struct {
	V      int             `json:"v,omitempty"`
	Result json.RawMessage `json:"result,omitempty"`
	Error  *Error          `json:"error,omitempty"`
}

Response is its answer: a result, or an error. V is the version the callee answered in, the caller's own; version 1 wrote none.

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router sends each call by the configuration: to the server in this process unless the module has a Function or a URL.

func NewRouter

func NewRouter(cfg Config, local Caller, lambda, http Caller) (*Router, error)

NewRouter builds the Caller. lambda and http may be nil when the configuration names no such target; a target whose transport is missing is an error here, not at the first call.

func (*Router) Call

func (r *Router) Call(ctx context.Context, module, method string, payload []byte) ([]byte, error)

Call implements Caller.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server dispatches calls to the methods a module registered.

func NewServer

func NewServer(module string) *Server

NewServer is the server of one module.

func (*Server) Dispatch

func (s *Server) Dispatch(ctx context.Context, req Request) Response

Dispatch answers one request. It never returns a Go error: a failure is the response's.

It answers version MinVersion to Version in the request's own version; a newer one is refused. The caller's trace continues in a server span, the caller's deadline bounds the method's context, and a payload or a result over the method's bound is refused without being echoed.

func (*Server) Handler

func (s *Server) Handler(verify Verifier, opts ...HandlerOption) http.Handler

Handler serves the module's calls at RPCPath. A call with no bearer, or one verify refuses, is a 401 and reaches no method. The caller class is the verified subject's (WithClasses, else the subject itself); the request body has no say in it.

func (*Server) Methods added in v1.76.0

func (s *Server) Methods() []Method

Methods lists the methods registered on s, sorted by name.

func (*Server) Module

func (s *Server) Module() string

Module is the module the server answers for.

func (*Server) SetLogger added in v1.76.0

func (s *Server) SetLogger(log *slog.Logger)

SetLogger makes the server log what it refuses or fails to answer, with the request id WithRequestID put on the context: a caller is told a code and nothing else, so without this the callee's log says nothing of a failed call. A server with no logger logs nothing; SetLogger does not replace one already set.

type Target

type Target struct {
	Function string `json:"function,omitempty"`
	URL      string `json:"url,omitempty"`
	Audience string `json:"audience,omitempty"`
	// TokenFile is where the pod's projected token for a URL target is mounted;
	// [DefaultTokenFile] when unset.
	TokenFile string `json:"tokenFile,omitempty"`
	// MaxBytes bounds a request payload and a result to this module in bytes,
	// above [DefaultMaxBytes] for a module whose methods list ([MaxBytes]); zero
	// is the default.
	MaxBytes int `json:"maxBytes,omitempty"`
}

Target is where one module is reached when it is not in this process. Set Function for a Lambda function (the alias of the caller's class), URL for a Kubernetes Service. Neither: the module is in this process.

type TokenSource

type TokenSource func(ctx context.Context, audience string) (string, error)

TokenSource yields the bearer for a call to the module whose audience is given (internal/consoleauth.Source, one per audience).

type Verifier

type Verifier func(ctx context.Context, bearer string) (subject string, err error)

Verifier checks the bearer of a call and says who it is (the token's subject). On Kubernetes it is a TokenReview of a projected ServiceAccount token whose audience is this module (docs/decisions/0071, 4).

Directories

Path Synopsis
Package dial is how a process turns the routing its document names into a modcall.Caller: Dial builds the modcall.Router for a modcall.Config, with the Lambda transport only in a build with the `lambda` tag, so that the Kubernetes build carries no Lambda client.
Package dial is how a process turns the routing its document names into a modcall.Caller: Dial builds the modcall.Router for a modcall.Config, with the Lambda transport only in a build with the `lambda` tag, so that the Kubernetes build carries no Lambda client.
Package lambdacall is the Lambda transport of internal/modcall: a call is a synchronous lambda:InvokeFunction of the callee's `live-<class>` alias for the caller class with the JSON envelope as payload, and a function answers one with Serve.
Package lambdacall is the Lambda transport of internal/modcall: a call is a synchronous lambda:InvokeFunction of the callee's `live-<class>` alias for the caller class with the JSON envelope as payload, and a function answers one with Serve.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL