Skip to content

Split update_pred into plan_once()/execute_once() entry points#4186

Draft
mgazza wants to merge 6 commits into
mainfrom
feat/plan-execute-split
Draft

Split update_pred into plan_once()/execute_once() entry points#4186
mgazza wants to merge 6 commits into
mainfrom
feat/plan-execute-split

Conversation

@mgazza

@mgazza mgazza commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

What

Refactors update_pred() into three separately callable pieces, with update_pred() itself becoming a pure composition of them so existing behaviour is unchanged:

  • plan_once(scheduled=True) — pre-flight fetches + calculate_plan + persist + rate publish; returns a plan artifact summary (recomputed, plan_valid, plan_version, plan_last_updated) or None on the existing pre-condition failures.
  • execute_once(refetch_inverter=True) — optional inverter re-read + execute_plan().
  • _post_run_bookkeeping(...) — the existing post-run tail, moved verbatim.

Also adds a monotonic plan_version to the plan artifact persisted by save_plan()/load_plan().

Note on plan_version: it is an in-memory monotonic counter persisted alongside the plan by save_plan() and restored by load_plan(); if the stored artifact expires (8h) or storage is unavailable it restarts from 0 on the next process start. Within a running process it strictly increases, which is the property downstream consumers rely on.

Why

For predbat.com we want to schedule planning and execution independently (event-driven replans on tariff drops / settings changes, rather than only the 5-minute boundary) and run many instances in one pooled process. That needs separately callable, re-entrant plan/execute entry points and a versioned plan artifact so a stale slow plan can never overwrite a newer one. Same pattern as the recent storage/cache provider split: the entry points live upstream, all pooling/scheduling stays in our SaaS layer, and self-hosted behaviour is untouched.

Parity

tests/test_plan_execute_split.py starts with six characterisation scenarios written against the ORIGINAL fused update_pred (fresh-plan reuse, recompute, aged-plan double-execute, inverter-fetch failure, zero rates, template) — they were committed passing before the refactor and pass unchanged after it.

Question

The aged-plan path recomputes via calculate_plan(recompute=True) but does not call save_plan(), unlike the main recompute path — so a plan recomputed on age isn't restored after a restart. This PR deliberately preserves that behaviour (and the characterisation test pins it). Was that intentional, or worth fixing in a follow-up?

🤖 Generated with Claude Code

mgazza and others added 6 commits July 4, 2026 01:33
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant