This document describes how marathon-match-api-v6 starts ECS scorer runners, how to monitor them while they are active, and how to stop them when the work is no longer needed.
The runner task is the ecs-runner Fargate container built from ecs-runner/Dockerfile. The API never launches an image directly; it launches the configured ECS task definition revision from each Marathon Match config.
Before any runner can start, the API service needs these ECS/runtime values:
ECS_CLUSTERECS_SUBNETSECS_SECURITY_GROUPSECS_CONTAINER_NAMEAWS_REGION(defaults tous-east-1)MARATHON_MATCH_API_URLREVIEW_API_URLREVIEW_TYPE_ID- Auth0 M2M settings used by the API and forwarded to the trusted parent runner for long-running token refresh
Each active challenge config also needs:
active: true- compiled tester with
compilationStatus = SUCCESS taskDefinitionNametaskDefinitionVersion- phase configs for
EXAMPLE,PROVISIONAL, and/orSYSTEM
All start paths converge on EcsService.launchScorerTask(...).
- A
marathonmatch.submission.receivedKafka message arrives withchallengeId,submissionId, and usuallymemberId. MarathonMatchSubmissionHandlerloads the challenge's Marathon Match config.- The handler skips the submission if the config is inactive, the challenge has no open phase, or no stored phase config matches the open challenge phase.
- For every matching phase config, the handler calls
EcsService.launchScorerTask(...).
This is the normal path for EXAMPLE and PROVISIONAL scoring during the Submission phase. If both phase configs map to the same open challenge phase, the API intentionally launches one runner per matching config. This is to handle the case where we want example and provisional tests to run during the submission phase in most cases.
POST /v6/marathon-match/challenge/:challengeId/test-submission stores the uploaded ZIP as an isolated Marathon Match validation run, then launches one ECS runner for the requested configType (default PROVISIONAL). The runner downloads the ZIP from Marathon Match API using VALIDATION_SUBMISSION_DOWNLOAD_URL, which is resolved against the normalized /v6/marathon-match API base, includes VALIDATION_RUN_ID in progress/final callbacks, skips Submission API artifact uploads, and ScoringResultService stores progress/final score details on the validation run instead of creating Review API summations. The response includes testSubmissionId, submissionId (same validation-run id for runner compatibility), status, taskArn, taskId, and usually cloudWatchLogsConsoleUrl.
Use GET /v6/marathon-match/challenge/:challengeId/test-submission/:testSubmissionId to poll for completion and display the score/metadata. We do this to support admins / copilots having the ability to test the scorer before the challenge launches without adding a real submission or review to the challenge.
POST /v6/marathon-match/challenge/:challengeId/rerun finds the latest submission per member and launches scorer tasks for the phase config that matches the currently open challenge phase. Use this after changing tester configuration or when current latest submissions need to be recalculated.
SYSTEM scoring starts from review orchestration:
- Autopilot creates or finds a pending Review API review for a latest submission.
- Autopilot calls
POST /v6/marathon-match/internal/system-scorewithreviewId,submissionId, andchallengeId. ScoringResultService.triggerSystemScore(...)loads the config, launches the SYSTEM runner, and schedules a pg-boss timeout guard usingsystemTestTimeout.
POST /v6/marathon-match/challenge/:challengeId/rerun/system restarts existing non-cancelled SYSTEM review records through the same SYSTEM launch path.
ECS Service:
marathon-match-api-v6/src/shared/modules/global/ecs.service.ts
Lines 85 to 86 in 955671e
EcsService.launchScorerTask(...) performs these steps before and during RunTask:
- Reads the configured ECS cluster, container name, network settings, API URL, review type, task definition family, and revision.
- Lists active
PENDINGandRUNNINGtasks for the task family, then describes them and inspects container override environment values. - Stops older active tasks for the same
challengeIdandmemberIdwhen a newer submission for that member is being launched. - Reuses an existing active task instead of launching a duplicate for the same challenge, submission, and phase config type.
- Enforces
ECS_SCORER_MAX_CONCURRENT_TASKSbefore creating more work. The default cap is20. - Fetches an M2M token and injects runner environment variables into the configured ECS container.
- Calls ECS
RunTaskwithlaunchType: FARGATE,awsvpcnetworking, andassignPublicIp: DISABLED. - Resolves the task definition's
awslogs-groupandawslogs-stream-prefix, then builds the deterministic stream name as<prefix>/<containerName>/<taskId>. - Logs and persists a
submissionRunnerLogmapping with the submission, task, cluster, container, task definition, CloudWatch log metadata, and phase config type.
The runner receives these main environment values:
TESTER_CONFIG_IDSUBMISSION_IDACCESS_TOKENMARATHON_MATCH_API_URLREVIEW_TYPE_IDTEST_PHASEPHASE_CONFIG_TYPEPHASE_START_SEEDPHASE_NUMBER_OF_TESTSREVIEW_IDfor SYSTEM scoringMEMBER_IDwhen known
Inside the task, the trusted Java parent runner:
- Fetches the challenge config, tester JAR, and tester metadata from
marathon-match-api-v6. - Downloads the submission from the configured Submission API URL.
- Posts an initial progress callback for
PROVISIONALandSYSTEMruns. - Starts the tester in an isolated child JVM. Submitted solution commands run as the separate
scoreruser with scrubbed environment, restricted filesystem access, and no outbound INET/INET6 socket creation. For standard generic-runner seeds, the child invokesmm-scorer-isolate --cleanup-scorer-statebefore and after each test case so scorer-owned files in fixed writable roots are reset between seeds. - Uploads public/private artifacts back through Submission API.
- Posts the final scoring callback to
POST /v6/marathon-match/internal/scoring-results. - Exits with code
0only after the final callback succeeds.
On failure, the runner writes failure artifacts, uploads them when possible, posts best-effort failed progress for tracked phases, logs runner.failure, and exits non-zero.
Every launch logs Launched ECS scorer task and Submission to ECS runner log mapping. The mapping is also stored in marathon_match.submissionRunnerLog.
The mapping contains:
submissionIdchallengeIdtaskArntaskIdclustercontainerNametaskDefinitionphaseConfigTypelogGrouplogStreamNamecloudWatchLogsConsoleUrl
Use the API endpoint when you have a submission ID:
curl -H "Authorization: Bearer $TOKEN" \
"$MARATHON_MATCH_API_URL/submissions/$SUBMISSION_ID/runner-logs"Add taskArn when a submission has multiple runner launches:
curl -H "Authorization: Bearer $TOKEN" \
"$MARATHON_MATCH_API_URL/submissions/$SUBMISSION_ID/runner-logs?taskArn=$TASK_ARN&limit=200"The endpoint returns all known mappings for the submission, the selected mapping, CloudWatch log events, and CloudWatch pagination tokens.
If you have logGroup and logStreamName from the mapping:
aws logs tail "$LOG_GROUP" \
--region "$AWS_REGION" \
--log-stream-names "$LOG_STREAM_NAME" \
--followUseful runner log markers include:
bootstrapapi.fetch-configapi.fetch-tester-jarapi.download-submissiontester.isolatedtester.invokeapi.progressartifacts.uploadapi.callbackrunner.failurecleanupexit
Success usually includes Scoring callback completed successfully, a runner result payload, and Exiting runner with code 0.
Use ECS when you need runtime/container status:
aws ecs describe-tasks \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--tasks "$TASK_ARN" \
--query 'tasks[0].{lastStatus:lastStatus,desiredStatus:desiredStatus,stoppedReason:stoppedReason,containers:containers[*].{name:name,lastStatus:lastStatus,exitCode:exitCode,reason:reason}}'lastStatus = STOPPED means ECS considers the task terminal. Check the container exitCode and reason to distinguish normal completion from runner failures, OOM stops, or manual stops.
For PROVISIONAL and SYSTEM, the runner posts progress to POST /v6/marathon-match/internal/scoring-progress. Review summation metadata is updated with:
testProcess:provisionalorsystemtestProgress: value from0to1testStatus:IN PROGRESS,SUCCESS, orFAILEDtimed_out: truefor SYSTEM timeout failures
The initial progress callback is posted when scoring starts. Routine per-test IN PROGRESS callbacks are throttled to at most once every 5 seconds, while completion and failure progress can still be posted immediately.
Treat IN PROGRESS summations as unavailable, even when they carry a placeholder score.
Completed scoring may report SUCCESS with nonzero failedTests when individual testcases timed out or crashed; FAILED is reserved for explicit scorer or skipped-scoring failures.
To stop future Marathon Match launches for a challenge, set the config inactive:
curl -X PUT "$MARATHON_MATCH_API_URL/challenge/$CHALLENGE_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"active":false}'This prevents new scoring dispatch for that config. It does not stop tasks that are already running.
There is no public HTTP endpoint that wraps EcsService.stopTask(...). For an operator-initiated kill, use ECS directly with the task ARN from the runner mapping:
aws ecs stop-task \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--task "$TASK_ARN" \
--reason "Marathon Match runner no longer needed"Then verify the task reached STOPPED:
aws ecs describe-tasks \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--tasks "$TASK_ARN" \
--query 'tasks[0].{lastStatus:lastStatus,desiredStatus:desiredStatus,stoppedReason:stoppedReason,containers:containers[*].{name:name,exitCode:exitCode,reason:reason}}'Manual ECS stops can interrupt the Java process before it posts final scoring results. After a manual stop, check Review summation metadata and decide whether to rerun, leave the result unavailable, or apply an operator-side failure workflow.
For submission-phase launches that include memberId, the API stops older active scorer tasks for the same challenge/member before launching the newer submission. The ECS stop reason is:
Superseded by newer Marathon Match submission <submissionId> for challenge <challengeId>.
This is the normal cleanup path for obsolete in-flight work when a competitor submits again.
Every SYSTEM launch schedules a system-test-timeout pg-boss job unless DISABLE_PG_BOSS=true or timeout scheduling is unavailable. The timeout uses systemTestTimeout from the challenge config and defaults to 24 hours.
When the timeout job runs, it:
- Describes the ECS task.
- Skips work if the task is already stopped.
- Checks whether SYSTEM scoring is already complete.
- Calls
EcsService.stopTask(...)if the task is still active and scoring is incomplete. - Writes a failed SYSTEM result with score
-1and metadata containingtimed_out: true,timeoutMs,taskArn, and a timeout message.
Use this only after confirming the task family is dedicated to Marathon Match scoring in the target environment:
aws ecs list-tasks \
--region "$AWS_REGION" \
--cluster "$ECS_CLUSTER" \
--family "$TASK_FAMILY" \
--desired-status RUNNING \
--query 'taskArns' \
--output textStop selected ARNs one by one with aws ecs stop-task. Repeat the list command with --desired-status PENDING if you also need to cancel queued tasks that have not reached RUNNING.
- Confirm the challenge config is active, tester compilation succeeded, and task definition values point to the intended ECS revision.
- Start work through the natural trigger: Kafka submission event, validation upload, rerun endpoint, or SYSTEM scoring endpoint.
- Capture
taskArn,taskId,logGroup, andlogStreamNamefrom the API response, API logs, orGET /submissions/:submissionId/runner-logs. - Monitor CloudWatch logs, ECS task status, and Review summation progress metadata.
- When work is obsolete, deactivate the config if needed, then stop the selected ECS task ARN.
- After a manual stop, verify ECS
STOPPEDstatus and clean up the scoring state by rerunning or marking the result through the appropriate operator workflow.