Maqola · 2026-08-16 · ~12 daqiqa o‘qiladi · o'rta
Botda rejalashtirilgan vazifalar: haftalik hisobot va anomaliya-alerting
Mundarija
Avvalgi qismlarda bot so'ralganda javob berdi. Endi teskari yo'nalish: Humo botida adminlar dushanba ertalab haftalik tahlilni, oy boshida oylik hisobotni va kun davomida g'ayrioddiy o'zgarish bo'lsa — darhol ogohlantirishni so'ramasdan oladi. Buning tagida ikkita kichik, lekin puxta o'ylangan mexanizm bor: restart'ga chidamli rejalashtiruvchi va oddiy (AI'siz!) anomaliya mantiqiy.
Qaysi asbob: cron, systemd timer yoki bot ichidagi loop?#
| Variant | Qachon mos |
|---|---|
| cron / systemd timer | Alohida skript sifatida ishlaydigan vazifa (backup, tozalash) — DevOps seriyasida ko'ramiz |
| Celery beat | Navbat allaqachon bor bo'lgan katta tizim — Celery haqida |
| Bot ichidagi asyncio loop | Bot doim ishlab turgan jarayon; vazifa bot konteksti (bot obyekti, adminlar ro'yxati) bilan ishlashi kerak |
Bot allaqachon 24/7 jarayon — unga yana bitta asyncio vazifa qo'shish eng arzon va eng sodda yo'l. Lekin ikkita tuzoqni hal qilish shart: restart (bot har deploy'da qayta ko'tariladi — hisobot ikki marta ketmasin yoki umuman o'tib ketmasin) va xato (bitta yiqilgan vazifa butun scheduler'ni o'ldirmasin).
1-qadam: davr kalitlari — restart'ga chidamlilikning siri#
Yechim g'oyasi: har vazifa o'z davri uchun bitta kalit oladi — haftalik
hisobot uchun "2026-W33", oylik uchun "2026-07", kunlik tekshiruv uchun
"2026-08-16". Yuborilgan kalitlar faylda saqlanadi. Restart bo'ldimi, o'chib
yondimi — kalit faylda bor ekan, vazifa qayta yuborilmaydi; fayl yo'q
bo'lsa — keyingi tekshiruvda yuboriladi. Cron'dagi "o'sha daqiqada o'chiq
bo'lsang, o'tib ketdi" muammosi ham yo'qoladi.
bot/
├── scheduler/
│ ├── __init__.py
│ ├── periods.py ← davr hisob-kitobi (sof funksiyalar — testlanadi)
│ ├── state.py ← yuborilgan kalitlar fayli
│ └── runner.py ← asyncio loop
├── reports/
│ ├── weekly.py ← hisobot matnini tuzish
│ └── anomaly.py ← anomaliya mantiqiy
└── app.py
# bot/scheduler/periods.py
from datetime import datetime, timedelta, timezone
# Toshkent: UTC+5, DST yo'q — shuning uchun oddiy timedelta bilan xavfsiz
TZ = timezone(timedelta(hours=5))
WEEKLY_HOUR = 9 # dushanba 09:00
MONTHLY_DAY = 1 # oyning 1-kuni
ANOMALY_HOUR = 20 # har kuni 20:00 da kunlik tekshiruv
def week_key(now: datetime) -> str:
"""O'tgan TO'LIQ hafta kaliti: '2026-W33'."""
last_sunday = now - timedelta(days=now.isoweekday())
year, week, _ = last_sunday.isocalendar()
return f"{year}-W{week:02d}"
def month_key(now: datetime) -> str:
"""O'tgan oy kaliti: '2026-07'."""
first_of_month = now.replace(day=1)
prev = first_of_month - timedelta(days=1)
return f"{prev.year}-{prev.month:02d}"
def due_task(now: datetime, sent: set[str]) -> tuple[str, str] | None:
"""Navbatdagi BITTA vazifa: (tur, davr_kaliti) yoki None.
Bitta chaqiruvda bitta vazifa — ikkitasi to'planib qolgan bo'lsa
(masalan bot bir hafta o'chiq turdi), navbatma-navbat yuboriladi,
adminlar oqimi bosilmaydi.
"""
if now.isoweekday() == 1 and now.hour >= WEEKLY_HOUR:
key = f"weekly:{week_key(now)}"
if key not in sent:
return ("weekly", key)
if now.day == MONTHLY_DAY and now.hour >= WEEKLY_HOUR:
key = f"monthly:{month_key(now)}"
if key not in sent:
return ("monthly", key)
if now.hour >= ANOMALY_HOUR:
key = f"anomaly:{now.date().isoformat()}"
if key not in sent:
return ("anomaly", key)
return None
Nega >= — aynan soatga tenglik emas
now.hour == 9 sharti bot 09:00 da bir daqiqa o'chiq bo'lsa vazifani
yo'qotadi. >= bilan esa "kechiksa ham yuboradi": 09:00 da o'chiq edi —
11:30 da ko'tarildi — hisobot 11:30 da ketadi. Davr kaliti takrorlanishdan
saqlaydi, >= o'tkazib yuborishdan.
2-qadam: holat fayli#
# bot/scheduler/state.py
import json
from pathlib import Path
STATE_FILE = Path("var/scheduler_state.json")
def load_sent() -> set[str]:
if not STATE_FILE.exists():
return set()
return set(json.loads(STATE_FILE.read_text()))
def mark_sent(key: str) -> None:
sent = load_sent()
sent.add(key)
STATE_FILE.parent.mkdir(exist_ok=True)
# oxirgi 200 kalit yetarli — fayl cheksiz o'smasin
STATE_FILE.write_text(json.dumps(sorted(sent)[-200:], ensure_ascii=False))
3-qadam: anomaliya — AI emas, arifmetika#
"Anomaliya" so'zi qo'rqinchli eshitiladi, lekin ko'p hollarda oddiy taqqoslash yetadi: bugungi ko'rsatkich o'tgan ikki haftaning shu kunlaridagi o'rtachasidan keskin farq qilyaptimi?
# bot/reports/anomaly.py
from dataclasses import dataclass
THRESHOLD = 0.5 # ±50% dan katta og'ish — e'tiborga loyiq
@dataclass(frozen=True)
class Anomaly:
metric: str
today: float
baseline: float
@property
def change_pct(self) -> int:
return round((self.today - self.baseline) / self.baseline * 100)
def detect(metrics: dict[str, float], baselines: dict[str, float]) -> list[Anomaly]:
"""metrics: bugungi qiymatlar; baselines: o'tgan davr o'rtachasi."""
found = []
for name, today in metrics.items():
base = baselines.get(name)
if not base: # tarix yo'q — taqqoslab bo'lmaydi
continue
if abs(today - base) / base > THRESHOLD:
found.append(Anomaly(name, today, base))
return found
Bu 15 qator kod Humo'da "bugun so'rovlar soni odatdagidan 70% kam — API'da muammo yo'qmi?" degan xabarni beradi. AI bu yerda shart emas — chegara aniq, mantiq tekshiriladigan. (AI'ning o'rni — topilgan anomaliyaga izoh yozish; buni keyingi qismda ko'ramiz.)
Avval deterministik, keyin aqlli
Alerting'da eng muhimi — ishonch: "bot jim = hammasi joyida" ishonchi. Deterministik mantiqni tekshirish oson, LLM'nikini — yo'q. Shuning uchun signal chiqarishni formulaga, signalni chiroyli tushuntirishni esa AI'ga bering — 1-qismdagi ajratish printsipi bu yerda ham ishlaydi.
4-qadam: runner — o'lmaydigan loop#
# bot/scheduler/runner.py
import asyncio
import logging
from datetime import datetime
from bot.reports import anomaly, weekly
from bot.scheduler.periods import TZ, due_task
from bot.scheduler.state import load_sent, mark_sent
log = logging.getLogger(__name__)
CHECK_EVERY = 60 # soniya
TASKS = {
"weekly": weekly.send_weekly_report,
"monthly": weekly.send_monthly_report,
"anomaly": anomaly.run_daily_check,
}
async def scheduler_loop(bot):
while True:
try:
now = datetime.now(TZ)
task = due_task(now, load_sent())
if task is not None:
kind, key = task
await TASKS[kind](bot) # vazifaning o'zi
mark_sent(key) # MUVAFFAQIYATDAN KEYIN belgilanadi
log.info("scheduler: %s yuborildi", key)
except Exception:
# bitta xato loop'ni o'ldirmasin: log yozamiz, keyingi aylanishda
# qayta urinamiz (kalit belgilanmagan — vazifa yo'qolmaydi)
log.exception("scheduler xatosi")
await asyncio.sleep(CHECK_EVERY)
# bot/app.py (ishga tushirish)
async def main():
# scheduler bot polling bilan YONMA-YON yashaydi
asyncio.create_task(scheduler_loop(bot))
await dp.start_polling(bot)
Ikki nozik qaror izohlansin:
mark_sent(key)vazifa muvaffaqiyatli o'tgandan keyin chaqiriladi: yuborish yiqilsa, kalit yozilmaydi va keyingi daqiqada qayta uriniladi. Teskari tartibda hisobot "yuborildi deb hisoblanib", aslida yo'qolardi;try/exceptbutun aylanani o'raydi: scheduler — uzun umrli jarayon, undagi istisno "bir martalik xato" emas, "bugungi hisobotlar o'ldi" degani.
5-qadam: test — vaqtni qo'lda berish#
due_task sof funksiya — datetime'ni qo'lda berib, har stsenariyni tekshiramiz:
# tests/test_periods.py
from datetime import datetime
from bot.scheduler.periods import TZ, due_task
def dt(day, hour):
return datetime(2026, 8, day, hour, 0, tzinfo=TZ) # 2026-08-17 — dushanba
def test_weekly_fires_monday_after_nine():
assert due_task(dt(17, 9), set()) == ("weekly", "weekly:2026-W33")
def test_weekly_not_resent_same_week():
sent = {"weekly:2026-W33"}
# dushanba kuni hisobot ketgan; endi navbat kunlik tekshiruvda (20:00 dan keyin)
assert due_task(dt(17, 21), sent) == ("anomaly", "anomaly:2026-08-17")
def test_quiet_before_hours():
assert due_task(dt(18, 8), {"weekly:2026-W33"}) is None
def test_missed_monday_still_fires_tuesday():
# bot dushanba butun kun o'chiq edi — seshanba ertalab baribir hafta kaliti yo'q...
# lekin due_task faqat dushanba tekshiradi: bu qat'iy qaror — hafta hisobot
# 24 soatdan ortiq kechiksa, keyingi haftani kutgan ma'qul (eskirgan tahlil
# foydali emas). O'zingizga moslang: shartga `<= 2` kun qo'shsangiz ham bo'ladi
assert due_task(dt(18, 9), set()) is None
Xulosa#
- Bot ichidagi asyncio loop — doim ishlab turgan jarayon uchun eng sodda scheduler; cron va systemd timer alohida skriptlar dunyosi;
- Restart'ga chidamlilik — davr kalitlari bilan:
weekly:2026-W33faylda bor ekan, hisobot takrorlanmaydi; yo'q ekan — kechikib bo'lsa ham yuboriladi; mark_sentfaqat muvaffaqiyatdan keyin — xato vazifani yo'qotmaydi;- Anomaliya uchun AI kerak emas — o'rtachadan og'ish formulasi yetadi, u tekshiriladi va unga ishoniladi;
- Sof
due_taskfunksiyasi — scheduler'ning butun aqli, va u to'liq testlanadi.
Seriya yakunida eng qizig'i: AI agent'ni botga integratsiya qilish — adminlar bazaga erkin savol beradi, agent SQL bilan javob topadi, suhbat tarixi va zaxira model bilan.