Skip to content

Add stream support - #11

Merged
vvoland merged 7 commits into
moby:masterfrom
vvoland:streams
Oct 20, 2026
Merged

vvoland merged 7 commits into
moby:masterfrom
vvoland:streams

Conversation

@vvoland

@vvoland vvoland commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add typed streaming to extension contracts.
A contract can now declare server-streaming and client-streaming methods with the new stream package.
mobyextgen generates protobuf streaming RPCs and adapters for these methods on both sides.

Server-streaming shape:

M(ctx context.Context, req *Request, out stream.Sender[Chunk]) error

Client-streaming shape:

M(ctx context.Context, in stream.Receiver[Chunk]) (*Reply, error)

Bidirectional streams and all other stream signatures are rejected.

Why stream.Sender and stream.Receiver

Why not channels

Channels are the Go-native way to pass a sequence of values.
But a stream in a contract can fail, and a channel cannot report that:

  • A channel carries values and a close signal, but no errors.
    Send failures, receive errors, and the final status would each need a second channel or a wrapper type.
  • The contract would have to say which side closes the channel and what a close means after an error.
    A send on a closed channel panics.
  • Every send must select on the context, or a stopped reader leaks the sending goroutine.
  • The buffer size becomes part of the contract and changes the backpressure.
  • Each side would need a goroutine to feed or drain the channel, plus cleanup on every exit path.

Why not iterators

Iterators are closer.
They are synchronous, they can stop early, and they need no goroutines.
But they fit only one direction, and they move the stream outside the method call.

For client streaming, the provider could take iter.Seq2[*Chunk, error] and range over it.
That works, but it still needs rules: an error ends the sequence, the sequence can be ranged only once, and a normal end is not an error.

For server streaming, the provider would have to return iter.Seq2[*Chunk, error]:

  • The method returns before any message is sent.
    The stream then runs after the call, outside the method's context.
  • The final status is no longer the method's return value.
    It becomes a convention, such as a last (nil, err) pair in the sequence.
  • Over gRPC, the client adapter would have to return an iterator that holds an open gRPC stream.
    If the caller never ranges over it, the stream stays open until the context ends.

A provider could take a yield func(*Chunk) bool instead, but a bool cannot say why the other side stopped.
That makes it a Sender without an error.

Sender and Receiver have the same shape in both directions and on both sides.
The stream stays inside the method call, and the method's return value is the final status.
Code that prefers iterators can still use them: stream.All reads a Receiver as an iter.Seq2, and stream.Copy sends an iter.Seq2 through a Sender.

Sender and Receiver

What a contract needs is a call that moves one message and returns an error:

type Sender[T any] interface { Send(*T) error }
type Receiver[T any] interface { Recv() (*T, error) }

The stream belongs to one method call:

  • Each Send and Recv reports its own error.
  • Recv returns io.EOF when the input ends normally.
  • The method's return value is the final status, as it is for unary methods.
  • Cancellation stays with the method's context.
  • Backpressure needs no extra code, because Send blocks until the receiver or the transport accepts the message.
  • No goroutines or buffers are needed.

In process, the caller and the provider use these interfaces directly, without an adapter.

Over gRPC, the generated adapters map these methods to gRPC streaming RPCs and convert between the contract structs and the protobuf messages.

The generated .proto now carries the contract interface's doc comment
on the service and each method's doc comment on its rpc.
The generated raw gRPC server and client interfaces repeat the method
comments.

Signed-off-by: Paweł Gronowski <git@grono.dev>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Client streams can hang after early server rejection, and grouped parameters can produce uncompilable adapters.

2 open findings
What changed in this PR

Adds typed server- and client-streaming support to extension contracts and generated gRPC adapters.

Changes:

  • Introduces typed stream interfaces and iterator helpers.
  • Extends parsing, protobuf descriptors, and wire generation for streaming.
  • Adds documentation and runtime coverage.
File Description
stream/​stream.go Defines streaming interfaces and helpers.
stream/​stream_test.go Tests stream helpers.
sdk/​sdkapi/​protogen/​wire.gen.go Adds generated RPC documentation.
sdk/​sdkapi/​extension.proto Documents the extension service.
internal/​launcher/​echo/​v1/​protogen/​wire.gen.go Adds generated Echo documentation.
internal/​launcher/​echo/​v1/​echo.proto Documents the Echo service.
example/​greeter/​v0/​greeter.proto Documents the example service.
docs/​DESIGN.md Describes streaming design and semantics.
docs/​AUTHORING.md Documents streaming contract syntax.
cmd/​mobyextgen/​wire.go Generates streaming gRPC adapters.
cmd/​mobyextgen/​testdata/​runtime_unary_contract.go.txt Provides unary runtime fixture.
cmd/​mobyextgen/​testdata/​runtime_transport_test.go.txt Tests transport backpressure and status handling.
cmd/​mobyextgen/​testdata/​runtime_server_contract.go.txt Provides server-streaming fixture.
cmd/​mobyextgen/​testdata/​runtime_contract.go.txt Provides mixed-method fixture.
cmd/​mobyextgen/​testdata/​runtime_client_contract.go.txt Provides client-streaming fixture.
cmd/​mobyextgen/​testdata/​runtime_behavior_test.go.txt Tests generated runtime behavior.
cmd/​mobyextgen/​streaming_test.go Tests parsing and generation.
cmd/​mobyextgen/​runtime_test.go Builds and tests generated packages.
cmd/​mobyextgen/​runtime_race_test.go Enables nested race testing.
cmd/​mobyextgen/​runtime_norace_test.go Configures non-race testing.
cmd/​mobyextgen/​proto.go Emits streaming protobuf RPCs.
cmd/​mobyextgen/​parse.go Parses and validates stream signatures.
cmd/​mobyextgen/​model.go Models method kinds and documentation.
cmd/​mobyextgen/​descriptor.go Marks descriptor streaming directions.
Files not reviewed (4)
  • cmd/mobyextgen/proto.go: Generated file
  • cmd/mobyextgen/wire.go: Generated file
  • internal/launcher/echo/v1/protogen/wire.gen.go: Generated file
  • sdk/sdkapi/protogen/wire.gen.go: Generated file

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/mobyextgen/parse.go Outdated
Comment thread cmd/mobyextgen/wire.go
Signed-off-by: Paweł Gronowski <git@grono.dev>
Signed-off-by: Paweł Gronowski <git@grono.dev>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Generated streaming adapters return timing-dependent cancellation status codes.

3 open findings
1 resolved since last review
Files not reviewed (4)
  • cmd/mobyextgen/proto.go: Generated file
  • cmd/mobyextgen/wire.go: Generated file
  • internal/launcher/echo/v1/protogen/wire.gen.go: Generated file
  • sdk/sdkapi/protogen/wire.gen.go: Generated file

🧠 Review effort: Balanced

Comment thread cmd/mobyextgen/wire.go Outdated
Comment thread cmd/mobyextgen/wire.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Streaming lifecycle changes need human validation, and an unresolved signature-validation issue can produce uncompilable adapters.

1 open finding
3 resolved since last review
Files not reviewed (4)
  • cmd/mobyextgen/proto.go: Generated file
  • cmd/mobyextgen/wire.go: Generated file
  • internal/launcher/echo/v1/protogen/wire.gen.go: Generated file
  • sdk/sdkapi/protogen/wire.gen.go: Generated file

🧠 Review effort: Balanced

Comment thread cmd/mobyextgen/parse.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The public streaming API and transport lifecycle need human validation, and runtime tests were not executed.

0 open findings

1 resolved since last review
Files not reviewed (4)
  • cmd/mobyextgen/proto.go: Generated file
  • cmd/mobyextgen/wire.go: Generated file
  • internal/launcher/echo/v1/protogen/wire.gen.go: Generated file
  • sdk/sdkapi/protogen/wire.gen.go: Generated file

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Generated-name collisions need correction, and the public streaming lifecycle and cancellation behavior require final human validation.

1 open finding
Files not reviewed (4)
  • cmd/mobyextgen/proto.go: Generated file
  • cmd/mobyextgen/wire.go: Generated file
  • internal/launcher/echo/v1/protogen/wire.gen.go: Generated file
  • sdk/sdkapi/protogen/wire.gen.go: Generated file

🧠 Review effort: Balanced

Comment thread cmd/mobyextgen/wire.go Outdated
@vvoland vvoland changed the title WIP: Add stream support Add stream support Oct 15, 2026
Contract methods with extra or missing parameters were accepted as
long as the last parameter was a request pointer, and the generator
emitted adapters that did not compile against the contract.

The generator now classifies each parameter and accepts only methods
with exactly two parameters, the second a request pointer, and an
error or (*Reply, error) result.
It rejects any other signature with one error that lists the accepted
shapes.

Signed-off-by: Paweł Gronowski <git@grono.dev>
Contracts can declare server-streaming methods with this shape:

    M(ctx, *Req, stream.Sender[T]) error

The generator emits them as protobuf server-streaming RPCs and adapts
the gRPC stream to stream.Sender on both sides.
Stream messages must be local, non-generic structs.
A provider that sends a nil message gets codes.Internal.

A runtime test generates fixture contracts and runs their generated
code over a real gRPC connection and directly in process.

Signed-off-by: Paweł Gronowski <git@grono.dev>
Contracts can declare client-streaming methods with this shape:

    M(ctx, stream.Receiver[T]) (*Resp, error)

The generator emits them as protobuf client-streaming RPCs and adapts
the gRPC stream to stream.Receiver on both sides.

The client adapter reads the caller's Receiver synchronously, so a
blocked transport send stops further reads.
A normal io.EOF from the Receiver closes the stream and waits for the
provider's reply.
An io.EOF from Send means the provider finished early, so the adapter
returns the provider's final status.
A nil message from the caller fails with codes.InvalidArgument.

Bidirectional streaming remains rejected.

Signed-off-by: Paweł Gronowski <git@grono.dev>
The generator treated the first method parameter as the context
without checking its type.
It accepted contracts whose first parameter was, for example, a
request pointer or an int.
The generated adapters pass a context.Context, so they did not compile
against such contracts.

The first parameter must now name context.Context.
The check resolves the file's imports, so it accepts a renamed import
of the standard context package and rejects a Context type from any
other package.

Test contracts that declared ctx interface{} now use context.Context.

Signed-off-by: Paweł Gronowski <git@grono.dev>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Public streaming contracts and transport cancellation semantics need human validation, with runtime tests not executed in this review.

0 open findings

1 resolved since last review
Files not reviewed (4)
  • cmd/mobyextgen/proto.go: Generated file
  • cmd/mobyextgen/wire.go: Generated file
  • internal/launcher/echo/v1/protogen/wire.gen.go: Generated file
  • sdk/sdkapi/protogen/wire.gen.go: Generated file

🧠 Review effort: Balanced

@vvoland
vvoland marked this pull request as ready for review October 15, 2026 20:36

@thaJeztah thaJeztah left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@vvoland
vvoland merged commit d2819d8 into moby:master Oct 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants