From bf08067910dbb4af405e58181dc13cd524229f22 Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:31:05 -0400 Subject: [PATCH 1/2] denis: introduce dummy assignments whose deadlines are manually triggered dummy assignments can be created using the new `dummy` option to configure initially all deadlines are set in the far future so that testing code can make submissions request oopsies etc without racing against the clock when the relevant scenario has been staged, the `trigger` option can be passed to configure specifying an assignment and component name and the due date will be set to the current moment and the corresponding event will be run synchronously. --- denis/configure.py | 46 ++++++++++++++++++++++++++++++ denis/start.py | 71 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/denis/configure.py b/denis/configure.py index c18060d9..7a0d7940 100755 --- a/denis/configure.py +++ b/denis/configure.py @@ -5,6 +5,9 @@ import db +# FIXME: if you are reading this in the year 9999... +far_future = 253401417420 + def main(): parser = ap(prog='configure', description='Configure assignments') @@ -40,6 +43,9 @@ def add_final(parser, required=True): add_peer_review(create_parser) add_final(create_parser) + dummy_parser = command_parsers.add_parser('dummy') + add_assignment(dummy_parser) + alter_parser = command_parsers.add_parser('alter') add_assignment(alter_parser) add_initial(alter_parser, required=False) @@ -57,6 +63,10 @@ def add_final(parser, required=True): command_parsers.add_parser('reload') + trigger_parser = command_parsers.add_parser('trigger') + add_assignment(trigger_parser) + trigger_parser.add_argument('-c', '--component', dest='component', required=True) + # Dictionary containing the desired command and all flags with their values kwargs = vars(parser.parse_args()) # Subparsers store their name in the destination `'command'` @@ -77,6 +87,16 @@ def create(assignment, initial, peer_review, final): print('cannot create assignment with duplicate name') +def dummy(assignment): + try: + db.Assignment.create(name=assignment, + initial_due_date=far_future, + peer_review_due_date=far_future, + final_due_date=far_future) + except db.peewee.IntegrityError: + print('cannot create assignment with duplicate name') + + def alter(assignment, initial, peer_review, final): alterations = {} if initial is not None: @@ -121,5 +141,31 @@ def reload(): os.kill(1, signal.SIGUSR1) +def trigger(assignment, component): + import signal + import ctypes + libc = ctypes.CDLL(None) + sigqueue = libc.sigqueue + sigqueue.restype = ctypes.c_int + sigqueue.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_int) + + if not (asn := db.Assignment.get_or_none(db.Assignment.name == assignment)): + print(f'no such assignment {assignment}') + return + match component: + case 'initial': + component_id = 0 + case 'peer': + component_id = 1 + case 'final': + component_id = 2 + case _: + print(f'no such component {component}') + return + + if err := sigqueue(1, signal.SIGRTMIN, asn.id * 3 + component_id): + print(f'sigqueue error: {err}') + + if __name__ == '__main__': exit(main()) diff --git a/denis/start.py b/denis/start.py index f9fff509..00bf85c4 100755 --- a/denis/start.py +++ b/denis/start.py @@ -7,6 +7,8 @@ import db +from configure import far_future + def spawn_waiter(timestamp, name, script): return subprocess.Popen(['/usr/local/bin/run-at', timestamp, script, name]) @@ -16,14 +18,49 @@ def in_the_future(ts): return datetime.datetime.now() < datetime.datetime.fromtimestamp(ts) +def handle_trigger(info): + if info.si_code != -1: + print('spurious sigrtmin without queuing info!', file=sys.stderr) + return False + assignment_id, component_id = divmod(info.si_status, 3) + if not (asn := db.Assignment.get_or_none(db.Assignment.id == assignment_id)): + print(f'trigger for non existant assignment id {assignment_id}!', file=sys.stderr) + return + match component_id: + case 0: + component = 'initial submission' + attr = 'initial_due_date' + program = './initial.py' + case 1: + component = 'peer review' + attr = 'peer_review_due_date' + program = './peer_review.py' + case 2: + component = 'final submission' + attr = 'final_due_date' + program = './final.py' + case _: + print(f'invalid component id {component_id}!', file=sys.stderr) + return + if not in_the_future(getattr(asn, attr)): + print(f'{component} for {asn.name} already passed!', file=sys.stderr) + return + + # update relevant deadline to current time so that dashboard etc behaves as expected + setattr(asn, attr, int(datetime.datetime.now().timestamp())) + asn.save() + subprocess.Popen([program, asn.name]).wait() + + def main(): - # neither of these handlers should actually run, + # this handler function should never actually run, # but because we are pid 1 in container we need - # to register handlers or they wont be delivered + # to register handlers or signals wont be delivered def signal_handler(*_): assert False signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGUSR1, signal_handler) + signal.signal(signal.SIGRTMIN, signal_handler) again = True while again: @@ -34,26 +71,40 @@ def signal_handler(*_): peer_review = assignment.peer_review_due_date final = assignment.final_due_date - if in_the_future(initial): + if initial == far_future: + pass + elif in_the_future(initial): procs.append(spawn_waiter(str(initial), name, './initial.py')) else: print(f'skipping initial for {name}', file=sys.stderr) - if in_the_future(peer_review): + if peer_review == far_future: + pass + elif in_the_future(peer_review): procs.append(spawn_waiter(str(peer_review), name, './peer_review.py')) else: print(f'skipping peer review for {name}', file=sys.stderr) - if in_the_future(final): + if final == far_future: + pass + elif in_the_future(final): procs.append(spawn_waiter(str(final), name, './final.py')) else: print(f'skipping final for {name}', file=sys.stderr) - # send SIGUSR1 to reload with new due dates, SIGTERM to exit - if signal.SIGUSR1 == signal.sigwait([signal.SIGUSR1, signal.SIGTERM]): - print('reloading', file=sys.stderr) - else: - again = False + while True: + # send SIGUSR1 to reload with new due dates, SIGTERM to exit, SIGRTMIN to trigger a deadline + info = signal.sigwaitinfo([signal.SIGUSR1, signal.SIGTERM, signal.SIGRTMIN]) + match info.si_signo: + case signal.SIGUSR1: + print('reloading', file=sys.stderr) + case signal.SIGTERM: + again = False + case signal.SIGRTMIN: + handle_trigger(info) + # no need to reload + continue + break for proc in procs: proc.terminate() From 0dd41ce0b7dd10c0e69317e86eb67376ae94e64a Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:57:48 -0400 Subject: [PATCH 2/2] denis: configure: actually report errors in exit code Right now configure.py always exits with code 0 because even though errors are detected, we just print a message. Introduce an `errx` helper that prints to stderr and exits the program and use throughout replacing the existing calls to `print` in error cases. --- denis/configure.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/denis/configure.py b/denis/configure.py index 7a0d7940..7e5962c0 100755 --- a/denis/configure.py +++ b/denis/configure.py @@ -2,6 +2,7 @@ from datetime import datetime from argparse import ArgumentParser as ap +import sys import db @@ -9,6 +10,11 @@ far_future = 253401417420 +def errx(msg): + print(msg, file=sys.stderr) + sys.exit(1) + + def main(): parser = ap(prog='configure', description='Configure assignments') @@ -84,7 +90,7 @@ def create(assignment, initial, peer_review, final): peer_review_due_date=peer_review, final_due_date=final) except db.peewee.IntegrityError: - print('cannot create assignment with duplicate name') + errx('cannot create assignment with duplicate name') def dummy(assignment): @@ -94,7 +100,7 @@ def dummy(assignment): peer_review_due_date=far_future, final_due_date=far_future) except db.peewee.IntegrityError: - print('cannot create assignment with duplicate name') + errx('cannot create assignment with duplicate name') def alter(assignment, initial, peer_review, final): @@ -106,12 +112,12 @@ def alter(assignment, initial, peer_review, final): if final is not None: alterations[db.Assignment.final_due_date] = final if not alterations: - return print('At least one new date must be specified') + errx('At least one new date must be specified') query = (db.Assignment .update(alterations) .where(db.Assignment.name == assignment)) if query.execute() < 1: - print(f'no such assignment {assignment}') + errx(f'no such assignment {assignment}') def remove(assignment): @@ -119,7 +125,7 @@ def remove(assignment): .delete() .where(db.Assignment.name == assignment)) if query.execute() < 1: - print(f'no such assignment {assignment}') + errx(f'no such assignment {assignment}') def dump(fmt_iso): @@ -138,7 +144,10 @@ def timestamp_to_formatted(timestamp): def reload(): import os import signal - os.kill(1, signal.SIGUSR1) + try: + os.kill(1, signal.SIGUSR1) + except OSError as e: + errx(f'kill: {e}') def trigger(assignment, component): @@ -150,8 +159,7 @@ def trigger(assignment, component): sigqueue.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_int) if not (asn := db.Assignment.get_or_none(db.Assignment.name == assignment)): - print(f'no such assignment {assignment}') - return + errx(f'no such assignment {assignment}') match component: case 'initial': component_id = 0 @@ -160,11 +168,10 @@ def trigger(assignment, component): case 'final': component_id = 2 case _: - print(f'no such component {component}') - return + errx(f'no such component {component}') if err := sigqueue(1, signal.SIGRTMIN, asn.id * 3 + component_id): - print(f'sigqueue error: {err}') + errx(f'sigqueue error: {err}') if __name__ == '__main__':