Operations

Dynamic config

Ductor's two dynamic-config systems — the tenant-scoped registry and the file-based feature-flag/A-B system — and how they differ.

Ductor has two distinct dynamic-config systems, and they are easy to confuse — the configuration guide and the code both flag the overlap. They solve different problems, live in different files, and are backed by different stores. This page disambiguates them. Neither is the same as static boot config (configs/ductor.yaml), which is loaded once by viper at process start and is not re-read.

Which config lives where

configs/README.md is the authoritative ownership rule. Boot-time service wiring (DB DSN, bind addresses, OIDC issuer, the AEAD master key) belongs in configs/ductor.yaml. Restart-free, per-tenant flips belong in the dynamicconfig registry. A key must have exactly one canonical owner — the other file must not redeclare it.

1. The dynamicconfig registry (Postgres/Redis-backed)

This is the operator-facing hot-reload plane: tenant-scoped settings that flip live, without a restart, per tenant where the setting's scope allows.

  • Codegen source: configs/dynamicconfig.yaml declares each Setting (its var_name, key, type, scope, and default). It is consumed by tools/dynamicconfig-gen/ to produce a typed registry; regenerate with make dynamicconfig-generate.
  • Runtime store: the live values are in Postgres + Redisnot in the YAML. The YAML only declares the settings and their defaults.
  • Reads: application code reads a value through the typed registry (dynconfig.DynamicConfig.Get(key, ctx)), which applies the per-tenant override chain.

Typical contents are kill switches, capacity quotas, and cluster rollout gates — things you want to flip during a canary or an incident without redeploying:

Example keyScopePurpose
routing.kill_switchtenantEmergency per-tenant routing kill switch
routing.disable_flow_controltenantDisable flow-control checks during routing
ductor.queue.shadow_modeglobalShadow-partition queue rollout (off/shadow/canary/on)
ductor.scheduling.enabledglobalMaster kill switch for the scheduling integration

2. The file-based feature-flag / A-B system

This is a separate system for feature flags and A/B experiments, driven by a watched file rather than the registry. Enable it under dynamic_config.*:

configs/ductor.yaml
dynamic_config:
  enabled: false
  config_file: /etc/ductor/dynamic.yaml
  watch_interval: 30s   # debounce for file-change detection

The referenced file carries three capabilities:

  • Config values — key/value settings that reload when the file changes.
  • Feature flags — boolean flags with percentage rollout (deterministic FNV-1a bucketing on flag key + context ID, so the same tenant always lands the same way) and targeting rules matching context attributes such as tenant_id or region.
  • A/B tests — weighted variant selection; weights are proportional ranges in [0, 1) and must sum to 1.0, each variant carrying a config map your code reads.

The file source watches with fsnotify. On change, the new file is parsed and validated, flags and tests are swapped atomically, and registered OnChange callbacks fire with the old and new snapshots.

File change(fsnotify) Parse + validate Atomic swapflags + tests OnChange callbacksold + new snapshots
dynamic.yaml
values:
  routing_timeout_ms: 5000
features:
  - key: experimental_strategy
    enabled: true
    percentage: 25          # 25% rollout
  - key: premium_features
    enabled: true
    rules:
      - attribute: tenant_id
        values: ["acme-corp", "enterprise-inc"]
ab_tests:
  - id: routing_strategy_test
    status: active          # active | paused | completed
    variants:
      - id: round_robin
        weight: 0.6
      - id: weighted
        weight: 0.4

Telling them apart

dynamicconfig registryfile-based feature flags
Sourceconfigs/dynamicconfig.yaml (codegen)a watched YAML file (dynamic_config.config_file)
Runtime storePostgres + Redisthe file itself (via fsnotify)
Enabled byalways available in the graphdynamic_config.enabled=true
Scopeper-tenant override chainpercentage rollout + targeting rules
Regeneratemake dynamicconfig-generaten/a (edit the file)
Best forkill switches, quotas, rollout gatesfeature flags, A/B experiments

Neither re-reads configs/ductor.yaml: boot-time static config is loaded once and only changes on restart.