CAICAIIs opened a new issue, #3751:
URL: https://github.com/apache/dubbo-go/issues/3751
### What problem would this solve?
A client without generated stubs — a gateway, a console, a test harness —
cannot call a typed stub, so it depends on Dubbo's generic contract, whose
triple wire form is the `$invoke` procedure. Triple dispatches by procedure
path, so the provider has to have `/<interface>/$invoke` registered. Plain HTTP
with protobuf JSON can also reach an IDL service, but that is a different
integration path: it bypasses the dubbo-go client, so it uses neither the
configured generic mode nor the reference's attachments, cluster and retry
policies.
dubbo-go registers `$invoke` for services exported in non-IDL mode only. A
service exported with an IDL `ServiceInfo` registers just its concrete methods,
so a generic call is rejected by the procedure router with `404 Not Found` and
never reaches the provider filter chain that would have dispatched it.
The asymmetry is the defect: `client.NewGenericService` always builds a
non-IDL consumer, while whether `/<interface>/$invoke` exists is decided by the
provider's export mode.
Downstream: apache/dubbo-go-pixiu#918 asks whether the gateway still needs a
descriptor-based IDL path.
### Current behavior (develop @ 302aa0e8)
1. The consumer always sends `$invoke`. `GenericService` declares its entry
point as `Invoke func(...) (any, error)` with the tag `dubbo:"$invoke"`
(`filter/generic/service.go:35`), and the proxy turns that tag into the
invocation name (`proxy/proxy.go:255`). `client.NewGenericService`
(`client/client.go:161`) additionally forces `WithIDL(constant.NONIDL)`,
`WithGeneric()` and hessian2, and the consumer filter recognizes the call by
name and arity (`filter/generic/util.go:41`). `constant.Generic = "$invoke"`
(`common/constant/default.go:58`). A typed call made through a generic
reference is rewritten to the same name (`filter/generic/filter.go:112`).
2. Triple routes by procedure path, and an unregistered procedure is an HTTP
404 (`protocol/triple/triple_protocol/method_route_mux.go:86,104`). The path is
joined on the client side from the reference interface and the method name
(`protocol/triple/triple_protocol/client.go:356`) and mirrored on the server by
`joinProcedure` (`protocol/triple/common.go:26`).
3. `$invoke` is registered on the non-IDL branch only.
`registerServiceHandlers` (`protocol/triple/server.go:241`) has three branches:
```go
if info != nil { // IDL export: only the methods
carried by ServiceInfo
} else if IDLMode == constant.NONIDL { // non-IDL export: reflection plus
$invoke
}
```
`buildGenericMethodInfo()` is appended inside
`createServiceInfoWithReflection` (`protocol/triple/server.go:765`, definition
at `:831`), so only non-IDL exports get `/<interface>/$invoke`, and
`handleServiceWithInfo` (`:521`) never creates a handler for it. The third
branch is the legacy compat path (`info == nil` with IDL mode) and is out of
scope here.
4. The provider side is otherwise ready. `generic_service` is part of
`DefaultServiceFilters` (`common/constant/default.go:69`), and
`genericServiceFilter.Invoke` (`filter/generic/service_filter.go:67`) resolves
the concrete method through `common.ServiceMap` (`:93`). IDL services are
registered there (`server/action.go:164`, with the interface name taken from
`info.InterfaceName` at `server/server.go:182`), and for services implemented
with generated protobuf parameters their argument types are protobuf messages,
which is what the `protobuf-json` generalizer expects
(`filter/generic/generalizer/protobuf_json.go:62`).
5. Since no invocation is ever created, none of that runs. The caller only
sees a bare 404, with no hint that the provider lacks a generic entry point.
### Proposed solution (optional)
Register the `$invoke` procedure for the IDL branch as well, next to the
concrete methods. `buildGenericMethodInfo()` already carries the `ReqInitFunc`
(`protocol/triple/server.go:831`) that declares the destination shape the
wrapper codec decodes into (`protocol/triple/triple_protocol/codec.go:319`,
slice branch `:321`, inner codec selected from the wrapper's `serializeType` at
`:331`), so this is a registration change rather than a new conversion layer.
One implementation choice to settle in review: append the method to
`info.Methods` for symmetry with the non-IDL branch, or register the procedure
directly through `registerMethodHandler(joinProcedure(intfName,
constant.Generic), buildGenericMethodInfo(), invoker, opts...)` so the
`ServiceInfo` handed to `saveServiceInfo` stays untouched. Non-IDL exports
already put `$invoke` into that list (`:765` → `:271` → `:661`, which also
drives the OpenAPI registration at `:690`), so appending would add `$invoke` to
the reflection and OpenAPI metadata of IDL services as well, while direct
registration leaves that metadata alone.
`TestHandleServiceWithInfoSaveServiceInfoOnlyOriginalMethods`
(`protocol/triple/server_test.go:554`) asserts that what `saveServiceInfo`
records stays close to the original method list, which is an argument for
direct registration.
A second question: register the entry for every IDL export, or make it
opt-in through a server option? The non-IDL branch registers it unconditionally
today.
Tests to add: a unit assertion in `protocol/triple/server_test.go`, next to
`TestHandleServiceWithInfoSaveServiceInfoOnlyOriginalMethods` (`:554`), that an
IDL export exposes the `$invoke` procedure. The end-to-end case with an IDL
provider and a generic client lives in dubbo-go-samples and would be tracked
separately.
### Scope
- unary only (`buildGenericMethodInfo` registers `CallUnary`); streaming
generic invocation is out of scope;
- generic modes covered: `protobuf-json`; map mode needs the check described
below;
- no client-side API change, no change to the non-IDL path.
### Acceptance
- An IDL triple provider exported through `server.Register(...)` can be
invoked with `NewGenericService(iface, WithProtocolTriple(),
WithGenericType("protobuf-json"), WithURL("tri://127.0.0.1:50052"))`.
- Map mode (`WithGenericType("true")`) against the same provider is expected
to work, to be confirmed while implementing: `MapGeneralizer.Realize` decodes
with mapstructure using the `m` tag (`filter/generic/generalizer/map.go:79`),
while generated protobuf structs carry `protobuf` and `json` tags, so
`repeated`, nested and enum fields need checking.
- A provider exported through `RegisterService` (non-IDL) keeps working
unchanged.
- A generic call naming a method the service does not expose still returns
the `"<method>" method is not found` error from `genericServiceFilter`
(`filter/generic/service_filter.go:97`) instead of a 404, with the name given
as registered in `common.ServiceMap` (canonical name or the first-rune-swapped
alias added by `suitableMethods`, `common/rpc_service.go:575`).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]