Documentation
¶
Overview ¶
Package operatorsvc holds the engine-side logic of an operator session: registering the operator and its worker, opening and closing the session that makes the worker live, applying action set deltas, authorizing an operator and its workers, and opening durable invocations.
It is shared by every operator host. internal/services/grpcoperator serves it over OperatorService to operators that run outside the engine, and internal/operator/hostinproc calls the same functions for operators hosted inside the dispatcher, so the two hosts differ only in how assigned actions reach the operator and in how the caller is authenticated.
The API takes no OperatorService protocol messages: callers pass plain values and get repository rows back. Errors carry connect codes because they are returned to RPC callers unchanged; in-process callers can ignore the codes.
Index ¶
- Constants
- Variables
- func WithTenant(ctx context.Context, tenant *sqlcv1.Tenant) context.Context
- type ActionHandler
- type CloseOpt
- type DispatcherBackend
- type DurableChannel
- type HandlerSession
- type OpenOpts
- type OperatorStore
- type Opt
- func WithAnalytics(a analytics.Analytics) Opt
- func WithDispatcherBackend(d DispatcherBackend) Opt
- func WithDispatcherId(id uuid.UUID) Opt
- func WithLogger(l *zerolog.Logger) Opt
- func WithMaxActionsPerOperator(n int64) Opt
- func WithMaxListenStreamsPerOperator(n int) Opt
- func WithNotifyInterval(d time.Duration) Opt
- func WithOperatorCacheTTL(d time.Duration) Opt
- func WithOperatorStore(s OperatorStore) Opt
- func WithValidator(v validator.Validator) Opt
- func WithWorkerStore(s WorkerStore) Opt
- type RegisterOpts
- type Registration
- type Service
- func (s *Service) AuthorizeOperator(ctx context.Context, tenant *sqlcv1.Tenant, operatorId uuid.UUID) (*sqlcv1.V1Operator, error)
- func (s *Service) AuthorizeWorker(ctx context.Context, tenant *sqlcv1.Tenant, operatorId uuid.UUID, ...) (*sqlcv1.GetWorkerForEngineRow, error)
- func (s *Service) Cleanup() error
- func (s *Service) DispatcherId() uuid.UUID
- func (s *Service) ListenStreamCount(operatorId uuid.UUID) int
- func (s *Service) OpenSession(ctx context.Context, tenant *sqlcv1.Tenant, op *sqlcv1.V1Operator, ...) (*Session, error)
- func (s *Service) PauseWorker(ctx context.Context, tenant *sqlcv1.Tenant, workerId uuid.UUID, paused bool) error
- func (s *Service) Register(ctx context.Context, tenant *sqlcv1.Tenant, opts RegisterOpts) (Registration, error)
- func (s *Service) SendStepActionEvent(ctx context.Context, tenant *sqlcv1.Tenant, op *sqlcv1.V1Operator, ...) (*contracts.ActionEventResponse, error)
- type Session
- func (ss *Session) ApplyDelta(ctx context.Context, add, remove []string) (bool, error)
- func (ss *Session) Close(ctx context.Context, fs ...CloseOpt) error
- func (ss *Session) Fin() <-chan bool
- func (ss *Session) Heartbeat(ctx context.Context, at time.Time) error
- func (ss *Session) OpenDurable(ctx context.Context, taskExternalId uuid.UUID, invocation int32) (DurableChannel, error)
- func (ss *Session) Operator() *sqlcv1.V1Operator
- func (ss *Session) Pause(ctx context.Context, paused bool) error
- func (ss *Session) Send(ctx context.Context, msg *v1contracts.OperatorListenResponse) error
- func (ss *Session) SendStepActionEvent(ctx context.Context, ev *contracts.StepActionEvent) error
- func (ss *Session) SessionId() uuid.UUID
- func (ss *Session) Tenant() *sqlcv1.Tenant
- func (ss *Session) WorkerId() uuid.UUID
- type StreamSession
- type WorkerStore
Constants ¶
const ( // HandshakeHoldLimit bounds the invocation responses held while the register-worker ack is // awaited. A session that exceeds it is refused rather than buffering without end. HandshakeHoldLimit = 256 // RetainedResponseLimit bounds what the receive loop retains for Recv over the life of the // invocation: responses queued for delivery plus entry completions held for an ack or an // expectation. The handshake bound covers a burst before the operator can read; this one // allows for an operator that reads but is momentarily behind a fan-out of completions, and // for entries the engine completes that the operator never registers. A channel past it // fails with ErrChannelClosed rather than buffer without end inside the engine process. RetainedResponseLimit = 4 * HandshakeHoldLimit )
const ( // DefaultMaxListenStreamsPerOperator caps the Listen streams one operator may hold open on // this engine replica at once. Every stream is a live worker with its own session and // action set, so the cap bounds what one tenant token can allocate here. DefaultMaxListenStreamsPerOperator = 100 // DefaultMaxActionsPerOperator caps the action links held across all workers of one // operator. Deltas that would exceed it are refused with ResourceExhausted. The cap is // checked by the repository inside the delta's transaction, so it holds across sessions // and across engine replicas. DefaultMaxActionsPerOperator = 1_000_000 // MaxActionsPerDelta caps the ids (adds plus removes) one delta may carry. MaxActionsPerDelta = 1000 )
Variables ¶
var ( ErrRequestInFlight = operator.ErrRequestInFlight ErrChannelClosed = operator.ErrChannelClosed ErrSessionEnded = operator.ErrSessionEnded )
The channel's errors are the contract's, so an operator sees the same sentinels whichever host opened its channel.
var ErrNoStream = errors.New("operatorsvc: session has no stream to send on")
ErrNoStream is returned by Send on a session whose delivery is a handler: there is no stream to write protocol messages onto.
Functions ¶
Types ¶
type ActionHandler ¶
type ActionHandler = operator.ActionHandler
ActionHandler receives assigned actions by direct call. It is the in-process delivery: the call runs on the dispatcher's delivery goroutine and its error requeues the task. It is the contract's handler type, so an operator written against pkg/operator is hosted here unchanged.
type CloseOpt ¶
type CloseOpt func(*closeOpts)
func WithoutPause ¶
func WithoutPause() CloseOpt
WithoutPause closes the session without pausing its worker. It is what a session whose operator pauses for itself uses: a gRPC operator pauses on its stream before it hangs up, and a stream that ends unexpectedly must leave the worker assignable so the operator's next connection resumes a worker that can be given work.
type DispatcherBackend ¶
type DispatcherBackend interface {
AddOperatorStreamSession(ctx context.Context, workerId uuid.UUID, sessionId uuid.UUID, stream *rpcstream.Sender[v1contracts.OperatorListenResponse]) StreamSession
AddOperatorSession(workerId uuid.UUID, sessionId uuid.UUID, handler ActionHandler) HandlerSession
NotifyNewWorker(ctx context.Context, tenant *sqlcv1.Tenant, workerId uuid.UUID)
SendStepActionEvent(ctx context.Context, req *contracts.StepActionEvent) (*contracts.ActionEventResponse, error)
RegisterDurableTask(ctx context.Context, externalId uuid.UUID) (chan<- *v1contracts.DurableTaskRequest, <-chan *v1contracts.DurableTaskResponse, error)
}
DispatcherBackend is what the service needs from the local dispatcher: session registration for either delivery, scheduler notification, task events, and the channel-backed durable session.
func NewDispatcherBackend ¶
func NewDispatcherBackend(d *dispatcher.DispatcherImpl) DispatcherBackend
NewDispatcherBackend adapts the local dispatcher to what an operator session needs from it.
type DurableChannel ¶
type DurableChannel = operator.DurableChannel
DurableChannel is the contract's channel; the in-process implementation is what OpenDurable returns.
type HandlerSession ¶
type HandlerSession interface {
SetPaused(paused bool)
Release()
}
HandlerSession is the dispatcher session handle for a handler-backed session; see dispatcher.OperatorHandlerSession. SetPaused is the same as on StreamSession.
type OpenOpts ¶
type OpenOpts struct {
// Stream is the guarded sender of the Listen stream the dispatcher fans actions out on.
Stream *rpcstream.Sender[v1contracts.OperatorListenResponse]
// Handler receives assigned actions by direct call.
Handler ActionHandler
// Worker is the worker row, when the caller already read it (the gRPC path reads it to
// authorize the session). It is loaded here when nil.
Worker *sqlcv1.GetWorkerForEngineRow
}
OpenOpts describes how a session delivers assigned actions to its operator. Exactly one of Stream and Handler is set: Stream encodes actions onto a gRPC stream, Handler receives them by direct call in the engine process.
type OperatorStore ¶
type OperatorStore interface {
GetOperatorById(ctx context.Context, operatorId uuid.UUID) (*sqlcv1.V1Operator, error)
UpsertOperator(ctx context.Context, tenantId uuid.UUID, opts repository.UpsertOperatorOpts) (*sqlcv1.V1Operator, error)
UpdateOperator(ctx context.Context, tenantId, operatorId uuid.UUID, opts repository.UpdateOperatorOpts) (*sqlcv1.V1Operator, error)
}
OperatorStore is the subset of repository.OperatorRepository the service uses. It is narrow so tests can substitute a double without stubbing the whole repository tree.
type Opt ¶
type Opt func(*opts)
func WithAnalytics ¶
func WithDispatcherBackend ¶
func WithDispatcherBackend(d DispatcherBackend) Opt
WithDispatcherBackend sets the local dispatcher sessions are registered with. Required.
func WithDispatcherId ¶
WithDispatcherId sets the id of the local dispatcher; every operator worker whose session is opened here is pinned to it, since the delivery path lives here. Required.
func WithLogger ¶
func WithMaxActionsPerOperator ¶
WithMaxActionsPerOperator caps the action links held across all workers of one operator; zero disables the cap.
func WithMaxListenStreamsPerOperator ¶
WithMaxListenStreamsPerOperator caps the stream-backed sessions one operator may hold open on this replica; zero disables the cap.
func WithNotifyInterval ¶
WithNotifyInterval sets the window scheduler notifications are folded into.
func WithOperatorCacheTTL ¶
WithOperatorCacheTTL sets how long an authorized operator row is reused.
func WithOperatorStore ¶
func WithOperatorStore(s OperatorStore) Opt
WithOperatorStore sets the operator rows the service registers and authorizes. Required.
func WithValidator ¶
func WithWorkerStore ¶
func WithWorkerStore(s WorkerStore) Opt
WithWorkerStore sets the worker rows the service creates, activates and links actions to. Required.
type RegisterOpts ¶
type RegisterOpts struct {
// OperatorId is an existing operator row, as claimed by the in-process claimer. Name, Kind
// and LeasingManager are taken from the row and no upsert happens; the row's worker_id is pointed
// at the session's worker, which is how ClaimOperators recognises the assignment on later
// polls.
OperatorId *uuid.UUID
// Name is the operator name, unique per (tenant, kind). Ignored when OperatorId is set.
Name string
// Kind is what the operator is. Only GRPC rows, contract operators, are upserted by name;
// the DAG operator's rows are the engine's own and are registered by OperatorId. Ignored
// when OperatorId is set.
Kind sqlcv1.V1OperatorKind
// LeasingManager is who keeps the operator alive, and is what the row is set to whether the
// upsert creates or finds it: SELF for a registration that holds its own stream or leaser,
// DISPATCHER for a row a dispatcher should claim through the claimer. Ignored when
// OperatorId is set.
LeasingManager sqlcv1.V1OperatorLeasingManager
// IsExemptFromLimits leaves the worker out of the tenant's worker and slot limits. It is a
// hosting fact: the in-process host sets it for every worker it creates, the wire never
// does.
IsExemptFromLimits bool
// WorkerName names the worker row. It defaults to the operator name, which is what one
// worker per connection looks like in the dashboard.
WorkerName string
// SlotConfig maps slot type to max units, defaulting to {"default": 100}.
SlotConfig map[string]int32
Labels map[string]*contracts.WorkerLabels
RuntimeInfo *contracts.RuntimeInfo
// ResumeWorkerId names a worker of this operator to reuse instead of creating one. A
// worker that no longer exists, or that belongs to another operator, is replaced by a new
// one rather than refused, since a caller only ever learns about its own workers.
ResumeWorkerId *uuid.UUID
}
RegisterOpts describes the operator to upsert, or the existing row to register under, and the worker to back this session.
type Registration ¶
type Registration struct {
Operator *sqlcv1.V1Operator
TenantId uuid.UUID
OperatorId uuid.UUID
WorkerId uuid.UUID
// Resumed reports whether WorkerId is the worker ResumeWorkerId named.
Resumed bool
}
Registration is the identity the engine assigned to the caller.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func (*Service) AuthorizeOperator ¶
func (s *Service) AuthorizeOperator(ctx context.Context, tenant *sqlcv1.Tenant, operatorId uuid.UUID) (*sqlcv1.V1Operator, error)
AuthorizeOperator resolves operatorId and checks that it is a self-leased GRPC operator owned by tenant. Lookups are cached for the cache's TTL; misses and errors are never cached, so a freshly created operator is visible on the next call. Only such operators are reachable from outside the engine, which is the only caller that authorizes today: a DAG row is the engine's own, and a row the engine leases is driven by the claimer, never over the wire.
func (*Service) AuthorizeWorker ¶
func (s *Service) AuthorizeWorker(ctx context.Context, tenant *sqlcv1.Tenant, operatorId uuid.UUID, workerId uuid.UUID) (*sqlcv1.GetWorkerForEngineRow, error)
AuthorizeWorker checks that workerId names a worker owned by the operator. It is the guard on every call that carries a worker id, so an operator cannot act on another operator's worker. A worker that does not exist is reported the same way as one owned by someone else.
func (*Service) DispatcherId ¶
DispatcherId is the dispatcher every session opened here pins its worker to.
func (*Service) ListenStreamCount ¶
ListenStreamCount reports the open stream-backed sessions of the operator on this replica.
func (*Service) OpenSession ¶
func (s *Service) OpenSession(ctx context.Context, tenant *sqlcv1.Tenant, op *sqlcv1.V1Operator, workerId uuid.UUID, opts OpenOpts) (*Session, error)
OpenSession makes the worker live: it re-pins the worker to this dispatcher, refreshes its action hash when a previous session left it pending, activates it under a fresh session id that doubles as the listener fence, registers the delivery with the dispatcher and notifies the scheduler. Stream-backed sessions are also counted against the per-operator stream cap; in-process sessions hold no stream, so only the action budget applies to them.
func (*Service) PauseWorker ¶
func (s *Service) PauseWorker(ctx context.Context, tenant *sqlcv1.Tenant, workerId uuid.UUID, paused bool) error
PauseWorker stops the scheduler assigning to the worker, or lets it be assigned to again. It returns once the change is committed. Register clears the pause when it resumes a worker, so an operator that crashed while paused comes back assignable. A live session pauses through Session.Pause, which also stops the session delivering.
func (*Service) Register ¶
func (s *Service) Register(ctx context.Context, tenant *sqlcv1.Tenant, opts RegisterOpts) (Registration, error)
Register upserts the operator by (tenant, name, kind), or loads the row OperatorId names, and creates the worker for this session, or resumes the worker named by ResumeWorkerId when it still belongs to the operator. The worker starts with no actions: the caller links them on its session.
func (*Service) SendStepActionEvent ¶
func (s *Service) SendStepActionEvent(ctx context.Context, tenant *sqlcv1.Tenant, op *sqlcv1.V1Operator, ev *contracts.StepActionEvent) (*contracts.ActionEventResponse, error)
SendStepActionEvent reports task progress for an action delivered to one of the operator's workers. The event's worker must belong to the operator; the dispatcher then handles it exactly like an SDK worker's event.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is one live worker: its dispatcher routing entry, its listener fence on the worker row, its action budget and its scheduler notifications. It lives from OpenSession until Close.
The methods that drive the session (Heartbeat, ApplyDelta, Send) are meant to be called from the one goroutine that owns the session's protocol loop, which is how the throttles read as one sequence; Pause and Close are safe to call from anywhere, and Close runs once.
func (*Session) ApplyDelta ¶
ApplyDelta validates and applies one change to the worker's action set and asks the scheduler to reload it. It reports whether the set changed; a delta that only repeats what the worker already has needs no notification, and the caller can still acknowledge it.
func (*Session) Close ¶
Close ends the session: pause, then drain, then deactivate. Pausing stops the scheduler assigning new work, releasing the dispatcher session stops anything further being delivered, and the deactivation, fenced on this session's id, marks the worker inactive. A host that waits for its operator's in-flight work does so between Pause and Close. A hash refresh the notifier still owed is done here, so the row never keeps a pending hash past its session.
The pause, the refresh and the deactivation run detached from ctx because the common exit is the operator being gone, at which point ctx is already cancelled. Close runs once; later calls return the first result.
func (*Session) Fin ¶
Fin fires when the dispatcher wants a stream-backed session hung up. A handler-backed session returns nil, which never selects: the dispatcher has no stream to reclaim.
func (*Session) Heartbeat ¶
Heartbeat records that the operator is alive. Writes are throttled to one per second, so a host that heartbeats faster costs nothing; a throttled call reports no error.
func (*Session) OpenDurable ¶
func (ss *Session) OpenDurable(ctx context.Context, taskExternalId uuid.UUID, invocation int32) (DurableChannel, error)
OpenDurable opens one durable invocation's pipe for the session's worker and runs the register-worker handshake: the first request names the worker running the invocation, and the engine's ack is consumed here so Recv only ever returns invocation traffic. The engine routes responses to the task as soon as it is registered, before the ack, so invocation traffic that arrives during the handshake (a completion restored for a resumed invocation, for instance) is held for the channel, under the same ack-before-entry ordering, up to HandshakeHoldLimit responses. ctx bounds the handshake; the invocation itself is detached from it and ends on Close.
func (*Session) Operator ¶
func (ss *Session) Operator() *sqlcv1.V1Operator
Operator is the operator row the session belongs to.
func (*Session) Pause ¶
Pause stops the scheduler assigning to the session's worker, or lets it be assigned to again. It returns once the change is committed, so a host that pauses before draining knows no further work will arrive: the dispatcher session stops delivering before the pause is written, and an action the scheduler assigned in the meantime goes back to the queue rather than to the operator. Resuming lifts the pause in the opposite order, so nothing is refused once the scheduler may assign again. The write is fenced on the session id like the deactivation: a session superseded by a newer one on the same worker leaves the worker's scheduling state to it.
func (*Session) Send ¶
func (ss *Session) Send(ctx context.Context, msg *v1contracts.OperatorListenResponse) error
Send writes a protocol message on a stream-backed session's stream, serialised with the dispatcher's own action sends.
func (*Session) SendStepActionEvent ¶
SendStepActionEvent reports task progress for an action delivered to the session's worker, on the tenant-scoped path in-engine operators use: the tenant is put on the context and the dispatcher handles the event exactly like an SDK worker's. An empty WorkerId is filled from the session; another worker's id is refused, since the session only ever speaks for its own worker.
func (*Session) SessionId ¶
SessionId is the id the worker's listener fence and the dispatcher session are keyed on.
type StreamSession ¶
type StreamSession interface {
Fin() <-chan bool
Send(ctx context.Context, msg *v1contracts.OperatorListenResponse) error
SetPaused(paused bool)
Release()
}
StreamSession is the dispatcher session handle for a stream-backed session; see dispatcher.OperatorStreamSession. SetPaused(true) makes the dispatcher return every action it is asked to deliver to the queue instead, so a pause the scheduler has not observed yet still delivers nothing.
type WorkerStore ¶
type WorkerStore interface {
CreateNewWorker(ctx context.Context, tenantId uuid.UUID, opts *repository.CreateWorkerOpts) (*sqlcv1.Worker, error)
GetWorkerForEngine(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID) (*sqlcv1.GetWorkerForEngineRow, error)
UpdateWorker(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID, opts *repository.UpdateWorkerOpts) (*sqlcv1.Worker, error)
ActivateWorkerListener(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID, sessionId uuid.UUID) (*sqlcv1.Worker, error)
DeactivateWorkerListener(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID, sessionId uuid.UUID) (*sqlcv1.Worker, error)
UpdateWorkerHeartbeat(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID, lastHeartbeatAt time.Time) error
UpsertWorkerLabels(ctx context.Context, workerId uuid.UUID, opts []repository.UpsertWorkerLabelOpts) ([]*sqlcv1.WorkerLabel, error)
PauseWorkerForListener(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID, sessionId uuid.UUID, paused bool) error
ApplyWorkerActionsDelta(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID, add, remove []string, maxOperatorLinks int64) (added, removed int, err error)
RefreshWorkerActionHash(ctx context.Context, tenantId uuid.UUID, workerId uuid.UUID) error
CountOperatorWorkerActions(ctx context.Context, tenantId uuid.UUID, operatorId uuid.UUID) (int64, error)
}
WorkerStore is the subset of repository.WorkerRepository the service uses.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package operatorsvctest holds in-memory doubles for the stores and the dispatcher an operator session talks to.
|
Package operatorsvctest holds in-memory doubles for the stores and the dispatcher an operator session talks to. |