Cloned DMOJ

This commit is contained in:
thanhluong 2020-01-21 15:35:58 +09:00
parent f623974b58
commit 49dc9ff10c
513 changed files with 132349 additions and 39 deletions

6
judge/bridge/__init__.py Normal file
View file

@ -0,0 +1,6 @@
from .djangohandler import DjangoHandler
from .djangoserver import DjangoServer
from .judgecallback import DjangoJudgeHandler
from .judgehandler import JudgeHandler
from .judgelist import JudgeList
from .judgeserver import JudgeServer

View file

@ -0,0 +1,67 @@
import json
import logging
import struct
from event_socket_server import ZlibPacketHandler
logger = logging.getLogger('judge.bridge')
size_pack = struct.Struct('!I')
class DjangoHandler(ZlibPacketHandler):
def __init__(self, server, socket):
super(DjangoHandler, self).__init__(server, socket)
self.handlers = {
'submission-request': self.on_submission,
'terminate-submission': self.on_termination,
'disconnect-judge': self.on_disconnect,
}
self._to_kill = True
# self.server.schedule(5, self._kill_if_no_request)
def _kill_if_no_request(self):
if self._to_kill:
logger.info('Killed inactive connection: %s', self._socket.getpeername())
self.close()
def _format_send(self, data):
return super(DjangoHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
def packet(self, packet):
self._to_kill = False
packet = json.loads(packet)
try:
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
except Exception:
logger.exception('Error in packet handling (Django-facing)')
result = {'name': 'bad-request'}
self.send(result, self._schedule_close)
def _schedule_close(self):
self.server.schedule(0, self.close)
def on_submission(self, data):
id = data['submission-id']
problem = data['problem-id']
language = data['language']
source = data['source']
priority = data['priority']
if not self.server.judges.check_priority(priority):
return {'name': 'bad-request'}
self.server.judges.judge(id, problem, language, source, priority)
return {'name': 'submission-received', 'submission-id': id}
def on_termination(self, data):
return {'name': 'submission-received', 'judge-aborted': self.server.judges.abort(data['submission-id'])}
def on_disconnect(self, data):
judge_id = data['judge-id']
force = data['force']
self.server.judges.disconnect(judge_id, force=force)
def on_malformed(self, packet):
logger.error('Malformed packet: %s', packet)
def on_close(self):
self._to_kill = False

View file

@ -0,0 +1,7 @@
from event_socket_server import get_preferred_engine
class DjangoServer(get_preferred_engine()):
def __init__(self, judges, *args, **kwargs):
super(DjangoServer, self).__init__(*args, **kwargs)
self.judges = judges

View file

@ -0,0 +1,411 @@
import json
import logging
import time
from operator import itemgetter
from django import db
from django.utils import timezone
from judge import event_poster as event
from judge.caching import finished_submission
from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase
from .judgehandler import JudgeHandler, SubmissionData
logger = logging.getLogger('judge.bridge')
json_log = logging.getLogger('judge.json.bridge')
UPDATE_RATE_LIMIT = 5
UPDATE_RATE_TIME = 0.5
def _ensure_connection():
try:
db.connection.cursor().execute('SELECT 1').fetchall()
except Exception:
db.connection.close()
class DjangoJudgeHandler(JudgeHandler):
def __init__(self, server, socket):
super(DjangoJudgeHandler, self).__init__(server, socket)
# each value is (updates, last reset)
self.update_counter = {}
self.judge = None
self.judge_address = None
self._submission_cache_id = None
self._submission_cache = {}
json_log.info(self._make_json_log(action='connect'))
def on_close(self):
super(DjangoJudgeHandler, self).on_close()
json_log.info(self._make_json_log(action='disconnect', info='judge disconnected'))
if self._working:
Submission.objects.filter(id=self._working).update(status='IE', result='IE')
json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading'))
def on_malformed(self, packet):
super(DjangoJudgeHandler, self).on_malformed(packet)
json_log.exception(self._make_json_log(sub=self._working, info='malformed zlib packet'))
def _packet_exception(self):
json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception'))
def get_related_submission_data(self, submission):
_ensure_connection() # We are called from the django-facing daemon thread. Guess what happens.
try:
pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = (
Submission.objects.filter(id=submission)
.values_list('problem__id', 'problem__time_limit', 'problem__memory_limit',
'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id',
'contest__participation__virtual', 'contest__participation__id')).get()
except Submission.DoesNotExist:
logger.error('Submission vanished: %s', submission)
json_log.error(self._make_json_log(
sub=self._working, action='request',
info='submission vanished when fetching info',
))
return
attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid,
date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1
try:
time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
.values_list('time_limit', 'memory_limit').get())
except LanguageLimit.DoesNotExist:
pass
return SubmissionData(
time=time,
memory=memory,
short_circuit=short_circuit,
pretests_only=is_pretested,
contest_no=part_virtual,
attempt_no=attempt_no,
user_id=uid,
)
def _authenticate(self, id, key):
result = Judge.objects.filter(name=id, auth_key=key, is_blocked=False).exists()
if not result:
json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication'))
return result
def _connected(self):
judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now()
judge.online = True
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
# Delete now in case we somehow crashed and left some over from the last connection
RuntimeVersion.objects.filter(judge=judge).delete()
versions = []
for lang in judge.runtimes.all():
versions += [
RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge)
for idx, (name, version) in enumerate(self.executors[lang.key])
]
RuntimeVersion.objects.bulk_create(versions)
judge.last_ip = self.client_address[0]
judge.save()
self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1])
json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated',
executors=list(self.executors.keys())))
def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete()
def _update_ping(self):
try:
Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load)
except Exception as e:
# What can I do? I don't want to tie this to MySQL.
if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006:
db.connection.close()
def _post_update_submission(self, id, state, done=False):
if self._submission_cache_id == id:
data = self._submission_cache
else:
self._submission_cache = data = Submission.objects.filter(id=id).values(
'problem__is_public', 'contest__participation__contest__key',
'user_id', 'problem_id', 'status', 'language__key',
).get()
self._submission_cache_id = id
if data['problem__is_public']:
event.post('submissions', {
'type': 'done-submission' if done else 'update-submission',
'state': state, 'id': id,
'contest': data['contest__participation__contest__key'],
'user': data['user_id'], 'problem': data['problem_id'],
'status': data['status'], 'language': data['language__key'],
})
def on_submission_processing(self, packet):
id = packet['submission-id']
if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge):
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'})
self._post_update_submission(id, 'processing')
json_log.info(self._make_json_log(packet, action='processing'))
else:
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='processing', info='unknown submission'))
def on_submission_wrong_acknowledge(self, packet, expected, got):
json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected))
def on_grading_begin(self, packet):
super(DjangoJudgeHandler, self).on_grading_begin(packet)
if Submission.objects.filter(id=packet['submission-id']).update(
status='G', is_pretested=packet['pretested'],
current_testcase=1, batch=False):
SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete()
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'})
self._post_update_submission(packet['submission-id'], 'grading-begin')
json_log.info(self._make_json_log(packet, action='grading-begin'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission'))
def _submission_is_batch(self, id):
if not Submission.objects.filter(id=id).update(batch=True):
logger.warning('Unknown submission: %s', id)
def on_grading_end(self, packet):
super(DjangoJudgeHandler, self).on_grading_end(packet)
try:
submission = Submission.objects.get(id=packet['submission-id'])
except Submission.DoesNotExist:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission'))
return
time = 0
memory = 0
points = 0.0
total = 0
status = 0
status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE']
batches = {} # batch number: (points, total)
for case in SubmissionTestCase.objects.filter(submission=submission):
time += case.time
if not case.batch:
points += case.points
total += case.total
else:
if case.batch in batches:
batches[case.batch][0] = min(batches[case.batch][0], case.points)
batches[case.batch][1] = max(batches[case.batch][1], case.total)
else:
batches[case.batch] = [case.points, case.total]
memory = max(memory, case.memory)
i = status_codes.index(case.status)
if i > status:
status = i
for i in batches:
points += batches[i][0]
total += batches[i][1]
points = round(points, 1)
total = round(total, 1)
submission.case_points = points
submission.case_total = total
problem = submission.problem
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
if not problem.partial and sub_points != problem.points:
sub_points = 0
submission.status = 'D'
submission.time = time
submission.memory = memory
submission.points = sub_points
submission.result = status_codes[status]
submission.save()
json_log.info(self._make_json_log(
packet, action='grading-end', time=time, memory=memory,
points=sub_points, total=problem.points, result=submission.result,
case_points=points, case_total=total, user=submission.user_id,
problem=problem.code, finish=True,
))
submission.user._updating_stats_only = True
submission.user.calculate_points()
problem._updating_stats_only = True
problem.update_stats()
submission.update_contest()
finished_submission(submission)
event.post('sub_%s' % submission.id_secret, {
'type': 'grading-end',
'time': time,
'memory': memory,
'points': float(points),
'total': float(problem.points),
'result': submission.result,
})
if hasattr(submission, 'contest'):
participation = submission.contest.participation
event.post('contest_%d' % participation.contest_id, {'type': 'update'})
self._post_update_submission(submission.id, 'grading-end', done=True)
def on_compile_error(self, packet):
super(DjangoJudgeHandler, self).on_compile_error(packet)
if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {
'type': 'compile-error',
'log': packet['log'],
})
self._post_update_submission(packet['submission-id'], 'compile-error', done=True)
json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'],
finish=True, result='CE'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission',
log=packet['log'], finish=True, result='CE'))
def on_compile_message(self, packet):
super(DjangoJudgeHandler, self).on_compile_message(packet)
if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'})
json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log']))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission',
log=packet['log']))
def on_internal_error(self, packet):
super(DjangoJudgeHandler, self).on_internal_error(packet)
id = packet['submission-id']
if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']):
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'})
self._post_update_submission(id, 'internal-error', done=True)
json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'],
finish=True, result='IE'))
else:
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission',
message=packet['message'], finish=True, result='IE'))
def on_submission_terminated(self, packet):
super(DjangoJudgeHandler, self).on_submission_terminated(packet)
if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'})
self._post_update_submission(packet['submission-id'], 'terminated', done=True)
json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission',
finish=True, result='AB'))
def on_batch_begin(self, packet):
super(DjangoJudgeHandler, self).on_batch_begin(packet)
json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id))
def on_batch_end(self, packet):
super(DjangoJudgeHandler, self).on_batch_end(packet)
json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id))
def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length):
super(DjangoJudgeHandler, self).on_test_case(packet)
id = packet['submission-id']
updates = packet['cases']
max_position = max(map(itemgetter('position'), updates))
if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1):
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission'))
return
bulk_test_case_updates = []
for result in updates:
test_case = SubmissionTestCase(submission_id=id, case=result['position'])
status = result['status']
if status & 4:
test_case.status = 'TLE'
elif status & 8:
test_case.status = 'MLE'
elif status & 64:
test_case.status = 'OLE'
elif status & 2:
test_case.status = 'RTE'
elif status & 16:
test_case.status = 'IR'
elif status & 1:
test_case.status = 'WA'
elif status & 32:
test_case.status = 'SC'
else:
test_case.status = 'AC'
test_case.time = result['time']
test_case.memory = result['memory']
test_case.points = result['points']
test_case.total = result['total-points']
test_case.batch = self.batch_id if self.in_batch else None
test_case.feedback = (result.get('feedback') or '')[:max_feedback]
test_case.extended_feedback = result.get('extended-feedback') or ''
test_case.output = result['output']
bulk_test_case_updates.append(test_case)
json_log.info(self._make_json_log(
packet, action='test-case', case=test_case.case, batch=test_case.batch,
time=test_case.time, memory=test_case.memory, feedback=test_case.feedback,
extended_feedback=test_case.extended_feedback, output=test_case.output,
points=test_case.points, total=test_case.total, status=test_case.status,
))
do_post = True
if id in self.update_counter:
cnt, reset = self.update_counter[id]
cnt += 1
if time.monotonic() - reset > UPDATE_RATE_TIME:
del self.update_counter[id]
else:
self.update_counter[id] = (cnt, reset)
if cnt > UPDATE_RATE_LIMIT:
do_post = False
if id not in self.update_counter:
self.update_counter[id] = (1, time.monotonic())
if do_post:
event.post('sub_%s' % Submission.get_id_secret(id), {
'type': 'test-case',
'id': max_position,
})
self._post_update_submission(id, state='test-case')
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
def on_supported_problems(self, packet):
super(DjangoJudgeHandler, self).on_supported_problems(packet)
self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
json_log.info(self._make_json_log(action='update-problems', count=len(self.problems)))
def _make_json_log(self, packet=None, sub=None, **kwargs):
data = {
'judge': self.name,
'address': self.judge_address,
}
if sub is None and packet is not None:
sub = packet.get('submission-id')
if sub is not None:
data['submission'] = sub
data.update(kwargs)
return json.dumps(data)

View file

@ -0,0 +1,268 @@
import json
import logging
import time
from collections import deque, namedtuple
from event_socket_server import ProxyProtocolMixin, ZlibPacketHandler
logger = logging.getLogger('judge.bridge')
SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id')
class JudgeHandler(ProxyProtocolMixin, ZlibPacketHandler):
def __init__(self, server, socket):
super(JudgeHandler, self).__init__(server, socket)
self.handlers = {
'grading-begin': self.on_grading_begin,
'grading-end': self.on_grading_end,
'compile-error': self.on_compile_error,
'compile-message': self.on_compile_message,
'batch-begin': self.on_batch_begin,
'batch-end': self.on_batch_end,
'test-case-status': self.on_test_case,
'internal-error': self.on_internal_error,
'submission-terminated': self.on_submission_terminated,
'submission-acknowledged': self.on_submission_acknowledged,
'ping-response': self.on_ping_response,
'supported-problems': self.on_supported_problems,
'handshake': self.on_handshake,
}
self._to_kill = True
self._working = False
self._no_response_job = None
self._problems = []
self.executors = []
self.problems = {}
self.latency = None
self.time_delta = None
self.load = 1e100
self.name = None
self.batch_id = None
self.in_batch = False
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
self._time_delta = deque(maxlen=6)
self.server.schedule(15, self._kill_if_no_auth)
logger.info('Judge connected from: %s', self.client_address)
def _kill_if_no_auth(self):
if self._to_kill:
logger.info('Judge not authenticated: %s', self.client_address)
self.close()
def on_close(self):
self._to_kill = False
if self._no_response_job:
self.server.unschedule(self._no_response_job)
self.server.judges.remove(self)
if self.name is not None:
self._disconnected()
logger.info('Judge disconnected from: %s', self.client_address)
def _authenticate(self, id, key):
return False
def _connected(self):
pass
def _disconnected(self):
pass
def _update_ping(self):
pass
def _format_send(self, data):
return super(JudgeHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
def on_handshake(self, packet):
if 'id' not in packet or 'key' not in packet:
logger.warning('Malformed handshake: %s', self.client_address)
self.close()
return
if not self._authenticate(packet['id'], packet['key']):
logger.warning('Authentication failure: %s', self.client_address)
self.close()
return
self._to_kill = False
self._problems = packet['problems']
self.problems = dict(self._problems)
self.executors = packet['executors']
self.name = packet['id']
self.send({'name': 'handshake-success'})
logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id'])
self.server.judges.register(self)
self._connected()
def can_judge(self, problem, executor):
return problem in self.problems and executor in self.executors
@property
def working(self):
return bool(self._working)
def get_related_submission_data(self, submission):
return SubmissionData(
time=2,
memory=16384,
short_circuit=False,
pretests_only=False,
contest_no=None,
attempt_no=1,
user_id=None,
)
def disconnect(self, force=False):
if force:
# Yank the power out.
self.close()
else:
self.send({'name': 'disconnect'})
def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id)
self._working = id
self._no_response_job = self.server.schedule(20, self._kill_if_no_response)
self.send({
'name': 'submission-request',
'submission-id': id,
'problem-id': problem,
'language': language,
'source': source,
'time-limit': data.time,
'memory-limit': data.memory,
'short-circuit': data.short_circuit,
'meta': {
'pretests-only': data.pretests_only,
'in-contest': data.contest_no,
'attempt-no': data.attempt_no,
'user': data.user_id,
},
})
def _kill_if_no_response(self):
logger.error('Judge seems dead: %s: %s', self.name, self._working)
self.close()
def malformed_packet(self, exception):
logger.exception('Judge sent malformed packet: %s', self.name)
super(JudgeHandler, self).malformed_packet(exception)
def on_submission_processing(self, packet):
pass
def on_submission_wrong_acknowledge(self, packet, expected, got):
pass
def on_submission_acknowledged(self, packet):
if not packet.get('submission-id', None) == self._working:
logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None),
self._working)
self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None))
self.close()
logger.info('Submission acknowledged: %d', self._working)
if self._no_response_job:
self.server.unschedule(self._no_response_job)
self._no_response_job = None
self.on_submission_processing(packet)
def abort(self):
self.send({'name': 'terminate-submission'})
def get_current_submission(self):
return self._working or None
def ping(self):
self.send({'name': 'ping', 'when': time.time()})
def packet(self, data):
try:
try:
data = json.loads(data)
if 'name' not in data:
raise ValueError
except ValueError:
self.on_malformed(data)
else:
handler = self.handlers.get(data['name'], self.on_malformed)
handler(data)
except Exception:
logger.exception('Error in packet handling (Judge-side): %s', self.name)
self._packet_exception()
# You can't crash here because you aren't so sure about the judges
# not being malicious or simply malforms. THIS IS A SERVER!
def _packet_exception(self):
pass
def _submission_is_batch(self, id):
pass
def on_supported_problems(self, packet):
logger.info('%s: Updated problem list', self.name)
self._problems = packet['problems']
self.problems = dict(self._problems)
if not self.working:
self.server.judges.update_problems(self)
def on_grading_begin(self, packet):
logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id'])
self.batch_id = None
def on_grading_end(self, packet):
logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id'])
self._free_self(packet)
self.batch_id = None
def on_compile_error(self, packet):
logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_compile_message(self, packet):
logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id'])
def on_internal_error(self, packet):
try:
raise ValueError('\n\n' + packet['message'])
except ValueError:
logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_submission_terminated(self, packet):
logger.info('%s: Submission aborted: %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_batch_begin(self, packet):
logger.info('%s: Batch began on: %s', self.name, packet['submission-id'])
self.in_batch = True
if self.batch_id is None:
self.batch_id = 0
self._submission_is_batch(packet['submission-id'])
self.batch_id += 1
def on_batch_end(self, packet):
self.in_batch = False
logger.info('%s: Batch ended on: %s', self.name, packet['submission-id'])
def on_test_case(self, packet):
logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id'])
def on_malformed(self, packet):
logger.error('%s: Malformed packet: %s', self.name, packet)
def on_ping_response(self, packet):
end = time.time()
self._ping_average.append(end - packet['when'])
self._time_delta.append((end + packet['when']) / 2 - packet['time'])
self.latency = sum(self._ping_average) / len(self._ping_average)
self.time_delta = sum(self._time_delta) / len(self._time_delta)
self.load = packet['load']
self._update_ping()
def _free_self(self, packet):
self._working = False
self.server.judges.on_judge_free(self, packet['submission-id'])

123
judge/bridge/judgelist.py Normal file
View file

@ -0,0 +1,123 @@
import logging
from collections import namedtuple
from operator import attrgetter
from threading import RLock
try:
from llist import dllist
except ImportError:
from pyllist import dllist
logger = logging.getLogger('judge.bridge')
PriorityMarker = namedtuple('PriorityMarker', 'priority')
class JudgeList(object):
priorities = 4
def __init__(self):
self.queue = dllist()
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
self.judges = set()
self.node_map = {}
self.submission_map = {}
self.lock = RLock()
def _handle_free_judge(self, judge):
with self.lock:
node = self.queue.first
while node:
if not isinstance(node.value, PriorityMarker):
id, problem, language, source = node.value
if judge.can_judge(problem, language):
self.submission_map[id] = judge
logger.info('Dispatched queued submission %d: %s', id, judge.name)
try:
judge.submit(id, problem, language, source)
except Exception:
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
self.judges.remove(judge)
return
self.queue.remove(node)
del self.node_map[id]
break
node = node.next
def register(self, judge):
with self.lock:
# Disconnect all judges with the same name, see <https://github.com/DMOJ/online-judge/issues/828>
self.disconnect(judge, force=True)
self.judges.add(judge)
self._handle_free_judge(judge)
def disconnect(self, judge_id, force=False):
for judge in self.judges:
if judge.name == judge_id:
judge.disconnect(force=force)
def update_problems(self, judge):
with self.lock:
self._handle_free_judge(judge)
def remove(self, judge):
with self.lock:
sub = judge.get_current_submission()
if sub is not None:
try:
del self.submission_map[sub]
except KeyError:
pass
self.judges.discard(judge)
def __iter__(self):
return iter(self.judges)
def on_judge_free(self, judge, submission):
with self.lock:
logger.info('Judge available after grading %d: %s', submission, judge.name)
del self.submission_map[submission]
self._handle_free_judge(judge)
def abort(self, submission):
with self.lock:
logger.info('Abort request: %d', submission)
try:
self.submission_map[submission].abort()
return True
except KeyError:
try:
node = self.node_map[submission]
except KeyError:
pass
else:
self.queue.remove(node)
del self.node_map[submission]
return False
def check_priority(self, priority):
return 0 <= priority < self.priorities
def judge(self, id, problem, language, source, priority):
with self.lock:
if id in self.submission_map or id in self.node_map:
# Already judging, don't queue again. This can happen during batch rejudges, rejudges should be
# idempotent.
return
candidates = [judge for judge in self.judges if not judge.working and judge.can_judge(problem, language)]
logger.info('Free judges: %d', len(candidates))
if candidates:
# Schedule the submission on the judge reporting least load.
judge = min(candidates, key=attrgetter('load'))
logger.info('Dispatched submission %d to: %s', id, judge.name)
self.submission_map[id] = judge
try:
judge.submit(id, problem, language, source)
except Exception:
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
self.judges.discard(judge)
return self.judge(id, problem, language, source, priority)
else:
self.node_map[id] = self.queue.insert((id, problem, language, source), self.priority[priority])
logger.info('Queued submission: %d', id)

View file

@ -0,0 +1,68 @@
import logging
import os
import threading
import time
from event_socket_server import get_preferred_engine
from judge.models import Judge
from .judgelist import JudgeList
logger = logging.getLogger('judge.bridge')
def reset_judges():
Judge.objects.update(online=False, ping=None, load=None)
class JudgeServer(get_preferred_engine()):
def __init__(self, *args, **kwargs):
super(JudgeServer, self).__init__(*args, **kwargs)
reset_judges()
self.judges = JudgeList()
self.ping_judge_thread = threading.Thread(target=self.ping_judge, args=())
self.ping_judge_thread.daemon = True
self.ping_judge_thread.start()
def on_shutdown(self):
super(JudgeServer, self).on_shutdown()
reset_judges()
def ping_judge(self):
try:
while True:
for judge in self.judges:
judge.ping()
time.sleep(10)
except Exception:
logger.exception('Ping error')
raise
def main():
import argparse
import logging
from .judgehandler import JudgeHandler
format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
logging.basicConfig(format=format)
logging.getLogger().setLevel(logging.INFO)
handler = logging.FileHandler(os.path.join(os.path.dirname(__file__), 'judgeserver.log'), encoding='utf-8')
handler.setFormatter(logging.Formatter(format))
handler.setLevel(logging.INFO)
logging.getLogger().addHandler(handler)
parser = argparse.ArgumentParser(description='''
Runs the bridge between DMOJ website and judges.
''')
parser.add_argument('judge_host', nargs='+', action='append',
help='host to listen for the judge')
parser.add_argument('-p', '--judge-port', type=int, action='append',
help='port to listen for the judge')
args = parser.parse_args()
server = JudgeServer(list(zip(args.judge_host, args.judge_port)), JudgeHandler)
server.serve_forever()
if __name__ == '__main__':
main()