|
4 | 4 | # pylint: disable=too-many-arguments |
5 | 5 |
|
6 | 6 | import time |
7 | | -from collections.abc import AsyncGenerator, AsyncIterator, Callable |
8 | 7 |
|
9 | 8 | import pytest |
10 | 9 | from aiocache import Cache |
11 | | -from asgi_lifespan import LifespanManager |
12 | | -from fastapi import FastAPI |
13 | 10 | from fastapi.security import HTTPBasicCredentials |
14 | 11 | from models_library.api_schemas_api_server.api_keys import ApiKeyInDB |
15 | | -from pydantic import PositiveInt |
16 | 12 | from pytest_mock import MockerFixture |
17 | 13 | from simcore_service_api_server.api.dependencies.authentication import ( |
18 | 14 | get_current_identity, |
19 | 15 | ) |
20 | | -from simcore_service_api_server.clients.postgres import get_engine |
21 | 16 | from simcore_service_api_server.repository.api_keys import ApiKeysRepository |
22 | 17 | from simcore_service_api_server.repository.users import UsersRepository |
23 | | -from sqlalchemy.ext.asyncio import AsyncEngine |
24 | | - |
25 | | -MAX_TIME_FOR_APP_TO_STARTUP = 10 |
26 | | -MAX_TIME_FOR_APP_TO_SHUTDOWN = 10 |
27 | | - |
28 | | - |
29 | | -@pytest.fixture |
30 | | -async def app_started(app: FastAPI, is_pdb_enabled: bool) -> AsyncIterator[FastAPI]: |
31 | | - # LifespanManager will trigger app's startup&shutown event handlers |
32 | | - async with LifespanManager( |
33 | | - app, |
34 | | - startup_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_STARTUP, |
35 | | - shutdown_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_SHUTDOWN, |
36 | | - ): |
37 | | - yield app |
38 | | - |
39 | | - |
40 | | -@pytest.fixture |
41 | | -async def async_engine(app_started: FastAPI) -> AsyncEngine: |
42 | | - # Overrides |
43 | | - return get_engine(app_started) |
44 | | - |
45 | | - |
46 | | -@pytest.fixture |
47 | | -def api_key_repo( |
48 | | - async_engine: AsyncEngine, |
49 | | -) -> ApiKeysRepository: |
50 | | - return ApiKeysRepository(db_engine=async_engine) |
51 | | - |
52 | | - |
53 | | -@pytest.fixture |
54 | | -def users_repo( |
55 | | - async_engine: AsyncEngine, |
56 | | -) -> UsersRepository: |
57 | | - return UsersRepository(db_engine=async_engine) |
58 | | - |
59 | | - |
60 | | -@pytest.fixture |
61 | | -async def api_key_in_db( |
62 | | - create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]], |
63 | | -) -> ApiKeyInDB: |
64 | | - """Creates a single API key in DB for testing purposes""" |
65 | | - return await anext(create_fake_api_keys(1)) |
66 | | - |
67 | | - |
68 | | -async def test_get_user_with_valid_credentials( |
69 | | - api_key_in_db: ApiKeyInDB, |
70 | | - api_key_repo: ApiKeysRepository, |
71 | | -): |
72 | | - # Act |
73 | | - result = await api_key_repo.get_user( |
74 | | - api_key=api_key_in_db.api_key, api_secret=api_key_in_db.api_secret |
75 | | - ) |
76 | | - |
77 | | - # Assert |
78 | | - assert result is not None |
79 | | - assert result.user_id == api_key_in_db.user_id |
80 | | - assert result.product_name == api_key_in_db.product_name |
81 | | - |
82 | | - |
83 | | -async def test_get_user_with_invalid_credentials( |
84 | | - api_key_in_db: ApiKeyInDB, |
85 | | - api_key_repo: ApiKeysRepository, |
86 | | -): |
87 | | - |
88 | | - # Generate a fake API key |
89 | | - |
90 | | - # Act - use wrong secret |
91 | | - result = await api_key_repo.get_user( |
92 | | - api_key=api_key_in_db.api_key, api_secret="wrong_secret" |
93 | | - ) |
94 | | - |
95 | | - # Assert |
96 | | - assert result is None |
97 | 18 |
|
98 | 19 |
|
99 | 20 | async def test_rest_dependency_authentication( |
@@ -125,7 +46,72 @@ async def test_cache_effectiveness_in_rest_authentication_dependencies( |
125 | 46 | monkeypatch: pytest.MonkeyPatch, |
126 | 47 | mocker: MockerFixture, |
127 | 48 | ): |
128 | | - """Test that caching reduces database calls and improves performance.""" |
| 49 | + """Test that caching reduces database calls and improves performance. |
| 50 | +
|
| 51 | + ## Memory Implications of aiocache |
| 52 | +
|
| 53 | + ### Memory Usage Characteristics |
| 54 | +
|
| 55 | + **aiocache** uses in-memory storage by default, which means: |
| 56 | +
|
| 57 | + 1. **Linear memory growth**: Each cached item consumes RAM proportional to the serialized size of the cached data |
| 58 | + 2. **No automatic memory limits**: By default, there's no built-in maximum memory cap |
| 59 | + 3. **TTL-based cleanup**: Items are only removed when they expire (TTL) or are explicitly deleted |
| 60 | +
|
| 61 | + ### Memory Limits & Configuration |
| 62 | +
|
| 63 | + **Available configuration options:** |
| 64 | +
|
| 65 | + ```python |
| 66 | + # Memory backend configuration |
| 67 | + cache = Cache(Cache.MEMORY, **{ |
| 68 | + 'serializer': { |
| 69 | + 'class': 'aiocache.serializers.PickleSerializer' |
| 70 | + }, |
| 71 | + # No built-in memory limit options for MEMORY backend |
| 72 | + }) |
| 73 | + ``` |
| 74 | +
|
| 75 | + **Key limitations:** |
| 76 | + - **MEMORY backend**: No built-in memory limits or LRU eviction |
| 77 | + - **Maximum capacity**: Limited only by available system RAM |
| 78 | + - **Risk**: Memory leaks if TTL is too long or cache keys grow unbounded |
| 79 | +
|
| 80 | + ### Recommendations for Your Use Case |
| 81 | +
|
| 82 | + **For authentication caching:** |
| 83 | +
|
| 84 | + 1. **Low memory impact**: User authentication data is typically small (user_id, email, product_name) |
| 85 | + 2. **Short TTL**: Your 120s TTL helps prevent unbounded growth |
| 86 | + 3. **Bounded key space**: API keys are finite, not user-generated |
| 87 | +
|
| 88 | + **Memory estimation:** |
| 89 | + ``` |
| 90 | + Per cache entry ≈ 200-500 bytes (user data + overhead) |
| 91 | + 1000 active users ≈ 500KB |
| 92 | + 10000 active users ≈ 5MB |
| 93 | + ``` |
| 94 | +
|
| 95 | + ### Alternative Approaches |
| 96 | +
|
| 97 | + **If memory becomes a concern:** |
| 98 | +
|
| 99 | + 1. **Redis backend**: |
| 100 | + ```python |
| 101 | + cache = Cache(Cache.REDIS, endpoint="redis://localhost", ...) |
| 102 | + ``` |
| 103 | +
|
| 104 | + 2. **Custom eviction policy**: Implement LRU manually or use shorter TTL |
| 105 | +
|
| 106 | + 3. **Monitoring**: Track cache size in production: |
| 107 | + ```python |
| 108 | + # Check cache statistics |
| 109 | + cache_stats = await cache.get_stats() |
| 110 | + ``` |
| 111 | +
|
| 112 | + **Verdict**: |
| 113 | + For authentication use case with reasonable user counts (<10K active), memory impact should be minimal with your current TTL configuration. |
| 114 | + """ |
129 | 115 |
|
130 | 116 | # Generate a fake API key |
131 | 117 | credentials = HTTPBasicCredentials( |
|
0 commit comments