Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cms/db/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from sqlalchemy.orm import relationship
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.schema import Column, ForeignKey, ForeignKeyConstraint, \
UniqueConstraint
UniqueConstraint, CheckConstraint
from sqlalchemy.types import Integer, Float, String, Unicode, DateTime, Enum, \
BigInteger

Expand Down Expand Up @@ -108,6 +108,21 @@ class Submission(Base):
default=True,
)

# Time and memory limits (in seconds and bytes) overriding the dataset's
# ones for this submission's evaluation; None to use the dataset's
# limits.
override_time_limit: float | None = Column(
Float,
CheckConstraint("override_time_limit > 0"),
nullable=True)
override_mem_limit: int | None = Column(
BigInteger,
CheckConstraint("override_mem_limit > 0",
name='submissions_override_mem_limit_check'),
CheckConstraint("MOD(override_mem_limit, 1048576) = 0",
name='submissions_override_mem_limit_check1'),
nullable=True)

@property
def short_comment(self) -> str:
"""The first line of the comment."""
Expand Down
12 changes: 10 additions & 2 deletions cms/grading/Job.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,14 @@ def from_submission(
info = "evaluate submission %d on testcase %s" % \
(submission.id, testcase.codename)

# A submission may carry its own (usually larger) limits.
time_limit = submission.override_time_limit \
if submission.override_time_limit is not None \
else dataset.time_limit
memory_limit = submission.override_mem_limit \
if submission.override_mem_limit is not None \
else dataset.memory_limit

# dict() is required to detach the dictionary that gets added
# to the Job from the control of SQLAlchemy
return EvaluationJob(
Expand All @@ -644,8 +652,8 @@ def from_submission(
executables=dict(submission_result.executables),
input=testcase.input,
output=testcase.output,
time_limit=dataset.time_limit,
memory_limit=dataset.memory_limit,
time_limit=time_limit,
memory_limit=memory_limit,
info=info
)

Expand Down
10 changes: 10 additions & 0 deletions cms/server/contest/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ def post(self, task_name: str):

# If the submission is performed by the administrator acting on behalf
# of a contestant, allow overriding.
override_time_limit = None
override_mem_limit = None
if self.impersonated_by_admin:
try:
official = self.get_boolean_argument("override_official", official)
Expand All @@ -159,6 +161,12 @@ def post(self, task_name: str):
override_min_interval = self.get_boolean_argument(
"override_min_interval", False
)
# Special (usually larger) limits (seconds / bytes) to evaluate
# the submission with.
override_time_str = self.get_argument("override_time_limit", "")
override_mem_str = self.get_argument("override_mem_limit", "")
override_time_limit = float(override_time_str) if override_time_str else None
override_mem_limit = int(override_mem_str) if override_mem_str else None
except ValueError as err:
self.json({"error": str(err)}, 400)
return
Expand All @@ -178,6 +186,8 @@ def post(self, task_name: str):
official,
override_max_number=override_max_number,
override_min_interval=override_min_interval,
override_time_limit=override_time_limit,
override_mem_limit=override_mem_limit,
)
self.sql_session.commit()
except UnacceptableSubmission as e:
Expand Down
24 changes: 23 additions & 1 deletion cms/server/contest/submission/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def accept_submission(
official: bool,
override_max_number: bool = False,
override_min_interval: bool = False,
override_time_limit: float | None = None,
override_mem_limit: int | None = None,
) -> Submission:
"""Process a contestant's request to submit a submission.

Expand All @@ -110,6 +112,10 @@ def accept_submission(
during the analysis mode.
override_max_number: skip checks for the maximum number of submissions
override_min_interval: skip checks for the minimum interval between submissions
override_time_limit: if set, a time limit (in seconds) overriding the
dataset's one to evaluate this submission with
override_mem_limit: if set, a memory limit (in bytes, a multiple of 1 MiB)
overriding the dataset's one to evaluate this submission with

return: the resulting submission, if all went well.

Expand Down Expand Up @@ -157,6 +163,20 @@ def accept_submission(
"after %d seconds from last submission."),
task.min_submission_interval.total_seconds())

# Check whether special time / memory limits conform to the constraints
# enforced by the database.

if override_time_limit is not None and override_time_limit <= 0:
raise UnacceptableSubmission(
N_("Invalid relaxed time limit!"),
N_("The relaxed time limit must be positive."))
if override_mem_limit is not None and (
override_mem_limit <= 0 or override_mem_limit % (1024 * 1024) != 0):
raise UnacceptableSubmission(
N_("Invalid relaxed memory limit!"),
N_("The relaxed memory limit must be a positive multiple of "
"1 MiB."))

# Process the data we received and ensure it's valid.

required_codenames = set(task.submission_format)
Expand Down Expand Up @@ -275,7 +295,9 @@ def accept_submission(
task=task,
participation=participation,
comment=received_filenames_joined,
official=official)
official=official,
override_time_limit=override_time_limit,
override_mem_limit=override_mem_limit)
sql_session.add(submission)

for codename, digest in digests.items():
Expand Down
8 changes: 8 additions & 0 deletions cmscontrib/updaters/update_from_1.5.sql
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,12 @@ WHERE user_test_managers.user_test_id IN (
)
AND user_test_managers.filename LIKE 'stub.%';

-- https://github.com/cms-dev/cms/pull/1731
-- Relaxed time and memory limits for submissions
Comment thread
veluca93 marked this conversation as resolved.
ALTER TABLE submissions ADD COLUMN override_time_limit double precision;
ALTER TABLE submissions ADD COLUMN override_mem_limit bigint;
ALTER TABLE submissions ADD CONSTRAINT submissions_override_time_limit_check CHECK (override_time_limit > 0);
ALTER TABLE submissions ADD CONSTRAINT submissions_override_mem_limit_check CHECK (override_mem_limit > 0);
ALTER TABLE submissions ADD CONSTRAINT submissions_override_mem_limit_check1 CHECK (MOD(override_mem_limit, 1048576) = 0);

COMMIT;
66 changes: 65 additions & 1 deletion cmstestsuite/unit_tests/server/contest/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
"""Unit tests for submission details API handlers."""

import unittest
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

from cms import FEEDBACK_LEVEL_RESTRICTED, FEEDBACK_LEVEL_FULL
from cms.server.contest.handlers.api import (
ApiSubmitHandler,
ApiSubmissionDetailsHandler,
ApiSubmissionFullDetailsHandler,
)
Expand Down Expand Up @@ -176,5 +177,68 @@ def test_impersonated_returns_full_details(self):
)


class TestApiSubmitHandler(BaseApiHandlerTest):
"""Weak limits may only be set when submitting with the admin token."""

def make_submit_handler(self, impersonated, arguments):
handler = self.create_handler(
ApiSubmitHandler, impersonated=impersonated
)
handler.sql_session = MagicMock()
handler.timestamp = MagicMock()
handler.request.files = {}
handler.request.arguments = arguments
return handler

def test_weak_limits_set_when_impersonated(self):
handler = self.make_submit_handler(
True,
{
"override_time_limit": [b"3.5"],
"override_mem_limit": [b"524288000"],
},
)
with patch(
"cms.server.contest.handlers.api.accept_submission"
) as accept:
accept.return_value = self.submission
handler.post("my_task")

kwargs = accept.call_args.kwargs
self.assertEqual(kwargs["override_time_limit"], 3.5)
self.assertEqual(kwargs["override_mem_limit"], 524288000)
self.assertEqual(handler.status_code, 200)

def test_weak_limits_ignored_for_contestants(self):
# A contestant sending the same arguments must not get them applied.
handler = self.make_submit_handler(
False,
{
"override_time_limit": [b"3.5"],
"override_mem_limit": [b"524288000"],
},
)
with patch(
"cms.server.contest.handlers.api.accept_submission"
) as accept:
accept.return_value = self.submission
handler.post("my_task")

kwargs = accept.call_args.kwargs
self.assertIsNone(kwargs["override_time_limit"])
self.assertIsNone(kwargs["override_mem_limit"])
self.assertEqual(handler.status_code, 200)

def test_weak_limits_invalid_value_rejected_when_impersonated(self):
handler = self.make_submit_handler(True, {"override_time_limit": [b"abc"]})
with patch(
"cms.server.contest.handlers.api.accept_submission"
) as accept:
handler.post("my_task")

self.assertEqual(handler.status_code, 400)
accept.assert_not_called()


if __name__ == "__main__":
unittest.main()
27 changes: 17 additions & 10 deletions docs/API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,24 @@ restrictions:
* Hidden participation is never blocked.

* Requests can carry special parameters (either in the URL or in ``POST`` data)
that bypasses further restrictions when set to ``1``:
that bypasses further restrictions:

* ``override_phase_check`` lets the operation proceed regardless of contest
phase (for example, you can submit even though the contest has already
ended).
* ``override_phase_check`` set to ``1`` lets the operation proceed
regardless of contest phase (for example, you can submit even though the
contest has already ended).

* ``override_official`` (in the submit endpoint) makes the submission count
as official regardless of contest phase.
* ``override_official`` set to ``1`` (in the submit endpoint) makes the
submission count as official regardless of contest phase.

* ``override_max_number`` (in the submit endpoint) skips all checks for the
maximum number of submits.
* ``override_max_number`` set to ``1`` (in the submit endpoint) skips all
checks for the maximum number of submits.

* ``override_min_interval`` (in the submit endpoint) skips all checks for
the minimum time interval between submits.
* ``override_min_interval`` set to ``1`` (in the submit endpoint) skips all
checks for the minimum time interval between submits.

* ``override_time_limit`` specifies an altered time limit for the submission
(floating point number, in seconds). Note that manager / controller wall
time limits in interactive tasks are not adjusted.

* ``override_mem_limit`` specifies an altered memory limit for the submission
(int, in bytes, must be a multiple of 1MiB).
Loading