"""Jadwal distribusi mingguan: jalankan distribute.py pada hari `DISTRIBUTE_WEEKDAY` jam `DISTRIBUTE_HOUR`, plus satu one-off `DISTRIBUTE_FIRST_RUN` (YYYY-MM-DD, default baked `2026-08-17` via config). Contoh produksi: `DISTRIBUTE_FIRST_RUN=2026-08-17` (Senin) lalu `DISTRIBUTE_WEEKDAY=sat` → putaran pertama Senin 17 Agt 10:00, berikutnya tiap Sabtu 10:00. Menunggu hingga batas terjadwal berikutnya (waktu lokal via `TZ`) lalu memanggil `distribute.main()`. Loop tak pernah keluar — service dijaga hidup oleh restart_policy swarm. Gagal distribusi dicatat lalu lanjut ke jadwal berikutnya (⊥ membunuh loop). Tanpa `VEX_BP_PRIVATE_KEY` run nyata raise RuntimeError yang jelas — ditangkap, dicetak, dicoba lagi di jadwal berikutnya. Berbeda dengan scan: ⊥ ada "distribusi-awal" saat start (SCAN_RUN_ON_START) — membayar di container start berisiko memakai snapshot basi; distribusi hanya berjalan pada jadwal. Scheduler helper (`next_boundary`, `_wait_db`) sengaja diduplikasi dari `scan_loop.py` (bukan di-import) karena kedua image Docker terpisah: `Dockerfile.dist` ⊥ membawa `scan_loop.py`/`get_voters.py`. """ import time from datetime import datetime, timedelta import config import db import distribute WEEKDAYS = { 'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3, 'fri': 4, 'sat': 5, 'sun': 6, } def parse_weekday(value): """Nama hari (`sat`) atau angka 0-6 → weekday Python (Senin=0).""" v = str(value).strip().lower() if v in WEEKDAYS: return WEEKDAYS[v] if v.isdigit() and 0 <= int(v) <= 6: return int(v) raise ValueError(f'DISTRIBUTE_WEEKDAY tak valid: {value!r}') def parse_first_run(value): """`YYYY-MM-DD` → date; kosong → None (⊥ one-off).""" if not value: return None return datetime.strptime(str(value).strip(), '%Y-%m-%d').date() def next_boundary(now, hour, weekday, first_run=None): """datetime berikutnya utk distribusi. Mingguan: `weekday` jam `hour`. Bila `first_run` diset, distribusi ⊥ jalan sebelum tanggal itu — Sabtu (atau hari mingguan) yang jatuh sebelum launch dilewati; event = {one-off `first_run` (jika masih depan)} ∪ {mingguan ≥ tanggal `first_run`}; pilih yang terdekat > `now`.""" days_ahead = (weekday - now.weekday()) % 7 weekly = (now.replace(hour=hour, minute=0, second=0, microsecond=0) + timedelta(days=days_ahead)) if weekly <= now: weekly += timedelta(days=7) if first_run is None: return weekly start = datetime(first_run.year, first_run.month, first_run.day, hour=hour) candidates = [] if start >= now: candidates.append(start) if weekly >= start: candidates.append(weekly) return min(candidates) def _wait_db(timeout=120): """Tunggu basis data siap (mariadb baru boot) sebelum loop.""" start = time.monotonic() while time.monotonic() - start < timeout: try: db.queryone('SELECT 1') return except Exception: time.sleep(3) raise RuntimeError('basis data tak siap setelah menunggu') def _run_distribute(label): print(f'[{label}] memulai distribusi...', flush=True) try: distribute.main() except Exception as exc: print(f'[Error] Distribusi gagal: {exc}', flush=True) def main(): hour = config.DISTRIBUTE_HOUR if not 0 <= hour <= 23: raise ValueError(f'DISTRIBUTE_HOUR tak valid: {hour!r}') weekday = parse_weekday(config.DISTRIBUTE_WEEKDAY) first_run = parse_first_run(config.DISTRIBUTE_FIRST_RUN) jadwal = (f'jam {hour} tiap hari {list(WEEKDAYS)[weekday]} (waktu lokal)' + (f', one-off {first_run.isoformat()}' if first_run else '')) print(f'Jadwal distribusi: {jadwal}') _wait_db() while True: now = datetime.now() target = next_boundary(now, hour, weekday, first_run) wait = (target - now).total_seconds() print(f'{now.isoformat(timespec="seconds")} tidur {wait / 3600:.2f} jam ' f'sampai {target.isoformat(timespec="seconds")}', flush=True) time.sleep(max(wait, 0)) _run_distribute('distribusi terjadwal') if __name__ == '__main__': main()