Traffic Control Development Guide

Traffic Control interacts with Traffic Server through the JSONRPC 2.0 endpoint. All interaction is done by following the JSONRPC 2.0 protocol.

Overall structure

../../_images/traffic_ctl-class-diagram.svg
  • The whole point is to separate the command handling from the printing part.

  • Printing should be done by an appropriate Printer implementation, this should support several kinds of printing formats.

  • For now, everything is printing in the standard output, but a particular printer can be implemented in such way that the output could be sent to a different destination.

  • JSONRPC requests have a base class that hides some of the basic and common parts, like id, and version. When deriving from this class, the only thing that needs to be override is the method

Important

CtrlCommand will invoke _invoked_func when executed, this should be set by the derived class

  • The whole design is that the command will execute the _invoked_func once invoked. This function ptr should be set by the appropriated derived class based on the passed parameters. The derived class have the option to override the execute() which is a virtual function and does something else. Check RecordCommand as an example.

Command implementation

  1. Add the right command to the ArgParser object inside the traffic_ctl.cc.

  2. If needed, define a new Command derived class inside the CtrlCommands file. if it’s not an new command level, and it’s a subcommand, then you should check the existing command to decide where to place it.

    • Implement the member function that will be dealing with the particular command, ie: (config_status())

    • If a new JsonRPC Message needs to be done, then implement it by deriving from shared::rpc::ClientRequest if a method is needed, or from shared::rpc::ClientRequestNotification if it’s a notification. More info can be found here Requests and Design. This can be done inside the RPCRequest.h file.

    Note

    Make sure you override the std::string get_method() const member function with the appropriate api method name.

  3. If needed define a new Printer derived class inside the CtrlPrinter file.

    • If pretty printing format will be supported, then make sure you read the _format member you get from the BasePrinter class.

  4. If it’s a new command level (like config, metric, etc), make sure you update the Command creation inside the traffic_ctl.cc file.

Implementation Example

Let’s define a new command for a new specific API with name == admin_new_command_1 with the following json structure:

$ traffic_ctl new-command new-subcommand1
  1. Update traffic_ctl.cc. I will ignore the details as they are trivial.

  2. Define a new Request.

    So based on the above json, we can model our request as:

    // RPCRequests.h
    struct NewCommandJsonRPCRequest : shared::rpc::ClientRequest {
       using super = shared::rpc::ClientRequest;
       struct Params {
          std::string named_var_1;
       };
       NewCommandJsonRPCRequest(Params p)
       {
          super::params = p; // This will invoke the built-in conversion mechanism in the yamlcpp library.
       }
       // Important to override this function, as this is the only way that the "method" field will be set.
       std::string
       get_method() const {
          return "admin_new_command_1";
       }
    };
    
  3. Implement the yamlcpp convert function, Yaml-cpp has a built-in conversion mechanism. You can refer to YAML for more info.

    // yaml_codecs.h
    template <> struct convert<NewCommandJsonRPCRequest::Params> {
       static Node
       encode(NewCommandJsonRPCRequest::Params const &params)
       {
          Node node;
          node["named_var_1"] = params.named_var_1;
          return node;
       }
    };
    
  4. Define a new command. For the sake of simplicity I’ll only implement it in the .h files.

    // CtrlCommands.h & CtrlCommands.cc
    struct NewCommand : public CtrlCommand {
       NewCommand(ts::Arguments args): CtrlCommand(args)
       {
          // we are interested in the format.
          auto fmt = parse_format(_arguments);
          if (args.get("new-subcommand1") {
             // we need to create the right printer.
             _printer = std::make_sharec<MyNewSubcommandPrinter>(fmt);
             // we need to set the _invoked_func that will be called when execute() is called.
             _invoked_func = [&]() { handle_new_subcommand1(); };
          }
          // if more subcommands are needed, then add them here.
       }
    
    private:
       void handle_new_subcommand1() {
          NewCommandJsonRPCRequest req{};
          // fill the req if needed.
          auto response = invoke_rpc(req);
          _printer->write_output(response);
       }
    };
    
  5. Define a new printer to deal with this command. We will assume that the printing will be different for every subcommand. so we will create our own one.

    class MyNewSubcommandPrinter : public BasePrinter
    {
       void write_output(YAML::Node const &result) override {
          // result will contain what's coming back from the server.
       }
    };
    

    In case that the format type is important, then we should allow it by accepting the format being passed in the constructor. And let it set the base one as well.

    MyNewSubcommandPrinter(BasePrinter::Format fmt) : BasePrinter(fmt) {}
    

    The way you print and the destination of the message is up to the developer’s needs, either a terminal or some other place. If the response from the server is a complex object, you can always model the response with your own type and use the built-in yamlcpp mechanism to decode the YAML::Node. write_output(YAML::Node const &result) will only have the result defined in the protocol, check Result for more detail. So something like this can be easily achieved:

    void
    GetHostStatusPrinter::write_output(YAML::Node const &result)
    {
       auto response = result.as<NewCommandJsonRPCRResponse>(); // will invoke the yamlcpp decode.
       // you can now deal with the Record object and not with the yaml node.
    }
    

Notes

There is code that was written in this way by design, RecordPrinter and RecordRequest are meant to be use by any command that needs to query and print records without any major hassle.

Configuration, traffic_ctl, Handler implementation