"""Refresh the ``cmdb.cost_facts_monthclose`` scheduled-job description. The job was rebuilt (2026-07-05) from a single-process inline loop that re-pulled every enabled cloud account's whole previous month under ONE handler timeout — a design that NEVER completed in prod (ran >6h and was reaped by the stale-running sweeper, then hit the 1800s timeout) — into a **fan-out dispatcher**: it splits the month into ``chunk_days``-day sub-windows and runs each through that account's own ``cmdb.cost_facts.`` job via ``execute_job`` (per-account timeout / lock / run-row / failure-notifier reused; failures isolated). The global row's ``description`` column was seeded verbatim by migration 247 and is NOT reseeded from the registry at boot, so the admin console (``/admin/cron/scheduled-jobs``) still renders the old, now-misleading "单例任务遍历所有账号" text. This migration refreshes it in place. It is a pure text UPDATE keyed by ``handler_key`` — idempotent, no-op where the row is absent (fresh test schemas built via ``create_all``). See ``handlers.py:run_cost_facts_monthclose`` for the authoritative behaviour and docs/CHANGELOG.md (2026-07-05). """ import sqlalchemy as sa from alembic import op revision = "384" down_revision = "383" branch_labels = None depends_on = None def _table_exists(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :n" ), {"n": name}, ).scalar() is not None HANDLER_KEY = "cmdb.cost_facts_monthclose" # New (dispatcher) description — mirrors the @register_scheduled_job text # in handlers.py. _DESC_NEW = ( "每月 5 号 07:00(Asia/Shanghai)的【月结重拉调度器】。把【上一个完整自然月】" "的 line-item 成本明细整月重新拉取,追平云厂商月末 finalize 的晚到核价修订" "(日累积 S3/ECR/Cost Explorer、未打 Owner 标签的 Bedrock 用量;2026-06 实测 " "AWS SG 5 月差 $3.84),使 cmdb_cost_facts 与云控制台对账到分。为什么需要:每日 " "cmdb.cost_facts. 只滚动重拉「昨天起往前 3 天」,使用日过了 3 天就冻结、" "再不追修订。⚠️ 本任务不再自己串行拉全部账号(旧实现:8 账号 × ~205 scope × " "整月 line-item 塞进一个 30min 超时,prod 从未跑完过 —— 2026-06 跑 >6h 被 " "stale-running 清扫、2026-07 命中 1800s 超时)。现在它是【扇出调度器】:对每个" "启用云账号,把目标月切成 chunk_days(默认 7)天子窗,逐个通过该账号自己的 " "cmdb.cost_facts. 任务(execute_job + 显式过去窗口 allow_past_window,绕过" "每日任务的 stale-pin 自愈)重拉。每个子窗复用该账号自己的超时/单飞锁/run 行/" "失败告警,慢账号或失败账号被隔离、可独立重试。调度本身以有限并发(max_parallel," "默认 3)后台 drain,秒级返回;完成情况以 cmdb.cost_facts. 的 run 历史为准," "本任务的 run 行只记录派发计划。config 可覆写:{\"start_date\",\"end_date\"}" "(任意区间回填)、chunk_days、chunk_timeout_secs、max_parallel。" ) # Old (inline-loop) description — for downgrade, verbatim from migration 247. _DESC_OLD = ( "每月 5 号 07:00(Asia/Shanghai)对所有启用的云账号,把【上一个完整自然月】" "的 line-item 成本明细整月重新拉取并 upsert 进 cmdb_cost_facts —— 对账用的" "「月结重拉」。为什么需要:每日 cmdb.cost_facts. 只滚动重拉「昨天起往前 " "3 天」,使用日过了 3 天就不再重拉;而 AWS Cost Explorer / 各云账单在月末 " "finalize 时还会继续核价修订(尤其日累积的 S3/ECR/Cost Explorer、未打 Owner " "标签的 Bedrock 用量),导致 Helix 冻结的旧值与云控制台终值出现小额漂移" "(2026-06 实测 AWS SG 5 月差 $3.84)。本任务在云厂商完成上月账单 finalize " "之后(~5 号)整月重灌一次,把所有晚到的修订追平,使 cmdb_cost_facts 与云控制台" "对账到分。与 cmdb.cost.nightly_recompute_allocation 不同:那个重「归属」已有" "数据,这个重「拉取」厂商金额,两者都需要、互不替代。单例任务,内部遍历所有 " "enabled 云账号,不随云账号数膨胀;逐账号走 cloud_sync:cost_facts:{id} 锁,绝不" "与每日 tick 并发。窗口可用 config={\"start_date\",\"end_date\"} 覆写做任意区间回填。" ) def _set_description(desc: str) -> None: bind = op.get_bind() if not _table_exists(bind, "scheduled_jobs"): return bind.execute( sa.text( "UPDATE scheduled_jobs SET description = :desc, updated_at = NOW() " "WHERE handler_key = :handler" ), {"desc": desc, "handler": HANDLER_KEY}, ) def upgrade() -> None: _set_description(_DESC_NEW) def downgrade() -> None: _set_description(_DESC_OLD)