Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
565 changes: 565 additions & 0 deletions doc/developer-guide/api/functions/TSCfgRegister.en.rst

Large diffs are not rendered by default.

198 changes: 197 additions & 1 deletion doc/developer-guide/config-reload-framework.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

.. include:: ../common.defs

.. default-domain:: cpp

.. _config-reload-framework:

Configuration Reload Framework
Expand Down Expand Up @@ -695,6 +697,198 @@ This means the same handler code works in both cases without branching:
}


.. _config-reload-plugin-api:

Plugin Configuration Reload
===========================

Plugins integrate with the same registry and same task tree described
above, through the public ``TSCfg*`` C++ API in ``ts/ts.h``. The
framework treats plugin-registered configs as first-class entries: they
appear in :option:`traffic_ctl config status`, accept inline YAML via
JSONRPC, honor file-mtime change detection, react to attached trigger
records, and follow the same :ref:`terminal state rule
<config-context-terminal-state>` as core handlers.

What changes for plugins is only the surface API:

- ``ConfigRegistry::register_config`` becomes :func:`TSCfgRegister`,
which takes a :type:`TSCfgRegistrationInfo` options struct.
- ``ConfigRegistry::attach`` becomes :func:`TSCfgAttachReloadTrigger`.
- ``ConfigRegistry::add_file_dependency`` becomes
:func:`TSCfgAddFileDependency`.
- ``ConfigContext`` becomes the opaque ``TSCfgLoadCtx`` handle, with
the same in-progress / complete / fail / log / supplied-yaml /
reload-directives / add-subtask operations exposed as plain
``TSCfgLoadCtx*`` functions.

Lifecycle and preconditions
---------------------------

All ``TSCfg*`` registration calls must be made from :func:`TSPluginInit`,
**after** :func:`TSPluginRegister` has succeeded. The framework reads the
calling plugin's canonical name from ``TSPluginRegister`` and attaches it
to every registered entry; plugins do not pass their name explicitly.
Calling :func:`TSCfgRegister` outside ``TSPluginInit``, before
``TSPluginRegister``, or with a null ``info`` returns ``TS_ERROR``.

The reload framework is global-plugin only. Remap plugins
(:func:`TSRemapInit` / :func:`TSRemapNewInstance`) cannot register
config entries.

Plugin example
--------------

A minimal global plugin that registers ``my_plugin.yaml`` and accepts
either file-driven or RPC-driven reload:

.. code-block:: cpp

#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);

// Optionally: announce that work has started.
TSCfgLoadCtxInProgress(ctx, "Reloading my_plugin");

std::string_view fn = TSCfgLoadCtxGetFilename(ctx);
if (!parse_file(state, std::string{fn})) {
TSCfgLoadCtxFail(ctx, "Failed to parse my_plugin.yaml");
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", PLUGIN_NAME);
return;
}

// Optional: trigger the handler whenever this record changes.
TSCfgAttachReloadTrigger(PLUGIN_NAME, "proxy.config.my_plugin.enabled");
}

The handler obeys the same terminal-state rule as core handlers - every
code path must end in ``TSCfgLoadCtxComplete`` or ``TSCfgLoadCtxFail``.
Deferred completion (return from the callback, finish from another
thread, then call Complete or Fail there) is fully supported; see the
deferred-handler example in :doc:`api/functions/TSCfgRegister.en`.

Plugin name attribution in ``traffic_ctl``
------------------------------------------

Because the plugin's canonical name is attached automatically by the
framework, :option:`traffic_ctl config status` tags every plugin-owned
entry with ``[plugin: <name>]``. After a successful reload of the
example plugin above:

.. code-block:: text

$ traffic_ctl config reload
✔ Reload scheduled [rldtk-1714061200]

Monitor : traffic_ctl config reload -t rldtk-1714061200 -m
Details : traffic_ctl config reload -t rldtk-1714061200 -s -l

$ traffic_ctl config status -t rldtk-1714061200
✔ Reload [success] — rldtk-1714061200
Started : 2026 Apr 25 14:30:12.345
Finished: 2026 Apr 25 14:30:12.349
Duration: 4ms

✔ 1 success ◌ 0 in-progress ✗ 0 failed (1 total)

Tasks:
✔ my_plugin [plugin: my_plugin] ················ 4ms
[Note] Reloading my_plugin
[Note] Reloaded my_plugin

When a plugin registers more than one entry under a single key (or
several plugins each register their own entries), the ``[plugin: ...]``
tag makes ownership unambiguous. Entries owned by core code carry no
``[plugin: ...]`` tag.

The same attribution is exposed under ``meta.plugin_name`` in the
JSONRPC :ref:`get_reload_config_status` response, so automation can
filter, group, or alarm on a per-plugin basis.

Inline RPC reload of a plugin entry uses the registry key as the
top-level YAML node:

.. code-block:: bash

$ traffic_ctl config reload --data '{"my_plugin": {"rules": ["x", "y"]}}'

The handler then calls ``TSCfgLoadCtxGetSuppliedYaml`` to read the
content and ``TSCfgLoadCtxGetReloadDirectives`` for any operator
directives passed via ``--directive``.

Test plugins
------------

The autest suite ships small plugins that exercise the public
``TSCfg*`` surface end-to-end. They are the recommended reference for
how to wire registration, handler logic, and deferred completion:

- ``tests/gold_tests/jsonrpc/plugins/cfg_plugin_test.cc`` - basic
registration and synchronous handler.
- ``tests/gold_tests/jsonrpc/plugins/cfg_plugin_directives_test.cc`` -
reading inline YAML and reload directives.
- ``tests/gold_tests/jsonrpc/plugins/cfg_plugin_deferred_test.cc`` -
asynchronous / deferred completion pattern.

The matching autests
(``config_reload_plugin_api.test.py`` and friends in
``tests/gold_tests/jsonrpc/``) demonstrate driving these plugins via
:program:`traffic_ctl` and validating both the task tree and the
``[plugin: <name>]`` attribution.

Reference
---------

:doc:`api/functions/TSCfgRegister.en` covers the full plugin-facing
surface: the :type:`TSCfgRegistrationInfo` options struct, the
registration / trigger / dependency / enable functions, and every
``TSCfgLoadCtx*`` operation available inside the handler callback.


Thread Model
============

Expand Down Expand Up @@ -949,7 +1143,9 @@ line is logged to ``diags.log``:
WARNING: Config reload [my-token] finished with failures: 1 succeeded, 1 failed (3 total) — run: traffic_ctl config status -t my-token

When the ``config.reload`` debug tag is enabled, a detailed dump of all subtasks and their
log entries is written to ``traffic.out`` / ``diags.log``:
log entries is written to ``traffic.out`` / ``diags.log``. The same tag covers diagnostics
from the plugin-facing API (:func:`TSCfgRegister` and friends), so a single tag is enough to
trace the full reload pipeline end-to-end:

.. code-block:: text

Expand Down
25 changes: 20 additions & 5 deletions include/mgmt/config/ConfigContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ namespace config
{
class ConfigRegistry;
}
namespace detail
{
class RecordTriggeredReloadContinuation;
}

///
/// @brief Context passed to config handlers during load/reload operations.
Expand All @@ -58,7 +62,7 @@ class ConfigRegistry;
/// At startup there is no active reload task, so all status operations
/// (in_progress, complete, fail, log) are safe **no-ops**. To keep the
/// existing code logic for loading/reloading this design aims to avoid
/// having two separate code paths for startup vs. reload handlers
/// having two separate code paths for startup vs. reload - handlers
/// can use the same API in both cases.
///
/// Usage:
Expand Down Expand Up @@ -92,7 +96,7 @@ class ConfigContext

~ConfigContext();

// Copy only move is intentionally suppressed.
// Copy only - move is intentionally suppressed.
// ConfigContext holds a weak_ptr (cheap to copy) and a YAML::Node (ref-counted).
// Suppressing move ensures that std::move(ctx) silently copies, keeping the
// original valid. This is critical for execute_reload()'s post-handler check:
Expand Down Expand Up @@ -166,6 +170,11 @@ class ConfigContext
/// For dependent contexts it is the label passed to add_dependent_ctx().
[[nodiscard]] std::string get_description() const;

/// Get the reload token identifying the current reload cycle.
/// All tasks within the same reload share the same token.
/// Returns empty string for default-constructed (no-op) contexts.
[[nodiscard]] std::string get_reload_token() const;

/// Create a dependent sub-task that tracks progress independently under this parent.
/// Each dependent reports its own status (in_progress/complete/fail) and the parent
/// task aggregates them. The dependent context also inherits the parent's supplied YAML node.
Expand All @@ -177,22 +186,27 @@ class ConfigContext
/// @code
/// if (auto yaml = ctx.supplied_yaml()) { /* use yaml node */ }
/// @endcode
/// @return copy of the supplied YAML node (cheap YAML::Node is internally reference-counted).
/// @return copy of the supplied YAML node (cheap - YAML::Node is internally reference-counted).
[[nodiscard]] YAML::Node supplied_yaml() const;

/// Get reload directives extracted from the _reload key.
/// Directives are operational parameters that modify how the handler performs
/// the reload (e.g. scope to a single entry, dry-run) distinct from config content.
/// the reload (e.g. scope to a single entry, dry-run) - distinct from config content.
/// The framework extracts _reload from the supplied node before passing content
/// to the handler, so supplied_yaml() never contains _reload.
/// Returns Undefined when no directives were provided (operator bool() == false).
/// @code
/// if (auto directives = ctx.reload_directives()) { /* use directives */ }
/// @endcode
/// @return copy of the directives YAML node (cheap YAML::Node is internally reference-counted).
/// @return copy of the directives YAML node (cheap - YAML::Node is internally reference-counted).
[[nodiscard]] YAML::Node reload_directives() const;

private:
/// Attach the registering plugin's name. A non-empty name marks the context's
/// task as plugin-originated; an empty view marks it as core. Used for
/// diagnostics and traffic_ctl status attribution.
void set_plugin_name(std::string_view name);

/// Set supplied YAML node. Only ConfigRegistry should call this during reload setup.
void set_supplied_yaml(YAML::Node node);

Expand All @@ -205,6 +219,7 @@ class ConfigContext

friend class ReloadCoordinator;
friend class config::ConfigRegistry;
friend class detail::RecordTriggeredReloadContinuation;
};

namespace config
Expand Down
38 changes: 24 additions & 14 deletions include/mgmt/config/ConfigRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ enum class ConfigSource {
FileAndRpc ///< Handler can also process YAML content supplied via RPC
};

/// Handler signature for config reload - receives ConfigContext
/// Handler can check ctx.supplied_yaml() for rpc-supplied content
/// Handler signature for config reload - receives ConfigContext by value.
/// Handler can check ctx.supplied_yaml() for rpc-supplied content.
using ConfigReloadHandler = std::function<void(ConfigContext)>;

///
Expand Down Expand Up @@ -104,19 +104,13 @@ class ConfigRegistry
std::string filename_record; ///< Record containing filename (e.g., "proxy.config.cache.ip_allow.filename")
ConfigType type; ///< YAML or LEGACY - we set that based on the filename extension.
ConfigSource source{ConfigSource::FileOnly}; ///< What content sources this handler supports
ConfigReloadHandler handler; ///< Handler function (empty = static file/not reloadable)
ConfigReloadHandler handler; ///< Reload handler (empty for static / non-reloadable entries)
std::vector<std::string> trigger_records; ///< Records that trigger reload
bool is_required{false}; ///< Whether the file must exist on disk
std::string plugin_name; ///< Plugin that registered this entry (empty for core).

/// Resolve the actual filename (reads from record, falls back to default)
std::string resolve_filename() const;

/// Whether this entry has a reload handler (false for static/non-reloadable files).
bool
has_handler() const
{
return static_cast<bool>(handler);
}
};

///
Expand All @@ -143,13 +137,21 @@ class ConfigRegistry
ConfigReloadHandler handler, ConfigSource source, std::initializer_list<const char *> trigger_records = {},
bool is_required = false);

/// @brief Register a plugin-owned config file (TSCfgRegister entry point).
///
/// @a plugin_name must be non-empty; it is recorded on the Entry and used
/// for diagnostics, log attribution, and traffic_ctl status output.
void register_plugin_config(const std::string &key, const std::string &plugin_name, const std::string &default_filename,
const std::string &filename_record, ConfigReloadHandler handler, ConfigSource source,
std::initializer_list<const char *> trigger_records = {}, bool is_required = false);

/// @brief Register a record-only config handler (no file).
///
/// Convenience method for modules that have no config file but need their
/// reload handler to participate in the config tracking system (tracing,
/// status reporting, traffic_ctl config reload).
///
/// This is NOT for arbitrary record-change callbacks use RecRegisterConfigUpdateCb
/// This is NOT for arbitrary record-change callbacks - use RecRegisterConfigUpdateCb
/// for that. This is for config modules like SSLTicketKeyConfig that are reloaded
/// via record changes and need visibility in the reload infrastructure.
///
Expand All @@ -164,7 +166,7 @@ class ConfigRegistry

/// @brief Register a non-reloadable config file (startup files).
///
/// Static files are registered for informational purposes only no reload
/// Static files are registered for informational purposes only - no reload
/// handler and no trigger records. This allows the registry to serve as the
/// single source of truth for all known configuration files, so that RPC
/// endpoints can gather and expose this information.
Expand Down Expand Up @@ -202,7 +204,8 @@ class ConfigRegistry
///
///
/// @param key The registered config key (must already exist)
/// @param filename_record Record holding the filename (e.g., "proxy.config.cache.ip_categories.filename")
/// @param filename_record Record holding the filename (e.g., "proxy.config.cache.ip_categories.filename"),
/// or nullptr / empty to use @a default_filename verbatim
/// @param default_filename Default filename when record value is empty (e.g., "ip_categories.yaml")
/// @param is_required Whether the file is required to exist
/// @return 0 on success, -1 if key not found
Expand Down Expand Up @@ -311,9 +314,16 @@ class ConfigRegistry
void setup_triggers(Entry &entry);

/// Internal: wire a record callback to fire on_record_change for a config key.
/// Does NOT modify trigger_records callers decide whether to store the record.
/// Does NOT modify trigger_records - callers decide whether to store the record.
int wire_record_callback(const char *record_name, const std::string &config_key);

/// Internal: split the rpc-passed YAML into _reload directives and remaining
/// content, and apply both slices onto @p ctx (via the friend relationship
/// with ConfigContext). On invalid _reload (non-map), the directives are
/// dropped with a warning. @p passed_config is mutated (the "_reload" key
/// is stripped).
static void apply_passed_config(ConfigContext &ctx, YAML::Node &passed_config, std::string_view key);

/// Hash for lookup.
struct StringHash {
using is_transparent = void;
Expand Down
Loading