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.
