From 97a93ece1c09db7f4d0f0a4503f33735367fd11f Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Thu, 8 Oct 2026 00:45:49 +0000 Subject: [PATCH] MAAP broker: read maap-py's config afresh before each client build maap-py 5.1.0 caches /api/environment/config with functools.cache, a failed read (None) included, so after one 502 every MAAP() in the process raised AttributeError without asking the API again; 2 of 1482 ATL1415 jobs on 2026-10-07 spent all their tries that way. Clear the cache before each build. Co-Authored-By: Claude Opus 5.5 --- pointCollection/io_utils.py | 18 +++++++++++++++++- tests/test_maap_broker.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pointCollection/io_utils.py b/pointCollection/io_utils.py index ea38621..bdf078e 100644 --- a/pointCollection/io_utils.py +++ b/pointCollection/io_utils.py @@ -228,6 +228,22 @@ def _s3fs_from_maap(daac, **kwargs): f'falling back to earthaccess for {daac}.') return None, None + def new_client(): + # maap-py (5.1.0) caches its /api/environment/config read with + # functools.cache -- a FAILED read (None, e.g. after a 502) included -- + # so after one failure every MAAP() in the process raises + # AttributeError without asking the API again (2 of 1482 jobs spent all + # their tries that way on 2026-10-07). Clear it before each build. + try: + from maap import config_reader + clear = getattr(getattr(config_reader, '_get_client_config', None), + 'cache_clear', None) + if clear is not None: + clear() + except ImportError: + pass + return MAAP(maap_host=os.environ.get('MAAP_API_HOST', 'api.maap-project.org')) + client = None attempts = len(MAAP_BROKER_PAUSES_S) + 1 t0 = time.monotonic() @@ -235,7 +251,7 @@ def _s3fs_from_maap(daac, **kwargs): for attempt in range(1, attempts + 1): try: if client is None: - client = MAAP(maap_host=os.environ.get('MAAP_API_HOST', 'api.maap-project.org')) + client = new_client() creds = client.aws.earthdata_s3_credentials(endpoint) return _s3fs_with_credentials(creds, **kwargs), _expiry_timestamp(creds) except Exception as exc: diff --git a/tests/test_maap_broker.py b/tests/test_maap_broker.py index 3772db7..7c633dd 100644 --- a/tests/test_maap_broker.py +++ b/tests/test_maap_broker.py @@ -184,3 +184,32 @@ def test_ps_scale_for_lat_values_are_unchanged(): # computed with the module-level filter still in place (main, 42f66cc) np.testing.assert_allclose(pc.ps_scale_for_lat(np.array([90., 70., 60.])), [1.03107857, 1.0, 0.96206753], rtol=1e-6) + + +def test_a_failed_config_read_is_read_again(broker, monkeypatch): + # maap-py caches /api/environment/config with functools.cache, a failed + # read (None) included: a rebuilt client must read it again + import functools + import types + reads = [] + + @functools.cache + def _get_client_config(maap_host): + reads.append(maap_host) + return None if len(reads) == 1 else {'service': {}} + + module = sys.modules['maap.maap'] + real = module.MAAP + + class CachedConfigMAAP(real): + def __init__(self, maap_host=None): + _get_client_config(maap_host).get('service') # AttributeError on a cached None + super().__init__(maap_host) + monkeypatch.setattr(module, 'MAAP', CachedConfigMAAP) + cfg = types.ModuleType('maap.config_reader') + cfg._get_client_config = _get_client_config + monkeypatch.setattr(sys.modules['maap'], 'config_reader', cfg, raising=False) + monkeypatch.setitem(sys.modules, 'maap.config_reader', cfg) + fs, _ = io_utils._s3fs_from_maap('NSIDC') + assert fs == ('fs', CREDS) + assert len(reads) == 2