Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
6
judge/bridge/__init__.py
Normal file
6
judge/bridge/__init__.py
Normal 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
|
67
judge/bridge/djangohandler.py
Normal file
67
judge/bridge/djangohandler.py
Normal 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
|
7
judge/bridge/djangoserver.py
Normal file
7
judge/bridge/djangoserver.py
Normal 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
|
411
judge/bridge/judgecallback.py
Normal file
411
judge/bridge/judgecallback.py
Normal 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)
|
268
judge/bridge/judgehandler.py
Normal file
268
judge/bridge/judgehandler.py
Normal 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
123
judge/bridge/judgelist.py
Normal 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)
|
68
judge/bridge/judgeserver.py
Normal file
68
judge/bridge/judgeserver.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue