Skip to content

Quarkus integration via shared transport Vert.x and Smithy Codegen extension#1205

Open
chenliu0831 wants to merge 1 commit into
smithy-lang:mainfrom
chenliu0831:main
Open

Quarkus integration via shared transport Vert.x and Smithy Codegen extension#1205
chenliu0831 wants to merge 1 commit into
smithy-lang:mainfrom
chenliu0831:main

Conversation

@chenliu0831
Copy link
Copy Markdown

@chenliu0831 chenliu0831 commented May 22, 2026

Issue #, if available: N/A, supersede #1196 since we decide to go single/unified server approach with shared transport first.

What

A Smithy-Java server that runs inside Quarkus. Users produce a @produces Service bean; operations mount on Quarkus's HTTP router as a single catch-all Vert.x route. Smithy operations share Quarkus's port.

Customer experience

  @Produces @Singleton
  Service coffeeShop() {
      return CoffeeShop.builder()
              .addGetMenuOperation(new GetMenu())
              .addCreateOrderOperation(new CreateOrder())
              .build();
  }

End-to-end example: examples/quarkus-server/ (CoffeeShop). The README walks through
publishToMavenLocal → quarkusDev → curl probes for restJson1, rpcv2Cbor, and rpcv2Json, plus a fall-through section that demonstrates the three resolution outcomes via observable 404 shapes.

Key abstractions to review

Reviewers can stop after these six classes and have the whole picture. Listed in the order a request flows through them.

:server:server-vertx

SmithyVertxServer (server-vertx/.../SmithyVertxServer.java) — the single public type,
where the whole module's behavior lives.

  • Handler<RoutingContext> that owns the ProtocolResolver, orchestrator pool, and dispatch
    loop.
  • create(services, protocols, options) constructs; handle(rc) is the route handler.
  • shutdown() drains workers — idempotent, returns the cached future.

ServerOptions — two knobs: workerCount and pathPrefix. Intentionally narrow.

  • Adding a knob requires an ADR.
  • No interceptor field (locked in by a tripwire test); no shutdownGrace (caller-bounded).

VertxRequestHeaders — adapter from Vert.x's MultiMap to Smithy's HttpHeaders.

  • The only hand-written adapter we need.
  • Review for case-sensitivity correctness and the MultiMap.size() delegation.

:quarkus-smithy (runtime)

SmithyVertxRecorder (quarkus-smithy/.../SmithyVertxRecorder.java) — Quarkus @Recorder
invoked at RUNTIME_INIT.

  • Collects @Produces Service beans from Arc, walks TCCL+own-loader for
    ServerProtocolProviders, builds ServerOptions, mounts the catch-all route, registers
    shutdown tasks.
  • Shutdown ordering matters: Quarkus's ShutdownContext runs LIFO, so route.remove() is
    registered after server.shutdown() to ensure it runs first.
  • Applies quarkus.smithy.server.shutdown-grace as a .get(grace, MS) deadline on the
    otherwise-unbounded server-shutdown future.

SmithyServerConfig@ConfigMapping for quarkus.smithy.server.*.

  • Three keys: path-prefix, workers, shutdown-grace.
  • Review for default values and omitted-config behavior.

:quarkus-smithy-deployment (build-time)

SmithyProcessor (quarkus-smithy-deployment/.../SmithyProcessor.java) — the Quarkus
@BuildStep glue.

  • Marks Service beans Unremovable so Arc keeps them even without a direct injection point.
  • Wires the recorder at RUNTIME_INIT; falls back from mainRouter to httpRouter when
    quarkus.http.root-path=/.
  • Companion SmithyCodeGenProvider (worth a glance) hooks Smithy code generation into
    quarkusGenerateCode.

E2E Tests

# Protocol Operation Request Expected Result
1 restJson1 GetMenu GET /menu 200, JSON menu
2 restJson1 CreateOrder PUT /order body {"coffeeType":"LATTE"} 200, returns id
3 restJson1 GetOrder GET /order/<id> 200, returns order
4 rpcv2Cbor GetMenu POST /service/CoffeeShop/operation/GetMenu + smithy-protocol: rpc-v2-cbor, body 0xa0 200, CBOR menu
5 rpcv2Cbor CreateOrder POST .../CreateOrder + cbor header, body {coffeeType:"LATTE"} 200, returns id
6 rpcv2Json GetMenu POST .../GetMenu + smithy-protocol: rpc-v2-json, body {} 200, JSON menu
7 rpcv2Json CreateOrder POST .../CreateOrder + json header, body {coffeeType:"ESPRESSO"} 200, returns id
8 no-claim POST /service/CoffeeShop/operation/GetMenu without smithy-protocol header 404, Quarkus default page (~358 bytes) — server called next()
9 claim-and-reject POST /service/CoffeeShop/operation/ with smithy-protocol: rpc-v2-cbor (malformed URI) 404, empty body — server intercepted before next()
10 unrelated path GET /q/notreal 404, Quarkus default page (~358 bytes) — fell through to Quarkus

Follow-ups/Caveats

  • Streaming-body design: the current implementation read the whole body, we need a streaming based implementation. We need to figure out how can we reuse the Netty server implementation.
  • Native-image support.
  • @streaming Blob operations.
  • CORS support on the Vert.x server (server-netty has it).

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

@chenliu0831
Copy link
Copy Markdown
Author

chenliu0831 commented May 22, 2026

Looks like there might be major flaw with the multi Vert.x routes mount approach which violate https://smithy.io/2.0/guides/wire-protocol-selection.html after chatting with Adwait. Doing another iteration...

This is done now

@chenliu0831 chenliu0831 force-pushed the main branch 2 times, most recently from 47c68e2 to fbac6ab Compare May 23, 2026 17:27
@chenliu0831 chenliu0831 changed the title POC: Quarkus integration via shared transport Vert.x and Smithy Codegen extension Quarkus integration via shared transport Vert.x and Smithy Codegen extension May 23, 2026
@chenliu0831 chenliu0831 marked this pull request as ready for review May 23, 2026 17:41
@chenliu0831 chenliu0831 requested a review from adwsingh May 23, 2026 17:41
* {@code ProtocolResolver}'s static SPI cache (keyed on its own
* classloader) does not see runtime protocol jars under Quarkus's
* partitioned classloader hierarchy.
*/
Copy link
Copy Markdown
Author

@chenliu0831 chenliu0831 May 23, 2026

Choose a reason for hiding this comment

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

Not too sure about how Quarkus resolve those jars from extension.

Smithy services run inside Quarkus applications via a new extension that
mounts every CDI-discovered Service bean on Quarkus's main HTTP router.
Smithy operations share the Quarkus HTTP server's port — no separate
Smithy listener.

## Customer-facing surface

Users produce a `@Produces Service` bean (the generated service stub):

    @ApplicationScoped
    public class CoffeeShopServerConfig {
        @produces @singleton
        Service coffeeShop() {
            return CoffeeShop.builder()
                    .addCreateOrderOperation(new CreateOrder())
                    .addGetMenuOperation(new GetMenu())
                    .addGetOrderOperation(new GetOrder())
                    .build();
        }
    }

The extension mounts a `SmithyVertxServer` on Quarkus's `Router` as a
single catch-all route. Per-request, a `ProtocolResolver` iterates a
precision-ordered list of `ServerProtocol`s and returns one of three
outcomes:

  - claim       -> dispatch to the operation
  - no-claim    -> ctx.next() (delegate to a sibling handler)
  - claim-and-reject -> 404 directly (request is Smithy's but malformed)

This implements the Smithy 2.0 Wire-protocol-selection guide.

End-to-end CoffeeShop example at `examples/quarkus-server/`. Standalone
Gradle build that consumes smithy-java via mavenLocal — required because
including it as a subproject causes Quarkus dev-mode workspace discovery
to substitute sibling raw `build/classes` for the published jars,
splitting classloaders in ways that break `:codecs:json-codec`'s
shadowJar.

## Modules

  - `:server:server-vertx` — `SmithyVertxServer implements
    Handler<RoutingContext>`, `ServerOptions`, `VertxRequestHeaders`.
    18 integration tests against a real Vert.x HTTP server.
  - `:quarkus-smithy` (runtime) — `SmithyVertxRecorder` and
    `SmithyServerConfig` (`@ConfigMapping` for
    `quarkus.smithy.server.{path-prefix, workers, shutdown-grace}`).
    The recorder collects `Service` beans from Arc, walks
    TCCL+own-loader for `ServerProtocolProvider`s (required because
    `ProtocolResolver`'s static SPI cache can't see runtime jars under
    `QuarkusClassLoader`), constructs the server, mounts it on the
    main router, and registers ordered shutdown tasks.
  - `:quarkus-smithy-deployment` (build-time) — `SmithyProcessor`
    `@BuildStep`s and `SmithyCodeGenProvider` that hooks Smithy
    code generation into `quarkusGenerateCode` (no `smithy-base`
    Gradle plugin needed).
  - `:quarkus-smithy-integration-tests` — `SmithyCodeGenProviderTest`.

## Cross-module changes

  - `:server:server-core`
      - `ProtocolResolver` gains `resolveOrEmpty(...)` returning
        `Optional<ServiceProtocolResolutionResult>` and a second ctor
        accepting a pre-loaded protocol list (for callers like the
        Quarkus recorder where the static SPI cache is blind).
      - `HttpResponseSerializer` is new: status + header-copy +
        content-type/length logic shared between Netty and the Vert.x
        server. Body exposed as the underlying `DataStream` so Netty
        preserves zero-copy via `Unpooled.wrappedBuffer(ByteBuffer)`.
      - `ServerProtocolProvider.precision()` Javadoc documents the AWS
        service-protocol scale (rpcv2Cbor=1 ... restXml=8).
  - `:server:server-netty` — `HttpRequestHandler.writeResponse` adopts
    `HttpResponseSerializer`. Behavior unchanged.
  - Provider precision values: `RpcV2CborProtocolProvider` -> 1,
    `RpcV2JsonProtocolProvider` -> 2, `AwsRestJson1ProtocolProvider`
    -> 7. Previously all 0 (precision sort was a no-op against
    classpath order).
  - `:aws:server:aws-server-restjson` — drops dead `routes` field and
    `smithyToVertxPath` helper (the Vert.x server no longer enumerates
    per-operation routes).

## Verification

  - `./gradlew :server:server-vertx:check` green. 19 integration tests
    against a real Vert.x HTTP server: protocol resolution outcomes,
    HTTP/2 round-trip, lifecycle, options, precision regression.
  - `./gradlew :server:server-core:check` green. New tests:
    `HttpResponseSerializerTest` (6) and `ProtocolResolverTest` (7).
  - `./gradlew :quarkus-smithy:build :quarkus-smithy-deployment:build`
    green (with `--no-configuration-cache` to work around a pre-existing
    Quarkus extension-validation gradle-plugin issue).
  - `examples/quarkus-server` end-to-end (`quarkusDev`):
    `GET /menu`, `PUT /order`, `GET /order/<id>` all 200 under
    restJson1; CoffeeShop also reachable via `POST
    /service/CoffeeShop/operation/<Op>` with `smithy-protocol:
    rpc-v2-cbor` and `smithy-protocol: rpc-v2-json` headers; rpcv2
    path with no header -> Quarkus default 404 via ctx.next(); rpcv2
    path with header but malformed URI -> server 404 with empty body
    (claim-and-reject). The empty-body 404 vs Quarkus's default-page
    404 confirms the resolution-outcome distinctions are observable.

## Known limitations (deferred)

  - `@streaming Blob` operations not supported. The recorder installs
    Vert.x's `BodyHandler` upstream of the server, fully buffering
    request bodies before resolution runs.
  - Native-image support is out of scope for this cut.
  - CORS support on the Vert.x server (Netty has it).
  - Cross-service `@http(uri)` collisions are silent at construction
    time — the matcher's tie-break wins.
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.

1 participant