In order to enhance the framework's extensibility, tRPC introduces the concept of filters, which takes inspiration from the Aspect-Oriented Programming (AOP) paradigm in Java. The approach involves setting up filter points before and after specific actions in the framework, and then inserting a series of filters at these points to extend the functionality of the framework, and it also facilitates integration with different ecosystems.
By leveraging the filter mechanism, the framework modularizes and plugins specific logic components related to interface requests, decoupling them from the concrete business logic and achieving reusability. Examples of filters include monitoring filters, logging filters, and authentication filters.
In terms of business logic, users can customize filters to implement functionalities such as service rate limiting and service monitoring. Therefore, it is essential to understand how to customize and use framework filters.
Next, let's first introduce the principles of filters and then explain how to implement and use custom filters.
tRPC-Cpp filters are implemented using an array traversal approach. It defines a set of filter points and executes the corresponding filter logic at each step of the invocation process. To understand the principle of implementing filters in the framework, it is important to understand the following key parts:
The framework's filter points are divided into server-side and client-side points, each with 10 filter points defined in filter_point.h. Typically, users only need to focus on a few filter points below. If you need to use other filter points, you can refer to the rpcz documentation.
enum class FilterPoint {
CLIENT_PRE_RPC_INVOKE = 0, ///< Before makes client RPC call
CLIENT_POST_RPC_INVOKE = 1, ///< After makes client RPC call
CLIENT_PRE_SEND_MSG = 2, ///< Before client sends the RPC request message
CLIENT_POST_RECV_MSG = 3, ///< After client receives the RPC response message
SERVER_POST_RECV_MSG = kServerFilterPrefix | 0, ///< After server receives the RPC request message
SERVER_PRE_SEND_MSG = kServerFilterPrefix | 1, ///< Before server sends the RPC response message
SERVER_PRE_RPC_INVOKE = kServerFilterPrefix | 2, ///< Before makes server RPC call
SERVER_POST_RPC_INVOKE = kServerFilterPrefix | 3, ///< After makes server RPC call
};
The filter points and execution flow are as follows:
Filter point | Description |
---|---|
CLIENT_PRE_RPC_INVOKE | This filter point is executed immediately after the user invokes an RPC interface. At this point, the request content has not been serialized yet. |
CLIENT_PRE_SEND_MSG | At this point, the request content has been serialized, and the next stage involves encoding and sending the request. |
CLIENT_POST_RECV_MSG | At this point, the response packet has been received and decoded, but the response content has not been deserialized yet. |
CLIENT_POST_RPC_INVOKE | At this point, the response content has been deserialized, indicating that the RPC invocation is completed. |
Filter point | Description |
---|---|
SERVER_POST_RECV_MSG | At this point, the request packet has been received and decoded, but the response content has not been deserialized yet. |
SERVER_PRE_RPC_INVOKE | At this point, the request content has been deserialized, and the next stage involves entering the RPC processing function. |
SERVER_POST_RPC_INVOKE | At this point, the RPC processing logic has been completed. This filter point is executed immediately after the RPC processing logic finishes. At this time, the response content has not been serialized yet. |
SERVER_PRE_SEND_MSG | At this point, the response content has been serialized, and the next stage involves encoding and sending the response. |
The framework calls the interceptor function of the FilterController
class at filter points. The client-side invokes the RunMessageClientFilters
function, while the server-side invokes the RunMessageServerFilters
function. At this point, all filters registered with the framework will execute their implementation of the operator()
function. The operator()
function is a pure virtual function in the base class of the framework filter. Each specific filter needs to override this function after inheriting from the base class. Then, use the context in the interface parameters to retrieve the necessary information and implement specific functionality.
The framework supports two types of filters based on their scope:
-
Global-level filters
These filters apply to all services under the server or client. They can be configured as follows:
server: filter: - sg1 - sg2 client: filter: - cg1 - cg2
-
Service-level filters
These filters only apply to the current service. They can be configured as follows:
server: service: - name: xxx filter: - ss1 - ss2 client: service: - name: xxx filter: - cs1 - cs2
Whether a filter is a global-level filter or a service-level filter depends on its configuration. If a filter is configured as both a global and service filter, it will be treated as a global filter.
The configuration and execution order of filters follow the following principles:
-
Filter points need to be paired
To ensure that filters can gracefully exit in case of failure, we require filters at filter points to be paired. The pairs consist of even numbers (N, N+1).
-
Filters are executed in the configured order
In terms of execution order, global filters' pre-filter points are executed in the order specified in the configuration, followed by service filters' pre-filter points. The execution order of post-filter points is the reverse of the pre-filter points, following the "First In Last Out" principle.
-
Filters support early exit:
If a filter fails at the pre-filter point, it will interrupt the execution of the filter chain, and the subsequent process will only execute the post-filter points of the filters that have already been executed (excluding the failed filter).
For more details, please refer to the filter design principles.
The overall steps are as follows:
Implement the filter -> Register the filter to framework -> Use the filter.
-
Server-side
Inherit from the
MessageServerFilter
class, override theGetFilterPoint
andoperator()
function to select the desired filter points and implement the logic of the filter.#include "trpc/filter/server_filter_base.h" class CustomServerFilter : public ::trpc::MessageServerFilter { public: std::string Name() override { return "custom_server"; } std::vector<::trpc::FilterPoint> GetFilterPoint() override { std::vector<::trpc::FilterPoint> points = {::trpc::FilterPoint::SERVER_PRE_RPC_INVOKE, ::trpc::FilterPoint::SERVER_POST_RPC_INVOKE}; return points; } void operator()(::trpc::FilterStatus& status, ::trpc::FilterPoint point, const ::trpc::ServerContextPtr& context) { // do something } };
-
Client-side
Inherit from the
MessageClientFilter
class, override theGetFilterPoint
andoperator()
function to select the desired filter points and implement the logic of the filter.#include "trpc/filter/client_filter_base.h" class CustomClientFilter : public ::trpc::MessageClientFilter { public: std::string Name() override { return "custom_client"; } std::vector<::trpc::FilterPoint> GetFilterPoint() override { std::vector<::trpc::FilterPoint> points = {::trpc::FilterPoint::CLIENT_PRE_RPC_INVOKE, ::trpc::FilterPoint::CLIENT_POST_RPC_INVOKE}; return points; } void operator()(::trpc::FilterStatus& status, ::trpc::FilterPoint point, const ::trpc::ClientContextPtr& context) { // do something } };
According to the usage pattern of the framework, there are the following two scenarios:
-
Program as a server
In this case, the program is implemented as a server by inherit
trpc::TrpcApp
of framework. To register filters in this scenario, you need to override thetrpc::TrpcApp::RegisterPlugins
interface and place the filter registration logic inside it.#include "trpc/common/trpc_plugin.h" int xxxServer::RegisterPlugins() { // register server-side filter auto server_filter = std::make_shared<CustomServerFilter>(); TrpcPlugin::GetInstance()->RegisterServerFilter(server_filter); // register client-side filter auto client_filter = std::make_shared<CustomClientFilter>(); TrpcPlugin::GetInstance()->RegisterClientFilter(client_filter); }
-
Program as a pure client
In this case, the program only needs to use the client and no need to inherit
trpc::TrpcApp
of framework. To register filters in this scenario, you need to place the filter registration logic beforetrpc::RunInTrpcRuntime
is called.#include "trpc/common/trpc_plugin.h" int main() { ... auto client_filter = std::make_shared<CustomClientFilter>(); TrpcPlugin::GetInstance()->RegisterClientFilter(client_filter); return ::trpc::RunInTrpcRuntime([]() { // do something }); }
Filter can be used by specifying them either through configuration files or code.
Using configuration files provides flexibility as adjustments can be made without modifying the code. The usage can be found in the section Filter types and execution order.
Alternatively, filters can be specified through code in the following scenarios:
-
When obtaining a
ServiceProxy
in the client proxy scenario usingGetProxy
, by specifying the desired service filters using theservice_filters
field inServiceProxyOption
. -
When starting a service in the server service scenario using
StartService
, by specifying the service filters using theservice_filters
field inServiceConfig
.
Note: Global-level filters can only be specified through configuration files.
By default, a filter object is shared among multiple ServiceProxy instances or multiple services. This can make programming inconvenient in some scenarios and also impact performance. For example, in a scenario where rate limiting is based on the calling behavior of a service, using a shared filter would require storing the service names and their calling behavior in a map structure, along with the need for locking when accessing the map. Therefore, one solution is to allow each ServiceProxy or service to have its own independent filter object. This eliminates the need for a map structure and locking, simplifying programming and improving performance.
To achieve this, the framework provides a corresponding solution. Users can override the Create function of the Filter base class to make each service have its own independent filter object.
virtual MessageClientFilterPtr Create(const std::any& param) { return nullptr; }
When the Create function returns a non-null result, it will be used as the filter object for the service. Otherwise, the shared filter will be used. Additionally, it is also possible to add independent configurations to the newly created filter in this scenario.
As an example of client proxy usage, the approach is as follows:
MyFilterConfig config;
trpc::ServiceProxyOption option;
...
option.service_filter_configs["my_filter"] = config; // "my_filter" is the filter name
In this way, when the Create
method of the filter is called, the associated filter configuration will be passed as a parameter to the Create
method. Users can then use std::any_cast
in the Create
method to retrieve the corresponding configuration.
trpc::MessageClientFilterPtr MyFilter::Create(const std::any& param) {
MyFilterConfig config;
if (param.has_value()) {
config = std::any_cast<MyFilterConfig>(param);
}
return std::make_shared<MyFilter>(config);
}