pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/feast-dev/feast/commit/63c5eb152a33bb30a75bf2d704e9aac310db2eab

fix: Fix intermittent async test failures for DynamoDB and Redis · feast-dev/feast@63c5eb1 · GitHub
Skip to content

Commit 63c5eb1

Browse files
committed
fix: Fix intermittent async test failures for DynamoDB and Redis
DynamoDB (integration tests): - Add conftest.py with dynamodb_local_environment fixture that creates a fully isolated Environment (its own DynamoDB Local container + FileDataSourceCreator) with dummy AWS credentials set *before* any boto client is instantiated. This prevents the expired STS session token present in CI from bleeding into the lazy aiobotocore client and causing UnrecognizedClientException on DynamoDB Local 2.x. - test_push_features_and_read_async: scope to mongodb only via the shared environment; add test_push_features_and_read_async_dynamodb that uses the isolated fixture for the same coverage. - test_async_online_retrieval_with_event_timestamps_dynamo: replace shared environment + broken monkeypatch fixture with the isolated dynamodb_local_environment fixture. Redis (unit test): - Replace asyncio.get_event_loop().run_until_complete() with asyncio.run() in test_online_write_batch_async_skip_dedup_single_pipeline to fix RuntimeError: There is no current event loop in thread 'MainThread' on Python 3.10+. Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent a4fde83 commit 63c5eb1

5 files changed

Lines changed: 220 additions & 5 deletions

File tree

infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ kind: Secret
33
metadata:
44
name: openlineage-secret
55
stringData:
6-
api_key: your-marquez-api-key # pragma: allowlist secret
6+
api_key: your-marquez-api-key #pragma: allowlist secret
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
Fixtures for online-store integration tests.
3+
"""
4+
5+
from typing import Dict
6+
7+
import pytest
8+
from testcontainers.core.container import DockerContainer
9+
from testcontainers.core.waiting_utils import wait_for_logs
10+
11+
from tests.universal.feature_repos.universal.online_store_creator import (
12+
OnlineStoreCreator,
13+
)
14+
15+
16+
class _SharedDbDynamoDBOnlineStoreCreator(OnlineStoreCreator):
17+
"""DynamoDB Local container started with ``-sharedDb -inMemory``.
18+
19+
Why ``-sharedDb``
20+
-----------------
21+
DynamoDB Local 2.x namespaces tables by the **access key ID** in the
22+
request signature. In CI, the sync ``boto3`` client and the async
23+
``aiobotocore`` client can resolve credentials from *different* sources
24+
(env vars, credential file, ``credential_process``, container IAM role,
25+
etc.) even after ``monkeypatch.setenv`` has set fake keys—because the
26+
credential chain is evaluated lazily and various caches may hold stale
27+
values.
28+
29+
When the two clients end up using *different* access keys, the sync
30+
client creates tables in namespace A while the async client queries
31+
namespace B, which is empty → ``ResourceNotFoundException``.
32+
33+
``-sharedDb`` collapses all namespaces into a single in-memory database,
34+
making table visibility completely independent of which credentials each
35+
client uses. This is the correct setting for integration tests that want
36+
to verify async read/write behaviour without caring about credential
37+
isolation.
38+
"""
39+
40+
def __init__(self, project_name: str, **kwargs):
41+
super().__init__(project_name)
42+
self.container = (
43+
DockerContainer("amazon/dynamodb-local:latest")
44+
.with_exposed_ports("8000")
45+
.with_command("-jar DynamoDBLocal.jar -sharedDb -inMemory")
46+
)
47+
48+
def create_online_store(self) -> Dict[str, str]:
49+
self.container.start()
50+
wait_for_logs(
51+
container=self.container,
52+
predicate="Initializing DynamoDB Local with the following configuration:",
53+
timeout=10,
54+
)
55+
exposed_port = self.container.get_exposed_port("8000")
56+
return {
57+
"type": "dynamodb",
58+
"endpoint_url": f"http://localhost:{exposed_port}",
59+
"region": "us-west-2",
60+
}
61+
62+
def teardown(self):
63+
self.container.stop()
64+
65+
66+
@pytest.fixture
67+
async def dynamodb_local_environment(monkeypatch, worker_id):
68+
"""Isolated, self-contained Environment for DynamoDB async tests.
69+
70+
Root cause of the async credential failures
71+
-------------------------------------------
72+
DynamoDB Local 2.x isolates tables **per access key ID**. In CI,
73+
``boto3`` (sync, used to provision tables via ``store.apply()``) and
74+
``aiobotocore`` (async, used for reads/writes in the test body) may
75+
resolve credentials from *different* sources even when ``monkeypatch``
76+
has set fake static keys—the credential chain is evaluated lazily and
77+
caches may hold stale values from a real AWS session configured in the
78+
runner environment.
79+
80+
When the two clients end up using different access key IDs they land in
81+
different DynamoDB Local namespaces:
82+
83+
* sync client → namespace ``KEY_A`` → tables exist ✓
84+
* async client → namespace ``KEY_B`` → tables not found → ``ResourceNotFoundException``
85+
86+
Fix: ``_SharedDbDynamoDBOnlineStoreCreator``
87+
--------------------------------------------
88+
The isolated container is started with ``-sharedDb -inMemory``. In
89+
shared-DB mode DynamoDB Local stores *all* tables in a single namespace
90+
regardless of the access key, so sync and async clients always see the
91+
same tables.
92+
93+
Why async + ``await fs.initialize()`` before yielding
94+
-----------------------------------------------------
95+
Calling ``await fs.initialize()`` eagerly creates the ``aiobotocore``
96+
client inside this fixture's event loop (the *same* loop the test will
97+
run in). This pre-caches:
98+
99+
1. ``FeatureStore._provider`` so the identical ``DynamoDBOnlineStore``
100+
instance is reused for the entire test.
101+
2. The aiobotocore client, which is now unambiguously pointed at our
102+
isolated container's ``endpoint_url``.
103+
104+
Yields
105+
------
106+
tuple[Environment, TestData]
107+
``(environment, (entities, datasets, data_sources))``
108+
"""
109+
from feast.infra.online_stores.dynamodb import DynamoDBOnlineStore
110+
from tests.universal.feature_repos.integration_test_repo_config import (
111+
IntegrationTestRepoConfig,
112+
)
113+
from tests.universal.feature_repos.repo_configuration import (
114+
construct_test_environment,
115+
construct_universal_test_data,
116+
)
117+
from tests.universal.feature_repos.universal.data_sources.file import (
118+
FileDataSourceCreator,
119+
)
120+
121+
# Set fake static credentials before any boto client is created.
122+
# These are accepted by DynamoDB Local regardless of validity.
123+
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "fakeaccesskey000000")
124+
monkeypatch.setenv(
125+
"AWS_SECRET_ACCESS_KEY", "fakesecretkey0000000000000000000000000000"
126+
)
127+
monkeypatch.delenv("AWS_SESSION_TOKEN", raising=False)
128+
monkeypatch.delenv("AWS_SECURITY_TOKEN", raising=False)
129+
# Prevent IMDS from injecting real session tokens on EC2-backed runners.
130+
monkeypatch.setenv("AWS_EC2_METADATA_DISABLED", "true")
131+
# Disable the container credentials provider (ECS/EKS IAM roles).
132+
monkeypatch.delenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", raising=False)
133+
monkeypatch.delenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", raising=False)
134+
# Ensure no profile redirects boto to a different credential source.
135+
monkeypatch.delenv("AWS_PROFILE", raising=False)
136+
monkeypatch.delenv("AWS_DEFAULT_PROFILE", raising=False)
137+
138+
# Reset class-level boto3 client caches so that no stale client from a
139+
# previous test in this worker bleeds into our isolated environment.
140+
DynamoDBOnlineStore._dynamodb_client = None
141+
DynamoDBOnlineStore._dynamodb_resource = None
142+
143+
config = IntegrationTestRepoConfig(
144+
provider="local",
145+
offline_store_creator=FileDataSourceCreator,
146+
online_store_creator=_SharedDbDynamoDBOnlineStoreCreator,
147+
online_store=None,
148+
)
149+
150+
environment = construct_test_environment(
151+
config,
152+
fixture_request=None,
153+
worker_id=worker_id,
154+
)
155+
environment.setup()
156+
157+
# FileDataSourceCreator writes only local Parquet files — no AWS calls.
158+
universal_test_data = construct_universal_test_data(environment)
159+
160+
# Eagerly initialise the aiobotocore client in *this* event loop so it
161+
# is guaranteed to point at our container and is reused throughout the
162+
# test body without lazy-init surprises.
163+
await environment.feature_store.initialize()
164+
165+
yield environment, universal_test_data
166+
167+
# Cleanly shut down the async client before the container disappears.
168+
await environment.feature_store.close()
169+
environment.teardown()
170+
171+
# Flush class-level caches so the next test starts completely fresh.
172+
DynamoDBOnlineStore._dynamodb_client = None
173+
DynamoDBOnlineStore._dynamodb_resource = None

sdk/python/tests/integration/online_store/test_push_features_to_online_store.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_push_features_and_read(store):
4747

4848

4949
@pytest.mark.integration
50-
@pytest.mark.universal_online_stores(only=["dynamodb", "mongodb"])
50+
@pytest.mark.universal_online_stores(only=["mongodb"])
5151
async def test_push_features_and_read_async(store):
5252
await store.push_async("location_stats_push_source", _ingest_df())
5353

@@ -56,3 +56,36 @@ async def test_push_features_and_read_async(store):
5656
entity_rows=[{"location_id": 1}],
5757
)
5858
assert_response(online_resp)
59+
60+
61+
@pytest.mark.asyncio
62+
@pytest.mark.integration
63+
@pytest.mark.universal_online_stores
64+
async def test_push_features_and_read_async_dynamodb(dynamodb_local_environment):
65+
"""Async push + async read for DynamoDB with a credential-isolated environment.
66+
67+
DynamoDB Local 2.x rejects requests that carry an expired AWS session
68+
token. In CI, real (possibly expired) STS credentials exist in the
69+
environment. The shared ``environment`` fixture resolves credentials
70+
before the async client is created, so those bad credentials bleed in.
71+
72+
This test uses ``dynamodb_local_environment``, which sets dummy
73+
credentials *before* any boto client is instantiated, guaranteeing that
74+
both the sync boto3 table-provisioning client and the async aiobotocore
75+
client start with clean, token-free credentials.
76+
"""
77+
environment, universal_test_data = dynamodb_local_environment
78+
store = environment.feature_store
79+
_, _, data_sources = universal_test_data
80+
81+
feature_views = construct_universal_feature_views(data_sources)
82+
location_fv = feature_views.pushed_locations
83+
store.apply([location(), location_fv])
84+
85+
await store.push_async("location_stats_push_source", _ingest_df())
86+
87+
online_resp = await store.get_online_features_async(
88+
features=["pushable_location_stats:temperature"],
89+
entity_rows=[{"location_id": 1}],
90+
)
91+
assert_response(online_resp)

sdk/python/tests/integration/online_store/test_universal_online.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,10 +532,19 @@ async def test_async_online_retrieval_with_event_timestamps(
532532

533533
@pytest.mark.asyncio
534534
@pytest.mark.integration
535-
@pytest.mark.universal_online_stores(only=["dynamodb"])
535+
@pytest.mark.universal_online_stores
536536
async def test_async_online_retrieval_with_event_timestamps_dynamo(
537-
environment, universal_data_sources
537+
dynamodb_local_environment,
538538
):
539+
"""Async online retrieval for DynamoDB with a credential-isolated environment.
540+
541+
Uses ``dynamodb_local_environment`` (its own DynamoDB Local container +
542+
FileDataSourceCreator) so that dummy credentials are set before any boto
543+
client is created. This avoids the expired-STS-token problem that
544+
occurs when aiobotocore lazily resolves credentials from the shared
545+
environment in CI.
546+
"""
547+
environment, universal_data_sources = dynamodb_local_environment
539548
await _do_async_retrieval_test(environment, universal_data_sources)
540549

541550

sdk/python/tests/unit/infra/online_store/test_redis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ async def _run():
468468
config, feature_view, data, progress=None
469469
)
470470

471-
asyncio.get_event_loop().run_until_complete(_run())
471+
asyncio.run(_run())
472472

473473
assert mock_async_client.pipeline.call_count == 1
474474
async_pipe.hmget.assert_not_called()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy