135 lines
4.8 KiB
Python
135 lines
4.8 KiB
Python
"""Jadwal distribusi: jalankan distribute.py pada hari `DISTRIBUTE_WEEKDAY`
|
||
(satu atau CSV, mis. `wed,sat`) 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=wed,sat` → putaran pertama Senin 17 Agt 10:00, berikutnya
|
||
tiap Rabu & 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_weekdays(value):
|
||
"""Satu hari (`sat`) atau CSV beberapa hari (`wed,sat`) → daftar weekday
|
||
(int, Senin=0) unik terurut. V44: jadwal bisa multi-hari per minggu."""
|
||
if not value:
|
||
raise ValueError('DISTRIBUTE_WEEKDAY kosong')
|
||
days = sorted({parse_weekday(d) for d in str(value).split(',') if d.strip()})
|
||
if not days:
|
||
raise ValueError(f'DISTRIBUTE_WEEKDAY tak valid: {value!r}')
|
||
return days
|
||
|
||
|
||
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, weekdays, first_run=None):
|
||
"""datetime berikutnya utk distribusi.
|
||
|
||
Mingguan: tiap hari di `weekdays` (list int) jam `hour`. Bila `first_run`
|
||
diset, distribusi ⊥ jalan sebelum tanggal itu — hari-hari mingguan yang
|
||
jatuh sebelum launch dilewati; event = {one-off `first_run` (jika masih
|
||
depan)} ∪ {mingguan ≥ tanggal `first_run`}; pilih yang terdekat > `now`."""
|
||
if isinstance(weekdays, int):
|
||
weekdays = [weekdays]
|
||
start = None
|
||
if first_run is not None:
|
||
start = datetime(first_run.year, first_run.month, first_run.day,
|
||
hour=hour)
|
||
candidates = []
|
||
if start is not None and start >= now:
|
||
candidates.append(start)
|
||
for weekday in weekdays:
|
||
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 start is None or 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}')
|
||
weekdays = parse_weekdays(config.DISTRIBUTE_WEEKDAY)
|
||
first_run = parse_first_run(config.DISTRIBUTE_FIRST_RUN)
|
||
hari = ' & '.join(list(WEEKDAYS)[d] for d in weekdays)
|
||
jadwal = (f'jam {hour} tiap hari {hari} (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, weekdays, 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()
|