feature: add runner online count to failure monitor (#16408)
This commit is contained in:
@@ -34,6 +34,7 @@ jobs:
|
|||||||
- name: Run Failure Analysis
|
- name: Run Failure Analysis
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
|
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
|
||||||
|
GH_PAT_FOR_RUNNER_ADMIN: ${{ secrets.GH_PAT_FOR_RUNNER_ADMIN }}
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
PYTHONIOENCODING: utf-8
|
PYTHONIOENCODING: utf-8
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -144,6 +144,85 @@ class SGLangFailuresAnalyzer:
|
|||||||
print(f"Error fetching logs for job {job_id}: {e}")
|
print(f"Error fetching logs for job {job_id}: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def get_online_runners(self) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
Fetch all self-hosted runners and their online status from GitHub API.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping runner label sets to their online/total counts.
|
||||||
|
E.g., {"8-gpu-h200-runner": {"online": 2, "total": 3, "busy": 1}}
|
||||||
|
"""
|
||||||
|
print("Fetching self-hosted runner status...")
|
||||||
|
try:
|
||||||
|
# Use separate admin token if available (needs repo admin scope)
|
||||||
|
runner_token = os.environ.get("GH_PAT_FOR_RUNNER_ADMIN") or self.token
|
||||||
|
runner_headers = {
|
||||||
|
"Authorization": f"token {runner_token}",
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
all_runners = []
|
||||||
|
url = f"{self.base_url}/repos/{self.repo}/actions/runners"
|
||||||
|
params = {"per_page": 100}
|
||||||
|
|
||||||
|
while url:
|
||||||
|
response = requests.get(
|
||||||
|
url, headers=runner_headers, params=params, timeout=30
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(
|
||||||
|
f" Warning: Runner API returned {response.status_code}: {response.text[:200]}"
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
data = response.json()
|
||||||
|
runners = data.get("runners", [])
|
||||||
|
all_runners.extend(runners)
|
||||||
|
|
||||||
|
# Check for next page in Link header
|
||||||
|
link_header = response.headers.get("Link", "")
|
||||||
|
next_url = None
|
||||||
|
if link_header:
|
||||||
|
links = link_header.split(", ")
|
||||||
|
for link in links:
|
||||||
|
if 'rel="next"' in link:
|
||||||
|
next_url = link.split(";")[0].strip("<>")
|
||||||
|
break
|
||||||
|
url = next_url
|
||||||
|
params = {} # Clear params for subsequent requests
|
||||||
|
|
||||||
|
print(f" Found {len(all_runners)} self-hosted runners")
|
||||||
|
|
||||||
|
# Group runners by their labels (excluding common labels like "self-hosted")
|
||||||
|
# A runner can have multiple labels, so count it for each relevant label
|
||||||
|
runner_stats_by_label = defaultdict(
|
||||||
|
lambda: {"online": 0, "total": 0, "busy": 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Common labels to exclude (not useful for grouping)
|
||||||
|
excluded_labels = {"self-hosted", "Linux", "X64", "ARM64"}
|
||||||
|
|
||||||
|
for runner in all_runners:
|
||||||
|
# Get all custom/relevant labels for this runner
|
||||||
|
labels = [
|
||||||
|
label.get("name", "")
|
||||||
|
for label in runner.get("labels", [])
|
||||||
|
if label.get("name", "") not in excluded_labels
|
||||||
|
]
|
||||||
|
|
||||||
|
# Count this runner for EACH of its relevant labels
|
||||||
|
for runner_label in labels:
|
||||||
|
runner_stats_by_label[runner_label]["total"] += 1
|
||||||
|
if runner.get("status") == "online":
|
||||||
|
runner_stats_by_label[runner_label]["online"] += 1
|
||||||
|
if runner.get("busy", False):
|
||||||
|
runner_stats_by_label[runner_label]["busy"] += 1
|
||||||
|
|
||||||
|
return dict(runner_stats_by_label)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error fetching runners: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
def parse_test_summary(self, logs: str) -> Optional[Dict]:
|
def parse_test_summary(self, logs: str) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
Parse the test summary block from job logs.
|
Parse the test summary block from job logs.
|
||||||
@@ -967,6 +1046,7 @@ class SGLangFailuresAnalyzer:
|
|||||||
runner_instance_data: Optional[Dict[str, Dict]] = None,
|
runner_instance_data: Optional[Dict[str, Dict]] = None,
|
||||||
runner_streak_data: Optional[Dict[str, Dict]] = None,
|
runner_streak_data: Optional[Dict[str, Dict]] = None,
|
||||||
runner_instance_streak_data: Optional[Dict[str, Dict]] = None,
|
runner_instance_streak_data: Optional[Dict[str, Dict]] = None,
|
||||||
|
online_runners: Optional[Dict[str, Dict]] = None,
|
||||||
# Test failures (per job -> per test)
|
# Test failures (per job -> per test)
|
||||||
job_test_failures: Optional[Dict[str, Dict[str, Dict]]] = None,
|
job_test_failures: Optional[Dict[str, Dict[str, Dict]]] = None,
|
||||||
# Config
|
# Config
|
||||||
@@ -1090,6 +1170,7 @@ class SGLangFailuresAnalyzer:
|
|||||||
runner_instance_streak_data if runner_instance_streak_data else {}
|
runner_instance_streak_data if runner_instance_streak_data else {}
|
||||||
),
|
),
|
||||||
"job_test_failures": job_test_failures if job_test_failures else {},
|
"job_test_failures": job_test_failures if job_test_failures else {},
|
||||||
|
"online_runners": online_runners if online_runners else {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save to JSON only if output file is specified
|
# Save to JSON only if output file is specified
|
||||||
@@ -1160,23 +1241,24 @@ class SGLangFailuresAnalyzer:
|
|||||||
summary_lines.append("</details>")
|
summary_lines.append("</details>")
|
||||||
summary_lines.append("")
|
summary_lines.append("")
|
||||||
|
|
||||||
# Queue Time Summary - COLLAPSIBLE
|
# Runner Statistics - COLLAPSIBLE
|
||||||
runner_stats = report_data.get("runner_stats", {})
|
runner_stats = report_data.get("runner_stats", {})
|
||||||
|
online_runners = report_data.get("online_runners", {})
|
||||||
if runner_stats:
|
if runner_stats:
|
||||||
summary_lines.append("<details>")
|
summary_lines.append("<details>")
|
||||||
summary_lines.append(
|
summary_lines.append(
|
||||||
"<summary>📊 Queue Time Summary by Runner Type (click to expand)</summary>"
|
"<summary>📊 Runner Statistics (by type) (click to expand)</summary>"
|
||||||
)
|
)
|
||||||
summary_lines.append("")
|
summary_lines.append("")
|
||||||
summary_lines.append(
|
summary_lines.append(
|
||||||
"_High queue times indicate that runner type may need more workers._"
|
"_High queue times indicate that runner type may need more workers. Online column shows current runner availability._"
|
||||||
)
|
)
|
||||||
summary_lines.append("")
|
summary_lines.append("")
|
||||||
summary_lines.append(
|
summary_lines.append(
|
||||||
"| Runner Type | Avg Queue | P90 Queue | # of Jobs Processed | Jobs Using This Runner |"
|
"| Runner Type | Online | Avg Queue | P90 Queue | # of Jobs Processed | Jobs Using This Runner |"
|
||||||
)
|
)
|
||||||
summary_lines.append(
|
summary_lines.append(
|
||||||
"|-------------|-----------|-----------|------|------------------------|"
|
"|-------------|--------|-----------|-----------|---------------------|------------------------|"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sort by P90 queue time descending (longest waits first)
|
# Sort by P90 queue time descending (longest waits first)
|
||||||
@@ -1191,6 +1273,25 @@ class SGLangFailuresAnalyzer:
|
|||||||
p90_queue = stats.get("p90_queue_time_seconds", 0)
|
p90_queue = stats.get("p90_queue_time_seconds", 0)
|
||||||
total_jobs = stats.get("total_jobs", 0)
|
total_jobs = stats.get("total_jobs", 0)
|
||||||
|
|
||||||
|
# Get online runner count for this runner type
|
||||||
|
# First try exact match, then fall back to substring match
|
||||||
|
online_count = online_runners.get(runner_key)
|
||||||
|
if not online_count:
|
||||||
|
# Fall back to substring match (but prefer longer matches)
|
||||||
|
best_match = None
|
||||||
|
best_match_len = 0
|
||||||
|
for online_key, online_stats in online_runners.items():
|
||||||
|
if online_key in runner_key or runner_key in online_key:
|
||||||
|
# Prefer longer matching keys (more specific)
|
||||||
|
if len(online_key) > best_match_len:
|
||||||
|
best_match = online_stats
|
||||||
|
best_match_len = len(online_key)
|
||||||
|
online_count = best_match
|
||||||
|
if online_count:
|
||||||
|
online_str = f"{online_count['online']}/{online_count['total']}"
|
||||||
|
else:
|
||||||
|
online_str = "N/A"
|
||||||
|
|
||||||
# Get unique job names that run on this runner
|
# Get unique job names that run on this runner
|
||||||
jobs_total = stats.get("jobs_total", {})
|
jobs_total = stats.get("jobs_total", {})
|
||||||
unique_jobs = list(jobs_total.keys())
|
unique_jobs = list(jobs_total.keys())
|
||||||
@@ -1214,11 +1315,11 @@ class SGLangFailuresAnalyzer:
|
|||||||
# Highlight if P90 queue time > 10 minutes (potential bottleneck)
|
# Highlight if P90 queue time > 10 minutes (potential bottleneck)
|
||||||
if p90_queue > 600:
|
if p90_queue > 600:
|
||||||
summary_lines.append(
|
summary_lines.append(
|
||||||
f"| <span style='color:orange'>`{display_name}`</span> | <span style='color:orange'>{avg_str}</span> | <span style='color:orange'>{p90_str}</span> | <span style='color:orange'>{total_jobs}</span> | {jobs_str} |"
|
f"| <span style='color:orange'>`{display_name}`</span> | <span style='color:orange'>{online_str}</span> | <span style='color:orange'>{avg_str}</span> | <span style='color:orange'>{p90_str}</span> | <span style='color:orange'>{total_jobs}</span> | {jobs_str} |"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
summary_lines.append(
|
summary_lines.append(
|
||||||
f"| `{display_name}` | {avg_str} | {p90_str} | {total_jobs} | {jobs_str} |"
|
f"| `{display_name}` | {online_str} | {avg_str} | {p90_str} | {total_jobs} | {jobs_str} |"
|
||||||
)
|
)
|
||||||
|
|
||||||
summary_lines.append("")
|
summary_lines.append("")
|
||||||
@@ -1949,6 +2050,9 @@ def main():
|
|||||||
runner_instance_streak_data,
|
runner_instance_streak_data,
|
||||||
) = analyzer.analyze_runner_health(runner_runs)
|
) = analyzer.analyze_runner_health(runner_runs)
|
||||||
|
|
||||||
|
# Fetch online runner status
|
||||||
|
online_runners = analyzer.get_online_runners()
|
||||||
|
|
||||||
# Analyze test-level failures for broken/high-failure-rate jobs
|
# Analyze test-level failures for broken/high-failure-rate jobs
|
||||||
# Combine all scheduled data for test failure analysis (main branch, most important)
|
# Combine all scheduled data for test failure analysis (main branch, most important)
|
||||||
all_scheduled_data = {
|
all_scheduled_data = {
|
||||||
@@ -1993,6 +2097,7 @@ def main():
|
|||||||
runner_instance_data,
|
runner_instance_data,
|
||||||
runner_streak_data,
|
runner_streak_data,
|
||||||
runner_instance_streak_data,
|
runner_instance_streak_data,
|
||||||
|
online_runners,
|
||||||
# Test failures
|
# Test failures
|
||||||
job_test_failures,
|
job_test_failures,
|
||||||
# Config
|
# Config
|
||||||
|
|||||||
Reference in New Issue
Block a user