Skip to content

Commit

Permalink
Add judge code
Browse files Browse the repository at this point in the history
  • Loading branch information
magnified103 committed Oct 13, 2024
1 parent 91b58d2 commit 9bdbfe9
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 15 deletions.
1 change: 1 addition & 0 deletions dmoj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
VNOJ_PROBLEM_ENABLE_EXPORT = False
VNOJ_PROBLEM_IMPORT_HOST = 'https://oj.vnoi.info'
VNOJ_PROBLEM_IMPORT_SECRET = ''
VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX = 'vnoj/'
VNOJ_PROBLEM_IMPORT_TIMEOUT = 5 # in seconds

CELERY_TIMEZONE = 'Asia/Ho_Chi_Minh'
Expand Down
10 changes: 6 additions & 4 deletions judge/bridge/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,22 @@ def reset_judges():
Judge.objects.update(online=False, ping=None, load=None)


def judge_daemon(run_monitor=False, problem_storage_globs=None):
def judge_daemon(config):
reset_judges()
Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \
.update(status='IE', result='IE', error=None)
judges = JudgeList()

monitor = None
if run_monitor:
if 'run_monitor' in config:
from judge.bridge.monitor import Monitor
monitor = Monitor(judges, problem_storage_globs or [])
problem_storage_globs = [(entry['storage_namespaces'], entry['problem_storage_globs'])
for entry in config['run_monitor']]
monitor = Monitor(judges, problem_storage_globs)

judge_server = Server(
settings.BRIDGED_JUDGE_ADDRESS,
partial(JudgeHandler, judges=judges, ignore_problems_packet=run_monitor),
partial(JudgeHandler, judges=judges, ignore_problems_packet=('run_monitor' in config)),
)
django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges))

Expand Down
68 changes: 63 additions & 5 deletions judge/bridge/monitor.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from contextlib import closing
import glob
import logging
import os
import threading
import time
from pathlib import Path
from urllib.request import urlopen

from django import db
from http.server import BaseHTTPRequestHandler, HTTPServer

from judge.models import Problem

Expand All @@ -27,6 +30,27 @@
logger = logging.getLogger('judge.monitor')


class JudgeControlRequestHandler(BaseHTTPRequestHandler):
signal = None

def update_problems(self):
if self.signal is not None:
self.signal.set()

def do_POST(self):
if self.path == '/update/problems':
logger.info('Problem update requested')
self.update_problems()
self.send_response(200)
self.end_headers()
self.wfile.write(b'As you wish.')
return
self.send_error(404)

def do_GET(self):
self.send_error(404)


def _ensure_connection():
db.connection.close_if_unusable_or_obsolete()

Expand Down Expand Up @@ -59,7 +83,7 @@ def on_any_event(self, event):


class Monitor:
def __init__(self, judges, problem_globs):
def __init__(self, judges, problem_globs, api_listen=None, update_pings=None):
if not has_watchdog_installed:
raise ImportError('watchdog is not installed')

Expand All @@ -68,33 +92,64 @@ def __init__(self, judges, problem_globs):

self.updater_exit = False
self.updater_signal = threading.Event()
self.propagation_signal = threading.Event()
self.updater = threading.Thread(target=self.updater_thread)
self.propagator = threading.Thread(target=self.propagate_update_signal)

self.update_pings = update_pings or []

if api_listen:
class Handler(JudgeControlRequestHandler):
signal = self.updater_signal

api_server = HTTPServer(api_listen, Handler)
self.api_server_thread = threading.Thread(target=api_server.serve_forever)

self._handler = SendProblemsHandler(self.updater_signal)
self._observer = Observer()

for root in set(map(find_glob_root, problem_globs)):
for _, dir_glob in problem_globs:
root = find_glob_root(dir_glob)
self._observer.schedule(self._handler, root, recursive=True)
logger.info('Scheduled for monitoring: %s', root)

def update_supported_problems(self):
problems = []
for dir_glob in self.problem_globs:
for storage_namespace, dir_glob in self.problem_globs:
for problem_config in glob.iglob(os.path.join(dir_glob, 'init.yml'), recursive=True):
if os.access(problem_config, os.R_OK):
problem_dir = os.path.dirname(problem_config)
problem = os.path.basename(problem_dir)
problems.append(problem)
if storage_namespace:
problems.append(storage_namespace + '/' + problem)
else:
problems.append(problem)
problems = set(problems)

_ensure_connection()
problem_ids = list(Problem.objects.filter(code__in=list(problems)).values_list('id', flat=True))
problem_ids = list(Problem.objects.filter(judge_code__in=list(problems)).values_list('id', flat=True))
self.judges.update_problems_all(problems, problem_ids)

def propagate_update_signal(self):
while True:
self.propagation_signal.wait()
self.propagation_signal.clear()
if self.updater_exit:
return

for url in self.update_pings:
logger.info('Pinging for problem update: %s', url)
try:
with closing(urlopen(url, data='')) as f:
f.read()
except Exception:
logger.exception('Failed to ping for problem update: %s', url)

def updater_thread(self) -> None:
while True:
self.updater_signal.wait()
self.updater_signal.clear()
self.propagation_signal.set()
if self.updater_exit:
return

Expand All @@ -106,8 +161,11 @@ def updater_thread(self) -> None:

def start(self):
self.updater.start()
self.propagator.start()
self.updater_signal.set()
try:
if hasattr(self, 'api_server_thread'):
self.api_server_thread.start()
self._observer.start()
except OSError:
logger.exception('Failed to start problem monitor.')
Expand Down
15 changes: 9 additions & 6 deletions judge/management/commands/runbridged.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import yaml
from django.core.management.base import BaseCommand

from judge.bridge.daemon import judge_daemon


class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--monitor', action='store_true', default=False,
help='if specified, run a monitor to automatically update problems')
parser.add_argument('--problem-storage-globs', nargs='*', default=[],
help='globs to monitor for problem updates')
def add_arguments(self, parser) -> None:
parser.add_argument('-c', '--config', type=str, help='file to load bridged configurations from')

def handle(self, *args, **options):
judge_daemon(options['monitor'], options['problem_storage_globs'])
if options['config']:
with open(options['config'], 'r') as f:
config = yaml.safe_load(f)
else:
config = {}
judge_daemon(config)
17 changes: 17 additions & 0 deletions judge/migrations/0210_problem_export.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Generated by Django 3.2.25 on 2024-10-10 09:25

from django.db import migrations, models
from django.db.models import F


def populate_judge_code(apps, schema_editor):
Problem = apps.get_model('judge', 'Problem')
Problem.objects.update(judge_code=F('code'))


class Migration(migrations.Migration):
Expand All @@ -20,4 +26,15 @@ class Migration(migrations.Migration):
('description', models.TextField(blank=True, verbose_name='description')),
],
),
migrations.AddField(
model_name='problem',
name='judge_code',
field=models.CharField(blank=True, max_length=100, verbose_name='judge code'),
),
migrations.RunPython(populate_judge_code, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name='problem',
name='judge_code',
field=models.CharField(max_length=100, verbose_name='judge code'),
),
]
3 changes: 3 additions & 0 deletions judge/models/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class Problem(models.Model):
is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False)
is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False,
help_text=_('Whether judges should be allowed to manage data or not.'))
judge_code = models.CharField(max_length=100, verbose_name=_('judge code'))
date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True,
help_text=_(
"Doesn't have the magic ability to auto-publish due to backward compatibility."))
Expand Down Expand Up @@ -573,6 +574,8 @@ def io_method(self):
return {'method': 'standard'}

def save(self, *args, **kwargs):
if not self.judge_code:
self.judge_code = self.code
is_clone = kwargs.pop('is_clone', False)
# if short_circuit = true the judge will stop judging
# as soon as the submission failed a test case
Expand Down
1 change: 1 addition & 0 deletions judge/views/problem_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def form_valid(self, form):
raise Http404('Request timed out')
problem = Problem()
problem.code = form.cleaned_data['new_code']
problem.judge_code = settings.VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX + problem.code
problem.name = problem_info['name']
problem.description = problem_info['description']
problem.time_limit = problem_info['time_limit']
Expand Down

0 comments on commit 9bdbfe9

Please sign in to comment.