File size: 6,372 Bytes
8a841b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a1d5dd
 
8a841b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a1d5dd
8a841b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"""
Unit tests for CacheLayer.

Tests:
  - Exact cache hit/miss
  - Exact cache stores and retrieves correctly
  - Prefix cache hit on matching prefix
  - Prefix cache miss on different prefix
  - Dedup lock acquire / release
  - Dedup wait (subscriber) receives published result
"""
import asyncio
import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch


SAMPLE_REQ = {
    "model": "edge/auto",
    "messages": [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain the concept of caching in distributed systems."},
    ]
}

SAMPLE_RESP = {
    "id": "test-completion-001",
    "object": "chat.completion",
    "model": "gpt-4o-mini",
    "choices": [{"index": 0, "message": {"role": "assistant", "content": "Caching stores frequently accessed data closer to the consumer..."}, "finish_reason": "stop"}],
    "usage": {"prompt_tokens": 25, "completion_tokens": 50, "total_tokens": 75, "estimated_cost_usd": 0.00003},
}

SAMPLE_RESP_2 = {
    "id": "test-completion-002",
    "object": "chat.completion",
    "model": "gpt-4o-mini",
    "choices": [{"index": 0, "message": {"role": "assistant", "content": "Different answer for a different request."}, "finish_reason": "stop"}],
    "usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30, "estimated_cost_usd": 0.00001},
}


@pytest.fixture
def redis_store():
    """In-memory fake Redis for cache tests."""
    store: dict = {}
    pubsub_messages: dict = {}  # channel → list of messages

    redis = AsyncMock()

    async def get(key):
        return store.get(key)

    async def set(key, value, *args, **kwargs):
        store[key] = value

    async def delete(key):
        store.pop(key, None)

    async def zadd(key, mapping):
        if key not in store:
            store[key] = {}
        store[key].update(mapping)

    async def zremrangebyrank(key, start, end):
        pass

    async def expire(key, ttl):
        pass

    async def zrange(key, start, end):
        if key not in store or not isinstance(store[key], dict):
            return []
        return list(store[key].keys())

    async def set_nx(key, value, **kwargs):
        if key in store:
            return None
        store[key] = value
        return True

    redis.get.side_effect = get
    redis.set.side_effect = set
    redis.delete.side_effect = delete
    redis.zadd.side_effect = zadd
    redis.zremrangebyrank.side_effect = zremrangebyrank
    redis.expire.side_effect = expire
    redis.zrange.side_effect = zrange

    # nx=True set
    async def set_with_nx(key, value, nx=False, ex=None, **kwargs):
        if nx and key in store:
            return None
        store[key] = value
        return True

    redis.set.side_effect = set_with_nx

    return redis, store


@pytest.fixture
def cache(redis_store):
    from inferroute.cache import CacheLayer
    redis, store = redis_store
    layer = CacheLayer()
    with patch("inferroute.cache.get_redis_client", return_value=redis):
        yield layer, store, redis


@pytest.mark.asyncio
async def test_exact_cache_miss(cache):
    layer, store, redis = cache
    result = await layer.lookup_exact(SAMPLE_REQ)
    assert result is None


@pytest.mark.asyncio
async def test_exact_cache_store_and_hit(cache):
    layer, store, redis = cache
    await layer.store_exact(SAMPLE_REQ, SAMPLE_RESP)
    result = await layer.lookup_exact(SAMPLE_REQ)
    assert result is not None
    assert result["id"] == "test-completion-001"


@pytest.mark.asyncio
async def test_exact_cache_different_request_is_miss(cache):
    layer, store, redis = cache
    await layer.store_exact(SAMPLE_REQ, SAMPLE_RESP)

    different_req = {
        "model": "edge/auto",
        "messages": [{"role": "user", "content": "What is the weather today?"}]
    }
    result = await layer.lookup_exact(different_req)
    assert result is None


@pytest.mark.asyncio
async def test_exact_cache_ignores_routing_and_metadata_keys(cache):
    """Two requests that differ only in routing/metadata keys should share the cache."""
    layer, store, redis = cache
    req_with_meta = {**SAMPLE_REQ, "routing": {"policy": "cost"}, "metadata": {"user": "alice"}}

    await layer.store_exact(SAMPLE_REQ, SAMPLE_RESP)

    # Should still hit even with extra keys
    result = await layer.lookup_exact(req_with_meta)
    assert result is not None


@pytest.mark.asyncio
async def test_prefix_cache_disabled(cache):
    """Verify that lookup_prefix always returns None as prefix answer caching is disabled."""
    layer, store, redis = cache

    long_req = {
        "model": "edge/auto",
        "messages": [
            {"role": "user", "content": "Explain the concept of caching in distributed systems and its impact on scalability and performance in microservices architectures."}
        ]
    }
    await layer.store_exact(long_req, SAMPLE_RESP)

    # Request with a truncated prompt that still matches prefix
    prefix_req = {
        "model": "edge/auto",
        "messages": [
            {"role": "user", "content": "Explain the concept of caching in distributed systems"}
        ]
    }
    result = await layer.lookup_prefix(prefix_req)
    assert result is None


@pytest.mark.asyncio
async def test_dedup_lock_acquire_and_release(cache):
    layer, store, redis = cache
    is_owner = await layer.try_acquire_dedup_lock(SAMPLE_REQ)
    assert is_owner is True

    await layer.release_dedup_lock(SAMPLE_REQ)
    # After release, another caller can acquire
    is_owner_2 = await layer.try_acquire_dedup_lock(SAMPLE_REQ)
    assert is_owner_2 is True


@pytest.mark.asyncio
async def test_dedup_lock_second_caller_blocked(cache):
    """Second call with same request should see lock is taken."""
    layer, store, redis = cache
    is_owner_1 = await layer.try_acquire_dedup_lock(SAMPLE_REQ)
    assert is_owner_1 is True
    is_owner_2 = await layer.try_acquire_dedup_lock(SAMPLE_REQ)
    # Second caller should NOT be owner
    assert is_owner_2 is False


@pytest.mark.asyncio
async def test_cache_key_determinism(cache):
    """Same logical request should always produce the same cache key."""
    layer, _, _ = cache
    key1 = layer._exact_key(SAMPLE_REQ)
    key2 = layer._exact_key(dict(SAMPLE_REQ))
    assert key1 == key2
    assert key1.startswith("inferroute:cache:exact:")