Skip to content

feat: migrate engine protocol to ConnectRPC - #4987

Open
abelanger5 wants to merge 4 commits into
mainfrom
belanger/connectrpc-engine
Open

abelanger5 wants to merge 4 commits into
mainfrom
belanger/connectrpc-engine

Conversation

@abelanger5

@abelanger5 abelanger5 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Description

Migrates the existing gRPC server to use ConnectRPC instead, which has full compatibility with the existing gRPC setup. This is needed for broader API compatibility with serverless environments, and even frontends (with compatibility with grpc-web) as we're working on more streaming features. This is a pre-requisite for the serverless-operator work to be completed.

The migration is pretty straightforward here because ConnectRPC is generated from protos as well, and the handler signatures for unary RPCs are the same. The primary difference is the streaming RPCs and the middleware, which has been extensively tested.

Some explicit risks are:

  1. The remove of the encoding step before locking the mutex for stream.Send. This means that we acquire a lock on the mutex before encoding the payload, which can add some latency / pessimistic locking behavior. This is a low risk, because the encoding time is miniscule compared to the network send time when we're in a flow control state (this change was theorized to solve a flow control issue, but it didn't end up being the root cause).
  2. Error code modification. The SDKs case on various gRPC error codes, and I (with Claude's assistance) audited the various places we're relying on gRPC errors across the SDKs to ensure compatibility, so this should be low / no risk.
  3. Changes to keepalive settings / gRPC streaming behavior. This is a bit of an unknown until we're deployed, but since gRPC relies on http/2 for keepalive functionality the migration to the http2 server should preserve the existing behavior.

Type of change

  • Refactor (non-breaking changes to code which doesn't change any behaviour)

Checklist

Changes have been:

  • Documented (where applicable)
  • Added to CHANGELOG (where applicable) -- see Keep a Changelog

🤖 AI Disclosure
  • I acknowledge that an LLM was used in the creation of this Pull Request, in accordance with Hatchet's AI_POLICY.md.
  • Details: Claude w/ Fable

abelanger5 and others added 2 commits September 19, 2026 07:18
Replace the google.golang.org/grpc server with connect handlers on
net/http. Existing SDKs keep speaking gRPC over HTTP/2 unchanged; the
same handlers now also answer gRPC-Web and the Connect protocol.

Server (internal/services/grpc):
- h2 over TLS, h2c when insecure, HTTP/1.1 for Connect and gRPC-Web;
  tls and mtls strategies use the same tls.Config as before
- HTTP/2 settings carry over the grpc-go behaviour: unlimited concurrent
  streams, receive windows from grpcStaticStreamWindowSize, server pings
  every 30s in place of keepalive, no enforcement policy so the 10s
  client keepalive every SDK uses is tolerated
- grpcMaxMsgSize applies to reads and sends
- responses are compressed only when the request was, as with grpc-go
- the OTLP TraceService Export method is mounted by hand
- graceful shutdown drains, then closes after the shutdown timeout

Middleware, same order as before: otel (otelconnect, trusting the remote
parent as otelgrpc did), logging with the same messages, fields and
levels, bearer auth, per-token rate limits, error normalisation, panic
recovery, then the unary extension interceptors from
ServerConfig.GRPCInterceptors. AddGRPCUnaryInterceptor and
pkg/grpc/middleware.CallbackInterceptor keep their names and now carry
connect.UnaryInterceptorFunc, so callers compile unchanged. grpc status
errors returned by extensions keep their code and message.

Services (dispatcher, admin, admin/v1, ingestor, otelcol) implement the
generated connect handler interfaces (simple mode, so unary signatures
are unchanged) and return connect errors with the same codes, messages
and details.

net/http panics on a stream write after its handler has returned, where
grpc-go returned an error, and the engine sends on streams from other
goroutines. Every streaming handler now sends through rpcstream.Sender,
which rejects sends after the handler is done. grpc.PreparedMsg has no
connect equivalent, so assigned actions are encoded inside Send.

pkg/client is not migrated here and doubles as the compatibility probe:
server_compat_test.go drives the server with grpc-go over h2c, TLS and
mTLS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Transport:
- a handler can always return: rpcstream.Sender interrupts a send that
  is still blocked on a peer which stopped reading, one second after
  Close, through the response write deadline
- a call's own timeout (grpc-timeout, Connect-Timeout-Ms) now bounds its
  request body reads and response writes, as it did with grpc-go
- unknown services and methods answer gRPC callers with a trailers-only
  Unimplemented status and grpc-go's messages instead of an HTTP 404
- the header list limit is back to grpc-go's 16 MiB
- the listener is HTTP/2 only again (h2, or unencrypted HTTP/2 when
  insecure); Connect and gRPC-Web are served over HTTP/2
- gzip decompression stops at the message size limit plus one byte
- a nonpositive grpcMaxMsgSize means the 4 MiB default, not no limit

Middleware:
- authentication and rate limits run in a connect request gate, on the
  request headers, before any body is read; rejected calls are still
  logged with the usual fields
- rate limit rejections carry the historical go-grpc-middleware text
- grpc status errors from extensions keep their details, and a wrapped
  coded error keeps the message grpc-go sent for it
- telemetry emits the span and metric names, attributes and status rule
  of the otelgrpc stats handler it replaces; otelconnect is dropped

Regression tests are ported from the reviewers' probes. Reports:
docs/reviews/connectrpc-engine-{review,security}.md in the workstation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hatchet-docs Ready Ready Preview Sep 21, 2026 1:34pm UTC

Request Review

@github-actions github-actions Bot added the engine Related to the core Hatchet engine label Sep 19, 2026
@abelanger5
abelanger5 requested a review from grutt September 19, 2026 12:12
@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no new actionable failures found and the previous comment-wording requirement fully addressed.

Summary

This PR migrates the engine RPC server from grpc-go to ConnectRPC while preserving gRPC compatibility and adding HTTP/1.1 support for Connect clients.

  • Generates and mounts Connect handlers for the engine services.
  • Reimplements authentication, errors, rate limiting, logging, telemetry, compression, and transport compatibility as Connect middleware.
  • Adds bounded HTTP/1.1 request-body and unary-response I/O while leaving server streams unbounded.
  • Expands compatibility and transport tests for gRPC, Connect, HTTP/1.1, streaming, shutdown, metadata, and error details.
  • Updates the Go version floor and ConnectRPC dependency.

Reviews (2) · Last reviewed commit: "refactor(engine): require every service ..."

Comment on lines +25 to +27
// The engine's gRPC server was instrumented by otelgrpc's stats handler. Spans and metrics keep
// its instrumentation scope, names, attributes and units so that existing dashboards, alerts
// and trace queries keep matching.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Comments Describe Old Behavior

These comments describe the previous implementation using phrases such as “was instrumented” and “has always used.” The repository requires comments to explain the current invariant without referring to old behavior. Rewrite them in terms of the compatibility contract that the current code maintains. The same issue appears in internal/services/grpc/middleware/logging.go:17-18 and internal/services/grpc/server.go:340-343. This repository requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…he Go floor

The point of the connect server is to let callers without gRPC reach the
engine: fetch in serverless runtimes speaks HTTP/1.1 by default and
cannot negotiate HTTP/2 over plaintext, and load balancers often speak
HTTP/1.1 to the backend. Turn HTTP/1.1 back on for both listener modes
and close the gap that made us turn it off:

- an HTTP/1.1 request body has a read deadline (30s, or the call's own
  timeout if sooner), so a partial body no longer holds a connection;
  calls without a body get none, since net/http is already watching the
  connection and an expiring deadline would cancel them
- an HTTP/1.1 unary response has a write deadline from its first byte;
  server streams stay unbounded
- one middleware owns every transport deadline, including the ones
  derived from grpc-timeout and Connect-Timeout-Ms

Unary and server-streaming procedures work over HTTP/1.1; bidirectional
streams need HTTP/2 and are refused with 505.

go 1.26.6 is the first 1.26 release with the fix for GO-2026-6089, the
missing timeout while reading an unencrypted HTTP/2 preface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread internal/services/admin/v1/server.go
Comment thread internal/services/admin/v1/server.go
Comment thread internal/services/admin/server_v1.go
Comment thread internal/services/admin/server_v1.go
Comment on lines -125 to -135
_, encodeSpan := telemetry.NewSpan(ctx, "encode-action")

msg := &grpc.PreparedMsg{}
err := msg.Encode(worker.stream, action)
if err != nil {
encodeSpan.RecordError(err)
encodeSpan.End()
return fmt.Errorf("could not encode action: %w", err)
}

encodeSpan.End()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this was done to reduce the chance of flow control for large messages, but ended up not being the root cause of the issue -- the encoding time is very small relative to the network send time, which was what caused flow control

Comment thread internal/services/grpc/middleware/auth.go
Comment on lines -32 to -33
forbidden := status.Errorf(codes.Unauthenticated, "invalid auth token")
token, err := auth.AuthFromMD(ctx, "bearer")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

existing clients are compatible, each SDK constructs authorization: bearer <token> and we only accept the first matched header

Comment thread internal/services/grpc/server.go Outdated
…ments as current rules

- NewServer rejects a missing ingestor, dispatcher, v1 dispatcher, admin
  or v1 admin service up front instead of mounting whatever was passed;
  only the OTel collector stays optional
- comments in the server and its middleware state the contract the code
  holds (log fields, telemetry names, compression rule, idle
  connections) rather than describing the previous implementation
- the wire test for idempotency collisions uses the real
  IdempotencyCollisionError and BulkTriggerIdempotencyCollisionError
  details, decoded by a grpc-go client the way the SDKs decode them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@abelanger5 abelanger5 changed the title feat: migrate engine protocol to ConnectRPC feat: migrate engine protocol to ConnectRPC Sep 21, 2026
@abelanger5
abelanger5 requested a review from mrkaye97 September 21, 2026 18:08

@mrkaye97 mrkaye97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel pretty good about this, should be easy enough to test on staging. there are no changes if we try to regenerate the SDKs right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine Related to the core Hatchet engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants