Processing outside the API — CloudKitty — branch stable/2026.2
Branch
stable/2026.2@0685cc330e83— repository https://opendev.org/openstack/cloudkitty — analysed on 2026-11-09
All background work runs in cloudkitty-processor, a cotyledon service manager that starts [orchestrator]/max_workers CloudKittyProcessor processes and [orchestrator]/max_workers_reprocessing CloudKittyReprocessor processes (both default to the CPU count; 0 disables) (cloudkitty/cli/processor.py:28-54, cloudkitty/orchestrator.py:56-69). There is no periodic task framework, no notification listener, no cache polling.
| Process | Trigger | Type | Purpose | Reads | Writes / emits | Defined at |
|---|---|---|---|---|---|---|
| processor worker | internal_run loop, then sleep([collect]/period) | timer (SQL + HTTP polling) | rate every ready period of every scope | fetcher, cloudkitty_storage_states, rules, collector | storage back end, cloudkitty_storage_states | cloudkitty/orchestrator.py:690-765 |
| reprocessor worker | same loop | timer (SQL polling) | execute reprocessing schedules | storage_scope_reprocessing_schedule, rules, collector | storage (delete + push), schedule progress | cloudkitty/orchestrator.py:768-806 |
| any worker | RPC quote | RPC call (topic cloudkitty, ns rating) | instant price | rules | — | cloudkitty/orchestrator.py:145-180 |
| any worker | RPC reset_state | RPC cast | reset scopes and purge their data | — | storage delete, cloudkitty_storage_states | cloudkitty/orchestrator.py:221-246 |
| every worker | RPC reload_modules, reload_module, enable_module, disable_module | RPC cast (fanout for the last three) | intended hot reload | — | in-memory flags only (no effect) | cloudkitty/orchestrator.py:182-207 |
1. Rating loop (CloudKittyProcessor)
- Trigger: an endless
while True: internal_run(); each pass ends withtime.sleep([collect]/period)(3600 s), so the effective cadence is period + processing time — the code itself notes the drift (cloudkitty/orchestrator.py:690-737). It starts immediately at worker start. - Purpose: bring every scope's rated data up to
now − wait_periods × period. - Flow:
- Load scopes from the fetcher and shuffle them (
cloudkitty/orchestrator.py:760-765). - For each scope, try the tooz lock without blocking; skip if taken (
cloudkitty/orchestrator.py:702-732). - Compute the next period: none yet → start of the current month; else
last + periodiflast + period + wait_periods × period < now(cloudkitty/orchestrator.py:283-287,cloudkitty/utils/__init__.py:200-223). - Create a
Workerfor the scope: load the enabled rating modules for this tenant (SQL), the metric file, a thread pool (cloudkitty/orchestrator.py:290-310). - Loop over ready periods (
cloudkitty/orchestrator.py:393-416): skip inactive scopes (cloudkitty/orchestrator.py:406-411); reload rules for the period start (cloudkitty/orchestrator.py:329-339); collect every metric in parallel (cloudkitty/orchestrator.py:356-391); rate (cloudkitty/orchestrator.py:509-521); filter points withskip_datapoints_expression(cloudkitty/orchestrator.py:462-507); push (cloudkitty/orchestrator.py:445-460); advancelast_processed_timestamp(cloudkitty/orchestrator.py:523-524).
- Load scopes from the fetcher and shuffle them (
- Catch-up: a scope that is behind (new scope, outage, reset) is processed period after period inside the same lock until it is up to date, while other scopes of this worker wait.
- Concurrency and idempotency: the per-scope tooz lock prevents two workers from rating the same scope; with the default
file://back end this holds only on one host (03 § 6). The state is advanced after the push; a crash between push and state update re-rates that period and stores it twice [inferred: the v2 back ends append, they do not upsert]. - Error behaviour: any exception is logged and the scope is retried at the next loop (
cloudkitty/orchestrator.py:716-725); a fetcher error escapesinternal_run[inferred: cotyledon restarts the worker process]. - Cost per loop and per worker: fetcher (Keystone: 1 + N projects requests); per scope and ready period: about
2 + services × 5 + fields × 5 + rulesSQL queries to reload rules (03 § 2.3),Mcollector calls (+Mresource searches with metadata), one or more storage writes. All workers fetch the full scope list and contend for the same locks.
2. RPC endpoints
| Method | Behaviour | Where |
|---|---|---|
quote(ctxt, res_data) | builds a frame [now, now + period) from res_data['usage'], runs an APIWorker (enabled modules, tenant_id=None), returns str(Decimal) | cloudkitty/orchestrator.py:145-180, cloudkitty/orchestrator.py:268-280 |
reset_state(ctxt, res_data) | for each scope (shuffled): blocking tooz lock, storage.delete(begin=last_processed_timestamp, end=None, filters={scope_key: scope_id}), then set_last_processed_timestamp | cloudkitty/orchestrator.py:221-246 |
reload_modules, reload_module(name) | set _global_reload / append to _pending_reload | cloudkitty/orchestrator.py:182-193 |
enable_module(name), disable_module(name) | set _module_state[name] | cloudkitty/orchestrator.py:195-207 |
The flags of the last four are read by get_reload_list / get_module_state (cloudkitty/orchestrator.py:131-143), whose only caller, process_messages, is a stub (cloudkitty/orchestrator.py:683-688). Module changes take effect because the Worker re-reads module state and rules from SQL every scope cycle, not because of these messages. reload_modules is cast with version 1.1 against a 1.0 endpoint (cloudkitty/api/v1/controllers/rating.py:51-53, cloudkitty/orchestrator.py:122-123).
3. Reprocessing loop (CloudKittyReprocessor)
- Trigger: same loop and sleep as § 1 (inherited), over schedules instead of scopes (
cloudkitty/orchestrator.py:768-800). - Data read: unfinished schedules (
current_reprocess_time IS NULL OR current_reprocess_time < end_reprocess_time), default limit 100, newest first (cloudkitty/storage_state/__init__.py:310-347) — more than 100 pending schedules are served 100 at a time. - Flow (
cloudkitty/orchestrator.py:527-631):- Lock named after the schedule (non-blocking).
- Check the scope still exists in
cloudkitty_storage_states(exactly one row), take itsscope_key. - Next period:
start_reprocess_time, thencurrent_reprocess_time + period, while≤ end_reprocess_time. - On the first period only: delete
[start, end)in storage for{scope_key: scope}. - Collect, rate with the rules active at that period, push.
current_reprocess_time = period start.
- Deactivated scopes: the inherited check skips a scope whose
activeis false, so its schedule stays pending until the scope is reactivated (cloudkitty/orchestrator.py:406-411). - Error behaviour: as § 1; a vanished scope raises at every loop (
cloudkitty/orchestrator.py:540-551).
4. Startup work
| Process | Work | Where |
|---|---|---|
| API | load stevedore rating modules (invoke_on_load, which queries SQL for module rules), storage driver, RPC client; read etc/cloudkitty/api_paste.ini | cloudkitty/api/v1/controllers/rating.py:33-58, cloudkitty/api/v1/__init__.py:39-57 |
| processor worker | load fetcher, collector (validates the metric file), storage; start RPC server and tooz coordinator | cloudkitty/orchestrator.py:634-666 |
cloudkitty-storage-init | backend.init() (InfluxDB v1: check retention policy; ES/OpenSearch: put mapping) + storage-state migrations | cloudkitty/cli/storage.py:21-25, cloudkitty/storage/v2/influx.py:757-764, cloudkitty/storage/v2/elasticsearch/__init__.py:103-111 |
5. Configuration options that drive background behaviour
| Option | Default | Effect | Where |
|---|---|---|---|
[collect]/period | 3600 | rating period and loop sleep | cloudkitty/collector/__init__.py:43-45 |
[collect]/wait_periods | 2 | lag before a period is rated | cloudkitty/collector/__init__.py:46-48 |
[collect]/scope_key | project_id | attribute that identifies a scope in collectors and storage | cloudkitty/collector/__init__.py:52-55 |
[orchestrator]/max_workers, max_workers_reprocessing | CPU count | number of worker processes | cloudkitty/orchestrator.py:56-69 |
[orchestrator]/max_threads | 16 | parallel metric collection per worker | cloudkitty/orchestrator.py:70-75 |
[orchestrator]/collector_request_timeout | 300 (min 30) | timeout for all metric collections of one period | cloudkitty/orchestrator.py:91-98 |
[orchestrator]/coordination_url | file:///var/lib/cloudkitty/locks | tooz back end | cloudkitty/orchestrator.py:51-55 |
[orchestrator]/skip_datapoints_expression | False | Python expression; true → point not stored | cloudkitty/orchestrator.py:76-90 |
[storage]/retention_period | 2400 h | declared, but get_retention() has no caller: no code purges or aggregates old data | cloudkitty/storage/v2/__init__.py:28-35, cloudkitty/storage/v2/__init__.py:158-163 |
Source: reverse-spec-cloudkitty/05-async-processing_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.