English | 中文
When a call to Redis, MySQL, or another store should reuse the RPC client's naming route (Selector), timeout control, and client filters (metrics, tracing, and so on), wrap the third-party database SDK into the Client.invoke path under a custom protocol. tRPC-Python ships Redis, MySQL, and Kafka plugins in the ecosystem repository python-database.
The overall workflow is:
- Pass the command via
req_head: Database SDKs usually take structured commands or arguments rather than serialized wire bytes. The caller passes empty bytesb""as the request body (withSerializationType.Noop) and attaches the command object viaOptions().set_req_head(command), which places it onmsg.client_req_head. - Register a pass-through
CLIENT_CODEC: Register a codec that returns the byte buffer unchanged so the protocol slot is valid. - Execute the SDK call in
CLIENT_TRANSPORT: At the end of the client filter chain,round_tripreads the resolved(host, port)fromoptions.addressand the command object frommsg.client_req_head, invokes the third-party database SDK, and writes the result back onto the command object.
Keep the roles of these two configuration fields distinct:
client.service.protocolselects theCLIENT_CODECandCLIENT_TRANSPORTplugins (routing the call into your database transport).client.service.targetuses its URI scheme (such asip://orpolar://) to select aNAMING_SELECTORplugin, which only resolves the target into a concrete(host, port)address.
The walkthrough below registers a generic protocol named store without tying it to a specific database engine.
When protocol: store is set in trpc_python.yaml or via call options, Options.set_protocol("store") looks up plugins in this exact order:
- Look up
CLIENT_CODEC: Callsget_client_codec(protocol). If no client codec is registered under that name,set_protocolreturns immediately without updating the parser or transport. The options instance keeps the defaulttrpctransport withopts.codec = None, andClient.invokesubsequently raisesClientEncodeError("client codec empty")before reaching the transport layer. - Look up
CLIENT_CHECKER: Once a codec exists, callsget_client_parser(protocol). Because the third-party database SDK manages its own framing and connections, you do not need to register a client checker (set_parser(None)has no effect on a customround_trip). - Look up
CLIENT_TRANSPORT: Callsget_client_transport(protocol). If a transport instance is found, it replaces the defaulttrpctransport initialized in_create_client_options.
Therefore, you must register both CLIENT_CODEC and CLIENT_TRANSPORT under store. Registering only the transport will cause protocol: store in YAML to be ignored.
Because the command object travels in msg.client_req_head, the codec does not frame or parse packets—it simply returns the byte payload as-is.
The first argument to both encode_data and decode_data is the call context. Keep both methods as ordinary synchronous functions (not async def), because _invoke_backend calls them directly without await.
from trpc_sdk.abc import BaseCodec
from trpc_sdk.abc import PluginType
from trpc_sdk.plugin import register_plugin
@register_plugin(PluginType.CLIENT_CODEC, "store")
class StoreCodec(BaseCodec):
"""Pass the body through without framing."""
def encode_data(self, context, body: bytes) -> bytes:
return body or b""
def decode_data(self, context, buffer: bytes) -> bytes:
return buffer or b""Subclass BaseClientTransport and implement the async method round_trip(ctx, req_data, options). It sits at the innermost point of the client filter chain. Every configured client interceptor wraps this round_trip.
Inside round_trip, everything needed to call the database SDK is available:
- Target address:
options.addressis a(host, port)tuple populated byload_node_configafter theSelectorresolvestarget. - Password: If
passwordis configured under the service entry in YAML, it is available atoptions.password. - Command object: Retrieved from
ctx.get_message().client_req_head(whilereq_dataisb"").
from trpc_sdk.abc import BaseClientTransport
from trpc_sdk.abc import PluginType
from trpc_sdk.plugin import register_plugin
@register_plugin(PluginType.CLIENT_TRANSPORT, "store")
class StoreTransport(BaseClientTransport):
"""Run one already-installed SDK call for a store command."""
async def round_trip(self, ctx, req_data: bytes, options):
message = ctx.get_message()
command = message.client_req_head
host, port = options.address
# Invoke the third-party database SDK here using host, port, options.password, and command.
command.result = None
return b""Return empty bytes b"". After round_trip returns, _invoke_backend calls codec.decode_data. When the decoded response body has length 0, the framework skips deserialization and does not fill a response object.
Configure the downstream storage service under client.service:
name: The callee service name. Whencalleeis omitted,parse_client_configkeys the client configuration table byname.protocol: Set tostoreto selectStoreCodecandStoreTransport.target: The target endpoint.ip://127.0.0.1:6379uses the built-inipselector, which parses the IP and port directly without DNS lookup.password(optional): Authentication secret, forwarded tooptions.passwordinround_trip.
client:
service:
- name: trpc.app.server.store
network: tcp
protocol: store
target: ip://127.0.0.1:6379
timeout: 1000When calling Client().invoke() from business code, pay attention to three details:
- Set the callee service name on the message before calling
invoke:get_optionsloads YAML configuration (load_client_config) usingmsg.callee_service_namebefore applying the per-callOptionslist. Always callmsg.set_callee_service_name("trpc.app.server.store")prior toinvoke. If you only passOptions().set_service_name(...), the YAML lookup has already happened with an empty name, and theprotocolandtargetfrom YAML will be missed. - Use
SerializationType.Noopwith an empty request body:NoopSerializer.serializeonly acceptsbytesorBodyinstances, so a customStoreCommandobject cannot be passed as the request bodyreq. Passb""as the request body,Bodyas the response class, and attachcommandviaOptions().set_req_head(command)(whichupdate_messagecopies tomsg.client_req_head). - Import the plugin module at startup: Make sure the module defining
StoreCodecandStoreTransportis imported before making calls so@register_pluginexecutes.
from trpc_sdk.abc import clone_client_message
from trpc_sdk.client import Client
from trpc_sdk.client import Options
from trpc_sdk.serialization import Body
from trpc_sdk.serialization import SerializationType
class StoreCommand:
def __init__(self, statement: str):
self.statement = statement
self.result = None
async def execute(ctx, statement: str):
ctx, _ = clone_client_message(ctx)
ctx.get_message().set_callee_service_name("trpc.app.server.store")
command = StoreCommand(statement)
await Client().invoke(
ctx,
b"",
Body,
[
Options()
.set_req_head(command)
.set_serialization_type(SerializationType.Noop),
],
)
return command.resultIf you are not loading configuration from YAML, chain .set_protocol("store").set_target("ip://127.0.0.1:6379") onto the same Options() instance.