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

Eventstreams #206

Merged
merged 66 commits into from
Aug 5, 2024
Merged

Eventstreams #206

merged 66 commits into from
Aug 5, 2024

Conversation

alextwoods
Copy link
Contributor

@alextwoods alextwoods commented Jun 26, 2024

Summary

Add support for event streams.

Changes in Hearth

  • Added Http2 Client/Connection + Request/Response
  • Add EventStream module with generic Encoder/Decoder class, Message, AsyncOutput
  • EventStream::Binary module with specific implementations of the MessageEncoder and MessageDecoder (+ test suite).

Changes in Codegen

  • Add hooks for protocol generators to determine supported transport for event streams. Some protocols (eg: rpcv2cbor) define an ordered list of transports.
  • Generate an EventStream module with handlers and output classes.
  • Generate builders for event stream operations (inital request)
  • Builders for Events
  • Parsers for Events + initial responses.

Testing

  • Binary MessageEncoder/MessageDecoder test suite
  • Integration test, spins up a custom "mirroring" Http2 server with a local socket and sends/receives events.
  • Manual testing on AWS services

TODO

  • get hearth test coverage to 100%
  • rubocop/code cleanups
  • Hearth RBS
  • Hearth public api docs
  • [done] Generated docs + examples
  • Generated RBS .

Event Stream Implementation Details

Output events

The http2 client gets an Http2 connection from the pool. An Http2 connection may support multiple streams.
Http2 connections have an independent thread which reads from the connection and writes the data to the Http/2 (the gems) http2 client which automaticaly will send the data to the correct stream.
Handlers on the stream will write that data to the response body.

The response body is set to an EventStream decoder (which has a write method and takes the data). This works from both Http1 and Http2.
The Decoder uses a MessageDecoder (these are protocol specific, but currently only one implementation.) and for each message in the chunk it will call
the user configured Event Handler's emit method.

The EventHandler will parse the message and based on headers (eg type), will call different parsing methods. For modeled events, these are
the specific type's code generated Parsers in the Parsers::EventStream module.

Once parsed, user's registered event handlers for the specific type are called with the event.
All of this is done in the non-main thread.

Input Events

For h2/bi directional streaming operations with input events, the operation returns an operation specific AsyncOutput. That
output object has "signal_" methods for each event type. That method uses a code generated builder to build a Message from the params.
The message is then encoded and signed by the Encoder. The output object has a reference to the EventStream encoder.
The Encoder keeps a prior_signature state that is used to keep track of event signing state. The output object also has a reference to the stream.
the encoded and signed message is then sent to the stream.

The async output also allows controlling the state of the stream - either with join or kill.

The event streams handlers middleware creates the Encoder and sets it to the body of the request. When the streams are established in
the http2 client the stream is set on the body (if it responds to it).

Initial requests in Http2 event streams:

For protocols that support http traits (eg httpHeader) - then they MUST NOT have members serialized to the body. These requests are easy,
the headers (and uri path/query ect) are sent when connecting.
For RPC protocols (no http trait support), then we need to send the initial request as an event message. When the Encoder is created, we take the initial body (which the operation builder must serialize as a Message) and create a read method on the encoder. The http2 client will then read data from the Encoder and send it immediatly after the header and before any other user signaled events. This also ensures that signing state is kept in the encoder correctly.

Important Codegen decisions/hooks:

ProtocolGenerators need to return the correct transport to use for event streams. For some protocols this may be based on properties of the trait (eg, rpcv2 and most aws protocols define these) - so these methods may need to inspect the model.

We have a new EventStream module that has the generated Handlers (one per operation with event stream outputs) and AsyncOutput classes (one per operation with event inputs).

Builders and Parsers each have a sub EventStream module with the builders/parses for events. These may use other parsers in the module.

Most of the building and parsing of events is protocol agnostic (ie, setting type headers, handling @eventHeaders trait, determining if its implicit/explicit payload, ect) so there is a lot of logic in the base classes, with limited abstract methods that protocls msut implement. However, its designed to be highly extensibe and all of the default methods are protected and could be overwritten if needed.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Copy link
Contributor

@mullermp mullermp left a comment

Choose a reason for hiding this comment

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

Looks good so far again. I undoubtedly missed some things, this is gigantic. Maybe the http-2 and testing parts can be a separate PR and the eventstreams codegen can be stubbed in this PR?

Gemfile Outdated Show resolved Hide resolved
module EventStream

# EventStreamHandler for the the {Client#start_event_stream} operation.
# Register event handlers using the +#on_<event_name>+ methods
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it needs markup markdown but I think maybe we don't care to do that.

end
end

module EventStream
Copy link
Contributor

Choose a reason for hiding this comment

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

Up to you, but if the shapes are guaranteed to not clash, I don't see a need for the nesting.

@@ -225,6 +242,50 @@ private void renderOperation(RubyCodeWriter writer, OperationShape operation) {
.closeBlock("end");
}

private void renderEventStreamOperation(
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that makes sense. As long as it's somewhat consistent across all things (builders, parsers, etc).

module Hearth
module EventStream
# Represents a HeaderValue in an EventStream::Message.
class HeaderValue
Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine to use header in that case, just making sure the naming is correct

hearth/lib/hearth/event_stream/binary/message_decoder.rb Outdated Show resolved Hide resolved
hearth/lib/hearth/event_stream/handler_base.rb Outdated Show resolved Hide resolved
hearth/lib/hearth/event_stream/middleware/handlers.rb Outdated Show resolved Hide resolved
# @param input
# @param context
# @return [Output | EventStream::AsyncOutput]
def call(input, context)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if you saw this

# frozen_string_literal: true

module Hearth
module EventStream
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if you saw this

@alextwoods alextwoods marked this pull request as ready for review August 1, 2024 20:03
@alextwoods alextwoods changed the title [WIP] Eventstreams Eventstreams Aug 5, 2024
@alextwoods alextwoods merged commit 21d4296 into main Aug 5, 2024
24 of 26 checks passed
@alextwoods alextwoods deleted the eventstreams branch August 5, 2024 19:40
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.

2 participants