Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base branch for protocol v2 #127

Open
wants to merge 77 commits into
base: main
Choose a base branch
from
Open

Base branch for protocol v2 #127

wants to merge 77 commits into from

Conversation

masad-frost
Copy link
Member

@masad-frost masad-frost commented May 15, 2024

All the changes are documented in Protocol.md but here's a summary:

  • Handle invalid client requests by sending a close with an error back
    • This was the main motivation for the change. While we could sort-of implement this error response without the other changes, things are setup in such a way where it is very hard to implement correctly without deeper changes in how we handle closing.
  • Add more robust closing mechanics
    • Half-close states
    • Close signals from read end of the pipes
    • Abort full-closure (for errors and cancellation)
  • Switch from Pushable and AsyncIterator APIs to a ReadStream and WriteStream
  • All procedures have init and some have input

While the changes are not strictly backwards compatible, hence the major protocol bump, the system can still operate across versions to some extent.

See PRs linked below for more information on the above

TODOs

.replit Outdated Show resolved Hide resolved
@@ -64,27 +64,43 @@ Note that this protocol specification does NOT detail the language-level specifi
1. `subscription`: the client sends 1 message, the server responds with m messages.

A server (also called a router) is made up of multiple 'services'. Each 'service' has multiple 'procedures'.
A procedure declares its type (`rpc | stream | upload | subscription`), an input message type (`Input`), an output message type (`Output`), an error type (`Error`), and the associated handler.
A procedure declares its type (`rpc | stream | upload | subscription`), an initial message (`Init`), an output message type (`Output`), an error type (`Error`), and the associated handler. `upload` and `stream` may define an input message type (`Input`), which means they accept further messages from the client.
Copy link
Member

Choose a reason for hiding this comment

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

it's still weird to me that 'additional' messages are called Input

feels like

  • Init should be Input
  • Input should be Stream or Body

(i just cooked up Stream off the top of my dome but that actually seems like the reasonable thing here)

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the direction that input gives. Body can mean output too. Maybe we should use request/response terminology.

Copy link
Member

Choose a reason for hiding this comment

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

Init could also mean output too, I agree though maybe request/terminology would help here

Copy link
Member Author

Choose a reason for hiding this comment

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

docs


Note that any procedure that has a client-to-server procedure stream (i.e. `stream` and `upload`) can optionally define a single initialization message to be sent to the server before the client starts sending the actual `Input` messages.
- `rpc`: `Init -> Result<Output, Error>`
- `upload`: `Init, AsyncIter<Input> -> Result<Output, Error>`
Copy link
Member

Choose a reason for hiding this comment

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

maybe replace AsyncIter + Pushable with readable/writable streams and provide TS definitions for those here

Copy link
Member

Choose a reason for hiding this comment

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

yeah could use an update here re: AsyncIter + Pushable

Copy link
Member Author

Choose a reason for hiding this comment

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

docs


Writers send messages that are recieved by the other side's reader. ONLY writers can end streams. Readers can send a signal to the writer that they are no longer interested in the stream, but the writer can choose to ignore this signal and continue sending messages.

Streams can be in a half-closed state. This happens when one party sends a close signal indicating that it will no longer send
Copy link
Member

Choose a reason for hiding this comment

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

maybe to clarify, we should say that any given stream is made up of a write side and read side

 A   |  B
1 <--|---  3
2  --|---> 4

1. A-side read
2. A-side write
3. B-side write
4. B-side read

half-closes can only be sent on 2 and 3

Copy link
Member Author

Choose a reason for hiding this comment

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

docs

masad-frost and others added 5 commits June 24, 2024 23:49
* Update PROTOCOL.md

* Update PROTOCOL.md

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* Update PROTOCOL.md

* half-close semantics

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* fmt

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Update PROTOCOL.md

* Include reader-writer semantics

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
* ReadStream implementation

* remove tee for now

* Make the stream itself an iterable

* Implement return for iterator

* comment
Copy link
Member

@jackyzha0 jackyzha0 left a comment

Choose a reason for hiding this comment

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

talked more IRL, going to think a bit more about the stream interface and ergonomics broadly

i think directionally very correct but could use a lot of simplification

input: Type.Object({ n: Type.Number() }),
output: Type.Object({ result: Type.Number() }),
requestData: Type.Object({ n: Type.Number() }),
responseData: Type.Object({ result: Type.Number() }),
errors: Type.Never(),
// note that a handler is unique per user RPC
async handler(ctx, { n }) {
Copy link
Member

Choose a reason for hiding this comment

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

probably needs an update too

Copy link
Member

Choose a reason for hiding this comment

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

same with await client.example.add.rpc below

Copy link
Member Author

Choose a reason for hiding this comment

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

docs


Note that any procedure that has a client-to-server procedure stream (i.e. `stream` and `upload`) can optionally define a single initialization message to be sent to the server before the client starts sending the actual `Input` messages.
- `rpc`: `Init -> Result<Output, Error>`
- `upload`: `Init, AsyncIter<Input> -> Result<Output, Error>`
Copy link
Member

Choose a reason for hiding this comment

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

yeah could use an update here re: AsyncIter + Pushable

- It is an explicit heartbeat, so the `AckBit` MUST be the only bit set.
- The payload MUST be `{ type: 'ACK' }`.
- Because this is a control message that is not associated with a specific stream, you MUST NOT set `serviceName` or `procedureName` and `streamId` can be something arbitrary (e.g. `heartbeat`).

There are 2 error payloads that are defined in the protocol sent from server to client, these codes are reserved:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
There are 2 error payloads that are defined in the protocol sent from server to client, these codes are reserved:
There are 3 error payloads that are defined in the protocol sent from server to client, these codes are reserved:

Copy link
Member Author

Choose a reason for hiding this comment

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

docs


`ProtocolError`s, just like service-level errors, are wrapped with a `Result`, which is further wrapped with `TransportMessage` and MUST have a `StreamAbortBit` flag. Please note that these are separate from user-defined errors, which should be treated just like any output message.

There are 4 `Control` payloads:
Copy link
Member

Choose a reason for hiding this comment

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

control payloads technically aren't normative i think, i just send these in the TS implementation so its easier to debug without needing to decode the controlFlags but controlFlags are enough

is the suggestion here that we change it to be normative?

Copy link
Member Author

Choose a reason for hiding this comment

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

docs


interface ControlHandshakeRequest {
type: 'HANDSHAKE_REQ';
protocolVersion: 'v2.0';
Copy link
Member

Choose a reason for hiding this comment

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

lfg

maybe we should have docs on how to handle v1 receiving a v2 request and vice versa

Copy link
Member Author

Choose a reason for hiding this comment

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

docs

router/server.ts Show resolved Hide resolved
router/server.ts Show resolved Hide resolved
router/streams.ts Show resolved Hide resolved
router/streams.ts Outdated Show resolved Hide resolved
util/testHelpers.ts Outdated Show resolved Hide resolved
@masad-frost
Copy link
Member Author

good review, i'll be sending some PRs and we can sync up afterwards

Got some comments on streams being complicated, so this is a take on
simplified API.

- Less methods and states that people need to understand
- Merge interfaces into a single object (i.e. a `stream` call would have
both read and write capabilities on the same object)
## Why

Follow up on #249 to actually use the new interfaces. Removed extra
refactor done in #249 that merges the interfaces as that proved to be a
challenging API (un-yak-shave 🙅 🐃)

## What changed

- Removed close requests (mostly cherry-picked from #248)
- Otherwise a simple swapping out of the interfaces

## Versioning

- [ ] Breaking protocol change
- [ ] Breaking ts/js API change

<!-- Kind reminder to add tests and updated documentation if needed -->
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.

None yet

2 participants