Conversation
gplanchat
added a commit
that referenced
this pull request
Sep 11, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One method per activity RPC the bridge uses, each delegating to a single abstract call(), so a transport implements one method and inherits the rest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The other half of the WorkflowService surface the bridge uses: start, signal, query, update, history, terminate, and the workflow and Nexus task queues. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mentation Collaborators stop depending on the generated stub: the interface returns responses and throws \RuntimeException with the gRPC status as its code, which is what GrpcUnary::wait already did. GrpcWorkflowServiceClient wraps the stub behind it; the abstract base carries the per-RPC methods. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GrpcUnary::wait disappears from the call sites: the client returns the response, and NOT_FOUND is caught by exception code where it used to be read off the status pair. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…face Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…all the interface The already-started and stale-task-token cases keep their gRPC codes (6 and 5), now read from the exception instead of the status pair. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…face The fake client returns responses and throws by gRPC code; no more UnaryCall handles. The tests no longer touch ext-grpc, so the extension requirement goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…k the interface Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ts workers follow The whole suite, workers included, then runs through whichever client the variable names; the ext-grpc requirement becomes a runtime skip that only applies to transport=grpc. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… exception Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Length-prefixed frames in and out, the status from the trailers, and the same exception the ext-grpc path throws. Kept free of curl so this has a test without a server. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gplanchat/durable-bridge-temporal-http: path repository, autoload, PHPStan and Psalm paths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CurlGrpcWorkflowServiceClient is one HTTP/2 POST per unary call, no ext-grpc, workers included. JsonGatewayRoutes lists the HTTP bindings of the RPCs the bridge uses; its test holds the table to the protobuf descriptor checked in with the stubs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Path placeholders are snake_case proto paths over the camelCase JSON; GET routes carry the rest as dotted query parameters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…suggestion JsonGatewayWorkflowServiceClient serves the client RPCs over the frontend HTTP port; a worker RPC throws UNIMPLEMENTED with the transport to use instead. The Temporal bridge no longer requires ext-grpc in Composer: the factory asserts it at runtime for transport=grpc only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e docs Start, describe and read an execution over the gateway without any worker; document transport= in the configuration reference (EN, FR), the bridge README and the bundle option. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
grpc (the default) builds the ext-grpc client; grpc-curl and http name the classes of the sibling durable-bridge-temporal-http package and fail with a clear message when it is not installed. The JSON gateway defaults to its own port, 7243. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… GrpcUnary::wait The operator service has no interface of its own yet; its stub still hands back a call handle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Which client the factory builds is not what they measure, and the ext-grpc one cannot be instantiated without the extension; the curl one keeps them runnable in a job without it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Unit suite, then the whole integration suite through transport=grpc-curl against the Temporal dev server with its HTTP port, JSON gateway test included. The only job that can prove the bridge installs and runs without the extension. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
temporal:// and temporal+tls:// are gRPC, temporal+http:// and temporal+https:// the server JSON gateway (port 7243 by default). tls=1 and transport= keep working beside the scheme; the Messenger factory and the nested-inner guard recognise the four schemes and the legacy ones through one predicate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ged once The default: ext-grpc when it is loaded, curl otherwise, decided at wiring time by a pure resolution every branch of which has a test. The fallback is one INFO line per process (PSR-3 when a logger is given, error_log for a bare worker); an explicit transport never falls back. effectiveTransport() tells a command what a process ended up with. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he transport Symfony passes its logger service, Laravel its bound PSR logger, Magento an optional constructor argument. durable:execution:diagnose and the two Laravel worker commands print the effective transport, where the question gets asked. Documentation rewritten in the scheme vocabulary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Its two test doubles extend AbstractWorkflowServiceClient instead of the stub, the dashboard provider and the two integration tests take the interface. Caught by the sample-app CI jobs, which the root suites never reach. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gplanchat
force-pushed
the
feat/temporal-http-bridge
branch
from
September 16, 2026 16:23
6323153 to
fdccc49
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A new package,
gplanchat/durable-bridge-temporal-http(src/Bridge/TemporalHttp), and the seam in the Temporal bridge that lets it in. The Temporal server speaks two transports; the bridge spoke one, through thegrpcPHP extension. It now speaks both, and the extension becomes optional.The seam:
WorkflowServiceClientInterfaceEvery collaborator of the bridge took the generated stub
Temporal\Api\Workflowservice\V1\WorkflowServiceClient, whose methods hand back a\Grpc\UnaryCallto wait on. That class extends\Grpc\BaseStub, so nothing in the bridge could even be loaded without the extension.They now take
WorkflowServiceClientInterface: one method per RPC the bridge uses (38), returning the response directly and throwing\RuntimeExceptionwith the gRPC status as its code on anything but OK. That contract is whatGrpcUnary::waitwas already producing, so the call sites lose a wrapper rather than gain one, and the places that readNOT_FOUNDorALREADY_EXISTSoff the status pair read it off the exception code instead. The per-RPC methods live once, in two traits behindAbstractWorkflowServiceClient; a transport implements a singlecall().Three implementations, chosen by the DSN scheme:
temporal://,temporal+tls://withext-grpcloadedGrpcWorkflowServiceClienttemporal://,temporal+tls://without it (ortransport=grpc-curl)CurlGrpcWorkflowServiceClienttemporal+http://,temporal+https://JsonGatewayWorkflowServiceClientUNIMPLEMENTED(12) and names the transport to useThe scheme names the wire and the encryption (
+tls,+https);tls=1stays accepted as the older spelling.transport=(autoby default) overrides what the scheme implies:grpcdemands the extension and fails without it,grpc-curlforces curl,httpis whattemporal+http://sets.autois resolved once, at wiring time, by a pure function (WorkflowServiceClientFactory::resolve): the extension when loaded, curl otherwise, an exception naming both remedies when neither is installed. The fallback to curl is logged once per process at INFO, through the PSR-3 logger the host passes (Symfony'sloggerservice, Laravel's bound logger, an optional Magento constructor argument) orerror_logfor a bare worker. An explicit transport never falls back.durable:execution:diagnose(Symfony) and the two Laravel worker commands print the effective transport, so the question "which one am I on" is answered where it gets asked. No fallback at call time: both transports talk to the same port, a call that fails on one fails on the other, and switching would double the timeouts while hiding the cause.What was checked
The route table of the JSON gateway is copied from the
google.api.httpannotations by hand, soJsonGatewayRoutesTestholds it to the protobuf descriptor checked in with the stubs: every route must appear there, and every interface RPC without a route must have no binding. A stub regeneration that moves a route fails there rather than as a 404 in production.Against
temporal server start-dev --http-port 7243:DURABLE_TEMPORAL_TRANSPORT=grpc-curl, workers included (TemporalServerTestCaseandworker.phpread the variable). Same result as throughext-grpc;JsonGatewayRoundTripTeststarts, describes and reads an execution over the gateway without any worker.A new CI job,
no-grpc, installs PHP without the extension, runs the unit suite, then the whole integration suite throughgrpc-curlagainst the dev server (temporalio/setup-temporal,--http-port 7243)..github/workflows/ci.ymlis on the supervised list; the job was asked for explicitly.Unit suite: same count as before, no test removed or skipped. Six unit test files that mocked the stub now mock the interface and no longer need
ext-grpcat all.Rules touched, on purpose
composer.jsonat three levels (root, Temporal bridge, the new package) andcomposer.lock: a new package cannot exist without them. The Temporal bridge movesext-grpcfromrequiretosuggestand gainspsr/log(already in the lock through Symfony) for the fallback line; the factory asserts the extension at runtime fortransport=grpconly. The root lock was refreshed offline for the two path packages only.documentation/adr/untouched. DUR006 already allows "gRPC/HTTP per implementation choices". DUR019 says "exclusively via gRPC":grpc-curlstill is gRPC,httpis not. Whether DUR019 wants a superseding ADR is a human call.Not in this PR
bin/splitsh-publish.shand the satellite repository for the new package: the repository and the PAT scope come first, then the line inSPLITS.transport=grpc.OperatorServiceClient(Nexus endpoints in the integration tests) keeps the raw stub andGrpcUnary::wait; nothing insrc/uses it.