Dynamic Updates¶
In this tutorial, you’ll learn how to use ConfigStore for dynamic
configuration updates, including watching for changes in etcd.
Learning Objectives¶
By the end of this tutorial, you’ll be able to:
Use
ConfigStorefor runtime configuration accessSubscribe to configuration changes
Enable automatic updates from watchable sources (e.g., etcd)
Step 1: Basic ConfigStore Usage¶
ConfigStore provides thread-safe access to configuration:
1from dataclasses import dataclass, field
2from varlord import Config
3
4@dataclass(frozen=True)
5class AppConfig:
6 host: str = field(default="0.0.0.0")
7 port: int = field(default=8000)
8
9cfg = Config(
10 model=AppConfig,
11 sources=[], # Defaults are automatically applied
12)
13
14# Load as store (supports dynamic updates)
15store = cfg.load_store()
16
17# Access configuration
18app = store.get()
19print(f"Host: {app.host}, Port: {app.port}")
20
21# Access via store.get() (store.host is not supported)
22print(f"Host: {store.get().host}, Port: {store.get().port}")
Expected Output:
Host: 0.0.0.0, Port: 8000
Host: 0.0.0.0, Port: 8000
Key Points:
load_store()returns aConfigStoreinstancestore.get()returns the current configuration model instanceAccess fields via
store.get().host, notstore.host
Step 2: Manual Reload¶
You can manually reload configuration:
1import os
2from dataclasses import dataclass, field
3from varlord import Config, sources
4
5@dataclass(frozen=True)
6class AppConfig:
7 port: int = field(default=8000)
8
9cfg = Config(
10 model=AppConfig,
11 sources=[
12 sources.Env(), # Defaults applied automatically
13 ],
14)
15
16store = cfg.load_store()
17
18# Initial value
19print(f"Initial port: {store.get().port}")
20
21# Change environment variable (no prefix needed)
22os.environ["PORT"] = "9000"
23
24# Manually reload
25store.reload()
26
27# New value
28print(f"Updated port: {store.get().port}")
Expected Output:
Initial port: 8000
Updated port: 9000
Important: Manual reload is useful when sources don’t support automatic watching (like environment variables or CLI arguments).
Step 3: Subscribing to Changes¶
You can subscribe to configuration changes:
1import os
2from dataclasses import dataclass, field
3from varlord import Config, sources
4
5@dataclass(frozen=True)
6class AppConfig:
7 port: int = field(default=8000)
8
9def on_config_change(new_config, diff):
10 print(f"Configuration changed!")
11 print(f" Added: {diff.added}")
12 print(f" Modified: {diff.modified}")
13 print(f" Deleted: {diff.deleted}")
14 print(f" New port: {new_config.port}")
15
16cfg = Config(
17 model=AppConfig,
18 sources=[
19 sources.Env(), # Defaults applied automatically
20 ],
21)
22
23store = cfg.load_store()
24store.subscribe(on_config_change)
25
26# Change configuration (no prefix needed)
27os.environ["PORT"] = "9000"
28store.reload() # Triggers callback
Expected Output:
Configuration changed!
Added: set()
Modified: {'port'}
Deleted: set()
New port: 9000
Key Points:
Subscribers are called when configuration changes
diffobject shows what changedCallbacks receive the new configuration and diff
Step 4: Automatic Updates with Etcd (Optional)¶
If you have etcd installed, you can enable automatic watching:
1from dataclasses import dataclass, field
2from varlord import Config, sources
3
4@dataclass(frozen=True)
5class AppConfig:
6 port: int = field(default=8000)
7 timeout: int = field(default=30)
8
9def on_config_change(new_config, diff):
10 print(f"Configuration updated from etcd!")
11 print(f" Changes: {diff.modified}")
12
13# Configure etcd source with watch enabled
14# Option 1: Direct configuration
15cfg = Config(
16 model=AppConfig,
17 sources=[
18 sources.Etcd(
19 host="127.0.0.1",
20 port=2379,
21 prefix="/app/",
22 watch=True, # Enable automatic watching
23 ca_cert="./cert/ca.cert.pem", # TLS certificates (if needed)
24 cert_key="./cert/key.pem",
25 cert_cert="./cert/cert.pem",
26 ), # Defaults applied automatically
27 ],
28)
29
30# Option 2: From environment variables (recommended)
31# Set ETCD_HOST, ETCD_PORT, ETCD_CA_CERT, etc. in environment
32# cfg = Config(
33# model=AppConfig,
34# sources=[
35# sources.Etcd.from_env(prefix="/app/", watch=True),
36# ],
37# )
38
39store = cfg.load_store() # Automatically enables watch
40store.subscribe(on_config_change)
41
42print("Watching etcd for changes...")
43print("Update /app/host, /app/port, or /app/timeout in etcd to see changes")
44# In production, your application would continue running here
Note: This requires etcd to be running and the etcd3 package installed.
Changes in etcd will automatically trigger configuration reloads and callbacks.
Key Points:
watch=Trueenables automatic watchingload_store()automatically detects and enables watchChanges in etcd automatically update configuration
Subscribers are notified of changes
Watch is only enabled if the source supports it (via
supports_watch())
Example: Watching Multiple Changes:
1from dataclasses import dataclass, field
2from varlord import Config, sources
3
4@dataclass(frozen=True)
5class AppConfig:
6 host: str = field()
7 port: int = field(default=8000)
8 debug: bool = field(default=False)
9
10callbacks_received = []
11
12def on_config_change(new_config, diff):
13 callbacks_received.append((new_config, diff))
14 print(f"Change #{len(callbacks_received)}:")
15 print(f" Modified: {diff.modified}")
16 print(f" Config: {new_config.host}:{new_config.port}")
17
18cfg = Config(
19 model=AppConfig,
20 sources=[
21 sources.Etcd.from_env(prefix="/app/", watch=True),
22 ],
23)
24
25store = cfg.load_store()
26store.subscribe(on_config_change)
27
28# In etcd, make multiple changes:
29# 1. /app/host = "example.com" -> triggers callback #1
30# 2. /app/port = "9000" -> triggers callback #2
31# 3. /app/host = "updated.com" -> triggers callback #3
32
33# All changes will trigger callbacks automatically
Example: Handling Added, Modified, and Deleted Keys:
1@dataclass(frozen=True)
2class AppConfig:
3 host: str = field()
4 port: int = field(default=8000) # Default value
5
6def on_config_change(new_config, diff):
7 if diff.added:
8 print(f"New keys added: {diff.added}")
9 if diff.modified:
10 print(f"Keys modified: {diff.modified}")
11 for key in diff.modified:
12 old_val, new_val = diff.modified[key]
13 print(f" {key}: {old_val} -> {new_val}")
14 if diff.deleted:
15 print(f"Keys deleted: {diff.deleted}")
16 # Deleted keys fall back to default values
17
18cfg = Config(
19 model=AppConfig,
20 sources=[
21 sources.Etcd.from_env(prefix="/app/", watch=True),
22 ],
23)
24
25store = cfg.load_store()
26store.subscribe(on_config_change)
27
28# Example scenarios:
29# 1. Add new key: /app/port = "9000" -> diff.added contains "port"
30# 2. Modify key: /app/port = "8000" -> diff.modified contains "port"
31# 3. Delete key: etcdctl del /app/port -> diff.deleted contains "port"
32# (port falls back to default 8000)
Example: Multiple Sources with Watch:
1from varlord import Config, sources
2
3@dataclass(frozen=True)
4class AppConfig:
5 host: str = field()
6 port: int = field(default=8000)
7
8cfg = Config(
9 model=AppConfig,
10 sources=[
11 sources.Etcd(host="...", prefix="/app1/", watch=True), # First source
12 sources.Etcd(host="...", prefix="/app2/", watch=True), # Second source (overrides first)
13 sources.Env(), # Env overrides both etcd sources
14 ],
15)
16
17store = cfg.load_store() # Watch enabled for both etcd sources
18
19def on_change(new_config, diff):
20 print(f"Config updated: {new_config.host}:{new_config.port}")
21
22store.subscribe(on_change)
23
24# Changes in either etcd source will trigger callbacks
25# Priority: /app1/ < /app2/ < Env
Step 5: Thread-Safe Access¶
ConfigStore is thread-safe:
1import threading
2import time
3from dataclasses import dataclass, field
4from varlord import Config
5
6@dataclass(frozen=True)
7class AppConfig:
8 counter: int = field(default=0)
9
10cfg = Config(
11 model=AppConfig,
12 sources=[], # Defaults applied automatically
13)
14
15store = cfg.load_store()
16
17def reader_thread():
18 for _ in range(10):
19 config = store.get()
20 print(f"Reader: counter = {config.counter}")
21 time.sleep(0.1)
22
23# Start reader thread
24thread = threading.Thread(target=reader_thread)
25thread.start()
26
27# Main thread can also access safely
28for i in range(5):
29 print(f"Main: counter = {store.get().counter}")
30 time.sleep(0.2)
31
32thread.join()
Expected Output (example):
Main: counter = 0
Reader: counter = 0
Reader: counter = 0
Main: counter = 0
Reader: counter = 0
...
Key Points:
Multiple threads can safely access
ConfigStoreConfiguration snapshots are atomic
No locking required for reading
Step 6: Complete Example¶
Here’s a complete example with subscriptions and manual reload:
1import os
2import time
3from dataclasses import dataclass, field
4from varlord import Config, sources
5
6@dataclass(frozen=True)
7class AppConfig:
8 host: str = field(default="0.0.0.0")
9 port: int = field(default=8000)
10 debug: bool = field(default=False)
11
12def on_config_change(new_config, diff):
13 print(f"\n[Callback] Configuration changed:")
14 if diff.modified:
15 for key in diff.modified:
16 print(f" {key} was modified")
17 if diff.added:
18 for key in diff.added:
19 print(f" {key} was added")
20 print(f" Current config: {new_config.host}:{new_config.port}")
21
22def main():
23 cfg = Config(
24 model=AppConfig,
25 sources=[
26 sources.Env(), # Defaults applied automatically
27 ],
28 )
29
30 store = cfg.load_store()
31 store.subscribe(on_config_change)
32
33 print("Initial configuration:")
34 config = store.get()
35 print(f" {config.host}:{config.port} (debug={config.debug})")
36
37 # Simulate configuration changes
38 print("\n--- Changing port ---")
39 os.environ["PORT"] = "9000"
40 store.reload()
41
42 time.sleep(0.5)
43
44 print("\n--- Changing host ---")
45 os.environ["HOST"] = "192.168.1.1"
46 store.reload()
47
48 time.sleep(0.5)
49
50 print("\n--- Final configuration ---")
51 config = store.get()
52 print(f" {config.host}:{config.port} (debug={config.debug})")
53
54if __name__ == "__main__":
55 main()
Expected Output:
Initial configuration:
0.0.0.0:8000 (debug=False)
--- Changing port ---
[Callback] Configuration changed:
port was modified
Current config: 0.0.0.0:9000
--- Changing host ---
[Callback] Configuration changed:
host was modified
Current config: 192.168.1.1:9000
--- Final configuration ---
192.168.1.1:9000 (debug=False)
Common Pitfalls¶
Pitfall 1: Expecting automatic updates from non-watchable sources
store = cfg.load_store()
# Change environment variable
os.environ["PORT"] = "9000"
# Configuration won't update automatically!
print(store.get().port) # Still 8000
Solution: Environment variables and CLI arguments don’t support automatic
watching. Use store.reload() to manually refresh, or use etcd for
automatic updates.
Pitfall 2: Modifying configuration objects
config = store.get()
config.port = 9000 # This will fail - config is frozen!
Solution: Configuration objects are immutable (frozen). To change configuration, update the source (e.g., etcd, environment) and reload.
Pitfall 3: Not handling exceptions in callbacks
def on_config_change(new_config, diff):
raise Exception("Error in callback") # This won't break the update
# But it will be silently ignored
Solution: Always handle exceptions in callbacks. The store will continue working even if a callback fails.
Best Practices¶
Use ConfigStore for long-running applications: Enables dynamic updates
Subscribe to changes: React to configuration updates
Use etcd for automatic updates: When you need real-time configuration
Handle reload errors gracefully: Updates are fail-safe
Next Steps¶
Now that you understand dynamic updates, let’s explore Advanced Features like custom priority policies and custom sources.