Hi all,

I'd like to propose a new plugin API that gives plugins
access to the config reload framework that core configs
already use - the same machinery behind token-based
reloads in `traffic_ctl config reload/status`. I have a working
branch with implementation, docs, and autests;

I know reading all this in an email is kinda painful, so I have
this md file with the exact same content but nicer.
https://gist.github.com/brbzull0/921fb88b2adb384836e2565d190c7ffc


TL;DR
-----

Today plugins can react to `traffic_ctl config reload` only via
TSMgmtUpdateRegister, which gives them a notification and nothing
else (no payload, no per-key targeting, no status surfacing, no
trigger records, no _reload directives, no companion files).

This proposal adds a small TSCfg* API that lets plugins register
config files with the same framework core configs already use,
so their reloads show up in `traffic_ctl config status` with full
state tracking, RPC payload routing, and a deferred-completion
contract.

Implementation, RST docs, and autests live on a feature branch, draft pr
also available.
(link at the bottom).


PROBLEM
-------

Today a plugin that wants to react to `traffic_ctl config reload` has
one main API:

    void TSMgmtUpdateRegister(TSCont contp,
                              const char *plugin_name,
                              const char *plugin_file_name = nullptr);

That gives plugins a notification when a reload is being kicked off - but
nothing more. The plugin is still on its own for everything

The result is that every plugin reinventing config reload ends up
incompatible with the rest of the framework. In-tree examples:
regex_revalidate re-implements its own file-mtime watching on top of
TSMgmtUpdateRegister.




PROPOSED SOLUTION
-----------------

A small TSCfg* API that lets a plugin plug into the existing
ConfigRegistry framework. It mirrors the core C++ API
(ConfigRegistry::register_config / ConfigContext::complete) but with
C-friendly types and an option-struct registration shape so we can
extend it without breaking ABI.

Two function families.

Registration (called from TSPluginInit):

    // Register a plugin config file + reload handler with the
    // framework. Required fields in `info`: key, config_path,
    // handler.
    TSReturnCode TSCfgRegister(const TSCfgRegistrationInfo *info);

    // Wire 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. The record must
    // already exist (e.g. created via TSMgmtIntCreate). Not a
    // generic value-change subscription: the plugin only sees its
    // own reload handler being called, with no record-change
    // payload. See "Scope notes" below.
    //
    // Core analog: ConfigRegistry::register_config() accepts a
    // `trigger_records` initializer-list at registration time
    // (e.g. ssl_multicert lists ~10 record names there), and
    // ConfigRegistry::attach(key, record) is the post-registration
    // form. TSCfgAttachReloadTrigger is the plugin-facing wrapper
    // for attach(); we don't expose the initializer-list shape on
    // TSCfgRegistrationInfo to keep the option struct ABI-stable.
    TSReturnCode TSCfgAttachReloadTrigger(string_view key,
                                          string_view record_name);

    // Declare a companion file. Changes to it invoke the plugin's
    // handler. Optional `dep_key` routes inline RPC content under
    // that top-level node to the same handler.
    TSReturnCode TSCfgAddFileDependency(
                     const TSCfgFileDependencyInfo *info);

Per-reload context (used inside the plugin's TSCfgLoadCb handler).
All ctx accessors are null-safe (passing nullptr is a no-op or
returns an empty value):

    // Mark task as IN_PROGRESS and emit an optional message.
    void TSCfgLoadCtxInProgress(TSCfgLoadCtx ctx, string_view msg);

    // Terminal: mark SUCCESS, emit optional message, free ctx.
    void TSCfgLoadCtxComplete(TSCfgLoadCtx ctx, string_view msg);

    // Terminal: mark FAILED, emit optional reason, free ctx.
    void TSCfgLoadCtxFail(TSCfgLoadCtx ctx, string_view msg);

    // Append an intermediate log entry without changing state.
    // Visible in `traffic_ctl config status`.
    void TSCfgLoadCtxAddLog(TSCfgLoadCtx ctx, TSCfgLogLevel level,
                            string_view msg);

    // Create a child subtask under `ctx`. The returned handle must
    // be terminated independently (Complete or Fail).
    TSCfgLoadCtx TSCfgLoadCtxAddSubtask(TSCfgLoadCtx ctx,
                                        string_view description);

    // 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 (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(ctx) - 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 SuppliedYaml
    // as the canonical RPC-detection signal.
    string_view TSCfgLoadCtxGetFilename(TSCfgLoadCtx ctx);

    // Reload-cycle correlation token (e.g. rldtk-<timestamp>); use
    // it in plugin log lines so they line up with
    // `traffic_ctl config status -t <token>`.
    string_view TSCfgLoadCtxGetReloadToken(TSCfgLoadCtx ctx);

     // YAML content supplied via a JSONRPC reload, or nullptr if no
    // inline payload was provided (file-driven reloads always return
    // nullptr here; canonical RPC-vs-file signal). When non-null,
    // the underlying YAML::Node has IsDefined() == true. TSYaml is
    // an opaque alias for YAML::Node*; the plugin reinterpret_cast's
    // it. (Yes, this ties the plugin ABI to yaml-cpp.)
    TSYaml TSCfgLoadCtxGetSuppliedYaml(TSCfgLoadCtx ctx);

    // YAML map extracted from the `_reload:` key of the supplied
    // payload (e.g. dry_run, scope filters), or nullptr if no
    // directives were sent or no RPC payload arrived. Operators
    // populate this via the `--directive` flag of `traffic_ctl
    // config reload`. Same TSYaml/yaml-cpp caveat as above.
    TSYaml TSCfgLoadCtxGetReloadDirectives(TSCfgLoadCtx ctx);

(Naming nit: GetSuppliedYaml and GetReloadDirectives are
asymmetric - both return a YAML handle. Open to renaming to
GetSuppliedYaml / GetReloadDirectivesYaml, or
GetSuppliedContent / GetReloadDirectives. Easier to settle now than
later.)

What this gives plugins, on top of TSMgmtUpdateRegister:

  - Per-key targeting. The handler runs only when this plugin's
    registered file or trigger record actually changes, or when the
    operator targets this plugin by key.
  - RPC payload. Plugins registered with TS_CFG_SOURCE_FILE_AND_RPC
    can receive YAML content supplied via JSONRPC and react to
    `_reload` directives.
  - Status surface. Reload outcome (success / fail / in-progress /
    timeout, plus log entries) is visible in
    `traffic_ctl config status` alongside core configs - for
    operator-driven `config reload`, file-change reloads, and
    record changes that happen during a reload cycle. (Standalone
    `traffic_ctl config set` on a trigger record runs the handler
    but does not surface in `config status`; same as core. See
    Scope notes below.)
  - Subtasks. A handler can split work into named subtasks that
    aggregate into the parent's status.
  - Companion files. TSCfgAddFileDependency declares an extra file
    whose changes invoke the same handler - and (optionally, via
    dep_key) routes RPC content to the same handler too.
  - Deferred completion. Handlers may stash the context, return,
    and finish on another thread later. Same contract core handlers
    already have.

Scope notes for TSCfgAttachReloadTrigger:

  - 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. The
        plugin must have already called TSCfgRegister for `key`;
        otherwise TSCfgAttachReloadTrigger returns TS_ERROR.
      * Multiplex one record across two keys, or attach in any
        shape other than (one record, one config key, per call).

  - When the file change is caused by `traffic_ctl config
    reload`, the handler runs as a subtask of that reload and
    surfaces in `traffic_ctl config status`. When the record is
    set standalone (`traffic_ctl config set` outside a reload
    cycle), the handler still runs and applies config, but no
    reload task is created so the invocation is invisible to
    `config status` - same as core record-triggered reloads
    outside a reload cycle.



EXAMPLE - visible in `traffic_ctl config status`
------------------------------------------------

    $ traffic_ctl config status -t my-token
    [OK] Reload [success] -- my-token
      Tasks:
       [OK] ip_allow ..............................   1ms
            [Note]  ip_allow.yaml loading ...
            [Note]  ip_allow.yaml finished loading
       [OK] my_plugin [plugin: my_plugin] .........  18ms
            [Note]  Reloaded my_plugin


draft PR -> https://github.com/apache/trafficserver/pull/13146
working branch ->
https://github.com/brbzull0/trafficserver/tree/plugin-config-registry-api-v2


Thanks,
Damian
Yahoo.

Reply via email to