Skip to content

Commit e837f6c

Browse files
authored
Merge pull request #679 from tisnik/lcore-741-quota-limiters-configuration
LCORE-741: Proper quota limiters configuration
2 parents b70eba7 + 10b81fd commit e837f6c

File tree

8 files changed

+509
-141
lines changed

8 files changed

+509
-141
lines changed

docs/config.png

-367 Bytes
Loading

docs/config.puml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class "Configuration" as src.models.config.Configuration {
4545
llama_stack
4646
mcp_servers : Optional[list[ModelContextProtocolServer]]
4747
name : str
48-
quota_handlers : Optional[QuotaHandlersConfig]
48+
quota_handlers : Optional[QuotaHandlersConfiguration]
4949
service
5050
user_data_collection
5151
dump(filename: str) -> None
@@ -135,11 +135,23 @@ class "PostgreSQLDatabaseConfiguration" as src.models.config.PostgreSQLDatabaseC
135135
user : str
136136
check_postgres_configuration() -> Self
137137
}
138-
class "QuotaHandlersConfig" as src.models.config.QuotaHandlersConfig {
138+
class "QuotaHandlersConfiguration" as src.models.config.QuotaHandlersConfiguration {
139139
enable_token_history : bool
140+
limiters : Optional[list[QuotaLimiterConfiguration]]
140141
postgres : Optional[PostgreSQLDatabaseConfiguration]
142+
scheduler : Optional[QuotaSchedulerConfiguration]
141143
sqlite : Optional[SQLiteDatabaseConfiguration]
142144
}
145+
class "QuotaLimiterConfiguration" as src.models.config.QuotaLimiterConfiguration {
146+
initial_quota : Annotated
147+
name : str
148+
period : str
149+
quota_increase : Annotated
150+
type : Literal['user_limiter', 'cluster_limiter']
151+
}
152+
class "QuotaSchedulerConfiguration" as src.models.config.QuotaSchedulerConfiguration {
153+
period : Annotated
154+
}
143155
class "SQLiteDatabaseConfiguration" as src.models.config.SQLiteDatabaseConfiguration {
144156
db_path : str
145157
}
@@ -184,7 +196,8 @@ src.models.config.JwtRoleRule --|> src.models.config.ConfigurationBase
184196
src.models.config.LlamaStackConfiguration --|> src.models.config.ConfigurationBase
185197
src.models.config.ModelContextProtocolServer --|> src.models.config.ConfigurationBase
186198
src.models.config.PostgreSQLDatabaseConfiguration --|> src.models.config.ConfigurationBase
187-
src.models.config.QuotaHandlersConfig --|> src.models.config.ConfigurationBase
199+
src.models.config.QuotaHandlersConfiguration --|> src.models.config.ConfigurationBase
200+
src.models.config.QuotaLimiterConfiguration --|> src.models.config.ConfigurationBase
188201
src.models.config.SQLiteDatabaseConfiguration --|> src.models.config.ConfigurationBase
189202
src.models.config.ServiceConfiguration --|> src.models.config.ConfigurationBase
190203
src.models.config.TLSConfiguration --|> src.models.config.ConfigurationBase

docs/config.svg

Lines changed: 167 additions & 136 deletions
Loading

src/models/config.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
FilePath,
1818
AnyHttpUrl,
1919
PositiveInt,
20+
NonNegativeInt,
2021
SecretStr,
2122
)
2223

@@ -564,11 +565,31 @@ class ByokRag(ConfigurationBase):
564565
db_path: FilePath
565566

566567

567-
class QuotaHandlersConfig(ConfigurationBase):
568+
class QuotaLimiterConfiguration(ConfigurationBase):
569+
"""Configuration for one quota limiter."""
570+
571+
type: Literal["user_limiter", "cluster_limiter"]
572+
name: str
573+
initial_quota: NonNegativeInt
574+
quota_increase: NonNegativeInt
575+
period: str
576+
577+
578+
class QuotaSchedulerConfiguration(BaseModel):
579+
"""Quota scheduler configuration."""
580+
581+
period: PositiveInt = 1
582+
583+
584+
class QuotaHandlersConfiguration(ConfigurationBase):
568585
"""Quota limiter configuration."""
569586

570587
sqlite: Optional[SQLiteDatabaseConfiguration] = None
571588
postgres: Optional[PostgreSQLDatabaseConfiguration] = None
589+
limiters: list[QuotaLimiterConfiguration] = Field(default_factory=list)
590+
scheduler: QuotaSchedulerConfiguration = Field(
591+
default_factory=QuotaSchedulerConfiguration
592+
)
572593
enable_token_history: bool = False
573594

574595

@@ -591,7 +612,9 @@ class Configuration(ConfigurationBase):
591612
default_factory=ConversationCacheConfiguration
592613
)
593614
byok_rag: list[ByokRag] = Field(default_factory=list)
594-
quota_handlers: QuotaHandlersConfig = Field(default_factory=QuotaHandlersConfig)
615+
quota_handlers: QuotaHandlersConfiguration = Field(
616+
default_factory=QuotaHandlersConfiguration
617+
)
595618

596619
def dump(self, filename: str = "configuration.json") -> None:
597620
"""Dump actual configuration into JSON file."""

tests/unit/models/config/test_dump_configuration.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
PostgreSQLDatabaseConfiguration,
1414
CORSConfiguration,
1515
Configuration,
16+
QuotaHandlersConfiguration,
17+
QuotaLimiterConfiguration,
18+
QuotaSchedulerConfiguration,
1619
ServiceConfiguration,
1720
InferenceConfiguration,
1821
TLSConfiguration,
@@ -175,6 +178,8 @@ def test_dump_configuration(tmp_path) -> None:
175178
"quota_handlers": {
176179
"sqlite": None,
177180
"postgres": None,
181+
"limiters": [],
182+
"scheduler": {"period": 1},
178183
"enable_token_history": False,
179184
},
180185
}
@@ -293,3 +298,201 @@ def test_dump_configuration_with_more_mcp_servers(tmp_path) -> None:
293298
"url": "http://localhost:8083",
294299
},
295300
]
301+
302+
303+
def test_dump_configuration_with_quota_limiters(tmp_path) -> None:
304+
"""
305+
Test that the Configuration object can be serialized to a JSON file and
306+
that the resulting file contains all expected sections and values.
307+
308+
Please note that redaction process is not in place.
309+
"""
310+
cfg = Configuration(
311+
name="test_name",
312+
service=ServiceConfiguration(
313+
tls_config=TLSConfiguration(
314+
tls_certificate_path=Path("tests/configuration/server.crt"),
315+
tls_key_path=Path("tests/configuration/server.key"),
316+
tls_key_password=Path("tests/configuration/password"),
317+
),
318+
cors=CORSConfiguration(
319+
allow_origins=["foo_origin", "bar_origin", "baz_origin"],
320+
allow_credentials=False,
321+
allow_methods=["foo_method", "bar_method", "baz_method"],
322+
allow_headers=["foo_header", "bar_header", "baz_header"],
323+
),
324+
),
325+
llama_stack=LlamaStackConfiguration(
326+
use_as_library_client=True,
327+
library_client_config_path="tests/configuration/run.yaml",
328+
api_key="whatever",
329+
),
330+
user_data_collection=UserDataCollection(
331+
feedback_enabled=False, feedback_storage=None
332+
),
333+
database=DatabaseConfiguration(
334+
sqlite=None,
335+
postgres=PostgreSQLDatabaseConfiguration(
336+
db="lightspeed_stack",
337+
user="ls_user",
338+
password="ls_password",
339+
port=5432,
340+
ca_cert_path=None,
341+
ssl_mode="require",
342+
gss_encmode="disable",
343+
),
344+
),
345+
mcp_servers=[],
346+
customization=None,
347+
inference=InferenceConfiguration(
348+
default_provider="default_provider",
349+
default_model="default_model",
350+
),
351+
quota_handlers=QuotaHandlersConfiguration(
352+
limiters=[
353+
QuotaLimiterConfiguration(
354+
type="user_limiter",
355+
name="user_monthly_limits",
356+
initial_quota=1,
357+
quota_increase=10,
358+
period="2 seconds",
359+
),
360+
QuotaLimiterConfiguration(
361+
type="cluster_limiter",
362+
name="cluster_monthly_limits",
363+
initial_quota=2,
364+
quota_increase=20,
365+
period="1 month",
366+
),
367+
],
368+
scheduler=QuotaSchedulerConfiguration(period=10),
369+
enable_token_history=True,
370+
),
371+
)
372+
assert cfg is not None
373+
dump_file = tmp_path / "test.json"
374+
cfg.dump(dump_file)
375+
376+
with open(dump_file, "r", encoding="utf-8") as fin:
377+
content = json.load(fin)
378+
# content should be loaded
379+
assert content is not None
380+
381+
# all sections must exists
382+
assert "name" in content
383+
assert "service" in content
384+
assert "llama_stack" in content
385+
assert "user_data_collection" in content
386+
assert "mcp_servers" in content
387+
assert "authentication" in content
388+
assert "authorization" in content
389+
assert "customization" in content
390+
assert "inference" in content
391+
assert "database" in content
392+
assert "byok_rag" in content
393+
assert "quota_handlers" in content
394+
395+
# check the whole deserialized JSON file content
396+
assert content == {
397+
"name": "test_name",
398+
"service": {
399+
"host": "localhost",
400+
"port": 8080,
401+
"auth_enabled": False,
402+
"workers": 1,
403+
"color_log": True,
404+
"access_log": True,
405+
"tls_config": {
406+
"tls_certificate_path": "tests/configuration/server.crt",
407+
"tls_key_password": "tests/configuration/password",
408+
"tls_key_path": "tests/configuration/server.key",
409+
},
410+
"cors": {
411+
"allow_credentials": False,
412+
"allow_headers": [
413+
"foo_header",
414+
"bar_header",
415+
"baz_header",
416+
],
417+
"allow_methods": [
418+
"foo_method",
419+
"bar_method",
420+
"baz_method",
421+
],
422+
"allow_origins": [
423+
"foo_origin",
424+
"bar_origin",
425+
"baz_origin",
426+
],
427+
},
428+
},
429+
"llama_stack": {
430+
"url": None,
431+
"use_as_library_client": True,
432+
"api_key": "**********",
433+
"library_client_config_path": "tests/configuration/run.yaml",
434+
},
435+
"user_data_collection": {
436+
"feedback_enabled": False,
437+
"feedback_storage": None,
438+
"transcripts_enabled": False,
439+
"transcripts_storage": None,
440+
},
441+
"mcp_servers": [],
442+
"authentication": {
443+
"module": "noop",
444+
"skip_tls_verification": False,
445+
"k8s_ca_cert_path": None,
446+
"k8s_cluster_api": None,
447+
"jwk_config": None,
448+
},
449+
"customization": None,
450+
"inference": {
451+
"default_provider": "default_provider",
452+
"default_model": "default_model",
453+
},
454+
"database": {
455+
"sqlite": None,
456+
"postgres": {
457+
"host": "localhost",
458+
"port": 5432,
459+
"db": "lightspeed_stack",
460+
"user": "ls_user",
461+
"password": "**********",
462+
"ssl_mode": "require",
463+
"gss_encmode": "disable",
464+
"namespace": "lightspeed-stack",
465+
"ca_cert_path": None,
466+
},
467+
},
468+
"authorization": None,
469+
"conversation_cache": {
470+
"memory": None,
471+
"postgres": None,
472+
"sqlite": None,
473+
"type": None,
474+
},
475+
"byok_rag": [],
476+
"quota_handlers": {
477+
"sqlite": None,
478+
"postgres": None,
479+
"limiters": [
480+
{
481+
"initial_quota": 1,
482+
"name": "user_monthly_limits",
483+
"period": "2 seconds",
484+
"quota_increase": 10,
485+
"type": "user_limiter",
486+
},
487+
{
488+
"initial_quota": 2,
489+
"name": "cluster_monthly_limits",
490+
"period": "1 month",
491+
"quota_increase": 20,
492+
"type": "cluster_limiter",
493+
},
494+
],
495+
"scheduler": {"period": 10},
496+
"enable_token_history": True,
497+
},
498+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Unit tests for QuotaHandlersConfiguration model."""
2+
3+
from models.config import QuotaHandlersConfiguration, QuotaSchedulerConfiguration
4+
5+
6+
def test_quota_handlers_configuration() -> None:
7+
"""Test the quota handlers configuration."""
8+
cfg = QuotaHandlersConfiguration(
9+
sqlite=None,
10+
postgres=None,
11+
limiters=[],
12+
scheduler=QuotaSchedulerConfiguration(period=10),
13+
enable_token_history=False,
14+
)
15+
assert cfg is not None
16+
assert cfg.sqlite is None
17+
assert cfg.postgres is None
18+
assert cfg.limiters == []
19+
assert cfg.scheduler is not None
20+
assert not cfg.enable_token_history
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Unit tests for QuotaLimiterConfig model."""
2+
3+
import pytest
4+
5+
from models.config import QuotaLimiterConfiguration
6+
7+
8+
def test_quota_limiter_configuration() -> None:
9+
"""Test the default configuration."""
10+
cfg = QuotaLimiterConfiguration(
11+
type="cluster_limiter",
12+
name="cluster_monthly_limits",
13+
initial_quota=0,
14+
quota_increase=10,
15+
period="3 seconds",
16+
)
17+
assert cfg is not None
18+
assert cfg.type == "cluster_limiter"
19+
assert cfg.name == "cluster_monthly_limits"
20+
assert cfg.initial_quota == 0
21+
assert cfg.quota_increase == 10
22+
assert cfg.period == "3 seconds"
23+
24+
25+
def test_quota_limiter_configuration_improper_value_1() -> None:
26+
"""Test the default configuration."""
27+
with pytest.raises(ValueError, match="Input should be greater than or equal to 0"):
28+
_ = QuotaLimiterConfiguration(
29+
type="cluster_limiter",
30+
name="cluster_monthly_limits",
31+
initial_quota=-1,
32+
quota_increase=10,
33+
period="3 seconds",
34+
)
35+
36+
37+
def test_quota_limiter_configuration_improper_value_2() -> None:
38+
"""Test the default configuration."""
39+
with pytest.raises(ValueError, match="Input should be greater than or equal to 0"):
40+
_ = QuotaLimiterConfiguration(
41+
type="cluster_limiter",
42+
name="cluster_monthly_limits",
43+
initial_quota=1,
44+
quota_increase=-10,
45+
period="3 seconds",
46+
)
47+
48+
49+
def test_quota_limiter_configuration_improper_value_3() -> None:
50+
"""Test the default configuration."""
51+
with pytest.raises(
52+
ValueError, match="Input should be 'user_limiter' or 'cluster_limiter'"
53+
):
54+
_ = QuotaLimiterConfiguration(
55+
type="unknown_limiter",
56+
name="cluster_monthly_limits",
57+
initial_quota=1,
58+
quota_increase=10,
59+
period="3 seconds",
60+
)

0 commit comments

Comments
 (0)