From 1fbd63a5b27fdd699b4b84c2f9cf626ad29ee442 Mon Sep 17 00:00:00 2001 From: John McCann Cunniff Jr <36013983+wabscale@users.noreply.github.com> Date: Mon, 12 Apr 2021 15:19:31 -0400 Subject: [PATCH] V2.2.9 fix assignment 3 sched (#81) * WIP midterm post * ADD midterm retro post * ADD new rpc services to mindebug * FIX mobile content shift * FIX header top padding * FIX add onclick wrapper to non public nav items * ADD docs to some utils functions * ADD docs to more utils functions and views * CHG reorganize minikube debug * FIX cache missing issue * FIX weird reaper crashing issue * ADD timestamp to usage plot * ADD regrade button to admin panel * ADD regrade queue * ADD rpc-regrade to restart script * CHG add check to reaper for user Nonetype --- Makefile | 24 +- api/anubis/rpc/pipeline.py | 7 +- api/anubis/rpc/seed.py | 2 + api/anubis/utils/assignments.py | 48 ++- api/anubis/utils/cache.py | 4 +- api/anubis/utils/data.py | 40 ++- api/anubis/utils/rpc.py | 20 +- api/anubis/utils/stats.py | 73 +++- api/anubis/utils/students.py | 68 +++- api/anubis/utils/submissions.py | 100 ++++-- api/anubis/utils/theia.py | 39 +- api/anubis/utils/visualizations.py | 48 ++- api/anubis/utils/webhook.py | 33 +- api/anubis/views/admin/regrade.py | 46 ++- api/anubis/views/admin/seed.py | 2 +- api/anubis/views/admin/stats.py | 8 +- api/anubis/views/public/webhook.py | 4 +- api/jobs/reaper.py | 19 +- docker-compose.yml | 11 + docs/design.pdf | Bin 1229473 -> 1229473 bytes docs/img/cluster.mmd.svg | 2 +- docs/img/rpc-queue-1.mmd.png | Bin 0 -> 14001 bytes docs/img/rpc-queue-1.mmd.svg | 1 + docs/img/rpc-queue-2.mmd.png | Bin 0 -> 12986 bytes docs/img/rpc-queue-2.mmd.svg | 1 + docs/img/rpc-queue-3.mmd.png | Bin 0 -> 20845 bytes docs/img/rpc-queue-3.mmd.svg | 1 + docs/img/submission-flow.mmd.svg | 2 +- docs/img/theia-pod.mmd.svg | 2 +- docs/mermaid/rpc-queue-1.mmd | 17 + docs/mermaid/rpc-queue-2.mmd | 18 + docs/mermaid/rpc-queue-3.mmd | 30 ++ kube/debug/.gitignore | 1 + kube/debug/build-theia.sh | 8 + .../provision.sh} | 9 +- kube/{debug.sh => debug/restart.sh} | 23 +- kube/{traefik.yml => debug/traefik.yaml} | 90 +---- kube/restart.sh | 1 + kube/templates/api.yml | 46 --- kube/templates/ingress.yml | 120 +++++++ kube/templates/rpc.yml | 56 +++ kube/templates/theia.yml | 24 -- kube/templates/web.yml | 28 -- kube/values.yaml | 5 +- web/src/App.jsx | 81 ++++- .../Admin/Assignment/AssignmentCard.jsx | 263 +++++++++----- web/src/Components/Footer.jsx | 4 +- web/src/Components/Header.jsx | 44 ++- web/src/Components/Navigation/NavList.jsx | 28 +- .../Public/Blog/AssignmentPackagingPost.jsx | 337 +++++++++--------- .../Components/Public/Blog/AssignmentPost.jsx | 172 ++++----- web/src/Components/Public/Blog/BlogImg.jsx | 27 ++ .../Public/Blog/ElevatorPitchPost.jsx | 150 ++++---- .../Public/Blog/MidtermRetroPost.jsx | 231 ++++++++++++ web/src/Main.jsx | 16 +- web/src/Navigation/Nav.jsx | 52 ++- web/src/Navigation/navconfig.jsx | 2 +- web/src/Pages/Admin/Assignments.jsx | 1 + web/src/Pages/Public/About.jsx | 4 +- web/src/Pages/Public/Blog.jsx | 53 ++- 60 files changed, 1703 insertions(+), 843 deletions(-) create mode 100644 docs/img/rpc-queue-1.mmd.png create mode 100644 docs/img/rpc-queue-1.mmd.svg create mode 100644 docs/img/rpc-queue-2.mmd.png create mode 100644 docs/img/rpc-queue-2.mmd.svg create mode 100644 docs/img/rpc-queue-3.mmd.png create mode 100644 docs/img/rpc-queue-3.mmd.svg create mode 100644 docs/mermaid/rpc-queue-1.mmd create mode 100644 docs/mermaid/rpc-queue-2.mmd create mode 100644 docs/mermaid/rpc-queue-3.mmd create mode 100644 kube/debug/.gitignore create mode 100755 kube/debug/build-theia.sh rename kube/{provision-debug.sh => debug/provision.sh} (96%) rename kube/{debug.sh => debug/restart.sh} (79%) rename kube/{traefik.yml => debug/traefik.yaml} (70%) create mode 100644 kube/templates/ingress.yml create mode 100644 web/src/Components/Public/Blog/BlogImg.jsx create mode 100644 web/src/Components/Public/Blog/MidtermRetroPost.jsx diff --git a/Makefile b/Makefile index a748004c7..9e496c282 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PERSISTENT_SERVICES := db traefik kibana elasticsearch-coordinating redis-master logstash adminer -RESTART_ALWAYS_SERVICES := api web-dev rpc-default rpc-theia +RESTART_ALWAYS_SERVICES := api web-dev rpc-default rpc-theia rpc-regrade PUSH_SERVICES := api web logstash theia-init theia-proxy theia-admin theia-xv6 @@ -55,20 +55,38 @@ debug: sleep 3 @echo 'running migrations' make -C api migrations + @echo '' @echo 'seed: http://localhost/api/admin/seed/' @echo 'auth: http://localhost/api/admin/auth/token/jmc1283' @echo 'site: http://localhost/' .PHONY: mindebug # Start the minimal cluster in debug mode -mindebug: build +mindebug: docker-compose up -d traefik db redis-master logstash docker-compose up \ -d --force-recreate \ - api web rpc-worker + api web rpc-default rpc-theia @echo 'Waiting a moment before running migrations' sleep 3 @echo 'running migrations' make -C api migrations + @echo '' + @echo 'seed: http://localhost/api/admin/seed/' + @echo 'auth: http://localhost/api/admin/auth/token/jmc1283' + @echo 'site: http://localhost/' + +.PHONY: mkdebug # Start minikube debug +mkdebug: + ./kube/debug/provision.sh + @echo '' + @echo 'seed: http://localhost/api/admin/seed/' + @echo 'auth: http://localhost/api/admin/auth/token/jmc1283' + @echo 'site: http://localhost/' + +.PHONY: mkrestart # Restart minikube debug +mkrestart: + ./kube/debug/restart.sh + @echo '' @echo 'seed: http://localhost/api/admin/seed/' @echo 'auth: http://localhost/api/admin/auth/token/jmc1283' @echo 'site: http://localhost/' diff --git a/api/anubis/rpc/pipeline.py b/api/anubis/rpc/pipeline.py index 02a6f8a00..2bb5d1706 100644 --- a/api/anubis/rpc/pipeline.py +++ b/api/anubis/rpc/pipeline.py @@ -100,7 +100,7 @@ def cleanup_jobs(batch_v1) -> int: return active_count -def test_repo(submission_id: str): +def create_submission_pipeline(submission_id: str): """ This function should launch the appropriate testing container for the assignment, passing along the function arguments. @@ -108,7 +108,7 @@ def test_repo(submission_id: str): :param submission_id: submission.id of to test """ from anubis.app import create_app - from anubis.utils.rpc import enqueue_webhook + from anubis.utils.rpc import enqueue_autograde_pipeline app = create_app() @@ -153,8 +153,7 @@ def test_repo(submission_id: str): "TOO many jobs - re-enqueue {}".format(submission_id), extra={"submission_id": submission_id}, ) - enqueue_webhook(submission_id) - time.sleep(1) + enqueue_autograde_pipeline(submission_id) exit(0) # Create job object diff --git a/api/anubis/rpc/seed.py b/api/anubis/rpc/seed.py index 96b53e61b..e79ce937c 100644 --- a/api/anubis/rpc/seed.py +++ b/api/anubis/rpc/seed.py @@ -16,6 +16,7 @@ TheiaSession, AssignmentQuestion, AssignedStudentQuestion, + AssignedQuestionResponse, ) from anubis.utils.data import rand from anubis.utils.questions import assign_questions @@ -149,6 +150,7 @@ def create_course(users): def seed_main(): # Yeet TheiaSession.query.delete() + AssignedQuestionResponse.query.delete() AssignedStudentQuestion.query.delete() AssignmentQuestion.query.delete() SubmissionTestResult.query.delete() diff --git a/api/anubis/utils/assignments.py b/api/anubis/utils/assignments.py index 40d683b21..0da9230b5 100644 --- a/api/anubis/utils/assignments.py +++ b/api/anubis/utils/assignments.py @@ -1,6 +1,6 @@ import traceback from datetime import datetime -from typing import Union, List, Dict +from typing import Union, List, Dict, Tuple from dateutil.parser import parse as date_parse, ParserError from sqlalchemy import or_, and_ @@ -64,11 +64,11 @@ def get_assignments(netid: str, course_id=None) -> Union[List[Dict[str, str]], N filters.append(Assignment.release_date <= datetime.now()) filters.append(Assignment.hidden == False) - assignments = Assignment.query\ + assignments = Assignment.query \ .join(Course).join(InCourse).join(User).filter( - User.netid == netid, - *filters - ).order_by(Assignment.due_date.desc()).all() + User.netid == netid, + *filters + ).order_by(Assignment.due_date.desc()).all() a = [a.data for a in assignments] for assignment_data in a: @@ -135,7 +135,17 @@ def get_submissions( return [s.full_data for s in submissions] -def assignment_sync(assignment_data): +def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]: + """ + Take an assignment_data dictionary from a assignment meta.yaml + and update any and all existing data about the assignment. + + * This includes the assignment object fields, assignment tests, + and assignment questions. * + + :param assignment_data: + :return: + """ assignment = Assignment.query.filter( Assignment.unique_code == assignment_data["unique_code"] ).first() @@ -170,24 +180,31 @@ def assignment_sync(assignment_data): return "Unable to parse datetime", 406 db.session.add(assignment) - db.session.commit() + # Go through assignment tests, and delete those that are now + # not in the assignment data. for assignment_test in AssignmentTest.query.filter( and_( AssignmentTest.assignment_id == assignment.id, AssignmentTest.name.notin_(assignment_data["tests"]), ) ).all(): + # Delete any and all submission test results that are still outstanding + # for an assignment test that will be deleted. SubmissionTestResult.query.filter( SubmissionTestResult.assignment_test_id == assignment_test.id, ).delete() + + # Delete the assignment test AssignmentTest.query.filter( AssignmentTest.assignment_id == assignment.id, AssignmentTest.name == assignment_test.name, ).delete() - db.session.commit() + # Run though the tests in the assignment data for test_name in assignment_data["tests"]: + + # Find if the assignment test exists assignment_test = ( AssignmentTest.query.filter( Assignment.id == assignment.id, @@ -197,14 +214,19 @@ def assignment_sync(assignment_data): .first() ) + # Create the assignment test if it did not already exist if assignment_test is None: assignment_test = AssignmentTest(assignment=assignment, name=test_name) db.session.add(assignment_test) - db.session.commit() - accepted, ignored, rejected = ingest_questions( - assignment_data["questions"], assignment - ) - question_message = {"accepted": accepted, "ignored": ignored, "rejected": rejected} + # Sync the questions in the assignment data + question_message = None + if 'questions' in assignment_data: + accepted, ignored, rejected = ingest_questions( + assignment_data["questions"], assignment + ) + question_message = {"accepted": accepted, "ignored": ignored, "rejected": rejected} + + db.session.commit() return {"assignment": assignment.data, "questions": question_message}, True diff --git a/api/anubis/utils/cache.py b/api/anubis/utils/cache.py index da05e86fd..1f32a359e 100644 --- a/api/anubis/utils/cache.py +++ b/api/anubis/utils/cache.py @@ -3,6 +3,6 @@ cache = Cache(config={"CACHE_TYPE": "redis"}) -@cache.cached(timeout=1) +@cache.memoize(timeout=1) def cache_health(): - pass + return None diff --git a/api/anubis/utils/data.py b/api/anubis/utils/data.py index 37874abdd..9670a5764 100644 --- a/api/anubis/utils/data.py +++ b/api/anubis/utils/data.py @@ -218,7 +218,14 @@ def split_chunks(lst, n): return _chunks -def rand(max_len=None): +def rand(max_len: int = None): + """ + Get a relatively random hex string of up + to max_len. + + :param max_len: + :return: + """ rand_hash = sha256(urandom(32)).hexdigest() if max_len is not None: return rand_hash[:max_len] @@ -226,7 +233,17 @@ def rand(max_len=None): def human_readable_to_bytes(size: str) -> int: - size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + """ + Convert a string in the form of 5GB and get an integer value + for the number of bytes in that data size. + + >>> human_readable_to_bytes('1 GiB') + >>> 1073741824 + + :param size: + :return: + """ + size_name = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB") size = size.split() # divide '1 GB' into ['1', 'GB'] num, unit = int(size[0]), size[1] # index in list of sizes determines power to raise it to @@ -236,16 +253,25 @@ def human_readable_to_bytes(size: str) -> int: return num * factor -def row2dict(row): - d = {} +def row2dict(row) -> dict: + """ + Convert an sqlalchemy object to a dictionary from its column + values. This function looks at internal sqlalchemy fields + to create a raw dictionary from the columns in the table. + + :param row: + :return: + """ + + raw = {} for column in row.__table__.columns: value = getattr(row, column.name) if isinstance(value, datetime): - d[column.name] = str(value) + raw[column.name] = str(value) continue - d[column.name] = value + raw[column.name] = value - return d + return raw diff --git a/api/anubis/utils/rpc.py b/api/anubis/utils/rpc.py index f81ccf74a..3e17ca768 100644 --- a/api/anubis/utils/rpc.py +++ b/api/anubis/utils/rpc.py @@ -2,7 +2,7 @@ from rq import Queue from anubis.config import config -from anubis.rpc.pipeline import test_repo +from anubis.rpc.pipeline import create_submission_pipeline from anubis.rpc.seed import seed_debug from anubis.rpc.theia import ( initialize_theia_session, @@ -29,20 +29,18 @@ def rpc_enqueue(func, queue=None, args=None): conn.close() -def enqueue_webhook(*args): +def enqueue_autograde_pipeline(*args, queue: str = 'default'): """Enqueues a test job""" - rpc_enqueue(test_repo, args=args) + rpc_enqueue(create_submission_pipeline, queue=queue, args=args) def enqueue_ide_initialize(*args): """Enqueue an ide initialization job""" - - rpc_enqueue(initialize_theia_session, 'theia', args=args) + rpc_enqueue(initialize_theia_session, queue='theia', args=args) def enqueue_ide_stop(*args): """Reap theia session kube resources""" - rpc_enqueue(reap_theia_session, queue='theia', args=args) @@ -51,9 +49,11 @@ def enqueue_ide_reap_stale(*args): rpc_enqueue(reap_stale_theia_sessions, queue='theia', args=args) -def seed(): - rpc_enqueue(seed_debug) +def enqueue_seed(): + """Enqueue debug seed data""" + rpc_enqueue(seed_debug, queue='default') -def create_visuals(*_): - rpc_enqueue(create_visuals_) +def enqueue_create_visuals(*_): + """Enqueue create visuals""" + rpc_enqueue(create_visuals_, queue='default') diff --git a/api/anubis/utils/stats.py b/api/anubis/utils/stats.py index ac1cbbe75..94ff1b557 100644 --- a/api/anubis/utils/stats.py +++ b/api/anubis/utils/stats.py @@ -8,9 +8,30 @@ @cache.memoize(timeout=5 * 60, unless=is_debug) -def stats_for(student_id, assignment_id): +def autograde(student_id, assignment_id): + """ + Get the stats for a specific student on a specific assignment. + + Pulls all submissions, then finds the most recent one that + has the most tests that passed. + + * This function is heavily cached as it is IO intensive on the DB * + + :param student_id: + :param assignment_id: + :return: + """ + + # best is the best submission seen so far best = None + + # best_count is the most tests that have + # passed for this student so far best_count = -1 + + # Iterate over all submissions for this student, tracking + # the best submission so far (based on number of tests passed). + # We start from the oldest submission and move to the newest. for submission in ( Submission.query.filter( Submission.assignment_id == assignment_id, @@ -20,17 +41,38 @@ def stats_for(student_id, assignment_id): .order_by(Submission.created.desc()) .all() ): + + # Calculate the number of tests that passed in this submission correct_count = sum( map(lambda result: 1 if result.passed else 0, submission.test_results) ) + # If the number of passed tests in this assignment is as good + # or better as the best seen so far, update the running best. if correct_count >= best_count: best_count = correct_count best = submission + + # return the submission id of the best if there is one, otherwise None return best.id if best is not None else None -def stats_wrapper(assignment, user_id, netid, name, submission_id): +def stats_wrapper(assignment: Assignment, user_id: str, netid: str, name: str, submission_id: str) -> dict: + """ + The autograde results require quite of bit more information than + just the id of the best submission. This function takes some high level + information about the best submission, and breaks it down into a large + dictionary of all the relevant data for the autograde result. + + * The admin panel uses all this extra data added by this function * + + :param assignment: + :param user_id: + :param netid: + :param name: + :param submission_id: + :return: + """ if submission_id is None: # no submission return { @@ -76,9 +118,27 @@ def stats_wrapper(assignment, user_id, netid, name, submission_id): @cache.memoize(timeout=60 * 60, unless=is_debug) -def bulk_stats(assignment_id, netids=None, offset=0, limit=20): +def bulk_autograde(assignment_id, netids=None, offset=0, limit=20): + """ + Bulk autograde an assignment. Optionally specify a subset of netids. + + The offset and limit are used here to have the results of this function + move as a window of the results. + + * Calculating the autograde results is very IO intensive on the db. For this, + these results are heavily cached. * + + :param assignment_id: + :param netids: + :param offset: + :param limit: + :return: + """ + + # Running list of the best submissions for each student bests = [] + # Find the assignment object assignment = ( Assignment.query.filter_by(name=assignment_id).first() or Assignment.query.filter_by(id=assignment_id).first() @@ -86,13 +146,18 @@ def bulk_stats(assignment_id, netids=None, offset=0, limit=20): if assignment is None: return error_response("assignment does not exist") + # Get the list of students to get autograde results for students = get_students_in_class(assignment.course_id, offset=offset, limit=limit) if netids is not None: students = filter(lambda x: x["netid"] in netids, students) + # Run through each of the students, getting the autograde results for each for student in students: - submission_id = stats_for(student["id"], assignment.id) + # Get the best submission for this student from this assignment + submission_id = autograde(student["id"], assignment.id) bests.append( + # Use the stats_wrapper function to add all the necessary + # metadata for the submission. stats_wrapper( assignment, student["id"], diff --git a/api/anubis/utils/students.py b/api/anubis/utils/students.py index 3c92af446..a789013b6 100644 --- a/api/anubis/utils/students.py +++ b/api/anubis/utils/students.py @@ -1,41 +1,71 @@ +from typing import List, Dict + from anubis.models import User, InCourse, Course from anubis.utils.cache import cache from anubis.utils.data import is_debug -@cache.cached(timeout=5, unless=is_debug) -def get_students(course_code=None): +@cache.memoize(timeout=60, unless=is_debug) +def get_students(course_code: str = None) -> List[Dict[str, dict]]: + """ + Get students by course code. If no course code is specified, + then all courses will be considered. + + * This response is cached for up to 60 seconds * + + :param course_code: + :return: + """ + + # List of sqlalchemy filters filters = [] + + # If a course code is specified, then add it to the filter if course_code is not None: filters = [Course.course_code == course_code] + + # Get all users, and break them into their data props return [ - s.data for s in User.query.join(InCourse).join(Course).filter(*filters).all() + s.data + for s in User.query.join(InCourse).join(Course).filter( + *filters + ).all() ] -@cache.cached(timeout=5, unless=is_debug) -def get_students_in_class(class_id, offset=None, limit=None): +@cache.memoize(timeout=60, unless=is_debug) +def get_students_in_class(course_id, offset=None, limit=None): + """ + Similar to the get_students function, this function + gets all the students in a particular course. This function + takes the course_id instead of the course code. + + * optionally accepts a offset and limit for the query * + * This response is cached for up to 60 seconds * + + :param course_id: + :param offset: + :param limit: + :return: + """ + + # If a limit and offset was specified, then use them + # in the query. if offset is not None and limit is not None: + # Get the users, and break them into their data props return [ u.data - for u in User.query.join(InCourse) - .join(Course) - .filter( - Course.id == class_id, + for u in User.query.join(InCourse).join(Course).filter( + Course.id == course_id, InCourse.owner_id == User.id, - ) - .limit(limit) - .offset(offset) - .all() + ).order_by(User.name.desc()).limit(limit).offset(offset).all() ] + # Get the users, and break them into their data props return [ u.data - for u in User.query.join(InCourse) - .join(Course) - .filter( - Course.id == class_id, + for u in User.query.join(InCourse).join(Course).filter( + Course.id == course_id, InCourse.owner_id == User.id, - ) - .all() + ).order_by(User.name.desc()).all() ] diff --git a/api/anubis/utils/submissions.py b/api/anubis/utils/submissions.py index 21cecf177..6fc033a6a 100644 --- a/api/anubis/utils/submissions.py +++ b/api/anubis/utils/submissions.py @@ -1,90 +1,148 @@ from datetime import datetime +from typing import List, Union from anubis.models import Submission, AssignmentRepo, User, db from anubis.utils.http import error_response, success_response -from anubis.utils.rpc import enqueue_webhook +from anubis.utils.rpc import enqueue_autograde_pipeline -def bulk_regrade_submission(submissions): +def bulk_regrade_submission(submissions: List[Submission]) -> List[dict]: """ Regrade a batch of submissions :param submissions: :return: """ + + # Running list of regrade dictionaries response = [] + + # enqueue regrade jobs for each submissions for submission in submissions: - response.append(regrade_submission(submission)) + response.append(regrade_submission(submission, queue='regrade')) + + # Pass back a list of all the regrade return dictionaries return response -def regrade_submission(submission): +def regrade_submission(submission: Union[Submission, str], queue: str = 'default') -> dict: """ Regrade a submission - :param submission: Union[Submissions, int] + :param submission: Union[Submissions, str] + :param queue: :return: dict response """ + # If the submission is a string, then we consider it to be a submission id if isinstance(submission, str): - submission = Submission.query.filter_by(id=submission).first() + # Try to query for the submission + submission = Submission.query.filter( + Submission.id == submission, + ).first() + + # If there was no submission found, then return an error status if submission is None: return error_response("could not find submission") + # If the submission is already marked as in processing state, then + # we can skip regrading this submission. if not submission.processed: return error_response("submission currently being processed") - submission: Submission + # Update the submission fields to reflect the regrade submission.processed = False submission.state = "regrading" submission.last_updated = datetime.now() + + # Reset the accompanying database objects submission.init_submission_models() - enqueue_webhook(submission.id) + # Enqueue the submission job + enqueue_autograde_pipeline(submission.id, queue=queue) - return success_response({"message": "regrade started"}) + return success_response({ + "message": "regrade started" + }) def fix_dangling(): """ Try to connect repos that do not have an owner. + A dangling submission is a submission that has not been matched to + a student. This happens when a student either does not give anubis + a github username, or provides an incorrect one. When this happens, + all submissions that come in for that repo are tracked, but not graded. + + The purpose of this function is to try to match assignment repos to + submissions that lack an owner. + :return: """ + + # Running list of fixed submissions fixed = [] - dangling_repos = AssignmentRepo.query.filter(AssignmentRepo.owner_id == None).all() - for dr in dangling_repos: - owner = User.query.filter(User.github_username == dr.github_username).first() + # Find Assignment Repos that do not have an owner_id + dangling_repos = AssignmentRepo.query.filter( + AssignmentRepo.owner_id == None, + ).all() + + # Iterate over all dangling repos + for dangling_repo in dangling_repos: + # Attempt to find an owner + owner = User.query.filter(User.github_username == dangling_repo.github_username).first() + # If an owner was found, then fix it if owner is not None: - dr.owner_id = owner.id - db.session.add_all((dr, owner)) + # Update the dangling repo + dangling_repo.owner_id = owner.id + db.session.add_all((dangling_repo, owner)) db.session.commit() - for s in dr.submissions: + # Find all the submissions that belong to that + # repo, fix then grade them. + for s in dangling_repo.submissions: + # Give the submission an owner s.owner_id = owner.id db.session.add(s) db.session.commit() + + # Update running tally of fixed submissions fixed.append(s.data) - enqueue_webhook(s.id) + # Enqueue a autograde job for the submission + enqueue_autograde_pipeline(s.id) + + # Find dangling submissions dangling_submissions = Submission.query.filter(Submission.owner_id == None).all() + + # Iterate through all submissions lacking an owner for s in dangling_submissions: - dr = AssignmentRepo.query.filter( + # Try to find a repo to match + dangling_repo = AssignmentRepo.query.filter( AssignmentRepo.id == s.assignment_repo_id ).first() - owner = User.query.filter(User.github_username == dr.github_username).first() + # Try to find an owner student + owner = User.query.filter(User.github_username == dangling_repo.github_username).first() + # If an owner was found, then fix and regrade all relevant if owner is not None: - dr.owner_id = owner.id - db.session.add_all((dr, owner)) + # Give the repo an owner + dangling_repo.owner_id = owner.id + db.session.add_all((dangling_repo, owner)) db.session.commit() + # Give the submission an owner s.owner_id = owner.id db.session.add(s) db.session.commit() + + # Update running tally of fixed submissions fixed.append(s.data) - enqueue_webhook(s.id) + + # Enqueue a autograde job for the submission + enqueue_autograde_pipeline(s.id) return fixed diff --git a/api/anubis/utils/theia.py b/api/anubis/utils/theia.py index fb4bc88b7..6bcc438e4 100644 --- a/api/anubis/utils/theia.py +++ b/api/anubis/utils/theia.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Union from werkzeug.utils import redirect @@ -42,11 +42,28 @@ def theia_redirect_url(theia_session_id: str, netid: str) -> str: def theia_redirect(theia_session: TheiaSession, user: User): + """ + Create a flask redirect Response for the specified theia session. + + :param theia_session: + :param user: + :return: + """ return redirect(theia_redirect_url(theia_session.id, user.netid)) @cache.memoize(timeout=3, unless=is_debug) def theia_list_all(user_id: str, limit: int = 10): + """ + List all theia sessions that are currently active. Order by the time + they were created. + + * Response is lightly cached * + + :param user_id: + :param limit: + :return: + """ theia_sessions: List[TheiaSession] = ( TheiaSession.query.filter( TheiaSession.owner_id == user_id, @@ -60,13 +77,29 @@ def theia_list_all(user_id: str, limit: int = 10): @cache.memoize(timeout=1, unless=is_debug) -def theia_poll_ide(theia_session_id: str, user_id: str): - theia_session = TheiaSession.query.filter( +def theia_poll_ide(theia_session_id: str, user_id: str) -> Union[None, dict]: + """ + Check the status of a theia session. This is called + when a theia session is created in the frontend. When + the spinner is going, this function is called until + the session is active. + + * Response is very lightly cached * + + :param theia_session_id: + :param user_id: + :return: + """ + + # Query for the theia session + theia_session: TheiaSession = TheiaSession.query.filter( TheiaSession.id == theia_session_id, TheiaSession.owner_id == user_id, ).first() + # If it was not found, then return None if theia_session is None: return None + # Else return the session data return theia_session.data diff --git a/api/anubis/utils/visualizations.py b/api/anubis/utils/visualizations.py index 741c01dd5..9e65eb292 100644 --- a/api/anubis/utils/visualizations.py +++ b/api/anubis/utils/visualizations.py @@ -45,7 +45,29 @@ def get_theia_sessions() -> pd.DataFrame: return theia_sessions -@cache.cached(timeout=360, unless=is_debug) +# def theia_propagate_usage(df: pd.DataFrame) -> pd.DataFrame: +# # df.reset_index(inplace=True) +# df.set_index('created', inplace=True, drop=True) +# for created in df.index: +# count = df.loc[created]['count'] +# assignment_id = df.loc[created]['assignment_id'] +# for i in range(1, 7): +# idx = pd.to_datetime(created + timedelta(hours=i)) +# if idx in df.index: +# logger.info(df.loc[idx]) +# df.loc[idx]['count'] += count +# else: +# df.append(pd.DataFrame( +# [[assignment_id, count]], +# index=[idx], +# columns=df.columns, +# )) +# logger.info(df.loc[idx]) +# break +# return df + + +@cache.memoize(timeout=360, unless=is_debug) def get_usage_plot(): import matplotlib.pyplot as plt import matplotlib.colors as mcolors @@ -77,25 +99,37 @@ def get_usage_plot(): legend_handles0.append( axs[0].axvline( x=assignment.due_date.date(), - color='red', + color=color, + linestyle='dotted', label=f'{assignment.name}', - ymax=0.1, ) ) legend_handles1.append( axs[1].axvline( x=assignment.due_date.date(), - color='red', + color=color, + linestyle='dotted', label=f'{assignment.name}', - ymax=0.1, ) ) - axs[0].legend(handles=legend_handles0, loc='upper center') + utcnow = datetime.utcnow().replace(microsecond=0) + + axs[0].text( + 0.97, 0.9, f'Generated {utcnow} UTC', + transform=axs[0].transAxes, fontsize=12, color='gray', alpha=0.5, + ha='right', va='center', + ) + axs[0].legend(handles=legend_handles0, loc='upper left') axs[0].set(title='Submissions over time', xlabel='time', ylabel='count') axs[0].grid(True) - axs[1].legend(handles=legend_handles1, loc='upper center') + axs[1].text( + 0.97, 0.9, f'Generated {utcnow} UTC', + transform=axs[1].transAxes, fontsize=12, color='gray', alpha=0.5, + ha='right', va='center', + ) + axs[1].legend(handles=legend_handles1, loc='upper left') axs[1].set(title='Cloud IDEs over time', xlabel='time', ylabel='count') axs[1].grid(True) diff --git a/api/anubis/utils/webhook.py b/api/anubis/utils/webhook.py index e137fb82d..8c1d5ffa3 100644 --- a/api/anubis/utils/webhook.py +++ b/api/anubis/utils/webhook.py @@ -21,6 +21,18 @@ def parse_webhook(webhook): def guess_github_username(assignment, repo_name): + """ + In order to match a webhook to a user, we need to know the github username + that the repo in question was made for. The github username is in the name + of the repo. + + This function takes the name of the repo, and does some parsing tricks to + take a fairly confident guess as to what the github username is. + + :param assignment: + :param repo_name: + :return: + """ repo_name_split = repo_name.split("-") unique_code_index = repo_name_split.index(assignment.unique_code) repo_name_split = repo_name_split[unique_code_index + 1:] @@ -41,17 +53,35 @@ def guess_github_username(assignment, repo_name): return user, github_username_guess -def check_repo(assignment, repo_url, github_username, user=None): +def check_repo(assignment, repo_url, github_username, user=None) -> AssignmentRepo: + """ + While processing the webhook, we need to check to see if we have + record of the repo. This function takes what it needs to create + the record of the repo if it didn't already exist. + + :param assignment: + :param repo_url: + :param github_username: + :param user: + :return: + """ + + # if the user is not None, then the submission is + # not dangling. if user is not None: repo = AssignmentRepo.query.filter( AssignmentRepo.assignment_id == assignment.id, AssignmentRepo.owner == user, ).first() + + # If the user is None, then the submission is + # dangling. else: repo = AssignmentRepo.query.filter( AssignmentRepo.repo_url == repo_url ).first() + # If the repo did not exist, then create it. if repo is None: repo = AssignmentRepo( owner=user, @@ -62,4 +92,5 @@ def check_repo(assignment, repo_url, github_username, user=None): db.session.add(repo) db.session.commit() + # Return the repo object return repo diff --git a/api/anubis/views/admin/regrade.py b/api/anubis/views/admin/regrade.py index 762070358..78b6604e4 100644 --- a/api/anubis/views/admin/regrade.py +++ b/api/anubis/views/admin/regrade.py @@ -1,20 +1,43 @@ from datetime import datetime, timedelta +import math from flask import Blueprint +from sqlalchemy import or_ -from anubis.models import Submission, Assignment, User, Course, InCourse +from anubis.models import Submission, Assignment from anubis.rpc.batch import rpc_bulk_regrade from anubis.utils.auth import require_admin from anubis.utils.data import split_chunks from anubis.utils.decorators import json_response from anubis.utils.elastic import log_endpoint from anubis.utils.http import error_response, success_response, get_number_arg -from anubis.utils.rpc import enqueue_webhook, rpc_enqueue +from anubis.utils.rpc import enqueue_autograde_pipeline, rpc_enqueue regrade = Blueprint("admin-regrade", __name__, url_prefix="/admin/regrade") -@regrade.route("/submission/") +@regrade.route("/status/") +@require_admin() +@json_response +def admin_regrade_status(assignment_id: str): + processing = Submission.query.filter( + Submission.assignment_id == assignment_id, + Submission.processed == False, + ).count() + + total = Submission.query.filter( + Submission.assignment_id == assignment_id, + ).count() + + return success_response({ + 'percent': f'{math.ceil(((total - processing)/total)*100)}% of submissions processed', + 'processing': processing, + 'processed': total - processing, + 'total': total, + }) + + +@regrade.route("/submission/") @require_admin() @log_endpoint("cli", lambda: "regrade-commit") @json_response @@ -38,17 +61,17 @@ def private_regrade_submission(commit): s.init_submission_models() # Enqueue the submission pipeline - enqueue_webhook(s.id) + enqueue_autograde_pipeline(s.id) # Return status return success_response({"submission": s.data, "user": s.owner.data}) -@regrade.route("/") +@regrade.route("/assignment/") @require_admin() @log_endpoint("cli", lambda: "regrade") @json_response -def private_regrade_assignment(assignment_name): +def private_regrade_assignment(assignment_id): """ This route is used to restart / re-enqueue jobs. @@ -65,7 +88,7 @@ def private_regrade_assignment(assignment_name): This solution isn't the fastest, but it gets the job done. - :param assignment_name: name of assignment to regrade + :param assignment_id: name of assignment to regrade :return: """ @@ -94,7 +117,9 @@ def private_regrade_assignment(assignment_name): ) # Find the assignment - assignment = Assignment.query.filter_by(name=assignment_name).first() + assignment = Assignment.query.filter( + or_(Assignment.id == assignment_id, Assignment.name == assignment_id) + ).first() if assignment is None: return error_response("cant find assignment") @@ -104,6 +129,7 @@ def private_regrade_assignment(assignment_name): Submission.owner_id is not None, *extra ).all() + submission_count = len(submissions) # Split the submissions into bite sized chunks submission_ids = [s.id for s in submissions] @@ -111,10 +137,10 @@ def private_regrade_assignment(assignment_name): # Enqueue each chunk as a job for the rpc workers for chunk in submission_chunks: - rpc_enqueue(rpc_bulk_regrade, 'default', args=[chunk]) + rpc_enqueue(rpc_bulk_regrade, 'regrade', args=[chunk]) # Pass back the enqueued status return success_response({ - "status": "chunks enqueued", + "status": f"{submission_count} submissions enqueued.", "submissions": submission_ids, }) diff --git a/api/anubis/views/admin/seed.py b/api/anubis/views/admin/seed.py index 65821de0f..0833ba57a 100644 --- a/api/anubis/views/admin/seed.py +++ b/api/anubis/views/admin/seed.py @@ -4,7 +4,7 @@ from anubis.utils.data import is_debug from anubis.utils.decorators import json_response from anubis.utils.http import success_response, error_response -from anubis.utils.rpc import seed as seed_ +from anubis.utils.rpc import enqueue_seed as seed_ seed = Blueprint("admin-seed", __name__, url_prefix="/admin/seed") diff --git a/api/anubis/views/admin/stats.py b/api/anubis/views/admin/stats.py index 6878c7a59..77b3a8356 100644 --- a/api/anubis/views/admin/stats.py +++ b/api/anubis/views/admin/stats.py @@ -6,7 +6,7 @@ from anubis.utils.elastic import log_endpoint from anubis.utils.http import success_response, error_response, get_number_arg from anubis.utils.questions import get_assigned_questions -from anubis.utils.stats import bulk_stats, stats_for, stats_wrapper +from anubis.utils.stats import bulk_autograde, autograde, stats_wrapper stats = Blueprint("admin-stats", __name__, url_prefix="/admin/stats") @@ -34,7 +34,7 @@ def private_stats_assignment(assignment_id, netid=None): limit = get_number_arg("limit", 10) offset = get_number_arg("offset", 0) - bests = bulk_stats(assignment_id, limit=limit, offset=offset) + bests = bulk_autograde(assignment_id, limit=limit, offset=offset) return success_response({"stats": bests}) @@ -46,7 +46,7 @@ def private_stats_for(assignment_id, user_id): Assignment.id == assignment_id, ).first() user = User.query.filter(User.id == user_id).first() - submission_id = stats_for(user_id, assignment_id) + submission_id = autograde(user_id, assignment_id) return success_response( { @@ -84,7 +84,7 @@ def private_submission_stats_id(assignment_id: str, netid: str): if assignment is None: return error_response('Assignment does not exist') - submission_id = stats_for(user.id, assignment.id) + submission_id = autograde(user.id, assignment.id) submission_full_data = None if submission_id is not None: diff --git a/api/anubis/views/public/webhook.py b/api/anubis/views/public/webhook.py index 0fba29be1..b3704fb03 100644 --- a/api/anubis/views/public/webhook.py +++ b/api/anubis/views/public/webhook.py @@ -16,7 +16,7 @@ from anubis.utils.elastic import log_endpoint, esindex from anubis.utils.http import error_response, success_response from anubis.utils.logger import logger -from anubis.utils.rpc import enqueue_webhook +from anubis.utils.rpc import enqueue_autograde_pipeline from anubis.utils.webhook import parse_webhook, guess_github_username, check_repo webhook = Blueprint("public-webhook", __name__, url_prefix="/public/webhook") @@ -192,6 +192,6 @@ def public_webhook(): ) # if the github username is not found, create a dangling submission - enqueue_webhook(submission.id) + enqueue_autograde_pipeline(submission.id) return success_response("submission accepted") diff --git a/api/jobs/reaper.py b/api/jobs/reaper.py index 9470a6577..c1b81ef72 100644 --- a/api/jobs/reaper.py +++ b/api/jobs/reaper.py @@ -8,8 +8,8 @@ from anubis.app import create_app from anubis.models import db, Submission, Assignment, AssignmentRepo, TheiaSession -from anubis.utils.rpc import enqueue_ide_stop, enqueue_ide_reap_stale, enqueue_webhook -from anubis.utils.stats import bulk_stats +from anubis.utils.rpc import enqueue_ide_stop, enqueue_ide_reap_stale, enqueue_autograde_pipeline +from anubis.utils.stats import bulk_autograde from anubis.utils.webhook import check_repo, guess_github_username @@ -78,10 +78,10 @@ def reap_stats(): ).all(): if submission.build is None: submission.init_submission_models() - enqueue_webhook(submission.id) + enqueue_autograde_pipeline(submission.id) for assignment in recent_assignments: - bulk_stats(assignment.id) + bulk_autograde(assignment.id) def reap_repos(): @@ -168,16 +168,21 @@ def reap_repos(): user, github_username = guess_github_username(assignment, repo_name) repo = check_repo(assignment, repo_url, github_username, user) + if user is None: + continue + # Check for broken submissions submissions = [] for submission in Submission.query.filter(Submission.assignment_repo_id == repo.id).all(): + if submission is None: + continue if submission.owner_id != user.id: print(f'found broken submission {submission.id}') submission.owner_id = repo.owner_id submissions.append(submission.id) db.session.commit() for sid in submissions: - enqueue_webhook(sid) + enqueue_autograde_pipeline(sid) # Check for missing submissions for commit in map(lambda x: x['node']['oid'], ref['target']['history']['edges']): @@ -196,7 +201,7 @@ def reap_repos(): db.session.add(submission) db.session.commit() submission.init_submission_models() - enqueue_webhook(submission.id) + enqueue_autograde_pipeline(submission.id) r = AssignmentRepo.query.filter(AssignmentRepo.repo_url == repo_url).first() if r is not None: @@ -212,7 +217,7 @@ def reap_repos(): db.session.commit() for sid in submissions: - enqueue_webhook(sid) + enqueue_autograde_pipeline(sid) if repo: print(f'checked repo: {repo_name} {github_username} {user} {repo.id}') diff --git a/docker-compose.yml b/docker-compose.yml index 15b7cd75d..ab7665406 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,17 @@ services: labels: - "traefik.enable=false" + rpc-regrade: + build: ./api + command: "rq worker -u redis://:anubis@redis-master regrade" + environment: + - "DEBUG=1" + - "DB_HOST=db" + volumes: + - "./api:/opt/app" + labels: + - "traefik.enable=false" + web: image: registry.osiris.services/anubis/web:latest build: ./web diff --git a/docs/design.pdf b/docs/design.pdf index 124839b552109cbd95d9d59bbf678979862fe745..0e0e2e7cd770b344e7e444d61f1d99e497861a5a 100644 GIT binary patch delta 469 zcmZ43>bx=WJ2UvZUXYU!xB5nb>gJ zc9(CBurX@fc;cGymH31ihCev&v8t2>)E}C?Cs$P3vc_cZDhrF9cfOrnXZt+1&n@pt z^%iw;f^b!+ZgBvbe8?nk%KYgRj7eLHnxvg@`_bs4wQ z*3RrczO!zhmatK``?b|wI@^!$dwHwJ;&UA*=iL;${JV=vKbquTES5Qbc8R9`7yfh4 z6S%oj^HPdSic%AEL9G0wtmbIO_Gm^BW&&bnAZ7t#Rv=~rVs;?r0Afxc<^p1FAm#yL zULfWJVtybN0Aj)I(TqY4+w@Hg%nZ$pOpPpc4NTMx4AeEb^nLSFToOxC6*OF|j0}tn z4Um;=Keb(G5v#M4ft$05xsjWRiKUZ)i?M~Hi>rmXo3pW-xru>`tGSzbgJ zc9(CBu$jdActXsEfci$oLlQzP@)swy{}R6UBzN_R6Au@^40XMD`sud(vbUD+Eu?k^ zzW?fFbLG&6O>Cd_Q;Ls?80GLiicZ+JB>CkIFYnGpu3RFg&ObT$^l1I(g9kPV>(@LJ z5I_9tQABK{nA2>9WIi(yhxrCB{AQA8-WV`x&3^J>^ZP#YRYm7chi(sdn%4&{gY>#FHVJ0AE24WT;wx8NAw20N&%+=Aw$jHUT#m&IY$k^D)$=KM!(Zs;e#lX
Anubis
users
Cluster
Traefik
Anubis Cloud IDE
API
Static
logging
State Stores
rpc_cluster
submission pipeline jobs
students
Admins
Ingress
Ingress
Ingress
Ingress
Ingress
Ingress
forward
forward
forward
create
create
create
log
log
log
cache or push rpc
cache or push rpc
cache or push rpc
log stream
push state updates
push state updates
push state updates
create
create
create
rpc poll
rpc poll
rpc poll
rpc poll
store
submission job 1337
submission job 1338
submission job 1339
pipeline API
RPC Worker
RPC Worker
RPC Worker
RPC Worker
Database
elasticsearch
redis cache
logstash
web static
web static
web static
API 1
API 1
API 1
theia proxy
theia proxy
theia session 1337
theia session 1338
theia session 1339
Traefik
TA
Professor
student
student
student
student
student
student
student
student
\ No newline at end of file +
Anubis
users
Cluster
Traefik
Anubis Cloud IDE
API
Static
logging
State Stores
rpc_cluster
submission pipeline jobs
students
Admins
Ingress
Ingress
Ingress
Ingress
Ingress
Ingress
forward
forward
forward
create
create
create
log
log
log
cache or push rpc
cache or push rpc
cache or push rpc
log stream
push state updates
push state updates
push state updates
create
create
create
rpc poll
rpc poll
rpc poll
rpc poll
store
submission job 1337
submission job 1338
submission job 1339
pipeline API
RPC Worker
RPC Worker
RPC Worker
RPC Worker
Database
elasticsearch
redis cache
logstash
web static
web static
web static
API 1
API 1
API 1
theia proxy
theia proxy
theia session 1337
theia session 1338
theia session 1339
Traefik
TA
Professor
student
student
student
student
student
student
student
student
\ No newline at end of file diff --git a/docs/img/rpc-queue-1.mmd.png b/docs/img/rpc-queue-1.mmd.png new file mode 100644 index 0000000000000000000000000000000000000000..98373de76c5472b4ecc1ae96daffabce06409e47 GIT binary patch literal 14001 zcmdtJWmFwewrm>aUdK=UXWE^$t$1^_LdkprZmK zK_+&p?*c4UE$4W6Z^VKOT3Kl=)|sj1vi=NV};mHQVuDy2h>Dd~+If*FP1EnY00>t0Zi*jn>U6FQR@tl@Sx$VZ>#zlw@N8L)g4Rr$ceD#lf} z&%$$U4FLjAS8-QnwnqgnkxAj2CZrP!>G8OtL(85<%fNXsSxzw7WOflZF>cVRco;ci zB}Q#VqB?mP&nAdfS2OqQo%!^wVl)33-Dn{Q6celzdDCw-(x0ZG<7RvdL8p2M{PiEE{T@2ElAhP z_T#3vPVw!ZjUwZvV?B#MBZRl73&|~}Y@#V7>4MJu^2-ts@;NyL!N@PEeRHEY-;hqK z_l8vv(@u2jKNsRyVqTg)VKrtLHIeA4;v66^lZK6!38c?lD9NRd?80sXY8orx>AVxA+_B+OCjyHR@CT5`u@&z)PFN%hYhFW>0dkv|h zOAieE9@;3r(qWNq9g5K5cj4}K{M(iU^DOTGzRqe|w>F7QfYzYBULfe#S6btSgcI)Q_n-%YM8E+2mu9sB&D z2vSf|@|RKeKur4^WgeqXLeFmBAu6i!{oNNvX3ETGU~dru%k`Ygkrz-z^^EH5uBD60=-b)5=t-Gqc9diiaN+xmv5 z!l%033@h*7%(^kvTzz=k$gqOwekYdpePxPQGb$K{KXw(E_RT|tJ zIc3J2zHDJM^RSwyVY8gR&)lrjWueHK>(b?~zs^73Z(gl8mmk&Sh*boT?q17{OiubM zni{)vSTQTbLGoFnpuJ4`-uU|t(L5IOIw?BKy_ zUN)?l7=0rulkn|-~JDA5jcbfOT0x!((@jR)a4(gY?T#N z#TyQTE#~W(fzUwlfSPL1H3UB55GpM%O@%Mj#LMx@G6S>bumZv*d#bIib?`j;L9p(L zY(x)=snnkt7g{h;w&=Txr z2g4*4v|8~TY}F*JwEJ8c6c$zb4yjjBF>lcd*-NEw=bfv6(QGyc;H#)4)OartMQU@v zCPZF4y7%FxFMB}Eaz=_KeX%7o)poTHP2|Tjbl4qPYB;6}9>4#xnS5Kp-tP=K~>>MTIr8wo>(CmMtqFin%njo)6hmOO2l zALSpP8Ea289SNd%nx)BiT1r(p`=>QG&iV0uLnWplK!Ob!LHezXaWqZ#H*oI7J{BTm zXvP!ugckNZpKj(`bJHeZ30A*XDG#jy8I*jljh8yhYNkSa<_u&U@D~>AN1jYz1X?>10O)h_FMsW9Ha{J z4YXQn2NBDV>Lk7Dchz9R~RDc3i-al`;UB-)wifT`LV`E**ieZ09PeN>WV3yP3Q1z_tg#By> z0@UpFs^?epR|`ne@0wSPcr^5xwsOW@fp&ta8hj90G_8CWw#ureI~Y5dT%ypZ=#3>= z1J_t|O#g!BvSR$^lj1hq3}uQIZr@Tn{D(^Z-!^R?4-k>Ev|qNQt4&rj#!w6_&BI3r zp}9Iw_^_hy(e)s;jeMqQ#@CsVn?RvLgWQRsrFpO=zPPZA1Z2oUR$>bg z$z$!GIm(s>hp$@_Bh7CGpPdO8u-pWt2O%r@zILd61Np9x#pEc$s9(Po;Ez|7_2fmd zicg+J;G3U*rg_E$eU;LLYkge7xwMw&a$nkeh}){$0##-~cgnNQm8c_F z9hROy>dOGP-5TFWH+kO5D#`|sREj{J;%v_c*GhKgK65ssO^U7Oo+FkdYFaMy^w5u( z&a4X%4%xD=!|Z%oA=6@m&QDjQ?qIc3cf&^Pj08!_bXdd#KxS@b+Hd`Ar)O- z)~8DX5>ySBOd${Py4MHALfFcQg8iJt{1FaSc1$jg z`hgex^ip9ml!6NR?|07KwQu5C`eL18&uyabGQ>AK5mFL{XRDucJR2l^W@<$T&@5~l zaadzVs^q-lD!=*4_-=Pm93#4Gn7^fpE(D#1{A<%q1)yp0E zqpg5?#D7u{0aHsVyaj;}@X=F`4U~)E}xsKp^2$J|SB->*8YXW^Dr-HC)W7 z>kF>)l5x{fc#2(7vSPg3e*i44-@8c+fpqZ6ZBusp(=^a!NG83Yuj;h1ZI$eZaW{wYa?8CX_`? zGF;|$Ek_9eW2CGifx)T#`Hc}`M#+n%6v~96*QW3CvEBAvYJv-e_Xz7g`cm!I6zol| ztFe^2wQ=(atola`csZTaWm0-_Bp`475YKngFCr_HZammywaSQ1^wWe?`wZOq-c2g* z#FE{c_RMA@*w*Fe(21*C6~))V8{EORaO=4*`oz?vHm5lQYlO zSDMN5Y{5?GPkc{^ax^{te_(+WD|)wma%sD}viS-ECB3OB4BO*boQgq#>S--LfUr(q zaIqQ-e~cc2tsx)e$;Gs+1|@DRgp{=*SM3=mlq-w#>_o{b>TjeHqL38UIn3woCksAv zvDZR@xY+AOy5|kRc;N2NOm+Z@|4O0u2Y3dW;DdkGr?hyD{adlD_za}+6&~o$x3z)x zFJ;?XiU`oE*V#E05ai@y*C|U-gilSw_{OTs&=dla3eW;r&Y{ z01%98k=5+s-W63T#Yt7^&9>yPez_xcN7Jb<+VR0zM~yiXGg!3`xjIH@#)=#uUHtp> z+o!U!Z)NN8fcHT_2NyfT*M-%s4+u|IOKFfG=j-}kmm`w07TqDM`3>oGIpw*L&0tZL zCg#}Z*_l~ka$_$lT3R6q#{HcONB-Cx#{9U)l~eW$0dCCjaCoJg;$oV<>HOSCA3_qE z$UoDREgt{I@w=QJAky&`p+SI?i2*YCsDmW_^7b#HteC2AW#+@(Yuah-5nwq$2I?MM zK8$cIwJh$f(}s-H=YZ8}O-<%?XZKCFFuUUEFPWg=ot@+m z4&VXvC*PrIk9DlKdQOIvXd6U>x`fBm+()kQG)I490#K-U?ts6OHFAR1__exWn8D!G z)tl$m+V2}AUyaozhwO3dry|#vi1+Mq&@6|uYz3{8w%Fr4vM%W1S>?Fdvp$Qe+>x|( z$Be)2!2xosf;QefLu^;k!oZ z+so`SG%3Sn5xl#sxq_+MJ@zYezqm$fq~r4iIPi3iy%Xqb)3$AFYI-nK-@=aNu+#fh z$8QyZv6`m!;KMIYDYgE0?10=ARh4@J1)+^?hFb0cCv+Y$x83j`Utxhh&5Y)1-$wMt zZ-B^^WgagtGa5g86InF0%_)C=AkI}#R2&@bGB;SfH0A<$BXX5sROGcRPKugv-Q*3k z#p7KZCNwDf>AdkBYro5sXakRD4BrW+BJOtz5EcMqu691A=x+~`ASdOof6ajtA3vez z-d|oFGJLq7uqfP*vPu+B2F@fA(HDN+4Em<230F2XK6nWyfOE!W+v8t1V2uGyC>JFNF2f#Ea2! z7VCE*eS+sJ46~azNWlDYu2w;82YbjMrl!hZ_P6oDQK{AGwo?tNGO8ZHZOv>6A(7}^ zQpSE`W~CG)8$WMv**y+{PbCsfsHT(f)ohjFqY zDWthyL+64Yp=yC&rabC@A&n$mj$H4x50EmgvAe6a@4RxOh=?}fysey}p|Tu{uoytm zRo>w~SpWc7a)o%Yp9QQS2VK(x7N<7g2+@FQ8zjMd{lTxYlLLsmnqVfNMpv)(E%AIx zfWjH40A6A+?7eRHvDXA(+@Yb8p8==-zuck3u!uV~IVoqHAlLBk~_E zI+{05xeKJJ=Iwoyt~NhdBoql*9ljlObVG%l4>o1mpP8S$yg$Cv(Ub$X%lQR*Qoq$-+wX2mafm4Da49b!o)pp9#UQ(kR+U4UOxVVV z1T9W^ix@BbMnL<5<&p5PjjOLr*Ltp4PdB^nGcc80d(9M?LHox|zo|-ohYHguWQNJ) z*(!Ozp<*i9zUgjIFf?>E2WyaT zHqx3-5VBvVVnKmCjoezsk+7ze6qxCwDn;DI*rNSbmJ{}Lm#WNMP3>=w7rAtTnf?*f zA}{-14{(e|lL;z~C(C@0q?$RSuHV^&cCp%k0$3rOoQjUXsqht%*V-DezS^SLsq>*M zd;@dpH*XO&DQap371f@zI&)a806v?|He%e8gJOl?u5^g*D381IyA47GrRPS*SF*_J zB2DVQl1_mD?q;hwv7V?vU%Sv7?M36tLKO@ruQnns zFRJojw6Jan5BUZ~f_!Er!+lR((6k}OShK2i5V8;hU8(9o`P&V8`ODMx_LA#quEEKe za#v%8Vv`wCP9Ig`EjlF};RmnXa@kV270LvP^`Z$3ol`7Uw<1_DKlYC>X%?FmgW3P4d0;GhM#>s&(Y{I<*i(6rj~qq zKSkv=VbtWgduF$|xH4LxLK_Zm$i6ScXzY~V{o*ho`#)u9#RrwcxQkUc$WNa>h4v7x zA`rrZ4A%QEa5x-mrw!i=)r#CIrEwR5{h|cij%ZgfNtu74R1*?vO!i=(JFTLobNR^R;vo9u~W*@naSgdk96JlDKcf#iR?TKWw zh?VAcg2jXQ?pZVv(sC@Fri(;~Hk)c-CV{cuC0Mn=K`x(%(*OAkMH=sGpfU}wd4Owi z4F2p)r9amtM_bPCjg~2gm*B1~ZN!cp;gAbe z@v+lNA|0qz8Z%>}l{8aDwb6%nW0uf_8#s$l`O`tI}4Y`l4OT5$Le7;{#(m z)Vc>uxNAxla{Znn&H{D>Z4pi+}@~N`a;~0A+2GP;bFLvgwmozmn4x72=~04>vV(ZadSS>%s0==ZHQ zj!Z_Sc4Rd*1yv7l{pb%89^peUOtk)<%2z zXIqDcQ9kl8!xe;I$O{S#Ub;M{EeAf?G}psOS$a-p7eA8*FxjlKA+B;Dd86L_RWg@q zuY%|{pSQfmI}W>GZ5wOQ%e7ZED{K}$#Ehtu&fmC{J~w%uKjI8R-Z^aXkd*o+65;AH z^N*J^o%<&~N$jyb6)z=1yEl>Yu$rJdHg*VjX^DhbA7^G}28a7QnT?H&fuSJ;BO~Me zlTWf)-H$6XE?(ZgsVS5Stp?$n!};y)ZNGR*dFlB&mm{_JL%qEpblY49DzuvnOiTi0 zk{BRB?HwJ=?&k(2#-5&@26nI8(E*ZVy}dyQ+sDU zcgiOnXSEh%KO)H2y%|)Y#|dXYd~He>A&%m zfbntJ5pO3ar~CVR$$3^ldhQ+`GJ4p~8Po{Db{RBDQ!}8dfu-7i^?NRJvrP`nxY1&> zw~~#f`R2KRSh!vmZ9!1_g06O4A-8tQ4zFjt=jE&r_(@sAO(zLgD1CL4A4LU5S_YwD z#Emp!Of58wF;Z8e2lBV^QD1mo>u&V81~yHp5qsh=a%GfQ>IG+^r- zU2He9{(`N+0)vzopNC^Y8nqIIB?WSt!$T4dhR!Yj0W!3(t&@|e?(VO^{I|waSgWh6 zzs3;pf5OCs0x77eiRh4amBzUm z$5Vkrl9bP<4k$nJxC3pF0<-4io~(RRkjO|8OZ@p9CJs%R{I4*v==RP5Xk4596K&&5 zU~&^&GMkM^<|7NMsQVoUgoh`?*6oeNInk0ZgJU9x?C4*KOsOB0yD)YJ+rQV(50^nt zQ&Zh*n2-)fRi0bgTBVl91Sgg`dItFY-PFA+x7Xs42!Xk9GNZ$D1Pk~0uck+$)AIil?eSmzJ zzwb#pA1|R|GwSu{ibYi0Zwid3bJr>Clf@j#T7Hh1ip?v-{9qp7PIB?Y& zKjw(sJp%ekaM_-jL-z48E0tDL!xAN72LA0B#XDN;o9b}6U}tnPuFWOK;DW)QIXd^^ zFCwrRNvNn`fe0ZM0F9(jrbhq#`gAtEurL@+$V>KDNm-d0P-}8(s<5UeF2BdE==bj| zy}jQBszZ3xVNCL)NAlIrbc4^UX_ zfL_C2HaMY4VECqqE^xxM4U|pic5DO%g+B-BASnTq_rKzX)_BM!2SBpL9FYAD!`>q%0SV=N!QW+nQ(>3n? zp&{cQf2iuZx`4Dae6#UnzrDR)uN>WupX_*?c9B^=uefSzY8H!?go9D|AwV#ef)0i! zsRArTDSaw7pp5+VfnUhgb&|!{&69YyjTn!FTVlwqJvB&Bwf&J86*aBlWcb^=I1jnBjsE@d zhDGv@)>ap}tWe+dow~#b-a>I0B=7J0DsdZoG3&GL-`L1=$XSyhD`&abK=e;`Ejqtn z1q`;5Ot3fM7e8z$O=W?OwhQsgTfyWugVZIod9)=)9_Q0_*d}2qv4WQ&#V;-`?VXsA ztJRg0LpnV@J-@n&w^3G5_|+OzoEOh3&-&WMhzMx8x~VCt)$3qqN2%D|-{1dNN=jHT z627#p{ai7s*|jjXo&9SR0tZinStFcl(0tCic%$%n)zBzD3EAi zhq#+Uz&BnOx3EKvYPbH(5#hh;fci~{B)s#8W|UcT zObCqE?agsn=3ZgFY+G~Al9Psv??fv;1kVJ8SP2jym!tV#G|FQe7;T&x631<#I4U%2 zgmY6sH+2wpNfdg+c}grpc7YDSD-Top@s?P@)I5`Wk2>paL~CBSmxItd&AYL_2r8I6slpB zVn-qK>6&L`r5E1rpJfl&YXphiMC4|tE9N!){cFcsJwZ@6LzWn0F3E<`MSTa3Kn}W= z%kwW%KG;y-i~!vF`g!tItCiHR^7;sOFInAr2O-a-T|Y|<{3&;XTy}gJcXpMgtN(%d zo+h)}112~A5F2H6c?X9}Pg+`8l=Sqsv#6+y4nD3+Nl6V44}S%qQ9)ybD2axqrj#Bn z4UMp>D*Mz$%VuGGe7vER6-|SD(=X+K?-CrXZf6V$bLOU|Uszd{l+Zlx;LhjlxQW^QhrUWfl5lc&lD}}jKKE%6zx$)uw zPW`=-8TUK5`xiC<&ZMk7%z|-hn2Aq+dRFSuByWv0cV-e9#iJoGz<#FNgAzU!?DJ<2 zFtAb-v)XKIFh9TBPWut(i0{E#TrwtbCWlocHG=ajBVTW$bYFmKT0o~ZbSq_^T~H7- zTXYrOy`SXi6CE53jfP(EHt6W+s8n?&GpVF3xVAHOQodNB9RdKgf7E3~MIq)>nSWPb zi*7s*(%CYUe&uL=zw**=j}|@O{akOgmZNdgJbpV`SCMCofN1?MN|}-ow3DmLR_{C) zi;?A4klBBaVR7UM-it#oDAFe*-)mo(R_aSrcWm~)fLL*L4G1nw&5x|eXOCRsDSZbXh0% zv5VCfSf5f*|D2W?DCK!A|L_^x+I5OE_3-w&+p(KE>v(3ycJcryOcdnm>!VT2l;WZa zupG#LukmMP;G5pqV`m~Z6cp6+4{wIw7&2xG6g>Zs>|pdHp0ISK^IvshQ1M^m+5jtQ8j*XPH-?b}6jQqgi>|NjGn6 z5A7#8<~J?;>w9-D^hlN|_^3=GUinH$1YTkeQSZXy|?Nsjztau<; zFRxzQCjsEs7gCXrML)THXf>;dv#!=rklrbkzJ$F{yqe3_0+V^aE&xRsh!&FB1ns+? z4{Sct;Pu2iLh|$XlaF#_9rp|An=JU+VbfEh&6h)|z0z8v?9YfA1H#Q`5+ z>QpBmE>uMP{*6IJLn9p(C?PK1yR@Y7@2AyVu^+8^^Gm$96`!QG!5NNdKX z1**@p(z)uoB6G{Qn3;u>yEyXaCc0HVdo2&eb_R+$hGH3vA*> z&mQAXrj+$Aa|LQDKQJ($YzbXb&@O6eIN{Dz1JU#$AU7BN#w?SxqQYn?SaKJ#MIdFKj6ey>2dYl4i2p12}hhf|t zWJJW@?TLT!l=8%p^7Hez4rYtuP_NTC?WGG_LEMWWfN)BzhldtnOifMaN|a%yvILW) zb2qzuVw#(|M&_h+b#+UQ0yOBvB_(0Y-=2E)r(K}K;Rasnyj-%pqn|$@a3ali#3C)w z@xTKs>*idhQU?Em&4O@^n|kj?MS%D$_2~Sr;MvLuUI1@7J{K{FKkZ^V7_(s?QHFfK9wXWY) zLk~FM#ohK)6*mK8O;{MyG~h1oi5YHodRc6=b4L^L(>U?# z>FK#YT#}njWkzjn84z$efQF)_Vv@OeczOXki$W|Q3Z$j?Q`s4H<>gTTh&~8lZHJwH zIN(;>e+p#4Yb|wqmOqr0m2q%z41jwBalZR#v5ML4RHsBc`;w>VPc{%I2#D)Y>v&@l zWJr)f>yI2lhfmIQ$?XX8eR{jw9g+aIL=vAzoX*vq^bq9b#ZO&1On$oh#s}vW1Qcw_ z^?!cga2YqSw2cICm_kJ&IKM@% zhZ;__M?8ysg=p%V?wU2?LmJbhV4-J+eBq@ZEFo90P!+6ZT4)EPgi>3s9F z!KwagC-`yBI+U_9y)9-vZE|;@&h0&FExdXoW}X~*x#lz-=I2&TEofxUL44V;{uoNOu^90UYJv*!Z~z;;RJ?Rj~4Nci~)f%GE*kcSVD zkXLk^_q#EIJ4-dj8f~uhnF3zn0G&i6P z_ghTcovnXhGsbvEGh_^L=U0F?mTS3x+^E>!evm*hP@aJkV9}eO{uB!b<+JMbLV|U+ zLDpR-^4e4kEIOL0Pk?+Ca7GUhZ<=a~Y*+cSDDgL(oc9rVFOuVUy$!q0Y(n_$^(8hj z5gGvj0ieP#{6DXMwGhe6%a12BLmn+u^d-`3Z|&^`rm&jl0_ir6<>l#a2>6+4aWV(` ziP^?O(aBtnB!e-;jMVXI8dJbkKy{{AsQ|w7>6CYbCCtjo>YXo|O{4~CHCXkGk0Szw zhdw|!WEqY^`xr>eNyQj(Z{yGQZ`esK1z#49@uCtCqO;vr9{{@QuidAW+)&A$2WdRC>@Lp(WZf!LJs5ZctGV<;3E-!_D{^U`s z(u)#{z!H*>fI-6N=zo2AB1Z|B%;d-49f}Ewh(JI@L}apAVyd^Ch2GvKPCR@1<3d4b z-?fm{sKJ3a?31dgs4~4MEXRu;C=lwOHzt9y+^V^Kt>3i00Ty9l0L*WJ!~A&zJMXDN zylw#*0Bx8@w^0M7P!depvm{hB)V&Km1lDdbewH^hQ0kIUk2}gbyC`le;GHezrNPp@ zL=hQT>E3K+bO(deax`m^%+sJCOj>ZH6o*IQk zYh8&JFIz0k=Fc)5XDg8OH8_IqSnY*(K+P3A>;1(wGZlU{#%?hQdRMJt;FsUa{;G0Y^bTg4Idg{y1*(RdbB_=?Z02>0Zi-%n0IC z4zcxA_D&6Cs`axYpqsFLUs+-5J?TE*WT!C9&;nC)G*=P;2-J9|Ka6}q+>qQ#;)~Rg zYS=tu75A?G4z@c=2+CU!ln4yA&OQFL*NUu-ZXLmj7C9>>Jp(hAy604HnadPf2{A~x zH(<+W%5Ar{awz-zpLBo*P+HHIVtt^e?<#*r`mulm#Z8jEk!s zv#dJGs>8)daL)z^bx{rMUP#`K8CSDHy7W{x~B%j56ikq~>ygg?rDn zx-8O_8NH)w_pVq$3!Kw~4MVg+$N`d-K+=5Ze1%7A(EGiFf(_B#5ZVRtyYeVNlDc9` zSkzRZL~?LKPBuHgGJMR>&*E}3PPSmCQQ3UDT`I5Yk6NM;MI4)qogD$tozxvktpd7T zp?jj|1xQXv$0tZx@kN2iyLFkJoS5*ijvDcJR_48{`CrWwF3@vT3}+=jmDiE1U}UIork}vt5f2<8{C~bJAT6N5U7TMK zUp(V69DR%u4k3w6jP;Z9vFq+vBn?6idcVzRic&%(q;Et#sm{7i_3M&jjtB$@QNI@P zD2Wd3znU;1(z?Kh3+p^CqH$aZ+VUA#Y|&xXul>`nUWPU%WEKSfX)6f;Z6yK_#Vgx~ zQcN0yAwlyXOP>>-XEsk*yyiBzKLY=mkxu~!pkY1!R)|!s!c!9pvX%4luR{N?g zQ`Wzdb9vUDB`BQ9kIfTl6!!B)9Qm`ZiukDZ99oMC0kQ{^eSb>*q}? zUC1`5%A^1>wekDr|y)(#8omCFjF49#unopA;h3gu{%)hd8K=YZ1s8se@vB03Ku z_|OW0P`Kvi?ldegkPK+=HFDT$2}vme9ZoR5YF;bUATlvN;cMfgCScaZ+q&xxlikP^IyR9VgK&rv7natAq(Q&FuaaNZCGAw9vLQ!&vxh<=mow& zgh`DQTV$iq%gOnXkiPc#wDXrn3CVVi8_y`RiPrhYgI8NHa%VAn5Q#25~$e- zB1X7hf2P|}tigXb5#Y0y+B#8`plHAMxA?yvot1Bp&BzHeEsWVsz&B_hDY0*&mBI%8 F{|}i4>#qO+ literal 0 HcmV?d00001 diff --git a/docs/img/rpc-queue-1.mmd.svg b/docs/img/rpc-queue-1.mmd.svg new file mode 100644 index 000000000..e3e0f7e38 --- /dev/null +++ b/docs/img/rpc-queue-1.mmd.svg @@ -0,0 +1 @@ +
Anubis
RPC Queue
RPC worker pool
enqueue
dequeue
API
worker
job1
...
job3
\ No newline at end of file diff --git a/docs/img/rpc-queue-2.mmd.png b/docs/img/rpc-queue-2.mmd.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d7e59c793f36cccd950d164de35227311afe3c GIT binary patch literal 12986 zcmc(GWn5IzxAtJ6(xP-p3rKfLON(?!!_YN!cZY<)Fobk>w@4!)H8e`?cg*9)8|S z26~AICkd3t^Z^tK{K`*uf|dlb551Q)LF(vQi_@JPZu8tUggv+EJ$8NlNHB2*3{t4A@%4#Ap;zU=)o zEAEIL@vVuRCHz*EQv41DebgUQr#ud9R&Pqmv5L{nCaS-L_+|u(ma2<-z9eQ#<#t!Q z7mt7iKEAwe;GLHz7tE}0TQa))=3dG#0{hTG!JN^UYI#FPNx2jG?=3Uukqu1_JDBAP z43B;{>)@)7F3LMO)f&PCpF%zrxmbW?RT<mrW)(+S{y)A99+qD-{=~EH#zMm7K70h^e*z;^;+G&G58{@6Ui2CHD7j z&tea*S1**QuA_GYoS&7v-cJC8+Ytd~;4T>)8obtIPu!82-lN!&7a}4KJ~KF9P>@OT zy~aK}&3Pk=(pgo1>`7p7>FK9L5i`5DHfd~}4E0%Su3SBn$0NUs>6%xpKG^C|Y1W~n zj30S0E_ca&O;^9xU`SkreYxf@;<}2S1B_5Nr%@LFc(&>dm!IR~0l&cIkL`zcnHnk< zW9y=~e%b^Hy`W=Yh=}gk&$Y~XSp^Rb4L&ujd#fwxj^VfGUi~L!(Z7&>O^R1SVtpz{ zCk3!UW#8N8bXwBC-C7x31ymcw$p&{UI{hlF(g7PB^Jdx0m(1vPBK{;-)ymb5I1JP8 z#V0!&cqApIluyeBol1mAGCh4>GpqRNg^j47W0`4KsrtJ%^Ct@P&(*bQR0B{GhmwfnL3-Uawt=9gcC=0&!eUpZ* zVsOl~w)dAr81ARa%D=(Uw;*TZ8JO z<7y!{r{oeImN)@vJ(bWCsS~WTE}kEBYPezeSx>~q<$KYg^eU~{MP1l9ans%BFpuSr z+Jn&dvDXf`P2Pvk8V(mZoe7@F&PqNYShu+>#Xltb9|x&`C$H0<;Km;cvye{P;VNLEsFW3`oXb06;I=>z^s}!0D zi_hAWAgav=wVBfzBZ7nZf@Qb1?nKKhh(>?0>4(fuAB??!Dc7x**~K26fQ=u38#zBRq~ zQgfs=eeErgusvaS`YzVE$-Sh+tD2kRmt9SVqp;JWxk|xIZr3dt^Atl^dDk3K{c$UI z?4uq>j^8%u0F;7CQ_fLyL{ddd?zwtT=aj6QZx|1;EQcGseN2X16@z0*p4$j7)7L{f*oS zZsxI<$QM$>Dvx-bZ@Ilm@sjZ5zu>F%@{7w8B7z^k`wSW0ly#r#C+-S zoY}0*&21XrPvxfVs&SKbE@@jl^d$3WYn3#_2DW+qwM#&e5Gmxn?bVCO!1yqeV4shb zuGA9mZLU#`)itN|W2=1`jf4-!eQ|ciG=Ht&{fi^-p>*GazLx0pocDJfcVBfh4o+eb z@5KVdpywNu&lGm0UdS4`AVj$JbFtR9|puEEe<=5Z-0|yyt>RQFI@?4|0Jz( z`c!bM!_}*}t7>ciED8@-L29E_yzV!RId?Vbma?20$}wgU(__@e7-ybCet65j-9Is^ zd*1g?)={P0?&vaSv?iI%2IFctEbKBxVk#L=U6BKEf^qrcifzD=@?`Vn3!k5>MOFjf zM2OwdHstMFwzti*b8@OGYyX^eqCI&Ua&&QXzG}Voxq^xLe6UyRU@n(Z+BM_ZpY0Kj z#3J=Hs(U^37r?9QAUuWR-vv9O?$du2RMebxqr-KNzRTAcMS1x$fZ&8>d{tW{fT4At zBveT!x|WOl2zbE=t(zq+u)YA+5w`^O$IqW>IV~f9;B{|B4l{nNPwBH(-IE;X`ucS! zQ3Ps7=ZvNxwD#|)$aMND%}R0aRSqJzrZH26c!;MDE<=3I-_V4BBp?WCj(!k!z zrBvuh3TrI#qpkwPIhs$J-zm3#xJ6g^4vT}w7k4XQK&3NQJVoe)Cn5=w|DkH($+d9r z(m+xal$~JF>^%PsU-c%$sLNiU`_}(40&nxR23Ln79+Ci(>tx z)xPZg@fT2kd{sSKserSiZ8r$;bB5U!^jbZ z<&rmhx=kk^hKzuLAwEydmBQ&E#qLL8&@S{1_Z}}EE_s^UwbQV zmicG^=17)&B8cpoKz|^kSOt5SA&x*Mg7FI_&5^ge=;oKA3Tp#vELW4H*j87ls~2wR zmOGFJ8QsmpJ3~O3DUlyW`w*PH#Q29t+sg!`_qg{H8#~bt`P-9qmef$hb={wNVUd$x zOEM`!=`Jr)Y8`pG?)PhvB_0zIOzBnrK!y-EK$~OeZel`>d+qVt)SMVl3T}z~RLI6` zt2O?jR6G6_cYa5*r*fHtB!!uqAV2?HB{Mz6%n<@eT|Rx+BumPS5GchoAXCzUKp^6? zWcseTV;wp40G3x(SDXBq{3f+;756?l!SKtLf7zvp*tFZPZ$;YZ0@B3rq4}Yk zK5Wc*wM!wFDjYmbHTJ6v?K>BbRn&b^sC|S5n-0BN&jm*j%PyHGzOVg^ zxjmVf-mg+WX-C`*B+ZAH-jj&{+0yz@@_Tsm*~bRf@G$D^8;wJUo4-PEWbN!W`{B8j ze|ipvH_U6e=~MIGIMf1ccr0+zI@?e*e^akv7mom5gp$k=8_} zg6INW*m_;)O6FYDU3y<+20aOWR0&?gp~Xr8p9x3y!wOtZ8B`0k;&;S1iNEyn2fhg` za5jb~M`{Y%yhqpxQPQ)fh~OU@NNg3YzazL$oSK5EuO=UNtbOhv0e#y|GVGhX6THC6 z{|J2q;ZM|~8@^Z0b&P0Z8a0vP1i;yq19Dfxe zHqNl(^*(yUQ?ZcaxX7+*rHXPdjS>PTJGg~plI~)D>QHb_tDaS9vL#d>x_#_Nmt|NA zZ3qvsnb?vfjs6f8M*Xvwnd7e4)vy2;GQ4~Qq#zRJN4V@JgCF+s)HL1f8Jg}D_;q2b zxngCXot#N!>ld`l;YFB9>PZrkQifXwU9K7W95zRz@<=DLF=b^=OZ`ZrH!o3syY8Vr zTmk|$jb{cEr%wZ(s6QKrmlG*GsShdYRL=Reu%90AKvpNISXIGz$`gJf!8T&jLGM9< z#nexAhVD6owbzYoihLxO&f}JwF?tbDC?O3a{>uDDSK(5wQLCvXXGyn`aneNl=sYN~ zTssOdhg0<}FqX0JnY+LvuSYwohM=*rvDXaWV(suSWZ5$S;3nvBqHiOL`fjv8&f+D^ zw5y%=g&33F=C_jI3k+~~TD!?8E`<1Rtuxi7?KX=%;Sug8-sTzU4T>6ZH%S8^#mqJ?S$DDYJ7e5dT%)G|o=o-d48U8lbMQ%;8KxSrL?F(?J7n zFZH_cR#FM2jJLH}s1KoaUaYK*x<}EI5z;8vr=aHzbEIK}@+UgkbOn@^-d)-GED60; z+im+#YMv{5HR&0*aDst$sAyTo;3_-#7mpSv&Z+SHo@niA*QH#))#mRfpiJb;@PzL< z=RFaVpe8L|Q~!pNAZ&E~^-6wEq;GNsDD`owHyv{&@_y!HuH{W|0IA@P>U_qq`FsuZ zp)kOqn)3QZ(@A)(NB5BKJ{N|A6cJ=n+^*P}z znYR5dQI$Peu9g0|ClYFp^{;HvQ`%whZ}hn1-xcIBA|sS3NuAmmt98hp*U{06>l%n7 zQH)7sd38D-W952Kc~LQJJy#tKe-Bfk_eIt||MDPWJ^HKdt1yxKrNO+@rPhtl(D;$3 zpLLZw7n%rX$}zT(SE1)taOG0fcd0xUi4k$K9#xkJHS4+h$FL|u1=5IID#I0=L}s;s ztUC`Ff8vw7W}`R0c1p{ci=jwlBNn+B^c9UfscgUkzblRY2#LiwJUtXki`VK|%*BPp zxSpfX3$&cOKA`~^RZEe$%qKUK*HiU>SICf?^8!-1~@ZXdxEz63;KYm_H<2xeYr&pLGI$+~bm~+Q@ zdb+t1igKqwDiv^Wb;&w^3Rv~qosaD#76?*BtAyk-R_Es|-v+>jsRF(zZ%#Mz)XGv> z@{sCvR?2MpWSb-UN1y-VKNygELJFsUitw+;_+W5xl2?=Xx|&>4ch4+zr$$iEQs7_j zszM-v2AdIHnj?n1ZERg%UwM}tPgD3+M1)bj;NH;vK}_7)AoYuGWS4X$I$BPaTpp)Q zLzWy3Q&-n)=Ez|z-oq_274q>Pn7%X-@l0Fhn?Ia^U*@Q7BL7+J-*G|zqlMPs-fxUO z9&dHEtLv+?9X4d$Y?qAUFaFiTdZrfq(N>ynyf+u*2D%Ng6V9Q#G93N?a7mh6^GTLQ;pw-7j=D|R!ybFzQH8T zxAsr$pl0U!_U@CPcPOSxg;}&}84tuNjroC@fM~8(N)gq=7jVpa%R%{r zRB;R-2_NR?^D)KsR4hEuCI%+J@APx+wYj0+Pb6PLPR_IdZ9FX9iL6Y`6H0dWSBH`K z?^GJUD$kFmHpX0=K7*K#UTOYn8(sK8qioIC;;})$WFMHQ}nZ@ZzZgBgWc3LnepIj8OntC{g{@-E{i9 zh=G)9t0XtVn9egc#)MM4t_W`@D-OP%%PxT!y=Zf%)*GArx>zc5-7u`*f_(kgZ zQOffnq0-e3!=le(tow?4TGkfL8W8B1NDf$AvX8;ukS$Y2OYr$G1T7=&G`lIU>+$F6 z!H;t~)xJUvcL!~sNZW;Due=QheM#t&r-KPensz$lU}Wnf$=|{u9z@8+cDY6TV$U)1 zOqJO>&FWv#+al!B(!}cfPTrANWo5>{gXDln2!#Waa74vuo%Ve@$uDQBPgHYd&I$IwMVkR!?kxM zq-(ae{pbdFj@WuHdkjg(dOw& zChYb6$6?34VqXlYgR5(S0*rgIP&tqfvPm$=tWzIlIaRD&qW)egia>O}-bSn16$zrw0XBb44 zj;7-pp%ddVr#BXSIjA|)!@!~)?}XL6$uIu66KFp!$MSqN0vMx(hTZG^ztKjTqkNZw z=zrknp_2?3?^agWH2mA@(|ZB;8A-oI_50z6ZTWgf$>77sYUAqKS_8n$YVGghr8YvV z-41G?orH1Q19i@EYRkFex#U`3 zSsxAt&rjN_0%c+;N`LBN#sI{V&F!ouC@APyxg=cB|1HH=`kiMH$|1b@aTmDg~T{*1ImM-dTDqe`NaX{*R}vt|zt? zOh*6xgEvlO&*7J9xBl90o(SxkR?Op#YxJFtHTxDyEJYD>(9s`ii)*gq3M@oy%(*)X zuj^m_^wgLMXhc}Nu<>*<2cu%0{w8#TBYfb?C|bH zWkyX_Mze0DWV>$}|D?%n>A}oQ+p~m_?llX2-4+)csTdMMtHs3@*By8nah>H`M7-Q77<*0EX6t3|pTD;8)!d{6gqFb3lVjTS9GKT)KKUJ<0Su5)@> zM9Vaa4YDbZNy7UhUjcS>azd$5u8U4COafwLWW3m$?b5Mp>q+IZIyyQs7)oJ#f{F^+ z8X*nAAnr}$gUr-`vCjWat^Mf?)YPPI(+{tM68QT13Sa(t>~XO-kRjx$S!sYmB^|TA z(6qixd0K75+s@S!E-@DGZho$(KA{cQg|! zPeoMmpZ4aX?hLB$ey(`qgLs?oALTK=?j8|VsYYzA&ca?ZeoGd?i~`@5+TODIaJ}tR zdVYpw_9L`rdm#s>#I=dzd}~P_>A{CTx)`M8_@bpt9y;Ix0{&v@obHZNC|WtDN>fV&H>NW*}mqvo}ORC)UwwG%%Nk`}4V@^K7NzV*n)kHka@Ar%Le4%gcj; zdxp#tMSOj9zZeVu03a6N5d&IW4;^nW9n#X%i&WojVk`pSOamTCld6NM!d}d_dGcUl zV%Azty##^5U|<1yLohN@$kAyj=+9p<9sUA0*6>`7rY&p=3nKJ8RxZ`KNtR`mJG{=U z7RC$tvvaZ#zmsb1{)~(a(-ltFij|)0AbG{6+lTwDaUop$ff>;jY7pubLmbr#jZ~@| zmQPE0tf2_E3FEKCR>>Y?c^bHw`^WiacjE&~bbi)+q~_!7n=To}b+SMe(*r13SI`w# zpl^uN0fIu2332($RrX_;0>tTj*S4~F8O_zw8yU9!sV_P!FAJTN|cVDt7c*=Wq-*IpR z4wm=yr?Xf^T-+7e5XT;OC@s;7_onP5&ij5!&TPNI)z3^J4+UO85F?&vDLJr}mcE9{ z64#=z%97pcbxrQUMIc)B@+sl<7c3AkM)A{`4ynB@RBrv%8mn(O_dOGZ0H~8`81NJw zXVdoAKr@vFj2UPRJx@M~AR96D1$tJj3i{pq!dY~j*{^v?7a$o-TKk2{BAeX5_nJtG zCK?4>8ZxrY0#i)OmP*3QzQ|MV!rPnQ`nwf-2af5sxh3;Q{~Gf&A76&`_kW~4&lK+s zlMg40+(HPn8y`E(CiIf)rUNK0YDNz*pB^ zLqZ$qwoiM1JKY6golQC)a2t}IxWNR^R4gr_PKjmrCYhik6lI98jk9Otes_EdqC1Zh z)N~cO_!rR{!MFRV(m<3!HEf5Dh{sm`V1X@@i=3RiwYBvUd2`6d!9fMt9CF$j4_@}U zi3Y+&mDMzJ2o`xvQxhMQ-E?))b=h>T)>s zg-K2`4+!+i{9!ME*Bvl?-W-Z} zd}%wxV5X#Pr1RpI_!af+^POHK-&r@CRTbUVNUiUA`tv)Wq4@otEm?#+wx0?d ze9miD#l?i$pR_0pzra+ie{%VhP|elJ$tn9@vBdNk6}(`kbcf&-9jfw3{Vh85-a@Iz zH4Tuh;HQs`=DQ$|s8_k#uV7hgqsL_&+II3HQ8FfcG{@AS^i@%rfUdUbOcMy$r&jubhQx%NQ~sv_Sf zI+uk2L><}EX+gY|GJ_i9pb`f%cgmBi$#p*2@f%vr$#l9*2G#vMxJvXTy`in)+r z;yzz$8NvUt=E={_C7>Tk22~sSoqo>w^kT?#o;xjC=C>Ts9?OZyBM5eMdCx^<%$EPYusEy|pz^YKH zgI^I4m|w0yHDSl!JQ)C}ehs)2sens2^VyjY_GS@+EbJVn@w<4(bAZ{^r1Rau?dcwG zqni20itF^53)i2B8@gP@wUbi)_E>T{yC35xv!q_L5dPoBpU{Z7Do6^V+Hx=s0)02W zPxCQou!KA?su5e8KguCMuAFPqH0gaPeUXOwnm#dbL#JKQspX4rrU?>3thIf`ca>Iy zOG>J+82@-}@uE}6H-@~`oZuKlug%WV7FwHYoWK_Jr0>A|){C5@aXwZ)bz*{5o8wOx zw2D#TwxOUCC{U{){qjA;3Sw;84f4zuTN*a_ybnAh;O*x_;yfq#in(? zez!Y>9V=XwG`xu83NN{5W>l`Sk4Ub&EsR?!u8Q<5aa(hAV3r%bPUm&~EimBirUwWU zydJGFXpenl9L*`@Tly$NKrw?6p^|6j;J7gm0DShB8&1>CQUyUwbcHjx02&%v$mm~5 z=&RBA{oD(IDpRG3YT}l?qdFBP%|?b1k9l-%r}CwUktrSaGc?4LH6@#W1S-!f=^L;} zv5Mz1?oHGPP>GQWV0CL{&c8)%`|Fx*vBQr}N{A5qV5A2}u&@Di#m;%~agLqLY@-c^ z!rx{?3#f-i5MsDF!$AM`tlSR>$2~BT+Xix9B&A-&^{T`m?bJQ$%tG$xP_Nu1#%UJp z*CY~6@!#Ur4uFY`N`c)kpf*}nIBlrjNhVi+o#kq*k=zH%$31yc4zfv%euy1n5+HH_ zA=JRo&~}pVq+?pHc#2sv)_-^h?(*{6mq_J&Bhj4eW$N;lR<`jy!spVcp0|MkxQm;1 zqg3n8BV>&3AoIy=_LhdU^|8Q7??1C5^aRqVH@Q6Wv^+dKcublf?Cpn;0CsbYh-)4f zsh1HYv*>TmRHfVuX;kP%k3wWLG6da^E@G}$jTNtPg5^tx1>H~V_vg68#EtWJ zy^x)yKFy@V zo-o)tsuiilhazr|=Uj;VzW$+$BCVe}BI#8D?8ITvOgiIMD?B1%b?cDl6)7o)5kenm zg-4OyG6GpTB`^IZYkis@;Ca=no#fWv-yd3F{8e?D?h(yF|axXWm%x_AS?I?HYTWT&P+19E1VW0D9L#f-j=oK@PxN8z<>w z0#$}S-KN8Y_vUdRSi8`*>Bg(kz}LecOf8v5OraCbBoGlIrEdWn@n71t%U)gHJR>~V zYJ;5O2ymx=M?yx~-H`2>m2@i-l9E+GucH{w{Nl%^b7%^i3s-UKQnN7%8~7c(1WBdv&{lj-er>!nOD_P02ag(eIq?uLMtIoS{!fPXGY3aX{MO}NdHU243H?xs6i4g zJimNrZ5vGoQXuQTeE1`xK*4mi9?9)Bdq_uy8bYvmFPj?kVa8u_^=c46SRhS2u=%J} zGp|zq`7OL+MsE1JSK+A@ItLnrWbdo*z&cVS5K;GW|zE-U{PL5Qx(RzOzu*xf&n^HUDdG7sCw>5dIb2)YJnZRQ`dJ8wb zhm?S9JYGg$2sY7Tm6;D9PZp%c;o+IYshF&TQ7JjgNygzlncXWF7gvhSAa1Kw{yA@b zIyEDYjC60>cAc46ivvdJ?YU!iPPPT$mOZk4?eUW{!@ca`*{4SA37h`P~y|M z-XyfM(1!D6k=DRCInG0Yk#)PoMV_E4k&@=LJN&A{lu-ylwYG+|bGYfHnwuh5=#5hM znrR6)maQ^TB_S=XQmDz?4n4*J*m;eOzdbWmS5H)=$|#ta0&sAbfD*Tl zcfUYGNt)zyJg4C3(pZRv2x6tBsw9Qo^64g?*%>K3qL|BA3N@QDr%Um8THU1{e!M@sX zqPc6K>8k&TFiqeNOS@eokdkKAr+gx3t756W5U3ER=*P+Z0LR62Pu~J&;D+e(Kqost zKmGqJGqw}Ft!9PN!j(4i)iVPt#csY_5#W# znKvK#Jkqg%1`)?AqVg&W=i|Lr}TQ+6OLV)c*S5BA7Lqwbu zb;VwWclo1#Zo=21Q{Q%K)^|49KbLRQ(N5*DQ=BgUvhzDjym-nY(a8x-MGWu0J>n6R z17Hh+b9s_@djG=~JQu%4$at(J=WYeC;3|;dm1)Yn)zPukpwBcQb+x_vvw2N}T0ZQt z;wqZkja334uhh!XY+%i!Ic(9GaX@~^4i~Qw&13^_f9DJbH}>haUs~Ij>ZFX1hpe&H zz^Tuu_GB~(#HC|40h$4~3_STgcztO9x(62|QYh}ree6rfT!I;Pb^JowkdSgL0f?M(U&Ca);l`NzEpUKCJRzEGyas8oESb)vtSh)9^JRvS+zaeM=Go)zSg6@U2_iPl zVO8Ut{)KvVFMW}9eD7iMzjv(ej}B1NRoa&5;K(2yT4bf0Bh!q?Q;^6wGCDNQY{gFJz!x;9r;z5Lx`EPE`)px}W#=L743l++AK;eQh6#4=p4V!o7% zGao}r#~IE;-NBS)XlO0qlD1iNd4T#r&BXBp%WNL+p4kcrB6_^vDrz4s_D7xXne!I8on>D z-;`>m#-PIgM(@UZ^Dh@Sxv*G~qp8J2p(umIyB!5QY1E|Krf?dadMN*E8L%b%<1%DX zfSHCP9v;5~a|11&%KCbC=hrL>%^xX1OO}&<<1`Uf2eHxC{7ymeI?GSI zN*N~8kPWyE{9rLutv9OHYB}W5)k#~BIgJpY7`9;;CpSP+55TJ}aJ9K@jQ+Cfxs$2T zrSbnJp?Tt-78^Oq{aq4J=s`G&=cbI-FY>Cu?OBvbxvf_O_7z1 z{K{d4M`CT=pZGS=@a8L>(h=6n)q^f+k`^y$f=2o0dJ~xi?fUO-aX)Dna38c5H?7hv zOOe5#RZCA3iATdr9fS)gZRmzl1F=s>LE%@pZ62{X+LuSn!^XqWwX~EWHu98St%#CT zWLrKzYR9wxKSZrs5;JaC0Xuk5BAS7DvhZZD4a3mT@DlaessT8(uCDGy&cC<0pXTm9 zp%jM0;U=T0QT%8b5ssF3zG91hsR)CiRL~OJw zlL`g|4NWEVSgJpR3x8}ZKW26K8=JTp$zJ+@-7T3G346F3M+QZJ*mKW%Pk}Tf7x!U{ z`NI*F4{SR4-x`=fe@AR@f0VliW!6UhV`wGDo1Wl?0j!h|%*A~!-<0*AQcq5xl8~iI kwtve0|99&5y(Om~=jfQ%*zW}X83iOGsVGq{W)Sdy0PBm19smFU literal 0 HcmV?d00001 diff --git a/docs/img/rpc-queue-2.mmd.svg b/docs/img/rpc-queue-2.mmd.svg new file mode 100644 index 000000000..0c5f21196 --- /dev/null +++ b/docs/img/rpc-queue-2.mmd.svg @@ -0,0 +1 @@ +
Anubis
RPC Queue
RPC worker pool
enqueue
dequeue
API
worker
New Theia Start Job
job1
...
job3
\ No newline at end of file diff --git a/docs/img/rpc-queue-3.mmd.png b/docs/img/rpc-queue-3.mmd.png new file mode 100644 index 0000000000000000000000000000000000000000..6f917573d29e36a0808c2e2d4bce8e1da8922448 GIT binary patch literal 20845 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 z
Anubis
RPC default Queue
RPC theia Queue
RPC default worker pool
RPC theia worker pool
enqueue theia start
enqueue regrade
dequeue
dequeue
API
worker
worker
New Theia Start Job
job3
...
job1
\ No newline at end of file diff --git a/docs/img/submission-flow.mmd.svg b/docs/img/submission-flow.mmd.svg index 449115139..53d9b0097 100644 --- a/docs/img/submission-flow.mmd.svg +++ b/docs/img/submission-flow.mmd.svg @@ -1 +1 @@ -
submission flow
github
anubis
rpc cluster
submision pipeline
processing
webhook
push job
kube job create
anubis api
database
pipeline api
report
test
build
clone
rpc worker
github
\ No newline at end of file +
submission flow
github
anubis
rpc cluster
submision pipeline
processing
webhook
push job
kube job create
anubis api
database
pipeline api
report
test
build
clone
rpc worker
github
\ No newline at end of file diff --git a/docs/img/theia-pod.mmd.svg b/docs/img/theia-pod.mmd.svg index 5d1fe08a6..0c466b3a0 100644 --- a/docs/img/theia-pod.mmd.svg +++ b/docs/img/theia-pod.mmd.svg @@ -1 +1 @@ -
pod
student
proxy
proxy instance
proxy instance
proxy instance
theia
highly replicated proxy deployment
autosave
pushes to github every 5 minutes
theia server container
volume
shared PV between containers with student repo
\ No newline at end of file +
pod
student
proxy
proxy instance
proxy instance
proxy instance
theia
highly replicated proxy deployment
autosave
pushes to github every 5 minutes
theia server container
volume
shared PV between containers with student repo
\ No newline at end of file diff --git a/docs/mermaid/rpc-queue-1.mmd b/docs/mermaid/rpc-queue-1.mmd new file mode 100644 index 000000000..e6224e7c0 --- /dev/null +++ b/docs/mermaid/rpc-queue-1.mmd @@ -0,0 +1,17 @@ +graph LR + subgraph Anubis + + api((API)) + + subgraph RPC Queue + job3 -.-> job2[...] -.-> job1 + end + + subgraph RPC worker pool + worker + end + + api -->|enqueue| job3 + job1 -->|dequeue| worker + + end \ No newline at end of file diff --git a/docs/mermaid/rpc-queue-2.mmd b/docs/mermaid/rpc-queue-2.mmd new file mode 100644 index 000000000..8aa7232e6 --- /dev/null +++ b/docs/mermaid/rpc-queue-2.mmd @@ -0,0 +1,18 @@ +graph LR + subgraph Anubis + + api((API)) + + subgraph RPC Queue + job4[New Theia Start Job] + job4 -.-> job3 -.-> job2[...] -.-> job1 + end + + subgraph RPC worker pool + worker + end + + api -->|enqueue| job4 + job1 -->|dequeue| worker + + end \ No newline at end of file diff --git a/docs/mermaid/rpc-queue-3.mmd b/docs/mermaid/rpc-queue-3.mmd new file mode 100644 index 000000000..2531be8bb --- /dev/null +++ b/docs/mermaid/rpc-queue-3.mmd @@ -0,0 +1,30 @@ +graph LR + subgraph Anubis + + api((API)) + + subgraph RPC default Queue + djob3 + djob2 + djob2 + djob3[job3] -.-> djob2[...] -.-> djob1[job1] + end + + subgraph RPC theia Queue + job4[New Theia Start Job] + end + + subgraph RPC default worker pool + dworker[worker] + end + + subgraph RPC theia worker pool + tworker[worker] + end + + api -->|enqueue theia start| job4 + api -->|enqueue regrade| djob3 + djob1 -->|dequeue| dworker + job4 -->|dequeue| tworker + + end \ No newline at end of file diff --git a/kube/debug/.gitignore b/kube/debug/.gitignore new file mode 100644 index 000000000..74ae8f94f --- /dev/null +++ b/kube/debug/.gitignore @@ -0,0 +1 @@ +init-secrets.sh \ No newline at end of file diff --git a/kube/debug/build-theia.sh b/kube/debug/build-theia.sh new file mode 100755 index 000000000..425b206ff --- /dev/null +++ b/kube/debug/build-theia.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cd $(dirname $(realpath $0)) +cd ../../ + +eval $(minikube docker-env) + +docker-compose build --parallel theia-admin theia-xv6 diff --git a/kube/provision-debug.sh b/kube/debug/provision.sh similarity index 96% rename from kube/provision-debug.sh rename to kube/debug/provision.sh index 1bf19a93f..6c0557710 100755 --- a/kube/provision-debug.sh +++ b/kube/debug/provision.sh @@ -2,6 +2,7 @@ # Change into the directory that this script is in cd $(dirname $0) +cd .. # Stop if any command has an error set -e @@ -66,7 +67,7 @@ kubectl label node minikube traefik=ingress --overwrite # Install a basic traefik configuration. This was pretty much entirely # pulled from the traefik documentation somewhere around traefik v2.1. echo 'Adding traefik resources...' -kubectl apply -f ./traefik.yml +kubectl apply -f ./debug/traefik.yaml # Add the bitnami helm charts helm repo add bitnami https://charts.bitnami.com/bitnami @@ -112,6 +113,10 @@ helm install redis \ --namespace anubis \ bitnami/redis +if [ -f debug/init-secrets.sh ]; then + bash debug/init-secrets.sh +fi + # Run the debug.sh script to build, then install all the stuff # for anubis. -exec ./debug.sh +exec ./debug/restart.sh diff --git a/kube/debug.sh b/kube/debug/restart.sh similarity index 79% rename from kube/debug.sh rename to kube/debug/restart.sh index 92e1cdbeb..d48b26c0d 100755 --- a/kube/debug.sh +++ b/kube/debug/restart.sh @@ -2,6 +2,7 @@ # Change into the directory that this script is in cd $(dirname $0) +cd .. # Stop if any commands have an error set -e @@ -45,15 +46,10 @@ pushd .. docker-compose build --parallel --pull api web logstash theia-proxy theia-init theia-sidecar popd -# Figure out if we are upgrading or installing -if helm list -n anubis | awk '{print $1}' | grep anubis &> /dev/null; then - HELM_COMMAND=upgrade -else - HELM_COMMAND=install -fi - # Upgrade or install minimal anubis cluster in debug mode -helm ${HELM_COMMAND} anubis . -n anubis \ +helm upgrade \ + --install anubis . \ + --namespace anubis \ --set "imagePullPolicy=IfNotPresent" \ --set "elasticsearch.storageClassName=standard" \ --set "debug=true" \ @@ -76,14 +72,3 @@ kubectl rollout restart deployments.apps/pipeline-api -n anubis kubectl rollout restart deployments.apps/rpc-default -n anubis kubectl rollout restart deployments.apps/rpc-theia -n anubis kubectl rollout restart deployments.apps/theia-proxy -n anubis - - -echo -echo 'seed: https://localhost/api/admin/seed/' -echo 'auth: https://localhost/api/admin/auth/token/jmc1283' -echo 'site: https://localhost/' - -# Only build theia if it doesnt already exist (it's a long build) -if ! docker image ls | awk '{print $1}' | grep -w '^registry.osiris.services/anubis/theia-admin$' &>/dev/null; then - docker-compose build --parallel theia-admin theia-xv6 -fi diff --git a/kube/traefik.yml b/kube/debug/traefik.yaml similarity index 70% rename from kube/traefik.yml rename to kube/debug/traefik.yaml index 4d2395754..46f9b7dc0 100644 --- a/kube/traefik.yml +++ b/kube/debug/traefik.yaml @@ -1,16 +1,3 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: traefik - ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - namespace: traefik - name: traefik-ingress-controller - ---- apiVersion: apps/v1 kind: DaemonSet metadata: @@ -18,7 +5,6 @@ metadata: name: traefik labels: app: traefik - spec: selector: matchLabels: @@ -34,14 +20,10 @@ spec: app: traefik spec: serviceAccountName: traefik-ingress-controller - nodeSelector: - traefik: ingress - hostNetwork: true containers: - name: traefik image: traefik:v2.2 - # imagePullPolicy: IfNotPresent securityContext: capabilities: add: @@ -49,62 +31,19 @@ spec: drop: - ALL args: - - "--accesslog" - - "--accesslog.filepath=/data/access.log" - - "--accesslog.format=json" + - "--api.dashboard=true" + - "--api.insecure=true" + - "--log.level=debug" - "--providers.kubernetescrd=true" - - "--entrypoints.web.address=:80" - - "--entrypoints.web.http.redirections.entryPoint.to=websecure" - - "--entrypoints.web.http.redirections.entryPoint.scheme=https" - "--entrypoints.websecure.address=:443" - - - "--certificatesresolvers.tls.acme.tlschallenge" - - "--certificatesresolvers.tls.acme.httpchallenge.entrypoint=web" - - "--certificatesresolvers.tls.acme.email=osiris@osiris.cyber.nyu.edu" - - "--certificatesresolvers.tls.acme.storage=/data/acme.json" - # Please note that this is the staging Let's Encrypt server. - # Once you get things working, you should remove that whole line altogether. - - # ports: - # - name: web - # containerPort: 80 - # hostPort: 80 - # - name: websecure - # containerPort: 443 - # hostPort: 443 - - - volumeMounts: - - name: data - mountPath: /data - - volumes: - - name: data - persistentVolumeClaim: - claimName: traefik-data --- -apiVersion: batch/v1 -kind: Job +apiVersion: v1 +kind: ServiceAccount metadata: - name: traefik-data-permission namespace: traefik -spec: - template: - spec: - restartPolicy: OnFailure - containers: - - name: volume-permissions - image: busybox:1.31.1 - command: ["sh", "-c", "chmod -Rv 600 /data/*"] - volumeMounts: - - name: data - mountPath: /data - volumes: - - name: data - persistentVolumeClaim: - claimName: traefik-data + name: traefik-ingress-controller --- apiVersion: v1 kind: PersistentVolumeClaim @@ -123,7 +62,6 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ingressroutes.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -132,13 +70,11 @@ spec: plural: ingressroutes singular: ingressroute scope: Namespaced - --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: middlewares.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -147,13 +83,11 @@ spec: plural: middlewares singular: middleware scope: Namespaced - --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ingressroutetcps.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -162,13 +96,11 @@ spec: plural: ingressroutetcps singular: ingressroutetcp scope: Namespaced - --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ingressrouteudps.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -177,13 +109,11 @@ spec: plural: ingressrouteudps singular: ingressrouteudp scope: Namespaced - --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: tlsoptions.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -192,13 +122,11 @@ spec: plural: tlsoptions singular: tlsoption scope: Namespaced - --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: tlsstores.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -207,13 +135,11 @@ spec: plural: tlsstores singular: tlsstore scope: Namespaced - --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: traefikservices.traefik.containo.us - spec: group: traefik.containo.us version: v1alpha1 @@ -222,13 +148,11 @@ spec: plural: traefikservices singular: traefikservice scope: Namespaced - --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: name: traefik-ingress-controller - rules: - apiGroups: - "" @@ -268,13 +192,11 @@ rules: - get - list - watch - --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: name: traefik-ingress-controller - roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole diff --git a/kube/restart.sh b/kube/restart.sh index a94e24c69..c20688fb6 100755 --- a/kube/restart.sh +++ b/kube/restart.sh @@ -8,3 +8,4 @@ kubectl rollout restart deployments.apps/pipeline-api -n anubis kubectl rollout restart deployments.apps/theia-proxy -n anubis kubectl rollout restart deployments.apps/rpc-default -n anubis kubectl rollout restart deployments.apps/rpc-theia -n anubis +kubectl rollout restart deployments.apps/rpc-regrade -n anubis diff --git a/kube/templates/api.yml b/kube/templates/api.yml index 2cd93f9e2..dd7850185 100644 --- a/kube/templates/api.yml +++ b/kube/templates/api.yml @@ -196,49 +196,3 @@ spec: - name: adminer port: 5002 targetPort: 8080 - ---- -# Strip prefix /api -apiVersion: traefik.containo.us/v1alpha1 -kind: Middleware -metadata: - name: strip-api - namespace: {{ .Release.Namespace }} - labels: - app: anubis - heritage: {{ .Release.Service | quote }} - release: {{ .Release.Name | quote }} -spec: - stripPrefix: - prefixes: - - "/api" - ---- -# Public Ingress Route /api/public/* -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingress.route.anubis.api.public - namespace: {{ .Release.Namespace }} - labels: - app: api - heritage: {{ .Release.Service | quote }} - release: {{ .Release.Name | quote }} -spec: - entryPoints: - - websecure - routes: - - kind: Rule - match: Host(`{{ .Values.domain }}`) && PathPrefix(`/api/`) - middlewares: - {{- if .Values.vpnOnly }} - - name: whitelist-vpn - namespace: traefik - {{- end }} - - name: strip-api - namespace: {{ .Release.Namespace }} - services: - - name: anubis - port: 5000 - tls: - certResolver: tls diff --git a/kube/templates/ingress.yml b/kube/templates/ingress.yml new file mode 100644 index 000000000..693fc0050 --- /dev/null +++ b/kube/templates/ingress.yml @@ -0,0 +1,120 @@ + +--- +# Strip prefix /api +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: strip-api + namespace: {{ .Release.Namespace }} + labels: + app: anubis + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} +spec: + stripPrefix: + prefixes: + - "/api" + +--- +# Public Ingress Route /api/public/* +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: ingress.route.anubis.api.public + namespace: {{ .Release.Namespace }} + labels: + app: api + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} +spec: + {{- if .Values.debug }} + entryPoints: + - web + {{- else }} + entryPoints: + - websecure + {{- end }} + routes: + - kind: Rule + match: Host(`{{ .Values.domain }}`) && PathPrefix(`/api/`) + middlewares: + {{- if .Values.vpnOnly }} + - name: whitelist-vpn + namespace: traefik + {{- end }} + - name: strip-api + namespace: {{ .Release.Namespace }} + services: + - name: anubis + port: 5000 + {{- if not .Values.debug }} + tls: + certResolver: tls + {{- end }} + +--- +# Public Ingress Route /* +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: ingress.route.anubis.web + namespace: {{ .Release.Namespace }} + labels: + app: web + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} +spec: + {{- if .Values.debug }} + entryPoints: + - web + {{- else }} + entryPoints: + - websecure + {{- end }} + routes: + - kind: Rule + match: Host(`{{ .Values.domain }}`) + {{- if .Values.vpnOnly }} + middlewares: + - name: whitelist-vpn + namespace: traefik + {{- end }} + services: + - name: web + port: 3000 + {{- if not .Values.debug }} + tls: + certResolver: tls + {{- end }} + +--- + +# Public Ingress Route ide.anubis.osiris.services +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: ingress.route.theia.public + namespace: anubis + labels: + app: theia + +spec: + {{- if .Values.debug }} + entryPoints: + - web + {{- else }} + entryPoints: + - websecure + {{- end }} + routes: + - kind: Rule + match: Host(`{{ .Values.theia.proxy.domain }}`) + services: + - name: theia-proxy + port: 5000 + {{- if not .Values.debug }} + tls: + certResolver: tls + {{- end }} + + diff --git a/kube/templates/rpc.yml b/kube/templates/rpc.yml index fc3dda375..bf771d475 100644 --- a/kube/templates/rpc.yml +++ b/kube/templates/rpc.yml @@ -121,6 +121,62 @@ spec: secretKeyRef: name: api key: secret-key +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rpc-regrade + namespace: {{ .Release.Namespace }} + labels: + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} + component: pipeline-rpc-regrade +spec: + selector: + matchLabels: + app: rpc-regrade + replicas: {{ .Values.rpc.regrade.replicas }} + {{- if .Values.rollingUpdates }} + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + {{- end }} + template: + metadata: + labels: + app: rpc-regrade + spec: + serviceAccountName: pipeline-rpc + dnsPolicy: ClusterFirst + containers: + - name: anubis-rpc-regrade + image: {{ .Values.api.image }}:{{ .Values.api.tag }} + imagePullPolicy: {{ .Values.imagePullPolicy }} + command: ["./rq-worker.sh", "regrade"] + env: + - name: "DEBUG" + value: {{- if .Values.debug }} "1"{{- else }} "0"{{- end }} + - name: "IMAGE_PULL_POLICY" + value: {{ .Values.imagePullPolicy }} + # sqlalchemy uri + - name: "DATABASE_URI" + valueFrom: + secretKeyRef: + name: api + key: database-uri + - name: "REDIS_PASS" + valueFrom: + secretKeyRef: + name: api + key: redis-password + - name: "SECRET_KEY" + valueFrom: + secretKeyRef: + name: api + key: secret-key --- diff --git a/kube/templates/theia.yml b/kube/templates/theia.yml index 377b48805..610bdf5de 100644 --- a/kube/templates/theia.yml +++ b/kube/templates/theia.yml @@ -74,30 +74,6 @@ spec: targetPort: 5000 ---- - -# Public Ingress Route /api/public/* -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingress.route.theia.public - namespace: anubis - labels: - app: theia - -spec: - entryPoints: - - websecure - routes: - - kind: Rule - match: Host(`{{ .Values.theia.proxy.domain }}`) - services: - - name: theia-proxy - port: 5000 - tls: - certResolver: tls - - --- apiVersion: networking.k8s.io/v1 diff --git a/kube/templates/web.yml b/kube/templates/web.yml index 86d863f6f..ff830ba84 100644 --- a/kube/templates/web.yml +++ b/kube/templates/web.yml @@ -60,31 +60,3 @@ spec: - name: web port: 3000 targetPort: 3000 - ---- -# Public Ingress Route /* -apiVersion: traefik.containo.us/v1alpha1 -kind: IngressRoute -metadata: - name: ingress.route.anubis.web - namespace: {{ .Release.Namespace }} - labels: - app: web - heritage: {{ .Release.Service | quote }} - release: {{ .Release.Name | quote }} -spec: - entryPoints: - - websecure - routes: - - kind: Rule - match: Host(`{{ .Values.domain }}`) - {{- if .Values.vpnOnly }} - middlewares: - - name: whitelist-vpn - namespace: traefik - {{- end }} - services: - - name: web - port: 3000 - tls: - certResolver: tls diff --git a/kube/values.yaml b/kube/values.yaml index db352bdd1..0e67bbe8b 100644 --- a/kube/values.yaml +++ b/kube/values.yaml @@ -45,9 +45,10 @@ logstash: rpc: default: - replicas: 5 - + replicas: 5 theia: + replicas: 5 + regrade: replicas: 5 theia: diff --git a/web/src/App.jsx b/web/src/App.jsx index 719d73f7a..3ff275e30 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -30,20 +30,42 @@ const useStyles = makeStyles(() => ({ backgroundImage: `url(/curvylines.png)`, backgroundRepeat: 'repeat', }, + main: { + width: '100%', + flexDirection: 'column', + padding: theme.spacing(6, 4), + marginBottom: theme.spacing(5), + }, + app: { + flex: 1, + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + drawer: { + width: drawerWidth, + flexShrink: 0, + }, + drawerPaper: { + width: drawerWidth, + }, + drawerHeader: { + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + // necessary for content to be below app bar + ...theme.mixins.toolbar, + }, content: { flexGrow: 1, - padding: theme.spacing(6, 4), + padding: theme.spacing(3), + marginBottom: '20px', transition: theme.transitions.create('margin', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), marginLeft: -drawerWidth, }, - main: { - width: '100%', - flexDirection: 'column', - marginBottom: theme.spacing(5), - }, contentShift: { transition: theme.transitions.create('margin', { easing: theme.transitions.easing.easeOut, @@ -51,20 +73,46 @@ const useStyles = makeStyles(() => ({ }), marginLeft: 0, }, - app: { - flex: 1, - display: 'flex', - flexDirection: 'column', - height: '100%', + menuButton: { + marginLeft: -theme.spacing(1), + }, + avatar: { + 'display': 'flex', + '& > *': { + margin: theme.spacing(1), + }, + }, + appBar: { + // height: 50, + transition: theme.transitions.create(['margin', 'width'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + }, + appBarShift: { + width: `calc(100% - ${drawerWidth}px)`, + marginLeft: drawerWidth, + transition: theme.transitions.create(['margin', 'width'], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), }, })); export default function App() { const classes = useStyles(); const query = useQuery(); - const [open, setOpen] = useState(true); + const [open, setOpen] = useState(window.innerWidth >= 960); // 960px is md const [showError, setShowError] = useState(!!query.get('error')); + const handleDrawerOpen = () => { + setOpen(true); + }; + + const handleDrawerClose = () => { + setOpen(false); + }; + return ( @@ -77,20 +125,23 @@ export default function App() { {(user) => (