{"id":29835239,"url":"https://github.com/livekit/psrpc","last_synced_at":"2025-07-29T12:17:37.323Z","repository":{"id":64589308,"uuid":"572758387","full_name":"livekit/psrpc","owner":"livekit","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-24T16:20:21.000Z","size":474,"stargazers_count":32,"open_issues_count":3,"forks_count":15,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-07-24T21:58:48.474Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/livekit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-12-01T00:56:26.000Z","updated_at":"2025-07-24T16:18:07.000Z","dependencies_parsed_at":"2022-12-14T00:21:22.811Z","dependency_job_id":"7638d1e1-97f4-4a8b-b614-45ee3d3ccba9","html_url":"https://github.com/livekit/psrpc","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/livekit/psrpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fpsrpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fpsrpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fpsrpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fpsrpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/livekit","download_url":"https://codeload.github.com/livekit/psrpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/livekit%2Fpsrpc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267685559,"owners_count":24127706,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-29T02:00:12.549Z","response_time":2574,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-07-29T12:17:36.736Z","updated_at":"2025-07-29T12:17:37.313Z","avatar_url":"https://github.com/livekit.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PubSub-RPC\n\nCreate custom protobuf-based golang RPCs built on pub/sub.\n\nSupports:\n* Protobuf service definitions\n* Use Redis, Nats, or a local communication layer\n* Custom server selection for RPC handling based on user-defined [affinity](#Affinity)\n* RPC topics - any RPC can be divided into topics, (e.g. by region)\n* Single RPCs - one request is handled by one server, used for normal RPCs\n* Multi RPCs - one request is handled by every server, used for distributed updates or result aggregation\n* Queue Subscriptions - updates sent from the server will only be processed by a single client\n* Subscriptions - updates sent be the server will be processed by every client\n\n## Usage\n\n### Protobuf\n\nPSRPC is generated from proto files, and we've added a few custom method options:\n```protobuf\nmessage Options {\n  // This method is a pub/sub.\n  bool subscription = 1;\n\n  // This method uses topics.\n  bool topics = 2;\n\n  TopicParamOptions topic_params = 3;\n\n  // The method uses bidirectional streaming.\n  bool stream = 4;\n\n  oneof routing {\n    // For RPCs, each client request will receive a response from every server.\n    // For subscriptions, every client will receive every update.\n    bool multi = 5;\n\n    // Your service will supply an affinity function for handler selection.\n    bool affinity_func = 6;\n\n    // Requests load balancing is provided by a pub/sub server queue\n    bool queue = 7;\n  }\n}\n\n```\n\nStart with your service definition. Here's an example using different method options:\n\n```protobuf\nsyntax = \"proto3\";\n\nimport \"options.proto\";\n\noption go_package = \"/api\";\n\nservice MyService {\n  // A normal RPC - one request, one response. The request will be handled by the first available server\n  rpc NormalRPC(MyRequest) returns (MyResponse);\n\n  // An RPC with a server affinity function for handler selection.\n  rpc IntensiveRPC(MyRequest) returns (MyResponse) {\n    option (psrpc.options).type = AFFINITY;\n  };\n\n  // A multi-rpc - a client will send one request, and receive one response each from every server\n  rpc GetStats(MyRequest) returns (MyResponse) {\n    option (psrpc.options).type = MULTI;\n  };\n\n  // A streaming RPC - a client opens a stream, the first server to respond accepts it and both send and\n  // receive messages until one side closes the stream.\n  rpc ExchangeUpdates(MyClientMessage) returns (MyServerMessage) {\n    option (psrpc.options).stream = true;\n  };\n\n  // An RPC with topics - a client can send one request, and receive one response from each server in one region\n  rpc GetRegionStats(MyRequest) returns (MyResponse) {\n    option (psrpc.options).topics = true;\n    option (psrpc.options).type = MULTI;\n  }\n\n  // A queue subscription - even if multiple clients are subscribed, only one will receive this update.\n  // The request parameter (in this case, Ignored) will always be ignored when generating go files.\n  rpc ProcessUpdate(Ignored) returns (MyUpdate) {\n    option (psrpc.options).subscription = true;\n  };\n\n  // A normal subscription - every client will receive every update.\n  // The request parameter (in this case, Ignored) will always be ignored when generating go files.\n  rpc UpdateState(Ignored) returns (MyUpdate) {\n    option (psrpc.options).subscription = true;\n    option (psrpc.options).type = MULTI;\n  };\n\n  // A subscription with topics - every client subscribed to the topic will receive every update.\n  // The request parameter (in this case, Ignored) will always be ignored when generating go files.\n  rpc UpdateRegionState(Ignored) returns (MyUpdate) {\n    option (psrpc.options).subscription = true;\n    option (psrpc.options).topics = true;\n    option (psrpc.options).type = MULTI;\n  }\n}\n\nmessage Ignored {}\nmessage MyRequest {}\nmessage MyResponse {}\nmessage MyUpdate {}\nmessage MyClientMessage {}\nmessage MyServerMessage {}\n```\n\n### Generation\n\nInstall `protoc-gen-psrpc` by running `go install github.com/livekit/psrpc/protoc-gen-psrpc`.\n\nIf using the custom options above, you'll also need to include [options.proto](protoc-gen-psrpc/options/options.proto).\nThe simplest way to do this is to include psrpc in your project, then run\n```shell\ngo list -json -m github.com/livekit/psrpc\n\n{\n\t\"Path\": \"github.com/livekit/psrpc\",\n\t\"Version\": \"v0.2.2\",\n\t\"Time\": \"2022-12-27T21:40:05Z\",\n\t\"Dir\": \"/Users/dc/go/pkg/mod/github.com/livekit/psrpc@v0.2.2\",\n\t\"GoMod\": \"/Users/dc/go/pkg/mod/cache/download/github.com/livekit/psrpc/@v/v0.2.2.mod\",\n\t\"GoVersion\": \"1.20\"\n}\n```\n\nUse the `--psrpc_out` with `protoc` and include the options directory.\n\n```shell\nprotoc \\\n  --go_out=paths=source_relative:. \\\n  --psrpc_out=paths=source_relative:. \\\n  -I /Users/dc/go/pkg/mod/github.com/livekit/psrpc@v0.2.2/protoc-gen-psrpc/options \\\n  -I=. my_service.proto\n```\n\nThis will create a `my_service.psrpc.go` file.\n\n### Client\n\nA `MyServiceClient` will be generated based on your rpc definitions:\n\n```go\ntype MyServiceClient interface {\n    // A normal RPC - one request, one response. The request will be handled by the first available server\n    NormalRPC(ctx context.Context, req *MyRequest, opts ...psrpc.RequestOpt) (*MyResponse, error)\n\n    // An RPC with a server affinity function for handler selection.\n    IntensiveRPC(ctx context.Context, req *MyRequest, opts ...psrpc.RequestOpt) (*MyResponse, error)\n\n    // A multi-rpc - a client will send one request, and receive one response each from every server\n    GetStats(ctx context.Context, req *MyRequest, opts ...psrpc.RequestOpt) (\u003c-chan *psrpc.Response[*MyResponse], error)\n\n    // A streaming RPC - a client opens a stream, the first server to respond accepts it and both send and\n    // receive messages until one side closes the stream.\n    ExchangeUpdates(ctx context.Context, opts ...psrpc.RequestOpt) (psrpc.ClientStream[*MyClientMessage, *MyServerMessage], error)\n\n    // An RPC with topics - a client can send one request, and receive one response from each server in one region\n    GetRegionStats(ctx context.Context, topic string, req *Request, opts ...psrpc.RequestOpt) (\u003c-chan *psrpc.Response[*MyResponse], error)\n\n    // A queue subscription - even if multiple clients are subscribed, only one will receive this update.\n    SubscribeProcessUpdate(ctx context.Context) (psrpc.Subscription[*MyUpdate], error)\n\n    // A subscription with topics - every client subscribed to the topic will receive every update.\n    SubscribeUpdateRegionState(ctx context.Context, topic string) (psrpc.Subscription[*MyUpdate], error)\n}\n\n// NewMyServiceClient creates a psrpc client that implements the MyServiceClient interface.\nfunc NewMyServiceClient(clientID string, bus psrpc.MessageBus, opts ...psrpc.ClientOpt) (MyServiceClient, error) {\n    ...\n}\n```\n\nMulti-RPCs will return a `chan *psrpc.Response`, where you will receive an individual response or error from each server:\n```go\ntype Response[ResponseType proto.Message] struct {\n    Result ResponseType\n    Err    error\n}\n```\n\nStreaming RPCs will return a `psrpc.ClientStream`. You can listen for updates from its channel, send updates, or close\nthe stream.\n\nSend blocks until the message has been received. When the stream closes the cause is available to both the server and\nclient from `Err`.\n```go\ntype ClientStream[SendType, RecvType proto.Message] interface {\n\tChannel() \u003c-chan RecvType\n\tSend(msg SendType, opts ...StreamOption) error\n\tClose(cause error) error\n\tErr() error\n}\n```\n\nSubscription RPCs will return a `psrpc.Subscription`, where you can listen for updates on its channel:\n\n```go\ntype Subscription[MessageType proto.Message] interface {\n    Channel() \u003c-chan MessageType\n    Close() error\n}\n```\n\n### ServerImpl\n\nA `\u003cServiceName\u003eServerImpl` interface will be also be generated from your rpcs. Your service will need to fulfill its interface:\n\n```go\ntype MyServiceServerImpl interface {\n    // A normal RPC - one request, one response. The request will be handled by the first available server\n    NormalRPC(ctx context.Context, req *MyRequest) (*MyResponse, error)\n\n    // An RPC with a server affinity function for handler selection.\n    IntensiveRPC(ctx context.Context, req *MyRequest) (*MyResponse, error)\n    IntensiveRPCAffinity(req *MyRequest) float32\n\n    // A multi-rpc - a client will send one request, and receive one response each from every server\n    GetStats(ctx context.Context, req *MyRequest) (*MyResponse, error)\n\n    // A streaming RPC - a client opens a stream, the first server to respond accepts it and both send and\n    // receive messages until one side closes the stream.\n    ExchangeUpdates(stream psrpc.ServerStream[*MyClientMessage, *MyServerMessage]) error\n\n    // An RPC with topics - a client can send one request, and receive one response from each server in one region\n    GetRegionStats(ctx context.Context, req *MyRequest) (*MyResponse, error)\n}\n```\n\n### Server\n\nFinally, a `\u003cServiceName\u003eServer` will be generated. This is used to start your rpc server, as well as register and deregister topics:\n\n```go\ntype MyServiceServer interface {\n    // An RPC with topics - a client can send one request, and receive one response from each server in one region\n    RegisterGetRegionStatsTopic(topic string) error\n    DeregisterGetRegionStatsTopic(topic string) error\n\n    // A queue subscription - even if multiple clients are subscribed, only one will receive this update.\n    PublishProcessUpdate(ctx context.Context, msg *MyUpdate) error\n\n    // A subscription with topics - every client subscribed to the topic will receive every update.\n    PublishUpdateRegionState(ctx context.Context, topic string, msg *MyUpdate) error\n\n    // Close and wait for pending RPCs to complete\n    Shutdown()\n\n    // Close immediately, without waiting for pending RPCs\n    Kill()\n}\n\n// NewMyServiceServer builds a RPCServer that can be used to handle\n// requests that are routed to the right method in the provided svc implementation.\nfunc NewMyServiceServer(serverID string, svc MyServiceServerImpl, bus psrpc.MessageBus, opts ...psrpc.ServerOpt) (MyServiceServer, error) {\n    ...\n}\n```\n\n## Affinity\n\n### AffinityFunc\n\nThe server can implement an affinity function for the client to decide which instance should take a SingleRequest.\nA higher affinity score is better, a score of 0 means the server is not available, and a score \u003c 0 means the server\nwill not respond to the request.\n\nFor example, the following could be used to return an affinity based on cpu load:\n```protobuf\nrpc IntensiveRPC(MyRequest) returns (MyResponse) {\n  option (psrpc.options).type = AFFINITY;\n};\n```\n\n```go\nfunc (s *MyService) IntensiveRPC(ctx context.Context, req *api.MyRequest) (*api.MyResponse, error) {\n    ... // do something CPU intensive\n}\n\nfunc (s *MyService) IntensiveRPCAffinity(_ *MyRequest) float32 {\n    return stats.GetIdleCPU()\n}\n```\n\n### SelectionOpts\n\nOn the client side, you can also set server selection options with single RPCs.\n\n```go\ntype SelectionOpts struct {\n    MinimumAffinity      float32       // (default 0) minimum affinity for a server to be considered a valid handler\n    MaxiumAffinity       float32       // (default 0) if \u003e 0, any server returning a max score will be selected immediately\n    AcceptFirstAvailable bool          // (default true)\n    AffinityTimeout      time.Duration // (default 0 (none)) server selection deadline\n    ShortCircuitTimeout  time.Duration // (default 0 (none)) deadline imposed after receiving first response\n}\n```\n\n```go\nselectionOpts := psrpc.SelectionOpts{\n    MinimumAffinity:      0.5,\n    AffinityTimeout:      time.Second,\n    ShortCircuitTimeout:  time.Millisecond * 250,\n}\n\nres, err := myClient.IntensiveRPC(ctx, req, psrpc.WithSelectionOpts(selectionOpts))\n```\n\nIn this example, a server will require at least 0.5 idle CPU to be selected for this `IntensiveRPC` request.\n\n## Error handling\n\nPSRPC defines an error type (`psrpc.Error`). This error type can be used to wrap any other error using the `psrpc.NewError` function:\n\n```go\nfunc NewError(code ErrorCode, err error) Error\n```\n\nThe `code` parameter provides more context about the cause of the error.\nA [variety of codes](https://github.com/livekit/psrpc/blob/main/errors.go#L39) are defined for common error conditions.\nPSRPC errors are serialized by the PSRPC server implementation, and unmarshalled (with the original error code) on the client.\nBy retrieving the code using the `Code()` method, the client can determine if the error was caused by a server failure,\nor a client error, such as a bad parameter. This can be used as an input to the retry logic, or success rate metrics.\n\nThe most appropriate HTTP status code for a given error can be retrieved using the `ToHttp()` method. This status code is generated from the associated error code.\nSimilarly, a grpc `status.Error` can be created from a `psrpc.Error` using the `ToGrpc()` method.\n\nA `psrpc.Error` can also be converted easily to a `twirp.Error`using the `errors.As` function:\n\n```go\nfunc As(err error, target any) bool\n```\n\nFor instance:\n\n```go\nfunc convertError(err error) {\n\tvar twErr twirp.Error\n\n\tif errors.As(err, \u0026twErr)\n\t\treturn twErr\n\t}\n\n\treturn err\n}\n```\n\nThis allows the twirp server implementations to interpret the `prscp.Errors` as native `twirp.Error`. Particularly, this\nmeans that twirp clients will also receive information about the error cause as `twirp.Code`. This makes sure that\n`psrpc.Error` created by psrpc server can be forwarded through PS and twirp RPC all the way to a twirp client error hook\nwith the full associated context.\n\n`psrpc.Error` implements the `Unwrap()` method, so the original error can be retrieved by users of PSRPC.\n\n## Interceptors\n\nInterceptors allow writing middleware for RPC clients and servers. Interceptors can be used to run code during the call\nlifecycle such as logging, recording metrics, tracing, and retrying calls. PSRPC defines four interceptor types which\nallow intercepting requests on the client and server.\n\n### ServerRPCInterceptor\n\n`ServerRPCInterceptor` are invoked by the server for calls to unary and multi RPCs.\n\n```go\ntype ServerRPCInterceptor func(ctx context.Context, req proto.Message, info RPCInfo, handler ServerRPCHandler) (proto.Message, error)\n\ntype ServerRPCHandler func(context.Context, proto.Message) (proto.Message, error)\n```\n\nThe `info` parameter contains metadata about the method including the name and topic. Calling the `handler` parameter\nhands off execution to the next interceptor.\n\n`ServerRPCInterceptor` are added to new servers with `WithServerRPCInterceptors`.\n\nInterceptors run in the order they are added so the first interceptor passed to `WithServerRPCInterceptors` is the first\nto receive a new requests. Calling the `handler` parameter invokes the second interceptor and so on until the service\nimplementation receives the request and produces a response.\n\n### ClientRPCInterceptor\n\n`ClientRPCHandler` are created by clients to process requests to unary RPCs.\n\n```go\ntype ClientRPCInterceptor func(info RPCInfo, handler ClientRPCHandler) ClientRPCHandler\n\ntype ClientRPCHandler func(ctx context.Context, req proto.Message, opts ...RequestOption) (proto.Message, error)\n```\n\n`ClientRPCInterceptor` are created by implementing `ClientRPCHandler` and passing the interceptor to new clients\nusing `WithClientRPCInterceptors`.\n\nThe `handler` parameter received by `ClientRPCInterceptor` should be called by the implementation of `ClientRPCHandler`\nto continue the call lifecycle.\n\n### ClientMultiRPCInterceptor\n\n`ClientMultiRPCHandler` are created by clients to process requests to multi RPCs. Because `ClientMultiRPCHandler`\nprocess several responses for the same request, implementations must define separate functions for each phase of the call\nlifecycle. The `Send` function is executed on outgoing request parameters. The `Recv` function is executed once for each\nresponse when it returns from a servers. `Close` is called when the deadline is reached.\n\n```go\ntype ClientMultiRPCInterceptor func(info RPCInfo, handler ClientMultiRPCHandler) ClientMultiRPCHandler\n\ntype ClientMultiRPCHandler interface {\n    Send(ctx context.Context, msg proto.Message, opts ...RequestOption) error\n    Recv(msg proto.Message, err error)\n    Close()\n}\n```\n\n`ClientMultiRPCInterceptor` are created by implementing `ClientMultiRPCHandler` and passing the interceptor to new\nclients using `WithClientMultiRPCInterceptors`.\n\nEach function in a `ClientMultiRPCInterceptor` should call the corresponding function in the handler received in\nthe `handler` parameter.\n\n### StreamInterceptor\n\n`StreamInterceptor` are created by both clients and servers to process streaming RPCs. The `Send` function is executed\nonce for each outgoing message. The `Recv` function is executed once for each incoming message. `Close` is called when\neither the local or remote host close the stream or if the stream receives a malformed message.\n\n```go\ntype StreamInterceptor func(info RPCInfo, handler StreamHandler) StreamHandler\n\ntype StreamHandler interface {\n    Recv(msg proto.Message) error\n    Send(msg proto.Message, opts ...StreamOption) error\n    Close(cause error) error\n}\n```\n\n`StreamInterceptor` are created by implementing `StreamHandler` and passing the interceptor to new clients or\nservers using `WithClientStreamInterceptors` and `WithServerStreamInterceptors`.\n\nEach function in a `StreamInterceptor` should call the corresponding function in the handler\nreceived in the `handler` parameter.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flivekit%2Fpsrpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flivekit%2Fpsrpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flivekit%2Fpsrpc/lists"}