Background Jobs Monitor
- summary:
פיצ’ר ה-Background Jobs Monitor מספק נראות (Observability) מלאה לכל ה-Jobs הרצים ברקע במערכת, כולל פעולות משתמש דינמיות (Drive, Reminders, Batch Operations).
סקירה כללית
מה הפיצ’ר נותן:
רישום מרכזי של כל ה-Jobs במערכת (
JobRegistry)מעקב בזמן אמת אחרי הרצות (
JobTracker)דשבורד ויזואלי ב-WebApp:
GET /jobs/monitorפקודות ChatOps בטלגרם:
/jobsהתראות אוטומטיות על כשלים ו-Jobs תקועים
API מלא לצריכה פרוגרמטית
רכיבים וקוד רלוונטי
קובץ |
תפקיד |
|---|---|
|
רישום הגדרות Jobs (Singleton) |
|
מעקב הרצות + שמירה ל-DB |
|
הגדרת אינדקסים ו-TTL |
|
aiohttp API (Bot-side) |
|
Flask API + דשבורד (WebApp-side) |
|
תבנית הדשבורד |
|
פקודת |
|
הגדרות Alerts ל-Jobs |
מסד נתונים
קולקציה: job_runs
כל הרצה נשמרת בקולקציה job_runs, ונמחקת אוטומטית בתום חלון השמירה —
JOB_RUNS_TTL_DAYS, ברירת מחדל 30 ימים.
אזהרה
עד אישיו #3331 העמוד הזה הבטיח TTL של 7 ימים, והוא לא היה קיים במסד:
קובץ ההגדרות שהכיל אותו לא יובא מאף מקום, ולכן שום אינדקס TTL לא נוצר.
החלון הורחב ל-30 יום כי ארבעה קוראים עובדים בלי חסם זמן — ”20 ההרצות
האחרונות“ של ג’וב שרץ לעיתים רחוקות, /jobs failed ב-ChatOps, וקישורי
?run_id= קבועים שמחולקים בטלגרם. מעבר לחלון, כל אלה מפסיקים למצוא את
ההרצה.
שדות עיקריים:
{
"run_id": "abc123def456", // מזהה ייחודי להרצה
"job_id": "cache_warming", // מזהה ה-Job
"status": "running", // pending|running|completed|failed|skipped
"progress": 45, // אחוזים (0-100)
"total_items": 100, // סה"כ פריטים
"processed_items": 45, // פריטים שעובדו
"started_at": ISODate(...),
"ended_at": ISODate(...),
"trigger": "scheduled", // scheduled|manual|api
"user_id": 123456, // אם קשור למשתמש ספציפי
"logs": [...], // עד 50 רשומות אחרונות
"error_message": "...", // במקרה כישלון
"failure_reason": "orphaned", // רק כשההרצה נסגרה מבחוץ (ראו "פיוס הרצות יתומות")
"owner_id": "srv-a1b2:71", // המופע שהריץ — לאבחון
"result": {...} // תוצאה סופית
}
אינדקסים:
הרשימה נמשכת מהמקור בזמן הבנייה, ולא מועתקת לכאן — בדיוק כדי שלא תתאר שוב מצב שאינו קיים. כל אינדקס והשאילתה שמצדיקה אותו מתועדים ב-docstring של הקובץ.
def job_runs_indexes() -> list[dict]:
"""הגדרות האינדקסים, בצורה ש-``safe_create_index`` מקבלת.
``expire_after_seconds`` מופיע רק על אינדקס **חד-שדה**: *"TTL indexes are
single-field indexes. Compound indexes do not support TTL and ignore the
expireAfterSeconds option"*
(מקור: https://www.mongodb.com/docs/manual/core/index-ttl/).
⚠️ **התיעוד אומר "ignore", והשרת עושה משהו אחר.** נמדד מול mongod 7.0.14:
היצירה נדחית ב-``OperationFailure`` — *"TTL indexes are single-field indexes,
compound indexes do not support TTL"*. בשתי ההתנהגויות המסקנה זהה — אינדקס
מורכב אינו נושא TTL — אבל מי שמסתמך על "ignore" יקבל אינדקס שלא נוצר בכלל.
"""
return [
{
"keys": [("run_id", 1)],
# מזהה ההרצה: עדכוני סטטוס וקישורים עמוקים עובדים דרכו.
"name": "idx_job_runs_id",
"unique": True,
},
{
"keys": [("job_id", 1), ("started_at", -1)],
"name": "idx_job_runs_job_time",
},
{
"keys": [("status", 1), ("started_at", -1)],
"name": "idx_job_runs_status_time",
},
{
# עולה ולא יורד: אינדקס חד-שדה נסרק לשני הכיוונים, ולכן המיון היורד
# ב-``$group`` עדיין נתמך, והכיוון אינו משנה ל-TTL.
"keys": [("started_at", 1)],
"name": "idx_job_runs_ttl",
"expire_after_seconds": job_runs_ttl_seconds(),
# בלי אכיפה, שינוי של JOB_RUNS_TTL_DAYS היה מתנגש עם האינדקס הקיים
# ונבלע כאזהרה — ה-retention הישן היה ממשיך למחוק לפי הערך הישן.
"enforce": True,
},
]
DatabaseManager._create_indexes קורא לפונקציה הזו בכל התחברות למסד, ומעביר
כל הגדרה ל-safe_create_index.
הערה
ה-TTL יושב על אינדקס חד-שדה נפרד. לפי MongoDB Manual, TTL Indexes אינדקס מורכב מתעלם
מ-expireAfterSeconds; מול mongod 7.0.14 נמדד שהשרת דוחה יצירה כזו
בשגיאה. כך או כך, אינדקס מורכב אינו מוחק מסמכים.
פיוס הרצות יתומות
הרצה מסומנת running כשהיא מתחילה, והסטטוס הסופי נכתב כשהיא נגמרת. אם
התהליך מת באמצע — דיפלוי, SIGKILL, קריסה — אף אחד לא כותב את הסוף.
הרשומה נשארת running עד שה-TTL מוחק אותה, ובינתיים היא נראית בדשבורד
כהרצה חיה ומוחזרת מ-/api/jobs/active.
איך מזהים. ג’ובים רצים רק תחת מנעול ה-singleton של מונגו, ויש לו מחזיק
אחד בכל רגע. לכן הרצה שהתחילה לפני שהתהליך הנוכחי קיבל את המנעול שייכת
למחזיק קודם. המבחן הוא זמן ולא זהות: owner_id נגזר מ-RENDER_INSTANCE_ID
ומ-PID, ו-PIDים חוזרים בקונטיינרים.
מה נכתב. status עובר ל-failed ולא לערך חדש — JobStatus זורק על
ערך שאינו מוכר, ומסנן ”ההרצות האחרונות“ בוובאפ מונה את הסטטוסים במפורש, כך
שערך חדש היה בלתי נראה בשני המקומות. הסיבה נכתבת בנפרד ב-failure_reason,
וכך /jobs failed ב-ChatOps מבדיל בין ג’וב שנפל בקוד שלו להרצה שאיש לא סגר.
למה באיחור. הפיוס אינו רץ מיד בעלייה אלא אחרי JOBS_ORPHAN_RECONCILE_DELAY_SECS.
בזמן דיפלוי התהליך הישן מקבל מ-Render זמן חסד לסגור, והוא יכול לסיים בהצלחה
הרצה שכבר באוויר. פיוס מיידי היה מסמן אותה ככשל בזמן שהיא עדיין עובדת.
ההשהיה גם מייתרת את שאלת הפרשי השעון בין קונטיינרים.
אזהרה
ההשהיה מכסה את מסלול הדיפלוי, שבו SIGKILL מבטיח שהתהליך הישן מת. היא
אינה מכסה את המסלול שבו ה-heartbeat של המחזיק הקודם נכשל בלי דיפלוי —
שם אין SIGKILL, והתהליך הישן יוצא רק כשבדיקת הבעלות שלו מספיקה לרוץ
(אישיו #3452).
במקרה הזה רשומה אחת עלולה להיות מסומנת failed אף שההרצה חיה. הפיוס
כותב לרשומה בלבד ואינו עוצר שום הרצה, וכתיבה סופית של ההרצה עצמה לעולם
אינה נחסמת — כלומר אם ההרצה מגיעה לסופה, התוצאה האמיתית דורסת את הסימון.
הגנה על הכתיבה. כל מסמך מעודכן בנפרד, בתנאי status: "running". הרצה
שהספיקה להסתיים בין השליפה לעדכון אינה נדרסת, והדחייה נרשמת ואינה נבלעת.
בכיוון הנגדי, _persist_run אינו מחזיר הרצה שהגיעה לסטטוס סופי בחזרה
ל-running — בלי זה עדכון התקדמות מאוחר היה פותח את הרשומה מחדש לנצח,
כי הפיוס רץ פעם אחת בעלייה ואינו חוזר.
בסיום נשלח אירוע job_runs_reconciled אחד עם המניין, ולא אירוע לכל הרצה.
תקרת זמן על הפיוס כולו. לקוח המוטור שהפיוס רץ עליו (_get_scheduler_motor_db
ב-main.py) נבנה בלי socketTimeoutMS, ובלי דדליין משלו קריאה שנתקעה על
הסוקט הייתה תוקעת ג’וב חד-פעמי לנצח — בשקט. ``reconcile_job_callback`` עוטף את
הפיוס ב-asyncio.wait_for עם RECONCILE_TIMEOUT_SECS, וכל תוצאה — דילוג,
חיתוך, כשל, או ”לא נמצא כלום“ — נרשמת בלוג בשם אירוע משלה (ראו קטלוג אירועים קנוניים).
הלוגים של ההרצה נכתבים בתוספת. הפיוס דוחף שורת לוג להרצה שסגר; כדי שהשורה
הזו תשרוד גם אם ההרצה עוד חיה ותכתוב את סופה, _persist_run דוחף רק את
השורות שטרם נכתבו ($push עם $slice) ואינו מחליף את המערך כולו.
UI / WebApp
גישה לדשבורד
נכנסים ל: https://your-app.onrender.com/jobs/monitor
הערה
הדשבורד דורש הרשאת Admin או Token תקין.
מה רואים בדשבורד
רשימת Jobs – כל ה-Jobs הרשומים במערכת עם סטטוס (מופעל/מושבת)
הרצות פעילות – Jobs שרצים כרגע עם אחוז התקדמות
היסטוריה – הרצות אחרונות (מה-DB)
לוגים – לחיצה על הרצה פותחת Modal עם לוגים מפורטים
Jobs סטטיים vs. דינמיים
סוג |
הסבר |
|
|---|---|---|
סטטי |
מוגדר ב- |
|
דינמי |
נוצר מפעולות משתמש (Drive/Reminders/Batch) |
|
Jobs דינמיים לא ניתנים להפעלה ידנית מהדשבורד כי אין להם callback קבוע – הם נוצרים כתגובה לפעולות משתמש ספציפיות.
API Reference
כל הנתיבים מוגנים ב-Bearer Token (DB_HEALTH_TOKEN).
GET /api/jobs
רשימת כל ה-Jobs הרשומים.
curl -H "Authorization: Bearer $DB_HEALTH_TOKEN" \
https://your-bot-api/api/jobs
תשובה:
{
"jobs": [
{
"job_id": "cache_warming",
"name": "Cache Warming",
"category": "cache",
"job_type": "repeating",
"enabled": true,
"interval_seconds": 3600,
"can_trigger": true
}
]
}
GET /api/jobs/active
הרצות פעילות בזמן אמת (מהזיכרון).
GET /api/jobs/{job_id}
פרטי Job ספציפי + היסטוריית 20 הרצות אחרונות.
GET /api/jobs/runs/{run_id}
פרטי הרצה בודדת כולל לוגים מלאים.
POST /api/jobs/{job_id}/trigger
הפעלה ידנית של Job.
תנאים:
ל-Job יש
callback_nameמוגדרה-Job מופעל (
enabled=true)אין הרצה פעילה (אלא אם
allow_concurrent=true)
curl -X POST \
-H "Authorization: Bearer $DB_HEALTH_TOKEN" \
https://your-bot-api/api/jobs/cache_warming/trigger
אבטחה
הגנה על ה-API
כל הנתיבים /api/jobs/* מוגנים ב-Bearer Token:
ב-aiohttp (Bot API): Token נבדק ב-
services/webserver.pyב-Flask (WebApp): פרוקסי לבוט עם אותו Token
הגדרה:
export DB_HEALTH_TOKEN="your-secure-random-token"
אזהרה
אל תחשפו את DB_HEALTH_TOKEN בלוגים או בצד לקוח!
ה-WebApp משתמש בו רק בצד השרת (Server-to-Server).
ENV וקונפיגורציה
משתני סביבה
משתנה |
ברירת מחדל |
תיאור |
|---|---|---|
|
(חובה) |
Token לאימות API |
|
– |
URL בסיס ל-API של הבוט (נדרש לטריגר) |
|
– |
Fallback ל- |
|
|
כמה ימים נשמרת הרצה לפני מחיקה אוטומטית (TTL על |
|
|
סף דקות לזיהוי Job תקוע |
|
|
אינטרוול בדיקת Jobs תקועים |
|
|
האם לסגור בעלייה הרצות שנשארו מתהליך קודם |
|
|
כמה שניות להמתין לפני הפיוס (מינימום 90) |
|
– |
URL לקישורים ללוגים בהודעות ChatOps |
ChatOps – פקודת /jobs
פקודות זמינות
פקודה |
תיאור |
|---|---|
|
סיכום כל ה-Jobs לפי קטגוריה |
|
הרצות פעילות כרגע + אחוז התקדמות |
|
10 כשלים אחרונים עם קישור ללוגים |
|
פרטי Job + 5 הרצות אחרונות |
|
Jobs בקטגוריה (backup/cache/sync/…) |
דוגמאות
רשימת הרצות פעילות:
/jobs active
⚡ הרצות פעילות:
🔄 `cache_warming` - 45% (450/1000)
[📋 לוגים](https://your-app/jobs/monitor?run_id=abc123)
כשלים אחרונים:
/jobs failed
❌ כשלים אחרונים:
❌ `drive_sync` 30/12 14:23
`Connection timeout after 30s`
[📋 לוגים](https://your-app/jobs/monitor?run_id=xyz789)
הערה
הקישורים ללוגים תלויים בהגדרת WEBAPP_URL.
Alerts
הגדרות ב-alerts.yml
alerts:
- name: job_failure_alert
event_pattern: "job_failed"
severity: error
cooldown_seconds: 300
message: "❌ Job {job_id} נכשל: {error}"
- name: job_stuck_alert
event_pattern: "job_stuck"
severity: critical
cooldown_seconds: 600
message: "⚠️ Job {job_id} תקוע כבר {minutes} דקות"
Flow של Alerts
JobTrackerקורא ל-emit_event("job_failed", ...)בכישלוןמערכת ה-Observability קולטת את האירוע
בדיקה מול
alerts.ymlו-cooldownשליחת התראה לערוץ המוגדר (Telegram/Slack)
זיהוי Jobs תקועים:
מתבצע ב-background loop (כל JOBS_STUCK_MONITOR_INTERVAL_SECS שניות):
סריקת
job_runsעםstatus=runningאם
started_atלפני יותר מ-JOBS_STUCK_THRESHOLD_MINUTESדקותפליטת
emit_event("job_stuck", ...)
Troubleshooting
”הטריגר לא עובד“
בעיה: לחיצה על כפתור ”הפעל“ בדשבורד לא עושה כלום.
פתרונות:
וודאו שהוגדר
BOT_JOBS_API_BASE_URL(אוBOT_API_BASE_URL)וודאו שהוגדר
DB_HEALTH_TOKENגם בבוט וגם ב-WebAppבדקו שה-URL נגיש מה-WebApp לבוט (אין firewall/network issues)
בדקו לוגים:
jobs_trigger_failedאוjobs_api_proxy_error
”אין jobs דינמיים בדשבורד“
בעיה: רואים רק Jobs סטטיים, לא רואים הרצות של Drive/Reminders.
סיבות אפשריות:
אין עדיין הרצות – המשתמש לא ביצע פעולות שיוצרות Jobs דינמיים
TTL – ההרצות נמחקו בתום חלון השמירה (
JOB_RUNS_TTL_DAYS)הרשאות – בדקו שיש גישה לקולקציית
job_runs
”לא מגיעים Alerts“
בעיה: Jobs נכשלים אבל לא מגיעות התראות.
פתרונות:
וודאו קיום ותקינות
config/alerts.ymlבדקו ש-
PyYAMLמותקןבדקו cooldown – ייתכן שהתראה נשלחה לאחרונה
וודאו שערוץ ההתראות מוגדר נכון
”Job תקוע אבל אין התראה“
בעיה: Job רץ כבר שעות ולא מגיעה התראת ”stuck“.
פתרונות:
בדקו ש-
JOBS_STUCK_THRESHOLD_MINUTESלא גדול מדיוודאו שה-background loop רץ (בדקו לוגים של
stuck_jobs_check)בדקו cooldown ב-alerts.yml (
cooldown_seconds: 600)
”השרת מחזיר 401/403“
בעיה: API מחזיר שגיאת הרשאה.
פתרונות:
וודאו שה-Token נשלח כ-
Authorization: Bearer <token>וודאו שאותו
DB_HEALTH_TOKENמוגדר בשני הצדדיםבדקו שאין רווחים/שורות חדשות ב-Token
”לוגים ריקים בדשבורד“
בעיה: לוגים לא מופיעים למרות שה-Job רץ.
סיבות:
לוגים נשמרים בפעימות (כל 10 לוגים או error)
אם ה-Job מסתיים מהר, ייתכן שהלוגים לא הספיקו להישמר
בדקו
logsבדוקומנט ב-MongoDB ישירות
Best Practices
הגדירו כל המשתנים הנדרשים לפני פריסה:
export DB_HEALTH_TOKEN="$(openssl rand -hex 32)" export BOT_JOBS_API_BASE_URL="https://your-bot.onrender.com" export WEBAPP_URL="https://your-webapp.onrender.com"
אל תשתמשו באותו Token ל-DB_HEALTH_TOKEN בסביבות שונות
הגדירו Alerts חכמים עם cooldown מתאים למניעת spam
נטרו את גודל הקולקציה – למרות ה-TTL, יכול להצטבר הרבה מידע
ראו גם
אובזרווביליות (Observability) – סקירה כללית של Observability
התראות (Alerts) – מערכת ההתראות
פקודות ChatOps – רשימת פקודות ChatOps
המיני Web App (סקירה) – סקירת WebApp