Files
databisnisid/distribute_loop.py
T

118 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()