Skip to content

Commit

Permalink
Report daily queue statistics to Discord (#409)
Browse files Browse the repository at this point in the history
* Send daily queue statistics to Discord

* Timezone fix

* Lint fix
  • Loading branch information
magnified103 authored Sep 26, 2024
1 parent 044bd23 commit 2e205fe
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 1 deletion.
1 change: 1 addition & 0 deletions additional_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
uwsgi
websocket-client
watchdog
matplotlib
12 changes: 12 additions & 0 deletions dmoj/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import socket

from celery import Celery
from celery.schedules import crontab
from celery.signals import task_failure

app = Celery('dmoj')
Expand All @@ -20,6 +21,17 @@
# Logger to enable reporting of errors.
logger = logging.getLogger('judge.celery')

# Load periodic tasks
app.conf.beat_schedule = {
'daily-queue-time-stats': {
'task': 'judge.tasks.webhook.queue_time_stats',
'schedule': crontab(minute=0, hour=0),
'options': {
'expires': 60 * 60 * 24,
},
},
}


@task_failure.connect()
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
Expand Down
3 changes: 3 additions & 0 deletions dmoj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@

VNOJ_LONG_QUEUE_ALERT_THRESHOLD = 10

CELERY_TIMEZONE = 'Asia/Ho_Chi_Minh'

# Some problems have a lot of testcases, and each testcase
# has about 5~6 fields, so we need to raise this
DATA_UPLOAD_MAX_NUMBER_FIELDS = 3000
Expand Down Expand Up @@ -180,6 +182,7 @@
'on_new_blogpost': None,
'on_error': None,
'on_long_queue': None,
'queue_time_stats': None,
}

SITE_FULL_URL = None # ie 'https://oj.vnoi.info', please remove the last / if needed
Expand Down
72 changes: 71 additions & 1 deletion judge/tasks/webhook.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import bisect
from datetime import datetime, timedelta
from io import BytesIO

import pytz
from celery import shared_task
from discord_webhook import DiscordEmbed, DiscordWebhook
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import F, FloatField
from django.db.models.functions import Cast


from judge.jinja2.gravatar import gravatar
from judge.models import BlogPost, Comment, Contest, Problem, Tag, TagProblem, Ticket, TicketMessage
from judge.models import BlogPost, Comment, Contest, Problem, Submission, Tag, TagProblem, Ticket, TicketMessage

__all__ = ('on_new_ticket', 'on_new_comment', 'on_new_problem', 'on_new_tag_problem', 'on_new_tag', 'on_new_contest',
'on_new_blogpost', 'on_new_ticket_message', 'on_long_queue')
Expand Down Expand Up @@ -208,3 +214,67 @@ def on_long_queue():
return

send_webhook(webhook, 'Long queue alert', None, None, url=f'{settings.SITE_FULL_URL}/submissions/?status=QU')


@shared_task
def queue_time_stats():
webhook_url = get_webhook_url('queue_time_stats')
if webhook_url is None:
return

end_time = (datetime.now(pytz.timezone(settings.CELERY_TIMEZONE))
.replace(hour=0, minute=0, second=0, microsecond=0))
start_time = end_time - timedelta(days=1)

queue_time = (Submission.objects.filter(date__gte=start_time, date__lte=end_time)
.filter(judged_date__isnull=False, rejudged_date__isnull=True)
.annotate(queue_time=Cast(F('judged_date') - F('date'), FloatField()) / 1000000.0)
.order_by('queue_time').values_list('queue_time', flat=True))

queue_time_ranges = [0, 1, 2, 5, 10, 30, 60, 120, 300, 600]
queue_time_labels = [
'',
'0s - 1s',
'1s - 2s',
'2s - 5s',
'5s - 10s',
'10s - 30s',
'30s - 1min',
'1min - 2min',
'2min - 5min',
'5min - 10min',
'> 10min',
]

def binning(x):
return bisect.bisect_left(queue_time_ranges, x, lo=0, hi=len(queue_time_ranges))

queue_time_count = [0] * len(queue_time_labels)
for group in map(binning, list(queue_time)):
queue_time_count[group] += 1

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
y_pos = range(len(queue_time_labels) - 1)
bar = ax.barh(y_pos, queue_time_count[1:])
ax.bar_label(bar, labels=[x if x > 0 else '' for x in queue_time_count[1:]], padding=2)
ax.margins(x=0.2)
ax.set_yticks(y_pos, labels=queue_time_labels[1:])
ax.invert_yaxis()
ax.set_xlabel('Number of submissions')
fig.tight_layout()

with BytesIO() as f:
plt.savefig(f, format='png')
f.seek(0)
chart_bytes = f.read()

embed = DiscordEmbed(title=f'Queue time, {start_time:%Y-%m-%d}', color='03b2f8')
embed.set_image('attachment://chart.png')
webhook = DiscordWebhook(url=webhook_url)
webhook.add_file(chart_bytes, 'chart.png')
webhook.add_embed(embed)
webhook.execute()

0 comments on commit 2e205fe

Please sign in to comment.