Skip to content

LCORE-4610: e2e tests for quota management - #3135

Open
max-svistunov wants to merge 5 commits into
lightspeed-core:mainfrom
max-svistunov:lcore-4610-quota-management-tests
Open

max-svistunov wants to merge 5 commits into
lightspeed-core:mainfrom
max-svistunov:lcore-4610-quota-management-tests

Conversation

@max-svistunov

Copy link
Copy Markdown
Contributor

Description

Part 2 of 2 for LCORE-4610: the quota e2e feature. It is stacked on #3134, so this PR also shows that PR's three commits until it merges; please review only the last two commits here: share the e2e stream and usage helpers and add the quota e2e feature and its configurations. Tests, their configuration and CI only; nothing under src/ changes.

E2E (tests/e2e/features/quota.feature, @cfg_quota, steps in steps/quota.py): with lightspeed-stack-quota.yaml (user quota of one token, cluster quota, /v1/infer enforced per user id) each of query, streaming_query, responses, streamed responses and infer is served once and then rejected with the exact quota error body; balances are checked against the response's own token counts; the cluster balance is shared across users and a rejected request costs nothing. With lightspeed-stack-quota-renewal.yaml (empty quotas renewed every two seconds) a fresh user is rejected and then served, observed by bounded polling. Every scenario uses a fresh user id, so the persisted quota database never matters. The feature is added to the other GitHub shard (both places in the workflow), the Makefile tag expression and tests/e2e/test_list.txt.

The quota database of the e2e fixtures lives at /tmp/quota.db, not under /tmp/data: nothing on the quota path creates a parent directory (sqlite3.connect does not), the image does not ship /tmp/data, and in the other shard it exists only because conversation-compaction.feature wrote transcripts there first. With the file directly under /tmp the feature does not depend on which feature ran before it.

The stream-reading and streaming_query end-event parsing the new steps need were private copies in steps/llm_query_response.py and steps/token_counters.py; they now live in tests/e2e/utils/utils.py (read_streamed_response, parse_streaming_query_end_event, and the DEFAULT_LLM_TIMEOUT that a2a.py also copied), imported by all of them.

tests/integration/test_quota_configuration.py goes with the feature: quota handlers loaded from a YAML file (tests/configuration/lightspeed-stack-quota-sqlite.yaml, db path from the environment) build the limiters and the token history and renew that database; a period SQLite cannot use is rejected at load; both e2e quota fixtures load in both deployment modes with the values the feature relies on, so fixture drift fails here and not in the e2e shard.

Docs: quota.feature and the @cfg_quota tag are added to docs/testing/e2e_scenarios.md and docs/testing/e2e_testing.md, the new fixtures to tests/e2e/configuration/README.md, and the new test modules to the README of each test directory.

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library [pyproject.toml + uv.lock]
  • Bump-up dependent library [requirements.*.txt for Konflux]
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement
  • Benchmarks improvement

Tools used to create PR

Identify any AI code assistants used in this PR (for transparency and review context)

  • Assisted-by: Claude Opus 4.8
  • Generated by: Claude Opus 4.8

Related Tickets & Documents

  • Related Issue # LCORE-4610
  • Closes # LCORE-4610

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

Run the configuration test:

uv run pytest tests/integration/test_quota_configuration.py

Expected: 7 tests pass.

Run the e2e feature alone, in either mode:

E2E_BEHAVE_TAG_EXPR='not @skip and @cfg_quota' make test-e2e-tagged-local

What was run here: the configuration test passes; the full tests/unit and tests/integration suites (without container_lifecycle) pass; ruff, black, pylint and pyright report nothing new on the touched files; behave --dry-run binds all 112 steps of the feature with no undefined step; make schema leaves docs/devel_doc/openapi.json unchanged.

What was not run here: the e2e feature against a running stack (no Docker on the machine). Its first real runs are the other shard of this PR's e2e workflow, in server and library mode, and the Konflux pipeline. Please also run the feature once on its own with the command above: the shard always has another feature running before it, and the standalone run is the one that proves the fixture carries no hidden precondition. The renewal scenario depends on the scheduler thread running in the deployed service (src/lightspeed_stack.py starts it before uvicorn).

The nightly providers workflow runs the whole test list, so it will run this feature against azure, google-vertex, watsonx and aws-bedrock. The streamed scenarios require the provider to report a positive token usage for a streamed turn (the existing token-counter steps accept zero there). A red quota.feature in that job means the provider reports no usage on streams, which is quota not being enforced for that provider: a bug to file, not an assertion to relax.

The quota scheduler and the token usage history had no direct tests:
nothing failed if a limiter type mapped to the wrong subject, if the
scheduler started without storage, or if the PostgreSQL statements took
different parameters from the SQLite ones.

Cover the subject mapping, the choice of statements, connection checks,
the startup conditions and reconnection attempts of the scheduler, and
the parameters of the PostgreSQL statements, which no test can run
against a real server here. For the token usage history cover the
running totals per user, provider and model, reconnecting after a closed
connection, running without storage, and a write failing loudly when its
table is gone.
LCORE-4606 showed that the renewal statements were never executed by any
test: on SQLite a quota was renewed at every scheduler run. These tests
run the real statements and scheduler functions on a real database.

They pin that a quota is renewed only once its period has elapsed and
once per period, for both the reset and the increase; that the time a
limiter stamps on a new row is read correctly by SQLite; that a zero
value skips its statement; that a renewal touches only the rows of its
own subject; and that the scheduler loop creates the table, survives a
failing limiter and reconnects after its connection died.

With initial_quota and quota_increase both set, the increase runs first
and stamps revoked_at, so the reset of the same run no longer matches
and the balance is not what the quota_limiter module documents. That is
pinned as a strict xfail rather than fixed here.
Add integration tests that send requests through the real endpoints with
real limiters on SQLite, so the client's view and the database are
compared.

They cover /v1/query, /v1/streaming_query, /v1/responses (blocking and
streamed) and /v1/infer: a turn is charged and the balance reported, an
exhausted user is rejected with the 429 body, a shield-blocked streamed
turn costs nothing, and /v1/infer is not enforced without a quota
subject. They also cover the shared cluster balance, which limiter is
named when both are exhausted, the token history, a limiter reconnecting
after its connection died, and an unreachable database giving a 500
instead of a served turn, also when the error comes after the model
answered.
The quota e2e steps need to read a streamed response and parse the end
event of /v1/streaming_query, which the token counter steps and the LLM
response steps each did with their own copy.

Move the stream reader, the end event parser and DEFAULT_LLM_TIMEOUT to
tests/e2e/utils/utils.py and use them from those step modules. Behaviour
does not change.
Quota management had no e2e coverage. Add tests/e2e/features/quota.feature
with its steps and configurations for library and server mode.

The scenarios check that /v1/query, /v1/streaming_query and /v1/responses
(blocking and streamed) report the balances and then reject the request
with the quota error, that /v1/infer does the same when the quota subject
is the user id, that the cluster balance is paid by every user and a
rejected request costs nothing, and that the scheduler renews an
exhausted balance while the service runs.

The feature has the new @cfg_quota tag, added to the "other" shard of the
e2e workflow and to the Makefile's tag expression, and to test_list.txt.
An integration test loads the e2e configurations and pins the values the
feature relies on, and renewal from a configuration file. The e2e
scenario list and testing docs are updated.
@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 1aee84a and 950fb4c.

Files selected for processing: 27
  • .github/workflows/e2e_tests.yaml
  • Makefile
  • docs/testing/e2e_scenarios.md
  • docs/testing/e2e_testing.md
  • tests/configuration/lightspeed-stack-quota-sqlite.yaml
  • tests/e2e/configuration/README.md
  • tests/e2e/configuration/library-mode/lightspeed-stack-quota-renewal.yaml
  • tests/e2e/configuration/library-mode/lightspeed-stack-quota.yaml
  • tests/e2e/configuration/server-mode/lightspeed-stack-quota-renewal.yaml
  • tests/e2e/configuration/server-mode/lightspeed-stack-quota.yaml
  • tests/e2e/features/quota.feature
  • tests/e2e/features/steps/README.md
  • tests/e2e/features/steps/a2a.py
  • tests/e2e/features/steps/llm_query_response.py
  • tests/e2e/features/steps/quota.py
  • tests/e2e/features/steps/token_counters.py
  • tests/e2e/test_list.txt
  • tests/e2e/utils/utils.py
  • tests/integration/_quota_helpers.py
  • tests/integration/endpoints/README.md
  • tests/integration/endpoints/test_quota_endpoints.py
  • tests/integration/test_quota_configuration.py
  • tests/integration/test_quota_scheduler.py
  • tests/unit/quota/README.md
  • tests/unit/quota/test_token_usage_history.py
  • tests/unit/runners/README.md
  • tests/unit/runners/test_quota_scheduler.py
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

This branch has not been deployed

No deployments
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.

1 participant