Skip to content

Commit f57526b

Browse files
mcayananMike Cayanan
andauthored
HC-611: Fix closed-index params in PIT methods (OpenSearch + base class) (#68)
* HC-611: Fix closed-index params in PIT methods (OpenSearch + base class) * bump version * updates based on testing * restore closed-index params for OSS search --------- Co-authored-by: Mike Cayanan <michael.d.cayanan@jpl.nasa.gov>
1 parent fa392e6 commit f57526b

4 files changed

Lines changed: 142 additions & 11 deletions

File tree

hysds_commons/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "2.1.0"
1+
__version__ = "2.1.1"
22
__description__ = "Common HySDS Functions, Utilities, Etc."
33
__url__ = "https://github.jpl.nasa.gov/hysds-org/hysds_commons"

hysds_commons/opensearch_utils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,22 @@ def _pit(self, **kwargs):
4444
if index is None:
4545
raise RuntimeError("OpenSearchUtility._pit: the search_after API must specify a index/alias")
4646

47-
pit = self.es.create_point_in_time(index=index, keep_alive=keep_alive)
47+
# Apply closed index params to PIT open call (HC-600).
48+
# PIT APIs only accept ignore_unavailable and expand_wildcards (not allow_no_indices).
49+
# Extract caller-specified params from kwargs first, then apply defaults.
50+
pit_params = {}
51+
for key, default_value in self.CLOSED_INDEX_PARAMS.items():
52+
if key != "allow_no_indices": # PIT APIs don't accept this param
53+
pit_params[key] = kwargs.pop(key, default_value)
54+
55+
pit = self.es.create_point_in_time(index=index, keep_alive=keep_alive, **pit_params)
4856
pit_id = pit["pit_id"]
4957

58+
# Once the PIT is open, strip any remaining indicesOptions from kwargs — OpenSearch/ES
59+
# rejects them on _search calls when a PIT is in the body.
60+
for key in self.CLOSED_INDEX_PARAMS:
61+
kwargs.pop(key, None)
62+
5063
size = kwargs.get("size", body.get("size"))
5164
if not size:
5265
kwargs["size"] = 1000

hysds_commons/search_utils.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,13 @@ def _pit(self, **kwargs):
170170
if index is None:
171171
raise RuntimeError("ElasticsearchUtility._pit: the search_after API must specify a index/alias")
172172

173-
# Apply closed index params (HC-600) - always apply since aliases can
174-
# resolve to multiple indices, some of which may be closed
173+
# Apply closed index params to PIT open call (HC-600).
174+
# PIT APIs only accept ignore_unavailable and expand_wildcards (not allow_no_indices).
175+
# Extract caller-specified params from kwargs first, then apply defaults.
175176
pit_params = {}
176-
for key, value in self.CLOSED_INDEX_PARAMS.items():
177-
kwargs.setdefault(key, value)
178-
pit_params[key] = kwargs[key]
177+
for key, default_value in self.CLOSED_INDEX_PARAMS.items():
178+
if key != "allow_no_indices": # PIT APIs don't accept this param
179+
pit_params[key] = kwargs.pop(key, default_value)
179180

180181
size = kwargs.get("size", body.get("size"))
181182
if not size:
@@ -188,12 +189,21 @@ def _pit(self, **kwargs):
188189
pit = None
189190
if self.flavor != "oss":
190191
pit = self.es.open_point_in_time(index=index, keep_alive=keep_alive, **pit_params)
192+
193+
# Once the PIT is open, strip any remaining indicesOptions from kwargs — OpenSearch/ES
194+
# rejects them on _search calls when a PIT is in the body.
195+
for key in self.CLOSED_INDEX_PARAMS:
196+
kwargs.pop(key, None)
197+
191198
body = {
192199
**body,
193200
**{"pit": {**pit, **{"keep_alive": keep_alive}}},
194201
}
195202
else:
196203
warnings.warn("Elasticsearch OSS does not support _pit, will use search_after without _pit...")
204+
# Restore closed-index params for OSS search (no PIT to conflict with)
205+
for key, default_value in self.CLOSED_INDEX_PARAMS.items():
206+
kwargs.setdefault(key, default_value)
197207
res = self.es.search(body=body, **kwargs)
198208

199209
records = []

test/test_search_utils.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from unittest.mock import MagicMock, patch
1111

1212
from hysds_commons.search_utils import SearchUtility
13+
from hysds_commons.opensearch_utils import OpenSearchUtility
1314

1415

1516
class ConcreteSearchUtility(SearchUtility):
@@ -23,6 +24,15 @@ def __init__(self):
2324
self.flavor = "default"
2425

2526

27+
class MockOpenSearchUtility(OpenSearchUtility):
28+
"""Mock OpenSearchUtility that skips real OpenSearch client initialization."""
29+
def __init__(self):
30+
self.es = MagicMock()
31+
self.engine = "opensearch"
32+
self.version = None
33+
self.flavor = None
34+
35+
2636
class TestIsWildcardIndex:
2737
"""Tests for _is_wildcard_index() static method."""
2838

@@ -294,27 +304,125 @@ def setup_method(self):
294304
}
295305
self.utility.es.close_point_in_time.return_value = {"succeeded": True}
296306

307+
def test_pit_does_not_pass_allow_no_indices_to_open_pit(self):
308+
"""open_point_in_time() should NOT receive allow_no_indices."""
309+
self.utility._pit(index="job_status-*", body={"query": {"match_all": {}}})
310+
call_kwargs = self.utility.es.open_point_in_time.call_args[1]
311+
assert "allow_no_indices" not in call_kwargs
312+
assert call_kwargs["ignore_unavailable"] is True
313+
assert call_kwargs["expand_wildcards"] == "open"
314+
315+
def test_pit_search_does_not_pass_indices_options(self):
316+
"""_search calls with PIT body must NOT include indicesOptions."""
317+
self.utility._pit(index="job_status-*", body={"query": {"match_all": {}}})
318+
call_kwargs = self.utility.es.search.call_args[1]
319+
assert "ignore_unavailable" not in call_kwargs
320+
assert "allow_no_indices" not in call_kwargs
321+
assert "expand_wildcards" not in call_kwargs
322+
297323
def test_pit_with_wildcard_applies_params_to_open_point_in_time(self):
298324
"""_pit() should apply closed index params to open_point_in_time for wildcard patterns."""
299325
self.utility._pit(index="job_status-*", body={"query": {"match_all": {}}})
300326

301-
# Verify open_point_in_time was called with closed index params
327+
# Verify open_point_in_time was called with closed index params (except allow_no_indices)
302328
call_kwargs = self.utility.es.open_point_in_time.call_args[1]
303329
assert call_kwargs["ignore_unavailable"] is True
304-
assert call_kwargs["allow_no_indices"] is True
305330
assert call_kwargs["expand_wildcards"] == "open"
306331

307332
def test_pit_with_single_index_applies_params_to_open_point_in_time(self):
308333
"""_pit() should apply params to open_point_in_time for single index (could be alias)."""
309334
self.utility._pit(index="job_status-current", body={"query": {"match_all": {}}})
310335

311-
# Verify open_point_in_time was called with closed index params
336+
# Verify open_point_in_time was called with closed index params (except allow_no_indices)
312337
call_kwargs = self.utility.es.open_point_in_time.call_args[1]
313338
assert call_kwargs["ignore_unavailable"] is True
314-
assert call_kwargs["allow_no_indices"] is True
315339
assert call_kwargs["expand_wildcards"] == "open"
316340

317341

342+
class TestOpenSearchPitMethod:
343+
"""Tests for OpenSearchUtility._pit() with closed index handling (HC-600)."""
344+
345+
def setup_method(self):
346+
"""Set up test fixtures."""
347+
self.utility = MockOpenSearchUtility()
348+
self.utility.es.create_point_in_time.return_value = {
349+
"pit_id": "opensearch_pit_id_123"
350+
}
351+
self.utility.es.search.return_value = {
352+
"hits": {"total": {"value": 0}, "hits": []}
353+
}
354+
self.utility.es.delete_point_in_time.return_value = {"succeeded": True}
355+
356+
def test_create_pit_applies_closed_index_params(self):
357+
"""create_point_in_time() should receive ignore_unavailable and expand_wildcards."""
358+
self.utility._pit(index="grq_v1.0_product-*", body={"query": {"match_all": {}}})
359+
call_kwargs = self.utility.es.create_point_in_time.call_args[1]
360+
assert call_kwargs["ignore_unavailable"] is True
361+
assert call_kwargs["expand_wildcards"] == "open"
362+
363+
def test_create_pit_does_not_pass_allow_no_indices(self):
364+
"""PIT open APIs don't accept allow_no_indices -- must not be passed."""
365+
self.utility._pit(index="grq", body={"query": {"match_all": {}}})
366+
call_kwargs = self.utility.es.create_point_in_time.call_args[1]
367+
assert "allow_no_indices" not in call_kwargs
368+
369+
def test_create_pit_applies_params_for_alias(self):
370+
"""create_point_in_time() should apply params for aliases (no wildcard in name)."""
371+
self.utility._pit(index="grq", body={"query": {"match_all": {}}})
372+
call_kwargs = self.utility.es.create_point_in_time.call_args[1]
373+
assert call_kwargs["ignore_unavailable"] is True
374+
assert call_kwargs["expand_wildcards"] == "open"
375+
376+
def test_create_pit_does_not_override_caller_params(self):
377+
"""Caller-specified closed-index params should not be overridden."""
378+
self.utility._pit(
379+
index="grq",
380+
body={"query": {"match_all": {}}},
381+
ignore_unavailable=False,
382+
expand_wildcards="all",
383+
)
384+
call_kwargs = self.utility.es.create_point_in_time.call_args[1]
385+
assert call_kwargs["ignore_unavailable"] is False
386+
assert call_kwargs["expand_wildcards"] == "all"
387+
388+
def test_pit_search_does_not_pass_indices_options(self):
389+
"""_search calls with PIT must NOT include indicesOptions."""
390+
self.utility._pit(index="grq", body={"query": {"match_all": {}}})
391+
call_kwargs = self.utility.es.search.call_args[1]
392+
assert "ignore_unavailable" not in call_kwargs
393+
assert "allow_no_indices" not in call_kwargs
394+
assert "expand_wildcards" not in call_kwargs
395+
396+
def test_pit_pagination_still_works(self):
397+
"""Verify pagination loop + PIT cleanup still functions correctly."""
398+
page1 = {
399+
"hits": {
400+
"total": {"value": 2},
401+
"hits": [
402+
{"_id": "1", "_source": {"id": "a"}, "sort": [1, "a"]},
403+
{"_id": "2", "_source": {"id": "b"}, "sort": [2, "b"]},
404+
],
405+
}
406+
}
407+
page2 = {"hits": {"total": {"value": 2}, "hits": []}}
408+
self.utility.es.search.side_effect = [page1, page2]
409+
410+
records = self.utility._pit(index="grq", body={"query": {"match_all": {}}})
411+
412+
assert len(records) == 2
413+
assert records[0]["_id"] == "1"
414+
assert records[1]["_id"] == "2"
415+
# Verify PIT was cleaned up
416+
self.utility.es.delete_point_in_time.assert_called_once_with(
417+
body={"pit_id": ["opensearch_pit_id_123"]}
418+
)
419+
420+
def test_missing_index_raises_runtime_error(self):
421+
"""_pit() should raise RuntimeError when no index is provided."""
422+
with pytest.raises(RuntimeError, match="must specify a index/alias"):
423+
self.utility._pit(body={"query": {"match_all": {}}})
424+
425+
318426
class TestClosedIndexParamsConstant:
319427
"""Tests for CLOSED_INDEX_PARAMS class constant."""
320428

0 commit comments

Comments
 (0)