Dynamic Updates¶
Varlord supports dynamic configuration updates via ConfigStore and source watch mechanisms.
Basic Usage¶
cfg = Config(...)
store = cfg.load_store() # Automatically enables watch if sources support it
# Get current configuration
current = store.get()
# Subscribe to changes
def on_change(new_config, diff):
print(f"Added: {diff.added}")
print(f"Modified: {diff.modified}")
print(f"Deleted: {diff.deleted}")
store.subscribe(on_change)
Watch Detection¶
load_store() automatically detects if any source supports watching and enables it automatically. You only need to enable watch in the source itself:
# Enable watch in the source
cfg = Config(
model=AppConfig,
sources=[
sources.Etcd(..., watch=True), # Enable watch here
# Model defaults applied automatically
],
)
# load_store() automatically detects and enables watch
store = cfg.load_store() # No watch parameter needed
Behavior Without Watch Support¶
If no sources support watch, load_store() and subscribe() still work, but callbacks will only be called when you manually call reload() and the configuration has changed:
# No watch support
cfg = Config(
model=AppConfig,
sources=[
sources.Env(), # Model defaults applied automatically
],
)
store = cfg.load_store() # ✅ Works, but watching=False
store.subscribe(on_change) # ✅ Works, callback is registered
# Callback will only be called on manual reload with changes
store.reload() # If config changed, callback is called
Note
For automatic updates, you must use a source that supports watch (e.g., Etcd(watch=True)).
Without watch support, callbacks are only called on manual reload() with configuration changes.
Change Events¶
The callback receives:
new_config: The new configuration instancediff: AConfigDiffobject with: -added: New keys -modified: Changed keys (old_value, new_value) -deleted: Removed keys
Thread Safety¶
ConfigStore is thread-safe:
import threading
def worker():
config = store.get() # Thread-safe
print(config.host)
# Multiple threads can safely access store.get()
for _ in range(10):
threading.Thread(target=worker).start()
Watch Support¶
Currently, only Etcd source supports watch:
cfg = Config(
model=AppConfig,
sources=[
sources.Etcd(..., watch=True), # Enable watch
# Model defaults applied automatically
],
)
store = cfg.load_store() # Watch automatically enabled if Etcd has watch=True
Example: Single Callback Subscription:
from varlord import Config
from varlord.sources import Etcd
from dataclasses import dataclass, field
@dataclass
class AppConfig:
host: str = field()
port: int = field(default=8000)
cfg = Config(
model=AppConfig,
sources=[
Etcd(
host="192.168.0.220",
port=2379,
prefix="/app/",
watch=True,
ca_cert="./cert/ca.cert.pem",
cert_key="./cert/key.pem",
cert_cert="./cert/cert.pem",
),
],
)
store = cfg.load_store()
def on_change(new_config, diff):
print(f"Config changed: {new_config.host}:{new_config.port}")
print(f" Modified keys: {diff.modified}")
store.subscribe(on_change)
# Changes in etcd will automatically trigger callbacks
Example: Multiple Callbacks:
store = cfg.load_store()
def callback1(new_config, diff):
print(f"Callback 1: {new_config.host}")
def callback2(new_config, diff):
print(f"Callback 2: {new_config.port}")
store.subscribe(callback1)
store.subscribe(callback2)
# Both callbacks will be called on configuration changes
Example: Handling Added, Modified, and Deleted Keys:
def on_change(new_config, diff):
if diff.added:
print(f"New keys added: {diff.added}")
if diff.modified:
print(f"Keys modified: {diff.modified}")
for key in diff.modified:
old_val, new_val = diff.modified[key]
print(f" {key}: {old_val} -> {new_val}")
if diff.deleted:
print(f"Keys deleted: {diff.deleted}")
store.subscribe(on_change)
Example: Multiple Sources with Watch:
from varlord import Config
from varlord.sources import Etcd, Env
# Multiple etcd sources with watch
cfg = Config(
model=AppConfig,
sources=[
Etcd(host="...", prefix="/app1/", watch=True), # First source
Etcd(host="...", prefix="/app2/", watch=True), # Second source (overrides first)
Env(), # Env overrides both etcd sources
],
)
store = cfg.load_store() # Watch enabled for both etcd sources
# Changes in either etcd source will trigger callbacks
# Priority: /app1/ < /app2/ < Env
Example: Etcd with Non-Watch Sources:
from varlord import Config
from varlord.sources import Etcd, Env
cfg = Config(
model=AppConfig,
sources=[
Etcd(host="...", prefix="/app/", watch=True), # Has watch
Env(), # No watch, but can override etcd
],
)
store = cfg.load_store() # Watch enabled (because etcd supports it)
def on_change(new_config, diff):
print(f"Config updated: {new_config.host}:{new_config.port}")
store.subscribe(on_change)
# Changes in etcd will trigger callbacks
# But env variables still override etcd values
Example: Load() vs Store.get() Consistency:
cfg = Config(
model=AppConfig,
sources=[
Etcd.from_env(prefix="/app/", watch=True),
],
)
store = cfg.load_store()
# Both methods return the same values
config_from_load = cfg.load()
config_from_store = store.get()
assert config_from_load.host == config_from_store.host
assert config_from_load.port == config_from_store.port
# After etcd changes, both still return consistent values
# (after watch processes the change)
Fail-Safe Updates¶
If an update fails (validation error, etc.), the old configuration is preserved:
Old configuration remains active
Error is logged
Subscribers are not notified
This ensures your application continues running with a valid configuration.
Example: Handling Update Failures:
@dataclass
class AppConfig:
port: int = field(default=8000)
cfg = Config(
model=AppConfig,
sources=[
Etcd.from_env(prefix="/app/", watch=True),
],
)
store = cfg.load_store()
def on_change(new_config, diff):
print(f"Config updated: {new_config.port}")
store.subscribe(on_change)
# If etcd has invalid value (e.g., "invalid"), update fails
# Old configuration (port=8000) remains active
# Callback is not called