|
15 | 15 | from hindsight_api.engine.cross_encoder import ( |
16 | 16 | FlashRankCrossEncoder, |
17 | 17 | LocalSTCrossEncoder, |
| 18 | + _release_rerank_heap, |
18 | 19 | _resolve_malloc_trim, |
19 | 20 | ) |
20 | 21 |
|
@@ -42,6 +43,17 @@ def test_module_level_trim_is_resolved(self): |
42 | 43 | """The module caches the resolved callable at import time.""" |
43 | 44 | assert callable(ce_module._malloc_trim) |
44 | 45 |
|
| 46 | + def test_release_rerank_heap_collects_then_trims(self): |
| 47 | + """Local reranker cleanup should free Python objects before trimming glibc.""" |
| 48 | + calls = [] |
| 49 | + with ( |
| 50 | + patch.object(ce_module.gc, "collect", lambda: calls.append("gc")), |
| 51 | + patch.object(ce_module, "_malloc_trim", lambda: calls.append("trim")), |
| 52 | + ): |
| 53 | + _release_rerank_heap() |
| 54 | + |
| 55 | + assert calls == ["gc", "trim"] |
| 56 | + |
45 | 57 |
|
46 | 58 | class TestLocalSTCrossEncoder: |
47 | 59 | """Unit tests for the SentenceTransformers-backed local reranker.""" |
@@ -126,28 +138,28 @@ async def test_predict_not_initialized_raises(self): |
126 | 138 | with pytest.raises(RuntimeError, match="not initialized"): |
127 | 139 | await encoder.predict([("q", "d")]) |
128 | 140 |
|
129 | | - async def test_predict_calls_malloc_trim_after_success(self): |
130 | | - """The trim callable must run after every successful predict batch.""" |
| 141 | + async def test_predict_releases_rerank_heap_after_success(self): |
| 142 | + """The cleanup hook must run after every successful predict batch.""" |
131 | 143 | encoder = self._make_encoder() |
132 | 144 | encoder._model.predict.return_value = [0.5] |
133 | 145 |
|
134 | | - trim_calls = [] |
135 | | - with patch.object(ce_module, "_malloc_trim", lambda: trim_calls.append("trim")): |
| 146 | + cleanup_calls = [] |
| 147 | + with patch.object(ce_module, "_release_rerank_heap", lambda: cleanup_calls.append("cleanup")): |
136 | 148 | await encoder.predict([("q", "doc")]) |
137 | 149 |
|
138 | | - assert trim_calls == ["trim"] |
| 150 | + assert cleanup_calls == ["cleanup"] |
139 | 151 |
|
140 | | - async def test_predict_calls_malloc_trim_even_on_exception(self): |
141 | | - """`finally` semantics: trim must run when the model raises mid-batch.""" |
| 152 | + async def test_predict_releases_rerank_heap_even_on_exception(self): |
| 153 | + """`finally` semantics: cleanup must run when the model raises mid-batch.""" |
142 | 154 | encoder = self._make_encoder() |
143 | 155 | encoder._model.predict.side_effect = RuntimeError("boom") |
144 | 156 |
|
145 | | - trim_calls = [] |
146 | | - with patch.object(ce_module, "_malloc_trim", lambda: trim_calls.append("trim")): |
| 157 | + cleanup_calls = [] |
| 158 | + with patch.object(ce_module, "_release_rerank_heap", lambda: cleanup_calls.append("cleanup")): |
147 | 159 | with pytest.raises(RuntimeError, match="boom"): |
148 | 160 | await encoder.predict([("q", "doc")]) |
149 | 161 |
|
150 | | - assert trim_calls == ["trim"] |
| 162 | + assert cleanup_calls == ["cleanup"] |
151 | 163 |
|
152 | 164 |
|
153 | 165 | class TestFlashRankCrossEncoder: |
@@ -214,43 +226,43 @@ def fake_rerank(request): |
214 | 226 | # Two unique queries -> two rerank calls. |
215 | 227 | assert encoder._ranker.rerank.call_count == 2 |
216 | 228 |
|
217 | | - def test_predict_sync_calls_malloc_trim_after_success(self): |
| 229 | + def test_predict_sync_releases_rerank_heap_after_success(self): |
218 | 230 | encoder = self._make_encoder() |
219 | 231 | encoder._ranker.rerank.return_value = [{"id": 0, "score": 0.5}] |
220 | 232 |
|
221 | 233 | fake_flashrank = MagicMock() |
222 | 234 | fake_flashrank.RerankRequest = lambda query, passages: MagicMock() |
223 | 235 |
|
224 | | - trim_calls = [] |
| 236 | + cleanup_calls = [] |
225 | 237 | with patch.dict("sys.modules", {"flashrank": fake_flashrank}): |
226 | | - with patch.object(ce_module, "_malloc_trim", lambda: trim_calls.append("trim")): |
| 238 | + with patch.object(ce_module, "_release_rerank_heap", lambda: cleanup_calls.append("cleanup")): |
227 | 239 | encoder._predict_sync([("q", "doc")]) |
228 | 240 |
|
229 | | - assert trim_calls == ["trim"] |
| 241 | + assert cleanup_calls == ["cleanup"] |
230 | 242 |
|
231 | | - def test_predict_sync_calls_malloc_trim_even_on_exception(self): |
| 243 | + def test_predict_sync_releases_rerank_heap_even_on_exception(self): |
232 | 244 | encoder = self._make_encoder() |
233 | 245 | encoder._ranker.rerank.side_effect = RuntimeError("flashrank boom") |
234 | 246 |
|
235 | 247 | fake_flashrank = MagicMock() |
236 | 248 | fake_flashrank.RerankRequest = lambda query, passages: MagicMock() |
237 | 249 |
|
238 | | - trim_calls = [] |
| 250 | + cleanup_calls = [] |
239 | 251 | with patch.dict("sys.modules", {"flashrank": fake_flashrank}): |
240 | | - with patch.object(ce_module, "_malloc_trim", lambda: trim_calls.append("trim")): |
| 252 | + with patch.object(ce_module, "_release_rerank_heap", lambda: cleanup_calls.append("cleanup")): |
241 | 253 | with pytest.raises(RuntimeError, match="flashrank boom"): |
242 | 254 | encoder._predict_sync([("q", "doc")]) |
243 | 255 |
|
244 | | - assert trim_calls == ["trim"] |
| 256 | + assert cleanup_calls == ["cleanup"] |
245 | 257 |
|
246 | | - def test_predict_sync_empty_pairs_does_not_trim(self): |
| 258 | + def test_predict_sync_empty_pairs_does_not_release_heap(self): |
247 | 259 | """The early `if not pairs: return []` short-circuits before the |
248 | | - try/finally, so trim doesn't fire on a no-op call. This is intentional |
| 260 | + try/finally, so cleanup doesn't fire on a no-op call. This is intentional |
249 | 261 | — nothing was allocated.""" |
250 | 262 | encoder = self._make_encoder() |
251 | 263 |
|
252 | | - trim_calls = [] |
253 | | - with patch.object(ce_module, "_malloc_trim", lambda: trim_calls.append("trim")): |
| 264 | + cleanup_calls = [] |
| 265 | + with patch.object(ce_module, "_release_rerank_heap", lambda: cleanup_calls.append("cleanup")): |
254 | 266 | encoder._predict_sync([]) |
255 | 267 |
|
256 | | - assert trim_calls == [] |
| 268 | + assert cleanup_calls == [] |
0 commit comments