Skip to content

Commit 8e414a2

Browse files
authored
Merge pull request #721 from tisnik/lcore-741-quota-limiters-in-configuration
LCORE-741: quota limiters in configuration
2 parents 22c3c64 + b312f60 commit 8e414a2

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

src/configuration.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
InferenceConfiguration,
2121
DatabaseConfiguration,
2222
ConversationCacheConfiguration,
23+
QuotaHandlersConfiguration,
2324
)
2425

2526
from cache.cache import Cache
2627
from cache.cache_factory import CacheFactory
2728

29+
from quota.quota_limiter import QuotaLimiter
30+
from quota.quota_limiter_factory import QuotaLimiterFactory
2831

2932
logger = logging.getLogger(__name__)
3033

@@ -48,6 +51,7 @@ def __init__(self) -> None:
4851
"""Initialize the class instance."""
4952
self._configuration: Optional[Configuration] = None
5053
self._conversation_cache: Optional[Cache] = None
54+
self._quota_limiters: list[QuotaLimiter] = []
5155

5256
def load_configuration(self, filename: str) -> None:
5357
"""Load configuration from YAML file."""
@@ -59,6 +63,10 @@ def load_configuration(self, filename: str) -> None:
5963

6064
def init_from_dict(self, config_dict: dict[Any, Any]) -> None:
6165
"""Initialize configuration from a dictionary."""
66+
# clear cached values when configuration changes
67+
self._conversation_cache = None
68+
self._quota_limiters = []
69+
# now it is possible to re-read configuration
6270
self._configuration = Configuration(**config_dict)
6371

6472
@property
@@ -143,6 +151,13 @@ def database_configuration(self) -> DatabaseConfiguration:
143151
raise LogicError("logic error: configuration is not loaded")
144152
return self._configuration.database
145153

154+
@property
155+
def quota_handlers_configuration(self) -> QuotaHandlersConfiguration:
156+
"""Return quota handlers configuration."""
157+
if self._configuration is None:
158+
raise LogicError("logic error: configuration is not loaded")
159+
return self._configuration.quota_handlers
160+
146161
@property
147162
def conversation_cache(self) -> Cache:
148163
"""Return the conversation cache."""
@@ -154,5 +169,16 @@ def conversation_cache(self) -> Cache:
154169
)
155170
return self._conversation_cache
156171

172+
@property
173+
def quota_limiters(self) -> list[QuotaLimiter]:
174+
"""Return list of all setup quota limiters."""
175+
if self._configuration is None:
176+
raise LogicError("logic error: configuration is not loaded")
177+
if not self._quota_limiters:
178+
self._quota_limiters = QuotaLimiterFactory.quota_limiters(
179+
self._configuration.quota_handlers
180+
)
181+
return self._quota_limiters
182+
157183

158184
configuration: AppConfig = AppConfig()

tests/unit/test_configuration.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ def _reset_app_config_between_tests() -> Generator:
1616
# ensure clean state before each test
1717
try:
1818
AppConfig()._configuration = None # type: ignore[attr-defined]
19+
AppConfig()._quota_limiters = [] # type: ignore[attr-defined]
1920
except Exception:
2021
pass
2122
yield
2223
# ensure clean state after each test
2324
try:
2425
AppConfig()._configuration = None # type: ignore[attr-defined]
26+
AppConfig()._quota_limiters = [] # type: ignore[attr-defined]
2527
except Exception:
2628
pass
2729

@@ -78,10 +80,18 @@ def test_default_configuration() -> None:
7880
# try to read property
7981
_ = cfg.conversation_cache_configuration # pylint: disable=pointless-statement
8082

83+
with pytest.raises(Exception, match="logic error: configuration is not loaded"):
84+
# try to read property
85+
_ = cfg.quota_handlers_configuration # pylint: disable=pointless-statement
86+
8187
with pytest.raises(Exception, match="logic error: configuration is not loaded"):
8288
# try to read property
8389
_ = cfg.conversation_cache # pylint: disable=pointless-statement
8490

91+
with pytest.raises(Exception, match="logic error: configuration is not loaded"):
92+
# try to read property
93+
_ = cfg.quota_limiters # pylint: disable=pointless-statement
94+
8595

8696
def test_configuration_is_singleton() -> None:
8797
"""Test that configuration is singleton."""
@@ -675,3 +685,124 @@ def test_configuration_with_in_memory_conversation_cache(tmpdir: Path) -> None:
675685
assert cfg.conversation_cache_configuration.memory is not None
676686
assert cfg.conversation_cache is not None
677687
assert isinstance(cfg.conversation_cache, InMemoryCache)
688+
689+
690+
def test_configuration_with_quota_handlers_no_storage(tmpdir: Path) -> None:
691+
"""Test loading configuration from YAML file with quota handlers configuration."""
692+
cfg_filename = tmpdir / "config.yaml"
693+
with open(cfg_filename, "w", encoding="utf-8") as fout:
694+
fout.write(
695+
"""
696+
name: test service
697+
service:
698+
host: localhost
699+
port: 8080
700+
auth_enabled: false
701+
workers: 1
702+
color_log: true
703+
access_log: true
704+
llama_stack:
705+
use_as_library_client: false
706+
url: http://localhost:8321
707+
api_key: test-key
708+
user_data_collection:
709+
feedback_enabled: false
710+
quota_handlers:
711+
limiters:
712+
- name: user_monthly_limits
713+
type: user_limiter
714+
initial_quota: 10
715+
quota_increase: 10
716+
period: "2 seconds"
717+
- name: cluster_monthly_limits
718+
type: cluster_limiter
719+
initial_quota: 100
720+
quota_increase: 10
721+
period: "10 seconds"
722+
scheduler:
723+
# scheduler ticks in seconds
724+
period: 1
725+
"""
726+
)
727+
728+
cfg = AppConfig()
729+
cfg.load_configuration(str(cfg_filename))
730+
731+
assert cfg.quota_handlers_configuration is not None
732+
assert cfg.quota_handlers_configuration.sqlite is None
733+
assert cfg.quota_handlers_configuration.postgres is None
734+
assert cfg.quota_handlers_configuration.limiters is not None
735+
assert cfg.quota_handlers_configuration.scheduler is not None
736+
737+
# check the quota limiters configuration
738+
assert len(cfg.quota_limiters) == 0
739+
740+
# check the scheduler configuration
741+
assert cfg.quota_handlers_configuration.scheduler.period == 1
742+
743+
744+
def test_configuration_with_quota_handlers(tmpdir: Path) -> None:
745+
"""Test loading configuration from YAML file with quota handlers configuration."""
746+
cfg_filename = tmpdir / "config.yaml"
747+
with open(cfg_filename, "w", encoding="utf-8") as fout:
748+
fout.write(
749+
"""
750+
name: test service
751+
service:
752+
host: localhost
753+
port: 8080
754+
auth_enabled: false
755+
workers: 1
756+
color_log: true
757+
access_log: true
758+
llama_stack:
759+
use_as_library_client: false
760+
url: http://localhost:8321
761+
api_key: test-key
762+
user_data_collection:
763+
feedback_enabled: false
764+
quota_handlers:
765+
sqlite:
766+
db_path: ":memory:"
767+
limiters:
768+
- name: user_monthly_limits
769+
type: user_limiter
770+
initial_quota: 10
771+
quota_increase: 10
772+
period: "2 seconds"
773+
- name: cluster_monthly_limits
774+
type: cluster_limiter
775+
initial_quota: 100
776+
quota_increase: 10
777+
period: "10 seconds"
778+
scheduler:
779+
# scheduler ticks in seconds
780+
period: 1
781+
"""
782+
)
783+
784+
cfg = AppConfig()
785+
cfg.load_configuration(str(cfg_filename))
786+
787+
assert cfg.quota_handlers_configuration is not None
788+
assert cfg.quota_handlers_configuration.sqlite is not None
789+
assert cfg.quota_handlers_configuration.postgres is None
790+
assert cfg.quota_handlers_configuration.limiters is not None
791+
assert cfg.quota_handlers_configuration.scheduler is not None
792+
793+
# check the storage
794+
assert cfg.quota_handlers_configuration.sqlite.db_path == ":memory:"
795+
796+
# check the quota limiters configuration
797+
assert len(cfg.quota_limiters) == 2
798+
assert (
799+
str(cfg.quota_limiters[0])
800+
== "UserQuotaLimiter: initial quota: 10 increase by: 10"
801+
)
802+
assert (
803+
str(cfg.quota_limiters[1])
804+
== "ClusterQuotaLimiter: initial quota: 100 increase by: 10"
805+
)
806+
807+
# check the scheduler configuration
808+
assert cfg.quota_handlers_configuration.scheduler.period == 1

0 commit comments

Comments
 (0)