Skip to content

Commit b234057

Browse files
committed
Add script for retrying all missed job webhooks
1 parent 5aa33ce commit b234057

File tree

1 file changed

+120
-0
lines changed

1 file changed

+120
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from concurrent.futures import Future, ThreadPoolExecutor, wait
2+
from dataclasses import dataclass
3+
from datetime import timedelta
4+
import re
5+
6+
from django.db import connections
7+
import djclick as click
8+
9+
from analytics.core.models import JobDataDimension
10+
from analytics.job_processor.utils import get_gitlab_handle
11+
12+
# The URL of the webhook handler service specified in the GitLab project settings.
13+
# This is the URL in the web_hook_logs table in the GitLab DB.
14+
WEBHOOK_URL = "http://webhook-handler.custom.svc.cluster.local"
15+
16+
17+
@dataclass
18+
class WebhookEvent:
19+
created_at: str
20+
build_id: int
21+
project_id: int
22+
webhook_id: int
23+
webhook_event_id: int
24+
25+
def __str__(self) -> str:
26+
return f"[{self.created_at}] build_id: {self.build_id}, project_id: {self.project_id}, webhook_id: {self.webhook_id}, webhook_event_id: {self.webhook_event_id}"
27+
28+
29+
def retry_webhook(webhook_event: WebhookEvent, dry_run: bool) -> None:
30+
if dry_run:
31+
click.echo(f"Would retry webhook {webhook_event}")
32+
return
33+
34+
click.echo(f"Retrying webhook {webhook_event}")
35+
gl = get_gitlab_handle()
36+
37+
# https://docs.gitlab.com/ee/api/project_webhooks.html#resend-a-project-webhook-event
38+
retry_url = f"/projects/{webhook_event.project_id}/hooks/{webhook_event.webhook_id}/events/{webhook_event.webhook_event_id}/resend"
39+
gl.http_post(retry_url)
40+
41+
42+
@click.command()
43+
@click.option(
44+
"--seconds",
45+
type=int,
46+
default=timedelta(days=1).total_seconds(),
47+
help="Retry webhooks that failed in the last N seconds",
48+
)
49+
@click.option(
50+
"--dry-run",
51+
is_flag=True,
52+
default=False,
53+
help="Print the webhooks that would be retried without actually retrying them",
54+
)
55+
def retry_failed_job_webhooks(seconds: int, dry_run: bool) -> None:
56+
with connections["gitlab"].cursor() as cursor:
57+
cursor.execute("BEGIN;")
58+
59+
cursor.execute(
60+
"""
61+
DECLARE webhook_cursor CURSOR FOR
62+
SELECT created_at, request_data, web_hook_id, id
63+
FROM public.web_hook_logs
64+
WHERE url = %s
65+
AND created_at > NOW() - INTERVAL %s;
66+
""",
67+
[WEBHOOK_URL, f"{seconds} seconds"],
68+
)
69+
70+
futures: list[Future] = []
71+
72+
with ThreadPoolExecutor() as executor:
73+
while True:
74+
# Fetch a batch of rows from the cursor
75+
cursor.execute("FETCH FORWARD %s FROM webhook_cursor", [5000])
76+
rows = cursor.fetchall()
77+
if not rows:
78+
break
79+
80+
webhook_events = [
81+
WebhookEvent(
82+
created_at=row[0],
83+
build_id=int(re.search(r"build_id: (\d+)", row[1]).group(1)),
84+
project_id=int(re.search(r"project_id: (\d+)", row[1]).group(1)),
85+
webhook_id=row[2],
86+
webhook_event_id=row[3],
87+
)
88+
for row in rows
89+
]
90+
91+
# Build a mapping of build ID to webhook event object for fast lookup by build ID
92+
build_id_to_webhook_mapping: dict[int, WebhookEvent] = {
93+
event.build_id: event for event in webhook_events
94+
}
95+
96+
# Collect all build IDs
97+
build_ids: set[int] = set(build_id_to_webhook_mapping.keys())
98+
99+
# Filter out build IDs that already have a corresponding JobDataDimension record
100+
existing_build_ids: set[int] = set(
101+
JobDataDimension.objects.filter(job_id__in=build_ids).values_list(
102+
"job_id", flat=True
103+
)
104+
)
105+
106+
# Calculate the missing build IDs
107+
missing_build_ids: set[int] = build_ids - existing_build_ids
108+
109+
# Retry the webhooks for the missing build IDs
110+
for build_id in missing_build_ids:
111+
futures.append(
112+
executor.submit(
113+
retry_webhook, build_id_to_webhook_mapping[build_id], dry_run
114+
)
115+
)
116+
117+
cursor.execute("CLOSE webhook_cursor;")
118+
cursor.execute("COMMIT;")
119+
120+
wait(futures)

0 commit comments

Comments
 (0)