Skip to content

Add persistent API quota tracking - #142

Open
Kanika0306 wants to merge 1 commit into
sugarlabs:mainfrom
Kanika0306:pr1-quota-persistence
Open

Add persistent API quota tracking#142
Kanika0306 wants to merge 1 commit into
sugarlabs:mainfrom
Kanika0306:pr1-quota-persistence

Conversation

@Kanika0306

Copy link
Copy Markdown

Summary

This PR solves Issue #141 by replacing the previous in-memory daily quota tracking system with a persistent database-backed implementation. Quota usage is now stored and enforced through the database, ensuring request limits remain accurate across sessions, engine recreation, and concurrent access.

What Changed

  • Added a dedicated APIQuota model to store per-API-key daily request counts
  • Introduced check_and_increment_quota() in app/quota.py for centralized quota handling
  • Replaced process-local memory quota tracking with database-backed quota checks in API routes
  • Added test coverage for:
    • first request quota decrement
    • quota exhaustion behavior
    • daily reset logic
    • persistence after engine recreation
    • concurrent request handling
  • Updated fallback handling so a temporarily missing quota row is treated as a database visibility/setup edge case rather than incorrectly returning a quota exceeded response

Why

The previous implementation relied on in-memory state, which was not durable across reconnects or restarts and was vulnerable to inconsistencies during concurrent access.

This also exposed an edge case where a temporarily missing quota row could be misinterpreted as quota exhaustion, causing valid requests to receive false:

429 Daily request quota exceeded

responses.

Result

Quota tracking is now:

  • Persistent across requests and sessions
  • Durable across engine recreation and reconnects
  • More reliable under concurrent access
  • Protected against false quota exhaustion cases

Closes #141

Testing

Executed:

PYTHONPATH=. ./.venv/bin/pytest tests/test_quota.py -q
PYTHONPATH=. ./.venv/bin/pytest -q

Copilot AI review requested due to automatic review settings May 16, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

app/routes/api.py:75

  • These endpoints re-read X-API-Key from request.headers and include a reverse lookup fallback by user_info['name']. Since verify_api_key already validated the header value, consider passing the validated api_key through (e.g., include it in the dependency return or store it on request.state) and dropping the name-based fallback. This reduces duplicated logic and avoids potential StopIteration/misattribution if the fallback ever runs (e.g., in tests or future refactors).
    api_key = request.headers.get("X-API-Key") if request else None
    if not api_key:
        api_key = next(
            key for key, value in settings.API_KEYS.items()
            if value['name'] == user_info['name']
        )
    quota = check_and_increment_quota(api_key, db)

Comment thread app/routes/api.py
Comment thread app/quota.py
Comment thread app/quota.py
Comment thread tests/test_quota.py
@Kanika0306

Copy link
Copy Markdown
Author

@copilot apply changes based on the comments in this thread

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (3)

app/routes/api.py:116

  • Same as above: quota enforcement re-derives api_key from the request / user name rather than reusing the already-validated key from verify_api_key, which duplicates logic and can raise StopIteration if the reverse lookup fails.
    api_key = request.headers.get("X-API-Key") if request else None
    if not api_key:
        api_key = next(
            key for key, value in settings.API_KEYS.items()
            if value['name'] == user_info['name']
        )
    quota = check_and_increment_quota(api_key, db)

app/routes/api.py:154

  • Same issue here: the endpoint reconstructs api_key (including a reverse lookup by user name) instead of reusing the validated API key from the auth dependency. Returning/storing the API key during auth would remove this repeated block and avoid StopIteration edge cases.
    api_key = request.headers.get("X-API-Key") if request else None
    if not api_key:
        api_key = next(
            key for key, value in settings.API_KEYS.items()
            if value['name'] == user_info['name']
        )
    quota = check_and_increment_quota(api_key, db)

app/routes/api.py:276

  • Same issue here: quota enforcement depends on reconstructing api_key instead of using the authenticated key from verify_api_key. Refactoring the dependency to provide the api_key would remove this repeated code and avoid reverse-lookup edge cases.
    api_key = request.headers.get("X-API-Key") if request else None
    if not api_key:
        api_key = next(
            key for key, value in settings.API_KEYS.items()
            if value['name'] == user_info['name']
        )
    quota = check_and_increment_quota(api_key, db)

Comment thread app/routes/api.py
Comment on lines +69 to +75
api_key = request.headers.get("X-API-Key") if request else None
if not api_key:
api_key = next(
key for key, value in settings.API_KEYS.items()
if value['name'] == user_info['name']
)
quota = check_and_increment_quota(api_key, db)
Comment thread app/quota.py

from fastapi import HTTPException
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
Comment thread app/quota.py
Comment on lines +90 to +93
# A missing row here is a visibility/setup issue, not quota exhaustion.
if row is None:
db.commit()
return {"remaining": max_req, "total": max_req}
Comment thread tests/test_quota.py
Comment on lines +109 to +140
try:
from sqlalchemy.pool import NullPool
engine = create_engine(
f"sqlite:///{db_path}",
connect_args={"check_same_thread": False, "timeout": 30},
poolclass=NullPool, # Each thread gets its own connection
)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)

def worker():
db = SessionLocal()
try:
return check_and_increment_quota("key1", db)
finally:
db.close()

with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(lambda _: worker(), range(10)))

assert len(results) == 10

# Verify DB count matches exactly
db = SessionLocal()
row = db.query(APIQuota).filter(APIQuota.api_key == "key1").first()
db.close()
assert row.request_count == 10, f"Expected 10, got {row.request_count}"

engine.dispose()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue: Quota Persistence and Inconsistent Quota State Across Sessions

2 participants