Handler implementation

Use this section as a guide for developing new rpc methods inside Traffic Server and how to expose them through the JSONRPC 2.0 endpoint. Before we start, it is worth mentioning some of the architecture of the current implementation. The whole RPC mechanism is divided in few components.

Json RPC manager

This class is the entrance point for both server calls and registered functions.

../../_images/JsonRPCManager.svg

Dispatcher class

  • Class that keeps track of all the registered methods and notifications that can be invoked by the RPC server. This class holds two hash tables containing methods and notifications which use the method name as a key.

  • This class internally consumes RPCRequestInfo objects and performs the invocation of the respective calls.

  • This class handlers the responses from the registered callbacks and it fills the appropriated RPCResponseInfo which then is passed back to the JsonRPCManager class.

JsonRPCManager class

  • Singleton class that handles the JSONRPC handler registration and JSONRPC handler invocation.

  • This class is the main entrance point for the RPC server through the handle_call(std::string_view req) function.

  • This class is the main entrance point for the handler to be able to register in the RPC logic. add_notification_handler and remove_notification_handler.

Implementing new handlers

There a a few basic concepts that needs to be known before implementing a new handler, this is an easy process and the complexity depends on the nature of the handler that you want to implement. Dealing with incoming and outgoing parameters is up to the developer, we will touch on some ways to deal with this through this guide.

Design

As requirement from the JsonRPCManager in order to be able to register inside the RPC management a function should implement the following signature:

Methods:

ts::Rv<YAML::Node> your_rpc_handler_function_name(std::string_view const &id, YAML::Node const &params);

// Plugins:
void (*TSRPCMethodCb)(const char *id, TSYaml params);

Notifications:

void your_rpc_handler_function_name(YAML::Node const &params);

// Plugins:
void (*TSRPCNotificationCb)(TSYaml params);
  • Incoming method request’s id will be passed to the handler, this is read only value as the server is expected to respond with the same value.

  • YAML::Node params is expected to be a Sequence or a Map, as per protocol this cannot be a single value, so do not expect things like: param=123 or param=SomeString.

  • The params can be empty and contains no data at all.

Note

Plugins should cast TSYaml to a YAML::Node. Regarding the yamlcpp library we will provide binary compatibility within the lifespan of a major release only.

It is important to know that method handlers are expected to respond to the requests, while notifications` will not respond with any data nor error. You can find more information in JSONRPC 2.0 Protocol or directly in the protocol specs JSONRPC.

Note

If there is no explicit response from the method, the protocol implementation will respond with success_response unless an error was specified.

Registration and Handling

JSONRPC Manager API

Handler registration should be done by using the JsonRPCManager singleton object. Note that there are a set of convenient helper functions that can be used to achieve registration through the singleton object.

namespace rpc {
    // this set of functions will call the singleton object and perform the same as by using the singleton directly.
    add_method_handler(...)
    add_notification_handler(...)
}
// Handler implementation
ts::Rv<YAML::Node>
my_handler_impl(std::string_view const &id, YAML::Node const &params)
{
    using namespace rpc::handlers::errors;
    return make_errata(Codes::SERVER, "Something happened in the server");
}

The actual registration:

#include "rpc/jsonrpc/JsonRPC.h"
...
rpc::add_method_handler("my_handler_impl", &my_handler_impl);

This API also accepts a RPCRegistryInfo pointer which will provide a context data for the particular handler, for instance it will display the provider’s name when the service descriptor gets called. There is a global object created for this purpose which can be used As a generic registry context object, core_ats_rpc_service_provider_handle is defined in the JsonRPC.h header. Please check get_service_descriptor for more information.

Notification example:

As mentioned before, notifications do not need to respond, as they are “fire and forget” calls, no id will be provided as part of the api.

void
my_notification_handler(YAML::Node const &params)
{
    // do something
    // all errors will be ignored by the server.
}

Registration for notifications uses a different API:

#include "rpc/jsonrpc/JsonRPC.h"
rpc::add_notification_handler("my_notification_handler", &my_notification_handler);

The registration API allows the client to pass a RPCRegistryInfo which provides extra information for a particular handler. Non plugins handlers should use the default provided Registry Info, located in the JsonRPC.h header file, core_ats_rpc_service_provider_handle. Plugins should use the one created by TSRPCRegister

Plugin API

Plugins have a different API to register and handle RPC. Unlike registering and handling rpc using the JSONRPC Manager directly, this API provides a different approach, it’s understood that plugins may not be able to respond a rpc with a valid response at the very same moment that they are called, this could be because the rpc needs to perform an intensive operation or because the data that should be returned by the handler is not yet ready, in any case plugin have the flexibility to say when they finished processing the request, either with a successful response or with an error. The JSONRPC manager will wait for the plugin to “mark” the current call as done.

RPC method registration and implementation examples

  1. No reschedule work on the rpc handler, we set the response in the same call. This runs on the RPC thread.

    #include <ts/ts.h>
    
    namespace {
        static const std::string MY_YAML_VERSION{"0.6.3"};
    }
    
    void
    my_join_string_handler(const char *id, TSYaml p)
    {
        YAML::Node params = *(YAML::Node *)p;
        // extract the strings from the params node
        std::vector<std::string> passedStrings;
        if (auto node = params["strings"]) {
            passedStrings = node.as<std::vector<std::string>>();
        } else {
            // We can't continue, let the JSONRPC Manager know that we have finished
            TSRPCHandlerError(NO_STRINGS_ERROR_CODE, " no strings field passed");
            return;
        }
    
        std::string join;
        std::for_each(std::begin(passedStrings), std::end(passedStrings), [&join](auto &&s) { join += s; });
        YAML::Node resp; // start building the response.
        resp["join"] = join; // add the join string into a "join" field.
        TSRPCHandlerDone(reinterpret_cast<TSYaml>(&resp)); // Let the JSONRPC Manager know that we have finished
    }
    
    void
    TSPluginInit(int argc, const char *argv[])
    {
        ...
        // Check-in to make sure we are compliant with the YAML version in TS.
        TSRPCProviderHandle rpcRegistrationInfo = TSRPCRegister("My plugin's info", "0.6.3");
        if (rpcRegistrationInfo == nullptr) {
            TSError("[%s] RPC handler registration failed, yaml version not supported.", PLUGIN_NAME);
        }
    
        if (TSRPCRegisterMethodHandler("join_strings", my_join_string_handler, rpcRegistrationInfo) == TS_ERROR) {
            TSDebug(PLUGIN_NAME, "%s failed to register", rpcCallName.c_str());
        } else {
            TSDebug(PLUGIN_NAME, "%s successfully registered", rpcCallName.c_str());
        }
    }
    
  2. RPC handling rescheduled to run on a ET TASK thread.

    #include <ts/ts.h>
    
    namespace {
        static const std::string MY_YAML_VERSION{"0.6.3"};
    }
    
    int
    merge_yaml_file_on_et_task(TSCont contp, TSEvent event, void *data)
    {
        // read the incoming node and merge it with the copy on disk.
        YAML::Node params = *static_cast<YAML::Node *>(TSContDataGet(contp));
    
        // we only care for a map type {}
        if (params.Type() != YAML::NodeType::Map) {
            TSRPCHandlerError(INVALID_PARAM_TYPE_CODE, "Handler is expecting a map.");
            return TS_SUCCESS;
        }
    
        // do the actual work.
        merge_yaml(params);
        // ...
        YAML::Node resp;
        TSContDestroy(contp);
        TSRPCHandlerDone(reinterpret_cast<TSYaml>(&resp));
    }
    
    void
    merge_yaml_file(const char *id, TSYaml p)
    {
        TSCont c = TSContCreate(merge_yaml_file_on_et_task, TSMutexCreate());
        TSContDataSet(c, p);
        TSContScheduleOnPool(c, 3000 /* no particular reason */, TS_THREAD_POOL_TASK);
    }
    
    void
    TSPluginInit(int argc, const char *argv[])
    {
        // ...
        // Check-in to make sure we are compliant with the YAML version in TS.
        TSRPCProviderHandle rpcRegistrationInfo = TSRPCRegister("My plugin's info", "0.6.3");
        if (rpcRegistrationInfo == nullptr) {
            TSError("[%s] RPC handler registration failed, yaml version not supported.", PLUGIN_NAME);
        }
    
        if (TSRPCRegisterMethodHandler("merge_yaml_file", merge_yaml_file, rpcRegistrationInfo) == TS_ERROR) {
            TSDebug(PLUGIN_NAME, "%s failed to register", rpcCallName.c_str());
        } else {
            TSDebug(PLUGIN_NAME, "%s successfully registered", rpcCallName.c_str());
        }
    }
    

Error Handling

JSONRPC Manager API

There are several ways to deal with internal handler errors. Errors are expected to be sent back to the client if the API was expressed that way and if the request was a method. We have defined some generic errors that can be used to respond depending on the nature of the registered handler, please check API Handler error codes for more info.

We recommend some ways to deal with this:

  1. Using the Errata from ts::Rv<YAML::Node>

This can be set in case you would like to let the server to respond with an JSONRPC 2.0 error, ExecutionError will be used to catch all the errors that are fired from within the function call, either by setting the proper errata or by throwing an exception. Please check the rpc-error-code and in particular ExecutionError = 9. Also check API Handler error codes

Important

Errors have preference over any other response, so if you set both, the errata and the YAML::Node response, then the former will be set in the response.

  1. Defining a custom error object and make it part of the response object.

  • This is up to the developer and the errors can be part of the response YAML::Node.

  • The JSONRPC Dispatcher will read that there is no error returned from the call and use the result to build the response. If this is what you are willing to respond to, then make sure that the error is not set in the ts::Rv.

  1. Exception.

    As long as the exception inherit from std::exception it will be handled by the jsonrpc manager, this error will be handled as like using the Errata object, this kind of errors will be part of the ExecutionError.

    The following example will generate this JSON response:

    ts::Rv<YAML::Node>
    foo(std::string_view const &id, YAML::Node const &params)
    {
       some_unhandled_operation_that_throws();
    }
    

Examples:

  • Respond with an error, no result field will be set in the response.

    ts::Rv<YAML::Node>
    respond_with_an_error(std::string_view const &id, YAML::Node const &params)
    {
        using namespace rpc::handlers::errors;
        return make_errata(Codes::SERVER, "Something happened in the server");
    }
    

    Server’s response.

    {
        "jsonrpc":"2.0",
        "error":{
            "code":9,
            "message":"Error during execution",
            "data":[
                {
                    "code":3000,
                    "message":"Something happened in the server"
                }
            ]
        },
        "id":"abcd-id"
    }
    

    Note

    make_errata hides some internal details when creating an errata.

  • Response with custom handler error. In this case, make sure that the API definition and documentation reflects this as so far we do not have json schemas to enforce any of this on the client side.

      ts::Rv<YAML::Node>
      respond_with_my_own_error(std::string_view const &id, YAML::Node const &params)
      {
          YAML::Node resp;
          resp["HandlerErrorDescription"] = "I can set up my own error in the result field.";
          return resp;
      }
    
    The "error" is part of the ``result``, in this case this could be used as any other field, the example would be the same.
    
    {
      "jsonrpc":"2.0",
      "result":{
          "HandlerErrorDescription":"I can set up my own error in the result field."
      },
      "id":"abcd-id"
    }
    

We have selected the ts::Rv<YAML::Node> as a message interface as this can hold the actual response/error.

Plugin API

When implementing rpc handlers inside a plugin, errors should be handled differently:

#include <ts/ts.h>

...

void
foo_handler(const char *id, TSYaml p)
{
    // read the incoming node and merge it with the copy on disk.
    YAML::Node params = *static_cast<YAML::Node *>(TSContDataGet(contp));

    if (check_if_error(params)) {
        TSRPCHandlerError(FOO_ERROR_CODE, "Some error descr.");
        return;
    }

    ...
}

Note

Errors can be a part of the request response(result field), this depends on the API design. The above example shows how to set an error that will be sent back as part of the Error field.

For more information check the TSRPCRegister for API details and Errors.

Unit test

All new methods exposed through the RPC server can be tested using the jsonrpc autest extension.

jsonrpc_client.test.text

This extension provides the ability to interact with the JSONRPC interface by using traffic_ctl as a client. As a helper for all new autest that needs to write and read jsonrpc message, there is also a new module jsonrpc.py which provides a nice and easy interface to write methods and notifications. This extension also provides the facility to write custom jsonrpc validations. Please check some of the following examples:

  1. Write custom jsonrpc message

    '''
    The newly added jsonrpc method was named 'foo_bar' and is expected to accept a list of fqdn.
    '''
    tr = Test.AddTestRun("Test JSONRPC foo_bar()")
    '''
    The following call to the Request object will generate this:
    {
        "id": "850d32a8-d5a7-11eb-bebc-fa163e6d2ec5",
        "jsonrpc": "2.0",
        "method": "foo_bar",
        "params": {
            "fqdn": ["yahoo.com", "trafficserver.org"]
        }
    }
    '''
    req = Request.foo_bar(fqdn=["yahoo.com", "trafficserver.org"])
    tr.AddJsonRPCClientRequest(ts, req)
    
  2. Custom response validation

    tr = Test.AddTestRun("Test update_host_status")
    
    Params = [
        {'name': 'yahoo', 'status': 'up'}
    ]
    
    tr.AddJsonRPCClientRequest(ts, Request.update_host_status(hosts=Params))
    
    def check_no_error_on_response(resp: Response):
        # we only check if it's an error.
        if resp.is_error():
            return (False, resp.error_as_str())
        return (True, "All good")
    
    tr.Processes.Default.Streams.stdout = Testers.CustomJSONRPCResponse(check_no_error_on_response)
    

CustomJSONRPCResponse

A tester class that will let you write your own response validation by dealing with the jsonrpc.Response object, please check the CustomJSONRPCResponse tester for more information.

AddJsonRPCClientRequest

This function will generate a json response as an output, internally it ses traffic_ctl file --format json as client. The output can be used and compared with a gold file. This also provides schema validation for the entire JSONRPC protocol as well as the param field against a specific schema file. You can specify schema_file_name with a valid json schema file to validate the entire JSONRPC 2.0 request(except the content of the params field). You can also set params_field_schema_file_name with a valid json schema file to validate only the params field.

Example:

The following, beside sending the request, will perform a JSONRPC 2.0 schema validation as well as the param field.

schema_file_name = 'schemas/jsonrpc20_request_schema.json'
params_schema_file_name = 'schemas/join_strings_request_params_schema.json'
tr.AddJsonRPCClientRequest(
    ts,
    file="join_strings_request.json",
    schema_file_name=schema_file_name,
    params_field_schema_file_name=params_schema_file_name)

JSONRPCResponseSchemaValidator

A tester class to perform a schema validation of the entire JSONRPC 2.0 request as well as the result param. You can specify schema_file_name with a valid json schema file to validate the entire JSONRPC 2.0 response(except the content of the result field). Also you can set result_field_schema_file_name with a valid json schema file to validate only the result field.

Example:

The following will add a Tester for a JSONRPC 2.0 schema validation as well as the result field.

tr = Test.AddTestRun('test valid response schema')
schema_file_name = 'schemas/jsonrpc20_response_schema.json'
result_schema_file_name = 'schemas/join_strings_request_result_schema.json'

# Add a tester.
tr.Processes.Default.Streams.stdout = Testers.JSONRPCResponseSchemaValidator(
    schema_file_name=response_schema_file_name,
    result_field_schema_file_name=result_schema_file_name)

Important Notes

  • You can refer to YAML for more info in how code/decode yaml content.

  • Remember to update API if you are adding a new handler.

  • If a new handler needs to be exposed through traffic_ctl please refer to traffic_ctl for a general idea and to Traffic Control Development Guide for how to implement a new command.

  • To interact directly with the JSONRPC 2.0 node, please check JSONRPC Endpoint

Configuration JSONRPC 2.0 Protocol Traffic Control Development Guide