Directives¶
Directives are implemented by subclassing Directive. The general convention is to prefix the directive name with “Do_” to form a class name.
Example¶
note¶
Consider a directive to log a “NOTE” in “diag.logs”. Fundamentally this can be done by calling the Traffic Server utility method ts::Log_Note and passing a string view. The directive is simply a wrapper around this, to extact the view and then log it.
By convention the class is named Do_note
and the declaration is
class Do_note : public Directive
{
using self_type = Do_note; ///< Self reference type.
using super_type = Directive; ///< Super type.
public:
static inline const std::string KEY{"note"}; ///< Name of directive.
/// Valid hooks for this directive.
static inline const HookMask HOOKS{MaskFor(Hook::POST_LOAD, Hook::TXN_START, Hook::CREQ, Hook::PREQ, Hook::URSP, Hook::PRSP,
Hook::PRE_REMAP, Hook::POST_REMAP, Hook::REMAP)};
/// Runtime invocation.
Errata invoke(Context &ctx) override;
/// Load from configuration.
static Rv<Handle> load(Config &cfg, CfgStaticData const *, YAML::Node drtv_node, swoc::TextView const &name,
swoc::TextView const &arg, YAML::Node key_value);
protected:
Expr _msg; ///< Message to log.
/** Constructor.
*
* @param msg Parsed feature expression for message.
*/
Do_note(Expr &&msg);
};
Each directive has a key that is the name of the directive, it is best to declare this as a
std::string
.
using super_type = Directive; ///< Super type.
public:
static inline const std::string KEY{"note"}; ///< Name of directive.
/// Valid hooks for this directive.
static inline const HookMask HOOKS{MaskFor(Hook::POST_LOAD, Hook::TXN_START, Hook::CREQ, Hook::PREQ, Hook::URSP, Hook::PRSP,
Hook::PRE_REMAP, Hook::POST_REMAP, Hook::REMAP)};
inline
enables the member to be initialized in the class declaration. In this case the
directive is usable on any hook and so all of them are listed. The class HookMask is used to
hold a bit mask of the valid hooks. The utility function MaskFor can be used to create a bit
mask from hook enumeration values.
using super_type = Directive; ///< Super type.
public:
static inline const std::string KEY{"note"}; ///< Name of directive.
/// Valid hooks for this directive.
static inline const HookMask HOOKS{MaskFor(Hook::POST_LOAD, Hook::TXN_START, Hook::CREQ, Hook::PREQ, Hook::URSP, Hook::PRSP,
Hook::PRE_REMAP, Hook::POST_REMAP, Hook::REMAP)};
The first requirement of the directive implementation is loading from the configuration to create an instance of the directive class. The framework, when expecting a directive, checks if the node is an object. If so, the keys in the object are checked for being a directive by matching the key name with a table of directive names. If there is a match, the corresponding load functor is invoked. Multiple arguments are passed to the functor.
Config& cfg
A reference to the configuration object, which contains the configuration loading context. See Config.
CfgStaticData
Every registered directive gets a config level block of information. This is a reference to that for the configuration being loaded. See CfgStaticData.
YAML::Node const& drtv_node
The directive map (YAML node). This is the YAML map that contains the directive key.
TextView const& name
The name of the directive. This is the same as the value in the directive table. In most cases it is irrelevant. In the cases of a group of similar directives, a single load functor could load all of them, distinguishing the exact case with this value.
TextView const& arg
A directive key can have an argument, which is additional text attached to the directive name with a period separator. Although implementationally arbitrary, the convention is the argument (if any) should be used to select the target of the directive if the value can’t be known at compile time. Data used to perform the directive should be in the value of the directive key.
YAML::Node const& key_value
The value of the directive key. Note this can be any type of YAML data, including nested objects. This is not processed beyond being validated as valid YAML.
It is the responsibility of the load functor to do any further processing of the key_value and construct an instance of the directive class using that information. If the functor is implemented as a method it must be a static method as by definition there is no directive instance yet.
Here is an example of using this directive.
do:
- note: "Something's happen here"
The “do” key contains a list of directives, each of which is an object. The first such object is the
note
object. It will be invoked with drtv_node being the object in the list for “do”, while
name will be “note” and key_value the string "Something's happening here"
.
Loading implementation is straight forward. The value is expected to be a feature expression and all that needs done is to store it in the instance for use at run time.
Rv<Directive::Handle>
Do_note::load(Config &cfg, CfgStaticData const *, YAML::Node drtv_node, swoc::TextView const &, swoc::TextView const &,
YAML::Node key_value)
{
auto &&[msg_fmt, msg_errata] = cfg.parse_expr(key_value);
if (!msg_errata.is_ok()) {
msg_errata.info(R"(While parsing message at {} for "{}" directive at {}.)", key_value.Mark(), KEY, drtv_node.Mark());
return std::move(msg_errata);
}
return Handle{new self_type{std::move(msg_fmt)}};
}
Config::parse_expr is used to parse the value as a feature expression. If not successful, the lower level error is augmented with context information about the directive and returned. Otherwise the parsed expression is stored in a newly created instance which is then returned.
When used at runtime, the invoke
method is called.
Errata
Do_note::invoke(Context &ctx)
{
TextView msg = ctx.extract_view(_msg);
ts::Log_Note(msg);
return {};
}
ctx is a per transaction context and serves as the root of accessible data structures. In this case the parsed feature expression is passed to Context::extract_view to extract the expression as a string, which is the logged.
redirect¶
A more complex example would be the redirect
directive.
redirect:
location: "http://example.one"
status: 302
body: "Redirecting to {this::location} - please update your links."
For loading from configuration, key-value
is an object, with keys location
, status
,
and body
, which must be handled by the directive implementation. FeatureGroup is a
support class to support directives with multiple keys. It is not required but does provide much of
the boiler plate needed in this situation.
Some of the complexity stems from the fact that while the directive must be invoked on a relatively early hook, the implementation must do some “fix ups” on a later hook and that information must be cached between hooks. A structure is defined to hold this data
struct CtxInfo {
TextView _location; ///< Redirect target location.
TextView _reason; ///< Status text.
};
The significant problem is not storing this information, but finding it later. A pointer cannot be stored in the instance, because there is only one per configuration instance which can be invoked in multiple transactions simultaneously. The solution is to reserve the storage at the configuration level which creates a fixed offset (per configuration) for storage in the per transaction context. This is a (somewhat) expensive resource as such storage is reserved on every transaction even if not used. The context storage reservation can vary between configuration instances (because the set of directives reserving storage can vary) therefore configuration storage must be obtained to store the reservation.
struct CfgInfo {
ReservedSpan _ctx_span; ///< Reserved span for @c CtxInfo.
};
Note the context data is shared among all instances of this directive. This is acceptable because for any specific transaction there can be only one redirection so it being overridden if multiple instances are invoked for a transaction is useful.
Configuration loading tracks how many instances of a directive have been loaded. The first time a directive is encountered, a functor is invoked. By default this is an empty function but it can be replaced per directive. This is used by the “redirect” directive.
Do_redirect::cfg_init(Config &cfg, CfgStaticData const *rtti)
{
auto cfg_info = rtti->_cfg_store.rebind<CfgInfo>().data();
new (cfg_info) CfgInfo; // Initialize the span.
// Only one redirect can be effective per transaction, therefore shared state per context is best.
cfg_info->_ctx_span = cfg.reserve_ctx_storage(sizeof(CtxInfo));
cfg.reserve_slot(FIXUP_HOOK); // needed to fix up "Location" field in proxy response.
return {};
}
This is passed the configuration instance and the per directive static information. This is useful for data that is per directive per configuration. For “redirect” this is for reserving per context storage and reserving a hook slot to use for the fix ups.
When using a feature group, each key becomes associated with an index and those indices are used to access key information. There is a presumption that the value for every key is a feature expression, but additional type checking can be done after loading. The group can be loaded as a single scalar, as a list, or as an object (set of keys). Keys can be marked as required in which case it is an error if the key is not present. After loading, the key indices can be found by name and cached for fast lookup during invocation.