Skip to content

Commit f2e2f7b

Browse files
author
Mark Saroufim
authored
Revert "Add per-user submission rate limit (1 per hour) (#436)" (#438)
This reverts commit 7d97a31.
1 parent 7d97a31 commit f2e2f7b

4 files changed

Lines changed: 1 addition & 177 deletions

File tree

src/kernelbot/api/api_utils.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -225,21 +225,6 @@ async def to_submit_info(
225225

226226
try:
227227
with db_context as db:
228-
# Per-user rate limit: max 1 submission per hour on Modal B200 for leaderboard 730
229-
if gpu_type == "B200":
230-
lb_id = db.get_leaderboard_id(leaderboard_name)
231-
if lb_id == 730:
232-
last_submission_time = db.check_user_rate_limit(user_id)
233-
if last_submission_time:
234-
raise HTTPException(
235-
status_code=429,
236-
detail=(
237-
f"Rate limit exceeded. You can submit once per hour. "
238-
f"Last submission: {last_submission_time.isoformat()}. "
239-
f"Consider using the NVIDIA runner instead of Modal for faster iteration."
240-
),
241-
)
242-
243228
leaderboard_item = db.get_leaderboard(leaderboard_name)
244229
gpus = leaderboard_item.get("gpu_types", [])
245230
if gpu_type not in gpus:
@@ -254,7 +239,7 @@ async def to_submit_info(
254239
except Exception as e:
255240
raise HTTPException(
256241
status_code=500,
257-
detail=f"Internal server error while validating submission: {e}",
242+
detail=f"Internal server error while validating leaderboard/GPU: {e}",
258243
) from e
259244

260245
try:

src/libkernelbot/leaderboard_db.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,23 +1172,6 @@ def cleanup_temp_users(self):
11721172
logger.exception("Could not cleanup temp users", exc_info=e)
11731173
raise KernelBotError("Database error while cleaning up temp users") from e
11741174

1175-
def check_user_rate_limit(self, user_id: str, hours: int = 1) -> Optional[datetime.datetime]:
1176-
"""Check if user has submitted within the last `hours` hours.
1177-
Returns the most recent submission_time if rate-limited, None if allowed."""
1178-
self.cursor.execute(
1179-
"""
1180-
SELECT submission_time
1181-
FROM leaderboard.submission
1182-
WHERE user_id = %s
1183-
AND submission_time > NOW() - INTERVAL '%s hours'
1184-
ORDER BY submission_time DESC
1185-
LIMIT 1
1186-
""",
1187-
(str(user_id), hours),
1188-
)
1189-
row = self.cursor.fetchone()
1190-
return row[0] if row else None
1191-
11921175
def validate_cli_id(self, cli_id: str) -> Optional[dict[str, str]]:
11931176
"""
11941177
Validates a CLI ID and returns the associated user ID if valid.

tests/test_admin_api.py

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Tests for admin API endpoints."""
22

3-
import datetime
43
from unittest.mock import MagicMock, patch
54

65
import pytest
@@ -406,101 +405,3 @@ def test_update_problems_with_errors(self, test_client, mock_backend):
406405
assert data["status"] == "ok"
407406
assert len(data["errors"]) == 1
408407
assert data["errors"][0]["name"] == "bad-problem"
409-
410-
411-
class TestSubmissionRateLimit:
412-
"""Test per-user submission rate limiting on Modal B200 for leaderboard 730."""
413-
414-
def test_rate_limit_blocks_b200_leaderboard_730(self, test_client, mock_backend):
415-
"""Second B200 submission to leaderboard 730 within 1 hour is rejected with 429."""
416-
mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)
417-
mock_backend.db.__exit__ = MagicMock(return_value=None)
418-
419-
recent_time = datetime.datetime.now(tz=datetime.timezone.utc)
420-
mock_backend.db.check_user_rate_limit = MagicMock(return_value=recent_time)
421-
mock_backend.db.get_leaderboard_id = MagicMock(return_value=730)
422-
mock_backend.db.validate_cli_id = MagicMock(
423-
return_value={"user_id": "123", "user_name": "testuser"}
424-
)
425-
426-
response = test_client.post(
427-
"/test-lb/B200/test",
428-
headers={"X-Popcorn-Cli-Id": "test-cli-id"},
429-
files={"file": ("solution.py", b"print('hello')", "text/plain")},
430-
)
431-
assert response.status_code == 429
432-
assert "Rate limit exceeded" in response.json()["detail"]
433-
assert "NVIDIA runner" in response.json()["detail"]
434-
435-
def test_rate_limit_skipped_for_non_b200(self, test_client, mock_backend):
436-
"""Rate limit is not enforced for non-B200 GPUs even on leaderboard 730."""
437-
mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)
438-
mock_backend.db.__exit__ = MagicMock(return_value=None)
439-
mock_backend.accepts_jobs = True
440-
441-
mock_backend.db.validate_cli_id = MagicMock(
442-
return_value={"user_id": "123", "user_name": "testuser"}
443-
)
444-
445-
mock_lb = MagicMock()
446-
mock_lb.__getitem__ = lambda self, key: {"gpu_types": ["H100"]}[key]
447-
mock_lb.get = lambda key, default=None: {"gpu_types": ["H100"]}.get(key, default)
448-
mock_backend.db.get_leaderboard = MagicMock(return_value=mock_lb)
449-
450-
response = test_client.post(
451-
"/test-lb/H100/test",
452-
headers={"X-Popcorn-Cli-Id": "test-cli-id"},
453-
files={"file": ("solution.py", b"print('hello')", "text/plain")},
454-
)
455-
# Should not hit rate limit at all — check_user_rate_limit should not be called
456-
assert response.status_code != 429
457-
458-
def test_rate_limit_skipped_for_other_leaderboard(self, test_client, mock_backend):
459-
"""Rate limit is not enforced for B200 on a leaderboard other than 730."""
460-
mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)
461-
mock_backend.db.__exit__ = MagicMock(return_value=None)
462-
mock_backend.accepts_jobs = True
463-
464-
recent_time = datetime.datetime.now(tz=datetime.timezone.utc)
465-
mock_backend.db.check_user_rate_limit = MagicMock(return_value=recent_time)
466-
mock_backend.db.get_leaderboard_id = MagicMock(return_value=999)
467-
mock_backend.db.validate_cli_id = MagicMock(
468-
return_value={"user_id": "123", "user_name": "testuser"}
469-
)
470-
471-
mock_lb = MagicMock()
472-
mock_lb.__getitem__ = lambda self, key: {"gpu_types": ["B200"]}[key]
473-
mock_lb.get = lambda key, default=None: {"gpu_types": ["B200"]}.get(key, default)
474-
mock_backend.db.get_leaderboard = MagicMock(return_value=mock_lb)
475-
476-
response = test_client.post(
477-
"/other-lb/B200/test",
478-
headers={"X-Popcorn-Cli-Id": "test-cli-id"},
479-
files={"file": ("solution.py", b"print('hello')", "text/plain")},
480-
)
481-
# Should not be rate limited since leaderboard ID is not 730
482-
assert response.status_code != 429
483-
484-
def test_rate_limit_allows_first_b200_submission(self, test_client, mock_backend):
485-
"""First B200 submission to leaderboard 730 passes the rate limit check."""
486-
mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)
487-
mock_backend.db.__exit__ = MagicMock(return_value=None)
488-
mock_backend.accepts_jobs = True
489-
490-
mock_backend.db.check_user_rate_limit = MagicMock(return_value=None)
491-
mock_backend.db.get_leaderboard_id = MagicMock(return_value=730)
492-
mock_backend.db.validate_cli_id = MagicMock(
493-
return_value={"user_id": "123", "user_name": "testuser"}
494-
)
495-
496-
mock_lb = MagicMock()
497-
mock_lb.__getitem__ = lambda self, key: {"gpu_types": ["B200"]}[key]
498-
mock_lb.get = lambda key, default=None: {"gpu_types": ["B200"]}.get(key, default)
499-
mock_backend.db.get_leaderboard = MagicMock(return_value=mock_lb)
500-
501-
response = test_client.post(
502-
"/test-lb/B200/test",
503-
headers={"X-Popcorn-Cli-Id": "test-cli-id"},
504-
files={"file": ("solution.py", b"print('hello')", "text/plain")},
505-
)
506-
assert response.status_code != 429

tests/test_leaderboard_db.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -605,51 +605,6 @@ def test_generate_stats(database, submit_leaderboard):
605605
}
606606

607607

608-
def test_check_user_rate_limit_no_submissions(database, submit_leaderboard):
609-
"""Test rate limit returns None when user has no submissions"""
610-
with database as db:
611-
result = db.check_user_rate_limit("999")
612-
assert result is None
613-
614-
615-
def test_check_user_rate_limit_recent_submission(database, submit_leaderboard):
616-
"""Test rate limit returns submission_time when user submitted recently"""
617-
submit_time = datetime.datetime.now(tz=datetime.timezone.utc)
618-
with database as db:
619-
db.create_submission(
620-
"submit-leaderboard", "file.py", 5, "code", submit_time, user_name="user"
621-
)
622-
result = db.check_user_rate_limit("5")
623-
assert result is not None
624-
assert abs((result - submit_time).total_seconds()) < 2
625-
626-
627-
def test_check_user_rate_limit_old_submission(database, submit_leaderboard):
628-
"""Test rate limit returns None when submission is older than the window"""
629-
old_time = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(hours=2)
630-
with database as db:
631-
db.create_submission(
632-
"submit-leaderboard", "file.py", 5, "code", old_time, user_name="user"
633-
)
634-
result = db.check_user_rate_limit("5")
635-
assert result is None
636-
637-
638-
def test_check_user_rate_limit_different_user(database, submit_leaderboard):
639-
"""Test rate limit only applies to the specific user"""
640-
submit_time = datetime.datetime.now(tz=datetime.timezone.utc)
641-
with database as db:
642-
db.create_submission(
643-
"submit-leaderboard", "file.py", 5, "code", submit_time, user_name="user5"
644-
)
645-
# User 6 should not be rate limited
646-
result = db.check_user_rate_limit("6")
647-
assert result is None
648-
# User 5 should be rate limited
649-
result = db.check_user_rate_limit("5")
650-
assert result is not None
651-
652-
653608
def test_get_user_submissions_empty(database, submit_leaderboard):
654609
"""Test get_user_submissions returns empty list for user with no submissions"""
655610
with database as db:

0 commit comments

Comments
 (0)