118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""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()
|