TSCfgRegister

Register a plugin’s configuration file with the Traffic Server reload framework and drive the per-reload context object delivered to the handler.

Synopsis

#include <ts/ts.h>
enum TSCfgSourceType

What content sources a plugin’s config handler supports.

enumerator TS_CFG_SOURCE_FILE_ONLY

Handler reloads only from a file on disk. RPC-supplied YAML is rejected.

enumerator TS_CFG_SOURCE_FILE_AND_RPC

Handler can also process YAML content supplied via RPC.

type TSCfgRegistrationInfo

Options struct passed to TSCfgRegister(). Fields are set with C++20 designated initializers; new fields may be appended in future Traffic Server versions without breaking source compatibility for existing plugins.

std::string_view key

Unique registry key, also used as the YAML node name when an operator reloads via traffic_ctl config reload --data '{<key>: {...}}'. Must be unique across all registered configs (core and plugin).

Convention. Use the plugin name (my_plugin). Keep the key alphanumeric + underscore; avoid dots, since traffic_ctl --directive my_plugin.dry_run=true parses the first . as the directive separator.

Plugins with multiple files:

  • If the files share the same logical config and should reload together, register once and add the rest via TSCfgAddFileDependency() (single task per reload).

  • If the files are genuinely independent and reloadable on their own, register each separately with a disambiguating key (e.g. my_plugin_main, my_plugin_aux). Each becomes its own task entry in traffic_ctl config status.

Required.

std::string_view config_path

Default config file path (absolute, or relative to sysconfdir). Required.

std::string_view filename_record

Fully-qualified record name that holds the active config filename, or {} (default) if the path is fixed. When set, the operator can change the active config file at runtime by editing this record; config_path is used as the fallback when the record is empty or unset. Optional.

TSCfgLoadCb handler

Reload callback invoked when the file changes, when an attached trigger record changes, or when the operator triggers an RPC reload. Required.

void *data

Opaque pointer passed unmodified to handler on every invocation. The plugin owns it; ATS does not copy or free it. Must outlive the registration - typically a static or once-allocated heap object stashed in TSPluginInit:

static PluginState state;
state.config_path = ...;

TSCfgRegistrationInfo info{};
info.handler = config_reload;
info.data    = &state;
TSCfgRegister(&info);
TSCfgSourceType source

Whether handler can also process YAML content supplied via RPC. Defaults to TSCfgSourceType::TS_CFG_SOURCE_FILE_ONLY.

bool is_required

Hint propagated to FileManager: marks the file as “required” in catalog/inspection output and emits a Dbg line if the file is missing at registration time. The framework does not enforce this flag at reload-time today: the handler is still invoked even when the file is missing or invalid, and the plugin chooses whether to fail the reload. Defaults to false.

type TSCfgFileDependencyInfo

Options struct passed to TSCfgAddFileDependency(). Only key and config_path are required.

std::string_view key

Parent registry key as passed to TSCfgRegister(). Required.

std::string_view config_path

Default companion file path (absolute, or relative to sysconfdir). Required.

std::string_view filename_record

Fully-qualified record name that holds the filename, or {} if the path is fixed. Optional.

std::string_view dep_key

Routing key for inline YAML supplied via JSONRPC. When non-empty, content delivered under this top-level node is routed to the parent entry’s handler, giving the plugin parity with core composite-config patterns. When empty (default), the dependency is file-change-only.

bool is_required

Hint propagated to FileManager (catalog/inspection only); see the equivalent field on TSCfgRegistrationInfo. Defaults to false.

type TSCfgLoadCtx

Opaque handle to a per-reload context. The context tracks the lifecycle of a single reload attempt for one registered config: it carries the reload token, supplied YAML (for RPC reloads), the filename (for file reloads), and the task state machine the framework uses to aggregate results.

typedef void (*TSCfgLoadCb)(TSCfgLoadCtx ctx, void *data)

Plugin reload callback signature. data is the opaque pointer the plugin supplied via TSCfgRegistrationInfo::data.

enum TSCfgLogLevel

Severity for log entries emitted via TSCfgLoadCtxAddLog().

enumerator TS_CFG_LOG_NOTE

Informational. Default level. Maps to DL_Note internally.

enumerator TS_CFG_LOG_WARNING

Concerning; the reload may still complete. Maps to DL_Warning.

enumerator TS_CFG_LOG_ERROR

Failure cause; pair with a subsequent TSCfgLoadCtxFail(). Maps to DL_Error.

The framework deliberately does not expose the fatal-class diagnostics levels here: a reload handler should not be able to terminate the process. Plugins that truly need debug-only output should use Dbg(ctl, ...) instead of this API.

TSReturnCode TSCfgRegister(const TSCfgRegistrationInfo *info)
bool TSCfgIsRegistered(std::string_view key)
TSReturnCode TSCfgAttachReloadTrigger(std::string_view key, std::string_view record_name)
TSReturnCode TSCfgAddFileDependency(const TSCfgFileDependencyInfo *info)
void TSCfgLoadCtxInProgress(TSCfgLoadCtx ctx, std::string_view msg)
void TSCfgLoadCtxComplete(TSCfgLoadCtx ctx, std::string_view msg)
void TSCfgLoadCtxFail(TSCfgLoadCtx ctx, std::string_view msg)
void TSCfgLoadCtxAddLog(TSCfgLoadCtx ctx, TSCfgLogLevel level, std::string_view msg)
TSCfgLoadCtx TSCfgLoadCtxAddSubtask(TSCfgLoadCtx ctx, std::string_view description)
std::string_view TSCfgLoadCtxGetFilename(TSCfgLoadCtx ctx)
std::string_view TSCfgLoadCtxGetReloadToken(TSCfgLoadCtx ctx)
TSYaml TSCfgLoadCtxGetSuppliedYaml(TSCfgLoadCtx ctx)
TSYaml TSCfgLoadCtxGetReloadDirectives(TSCfgLoadCtx ctx)

Description

These functions are the plugin-facing entry points to the Traffic Server configuration reload framework. A plugin registers a config file together with a reload callback, and the framework drives the callback whenever the file changes, an attached trigger record changes, or an operator triggers a reload via JSONRPC.

The plugin name (set via TSPluginRegister()) is automatically attached to every registered entry, so reload-trace logs and traffic_ctl config status output identify which plugin owns which entry as [plugin: <name>]. Plugins do not pass their name explicitly.

Registration

TSCfgRegister()

Registers a config file and reload handler.

Must be called from TSPluginInit(), after TSPluginRegister(). Returns TS_ERROR if called outside TSPluginInit, before TSPluginRegister, or with a null or incomplete info struct.

If another plugin (or core) has already registered the same key, the duplicate registration is rejected, a warning is logged identifying both owners, and TS_ERROR is returned. The colliding registration is not installed, so the handler would never run; the plugin can detect this and log or bail out. Plugins that may be loaded more than once can probe first with TSCfgIsRegistered() to avoid the duplicate attempt entirely.

TSCfgIsRegistered()

Returns true if key is already registered (by any plugin or core), false otherwise (including for an empty key). Plugins that may be loaded more than once - e.g. by an admin retry, or as a remap plugin instance - can call this before TSCfgRegister() to skip a duplicate registration that would otherwise be rejected.

TSCfgAttachReloadTrigger()

Wires a record so that changing its value re-runs the reload handler registered for key. Internally registers a record-change callback (RecRegisterConfigUpdateCb) and routes the event back into the same reload pipeline as file changes: the plugin’s TSCfgLoadCb is invoked with the resolved file path available via TSCfgLoadCtxGetFilename(), exactly as it would be for an on-disk file change. May be called multiple times to attach more than one trigger record to the same entry.

Core analog. ConfigRegistry::register_config() accepts a trigger_records initializer-list at registration time (e.g. ssl_multicert lists ~10 record names there). The post-registration form is ConfigRegistry::attach(). This function is the plugin-facing wrapper for the latter; the initializer-list shape is intentionally not exposed on TSCfgRegistrationInfo so the option struct stays ABI-stable.

It triggers a reload, nothing else. Specifically the plugin cannot:

  • Register a free-form record-change callback. There is no TSRecordRegisterChangeCb today; the underlying primitive (RecRegisterConfigUpdateCb) is internal-only.

  • Receive record-change details. The handler gets no record name, no old/new value, no event payload. Use TSMgmt*Get() inside the handler if it needs the value.

  • Subscribe a record without a registered config key. Calling with an unknown key returns TS_ERROR.

  • Multiplex one record across two keys, or attach in any shape other than (one record, one config key, per call).

The reload is always treated as file-driven (no RPC payload): TSCfgLoadCtxGetSuppliedYaml() and TSCfgLoadCtxGetReloadDirectives() return nullptr for record-triggered invocations. Standalone record changes (i.e. traffic_ctl config set outside an active reload cycle) invoke the handler with an empty context that does not surface in traffic_ctl config status - same as core record-triggered reloads outside a reload cycle.

TSCfgAddFileDependency()

Adds a companion file to a previously registered key. When the companion file changes on disk - or when an operator submits inline YAML under TSCfgFileDependencyInfo::dep_key via JSONRPC, when set - the plugin’s handler is invoked. With dep_key empty the dependency is file-change-only.

Per-reload context (TSCfgLoadCtx)

A TSCfgLoadCtx is created by the framework before invoking the plugin’s TSCfgLoadCb. The handler must drive the context to a terminal state - either TSCfgLoadCtxComplete() or TSCfgLoadCtxFail() - so the reload tree can finish. See Terminal State Rule for the complete contract.

State transitions:

TSCfgLoadCtxInProgress()

Set the task to IN_PROGRESS and emit an optional progress message. Calling it more than once on the same context is discouraged - use TSCfgLoadCtxAddLog() for periodic log output. Pass {} for msg if no message is needed.

TSCfgLoadCtxComplete()

Mark the task as successfully completed. Pass {} for msg if no message is needed. This call finalizes and invalidates ctx: do not use ctx afterwards. A later call on the same handle (a second Complete/Fail, or any accessor) is ignored and logged as misuse rather than acting on freed state, but plugins must not rely on that as normal control flow.

TSCfgLoadCtxFail()

Mark the task as failed. Pass {} for msg if no message is needed. This call finalizes and invalidates ctx with the same post-finalize semantics as TSCfgLoadCtxComplete().

TSCfgLoadCtxAddLog()

Append a log entry at level to the task. Visible in traffic_ctl config status output and in the get_reload_config_status JSONRPC response. Does not change the task state.

TSCfgLoadCtxAddSubtask()

Create a child subtask. Returns a new context that the plugin must independently drive to a terminal state. Use this when a single config reload spawns several distinct units of work that should be tracked separately. The parent’s status aggregates from its children: any child failing causes the parent to be marked failed.

The subtask is born in CREATED and follows the same state machine as the parent. Two transition patterns are valid:

Inputs:

TSCfgLoadCtxGetFilename()

Returns the path the framework expects this handler to read. Two-step resolution:

  1. If the registration’s filename_record was set AND the record currently has a non-empty value, that value is returned (the operator can override the filename at runtime via traffic_ctl config set <record>).

  2. Otherwise, returns config_path as-registered.

Most plugins don’t set filename_record and could equivalently use their own stashed copy of the registered path - this function is the canonical way to get the filename only when filename_record is in play.

Always populated for plugin handlers, including on RPC reloads. To detect RPC content, check TSCfgLoadCtxGetSuppliedYaml() - not this function.

Core analog. ConfigReloadTask::get_filename(). For core handlers, the value is empty on RPC reloads; the plugin wrapper always populates it so plugins can use TSCfgLoadCtxGetSuppliedYaml() as the canonical RPC-detection signal.

TSCfgLoadCtxGetReloadToken()

Returns the reload-cycle correlation token (e.g. rldtk-<timestamp>). Useful for plugin-side log lines that need to match the operator’s traffic_ctl config status -t <token> view.

TSCfgLoadCtxGetSuppliedYaml()

Returns the YAML content supplied via the JSONRPC reload payload as an opaque TSYaml handle (a YAML::Node *). The framework-reserved _reload directives are stripped before delivery. The returned node is undefined when the reload was driven by file change rather than RPC. Only meaningful when the entry was registered with TSCfgSourceType::TS_CFG_SOURCE_FILE_AND_RPC.

TSCfgLoadCtxGetReloadDirectives()

Returns the YAML map extracted from the _reload key in the RPC-supplied content as an opaque TSYaml handle. Used by handlers that need scoped reload behavior (for example, reload only one entry, dry-run mode, or a version constraint). See Configuration Reload Framework for the directive convention.

The two std::string_view accessors return views whose backing storage is owned by the framework and is valid for the lifetime of the handler call. Copy the data if you need to retain it past the callback return. The TSYaml accessors return handles whose lifetime is also tied to the handler call.

Lifecycle and threading

The handler runs on an ET_TASK thread. It does not have to complete synchronously: the plugin may stash ctx in its own state, return from the callback, finish work on a different thread, and then call TSCfgLoadCtxComplete() or TSCfgLoadCtxFail() from there. This is the standard pattern for plugins that perform asynchronous I/O during reload.

If the handler never reaches a terminal state, the per-reload progress checker eventually marks the task as TIMEOUT and unblocks the overall reload (see Terminal State Rule). The context handle then leaks for the process lifetime, so reaching a terminal state on every code path is mandatory.

Restrictions

Example - registration and synchronous handler

#include <ts/ts.h>
#include <string>

namespace
{
constexpr char PLUGIN_NAME[] = "my_plugin";

struct PluginState {
  std::string config_path;
};

void
config_reload(TSCfgLoadCtx ctx, void *data)
{
  auto *state = static_cast<PluginState *>(data);

  std::string_view filename = TSCfgLoadCtxGetFilename(ctx);
  if (filename.empty()) {
    TSCfgLoadCtxFail(ctx, "no filename available");
    return;
  }

  if (!parse_and_apply(state, std::string{filename})) {
    TSCfgLoadCtxFail(ctx, "parse failed");
    return;
  }

  TSCfgLoadCtxComplete(ctx, "Reloaded my_plugin");
}
} // anonymous namespace

void
TSPluginInit(int /* argc */, const char * /* argv */[])
{
  TSPluginRegistrationInfo plugin{};
  plugin.plugin_name   = PLUGIN_NAME;
  plugin.vendor_name   = "Example Inc.";
  plugin.support_email = "support@example.com";

  if (TSPluginRegister(&plugin) != TS_SUCCESS) {
    TSError("[%s] plugin registration failed", PLUGIN_NAME);
    return;
  }

  static PluginState state;
  state.config_path = std::string{TSConfigDirGet()} + "/my_plugin.yaml";

  TSCfgRegistrationInfo info{};
  info.key         = PLUGIN_NAME;
  info.config_path = state.config_path;
  info.handler     = config_reload;
  info.data        = &state;
  info.source      = TS_CFG_SOURCE_FILE_AND_RPC;
  info.is_required = false;
  if (TSCfgRegister(&info) != TS_SUCCESS) {
    TSError("[%s] TSCfgRegister failed for '%s'", PLUGIN_NAME,
            state.config_path.c_str());
    return;
  }

  // Optional: attach a record so changing it fires the handler.
  TSCfgAttachReloadTrigger(PLUGIN_NAME, "proxy.config.my_plugin.enabled");
}

Example - RPC content with directives

#include <yaml-cpp/yaml.h>

void
config_reload(TSCfgLoadCtx ctx, void * /* data */)
{
  auto *yaml = static_cast<YAML::Node *>(TSCfgLoadCtxGetSuppliedYaml(ctx));
  auto *dirs = static_cast<YAML::Node *>(TSCfgLoadCtxGetReloadDirectives(ctx));

  if (dirs && dirs->IsDefined()) {
    // operator passed --directive ...
  }
  if (yaml && yaml->IsDefined()) {
    // RPC-supplied content; do not read the file.
  } else {
    // file-driven reload; read TSCfgLoadCtxGetFilename(ctx).
  }
  TSCfgLoadCtxComplete(ctx, {});
}

Example - deferred completion

A plugin that schedules background work returns from the callback without calling Complete or Fail; it stashes ctx and finishes from another thread:

struct Work { TSCfgLoadCtx ctx; PluginState *state; };

void
config_reload(TSCfgLoadCtx ctx, void *data)
{
  auto *work = new Work{ctx, static_cast<PluginState *>(data)};
  // schedule heavy_work(work) on a worker thread; it will eventually
  // call TSCfgLoadCtxComplete(work->ctx, {}) (or Fail) and delete work.
  schedule_async(work);
}

A runnable example of the deferred pattern lives in tests/gold_tests/jsonrpc/plugins/cfg_plugin_deferred_test.cc.

See Also

Configuration Reload Framework is the complete framework guide, covering the registry, terminal-state rule, RPC reload payload format, [plugin: <name>] attribution in traffic_ctl config status, diagnostic macros, and diags.log summaries.