Visitar URL original
feat: Add optional GLIDE client for Redis online reads by chandlerok · Pull Request #6870 · feast-dev/feast · GitHub
Skip to content

feat: Add optional GLIDE client for Redis online reads - #6870

Merged
ntkathole merged 2 commits into
feast-dev:masterfrom
chandlerok:feat/redis-glide-client
Oct 1, 2026
Merged

ntkathole merged 2 commits into
feast-dev:masterfrom
chandlerok:feat/redis-glide-client

Conversation

@chandlerok

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

Adds an opt-in GLIDE-backed Redis online read path for Python get_online_features.

The existing redis-py path does command packing, socket parsing, and response handling in Python. Under thread contention, that work repeatedly reacquires the GIL and can make online reads much slower than single-threaded benchmarks suggest.

This PR adds a client: glide option to the Redis online store config. When enabled, Feast uses GLIDE for the batched Redis read while preserving the existing Redis storage layout and default redis-py behavior.

This PR intentionally does not include the HGETALL threshold optimization. That can be evaluated separately.

Scope

  • RedisOnlineStoreConfig gains client: redis | glide, defaulting to redis.
  • Only the synchronous read paths change (online_read and the batched per-feature-view read behind get_online_features). Writes and get_online_features_async keep using redis-py.
  • GLIDE reads the same entity keys (serialize_entity_key + project) and the same hashed field names (_mmh3(f"{fv}:{name}") plus _ts:<fv>) that Feast already writes, so existing data needs no migration.
  • connection_string support: host/port, db, password, username, ssl, socket_timeout, socket_connect_timeout. Unknown parameters are logged and ignored. Sentinel is rejected with a clear error because GLIDE has no Sentinel support.
  • valkey-glide-sync>=2.5,<3 is a new optional extra, feast[glide]. It is imported lazily so users who never opt in are unaffected. Configuring client: glide without it raises FeastExtrasDependencyImportError naming the extra.

Implementation notes

The read client is selected in one place, RedisOnlineStore._read_hash_fields, which takes a list of (key, fields) HMGET commands and returns one reply per command. Both clients produce the same ordering, so the existing response conversion (_convert_redis_values_to_protobuf) and timestamp handling are untouched.

_glide_hmget_batch builds a single Batch(is_atomic=False) (or ClusterBatch(is_atomic=False) for cluster) and calls exec(batch, raise_on_error=True) once, so the whole fetch is one FFI call.

Three type annotations were corrected because the GLIDE path made the existing inaccuracy visible: _generate_hset_keys_for_features returns hashed bytes keys, not str, and the value sequences accepted by _convert_redis_values_to_protobuf / _get_features_for_entity legitimately contain None for missing fields.

Which issue(s) this PR fixes:

Related to #6856.

Checks

  • I've made sure the tests are passing.
  • My commits are signed off (git commit -s)
  • My PR title follows conventional commits format

Testing Strategy

  • Unit tests
  • Integration tests
  • Manual tests
  • Testing is not required for this change

Commands run:

# Unit tests
uv run python -m pytest sdk/python/tests/unit/infra/online_store/test_redis.py -q
# 35 passed

# Redis-related unit tests together
uv run python -m pytest sdk/python/tests/unit/infra/online_store/test_redis.py \
    sdk/python/tests/unit/infra/online_store/test_redis_versioning.py \
    sdk/python/tests/unit/infra/online_store/test_helpers.py -q
# 65 passed

# Integration tests
uv run python -m pytest --integration \
    sdk/python/tests/integration/online_store/test_redis_glide_client.py -q
# 2 passed

# Lint and format
uv run ruff check <changed files>            # All checks passed!
uv run ruff format --check <changed files>   # 3 files already formatted

# Types
uv run bash -c "cd sdk/python && mypy feast/infra/online_stores/redis.py"
# Success: no issues found in 1 source file

Notes on the test runs

  • The integration test seeds rows with the default redis-py client, then reads them back with client: redis and client: glide and asserts the results are byte-identical, both for online_read and for the batched _read_features_per_fv response (including a missing entity). If GLIDE used different keys or field names it would read NOT_FOUND instead of matching.
  • I ran the integration test against a live Valkey 9 on localhost:6379 because the Docker daemon on my machine could not create containers (error creating temporary lease: read-only file system). The committed test uses testcontainers with redis:7, matching the existing test_redis_versioning.py, and is skipped when Docker is unavailable.
  • glide is added to the ci extra in pyproject.toml, but the pinned lock files (sdk/python/requirements/py*-ci-requirements.txt) need a make lock-python-dependencies-all run before CI installs it. Until then the tests that need the real package skip via importorskip; the mock-based tests, including the missing-dependency error test, still run.
  • mypy on feast/infra/online_stores/redis.py is clean. test_redis.py already had 19 mypy errors at master (unrelated pre-existing assertions on Optional dicts and list invariance in online_write_batch test data); this branch reduces that to 16 and adds none. I left those pre-existing errors alone to keep this diff focused.

Misc

The GLIDE batch is non-atomic by design, matching the existing pipeline(transaction=False) behavior. valkey-glide-sync 2.5.0 and 2.5.3 were both checked to confirm the Batch/ClusterBatch/config signatures used here exist across the 2.5 line.

I am happy to add glide to the ci lock files in this PR if you would rather have CI coverage of the GLIDE path immediately.

@chandlerok
chandlerok marked this pull request as ready for review September 25, 2026 15:29
@chandlerok
chandlerok requested a review from a team as a code owner September 25, 2026 15:29
@chandlerok

Copy link
Copy Markdown
Contributor Author

@patelchaitany would you mind reviewing this when you have a chance? It is the valkey-glide half of #6856 on its own; the HGETALL threshold change is deliberately left out.

(Posting this as a comment because the review-request API requires write access to this repo, which a fork author does not have.)

@chandlerok
chandlerok marked this pull request as draft September 25, 2026 15:38
@chandlerok

Copy link
Copy Markdown
Contributor Author

@ntkathole tagging you as the right reviewer for this one: it extends the Redis online store read path from #6337, and you have merged most of the recent Redis online store changes (#6446, #6656).

Scope: an opt-in client: glide on RedisOnlineStoreConfig that runs the batched HMGET as a single non-atomic valkey-glide batch, so the fetch runs off the GIL. redis-py stays the default; writes and async reads are unchanged; the HGETALL threshold change is deliberately not included.

@chandlerok
chandlerok marked this pull request as ready for review September 25, 2026 20:38
@codecov-commenter

codecov-commenter commented Sep 26, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 58.90411% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.62%. Comparing base (81673d9) to head (6dd260a).

Files with missing lines Patch % Lines
sdk/python/feast/infra/online_stores/redis.py 58.90% 30 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #6870      +/-   ##
==========================================
+ Coverage   48.58%   48.62%   +0.03%     
==========================================
  Files         427      427              
  Lines       53787    53845      +58     
  Branches     7834     7844      +10     
==========================================
+ Hits        26132    26181      +49     
- Misses      25788    25797       +9     
  Partials     1867     1867              
Flag Coverage Δ
go-feature-server 30.58% <ø> (ø)
python-unit 49.99% <58.90%> (+0.04%) ⬆️
Files with missing lines Coverage Δ
sdk/python/feast/infra/online_stores/redis.py 60.87% <58.90%> (+3.12%) ⬆️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 81673d9...6dd260a. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ntkathole ntkathole left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the well-structured PR! Left a couple of suggestions below.

# Only set when `client: glide` is configured, so the native extension stays
# untouched for users who never opt in.
_glide_client: Optional[Any] = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: _glide_client is cached here but never cleaned up. The existing teardown() closes the redis-py client — the GLIDE client should get the same treatment to release native (Rust-side) resources, file descriptors, and the connection pool.

Something like:

def teardown(self, config, tables, entities):
    ...
    if self._glide_client:
        self._glide_client.close()
        self._glide_client = None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d12d035. teardown() now closes self._glide_client and sets it back to None, so the native client is released as you suggested.

One note: the redis-py client was not actually being closed in teardown() before this change either (the method only deletes keys), so I scoped this to the GLIDE client rather than also changing the redis-py lifecycle in this PR. Happy to close that one too if you'd prefer it here.


if online_store_config.client == RedisClient.glide:
glide_sync = _load_glide_sync()
return _glide_hmget_batch(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_glide_sync() is called on every read when client == RedisClient.glide. While Python caches imported modules in sys.modules, this still incurs a function-call + dict-lookup overhead per read that is easy to avoid.

Consider caching the module reference on the instance alongside _glide_client, e.g.:

_glide_sync: Optional[Any] = None

def _get_glide_sync(self):
    if self._glide_sync is None:
        self._glide_sync = _load_glide_sync()
    return self._glide_sync

Then _read_hash_fields and _get_glide_client can both use self._get_glide_sync() instead of calling _load_glide_sync() each time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d12d035. Added _get_glide_sync(), which imports the module once and caches it on the instance; both _read_hash_fields and _get_glide_client now go through it instead of calling _load_glide_sync() each read.

@ntkathole

Copy link
Copy Markdown
Member

Also, please fix the linting error in CI

@chandlerok

Copy link
Copy Markdown
Contributor Author

Updated in d12d035. Both review suggestions are addressed and the lint failure is fixed:

  • teardown() closes the GLIDE client and clears the cached reference, releasing the native client.
  • glide_sync is cached per store via _get_glide_sync(), so reads no longer re-lookup the module.
  • lint-python fix: the failure was a detect-secrets false positive on the hunter2 test literals in test_redis.py, now allowlisted with # pragma: allowlist secret.

Testing (local, against a real Valkey 9.1.2 on 127.0.0.1:6379)

The Docker daemon here can't create containers (permission denied on the socket), so the testcontainers-based integration test was run against the local Valkey instead:

  • pytest test_redis.py test_redis_versioning.py test_helpers.py → 68 passed (3 new tests: module caching, teardown close, teardown no-op).
  • The two committed integration tests (test_glide_online_read_matches_redis_py, test_glide_batched_read_matches_redis_py) run against the local Valkey → 2 passed.
  • Full FeatureStore.get_online_features e2e with client: glide: apply a real repo, seed via redis-py, read through the public API → results identical to client: redis, including the missing entity.
  • Real-client lifecycle e2e: teardown() closes the actual GlideClient, a subsequent read recreates it and still returns correct values.
  • ruff check / ruff format --check clean; mypy feast/infra/online_stores/redis.py clean with the CI-pinned mypy==1.11.2 and types-protobuf==3.19.22.

The checks on this fork PR show action_required, so they need a maintainer to approve before the workflows actually run.

@chandlerok
chandlerok force-pushed the feat/redis-glide-client branch from d12d035 to 93c88d2 Compare September 30, 2026 21:34
@chandlerok

Copy link
Copy Markdown
Contributor Author

Correction: the head is now 93c88d2 (same tree as d12d035). That commit only changes the author/committer metadata — the first push reset the author name to chandlerok while the sign-off line said Chandler King, which tripped DCO. Re-authored so the sign-off matches, DCO is green again. No code change.

@ntkathole
ntkathole force-pushed the feat/redis-glide-client branch from 93c88d2 to b888c99 Compare October 1, 2026 13:13
Adds a `client: glide` option to RedisOnlineStoreConfig. When set, the
synchronous read paths (online_read and the batched read behind
get_online_features) issue their HMGET commands as one non-atomic GLIDE
batch, so the fetch runs off the GIL instead of in Python per command.

Default behavior is unchanged: redis-py remains the default client, all
writes and async reads keep using redis-py, and GLIDE reads the same
entity keys and hashed feature fields Feast already writes.

valkey-glide-sync is an optional dependency behind the new `feast[glide]`
extra. Configuring `client: glide` without it installed raises a Feast
extras import error naming the extra to install.

This does not include the HGETALL threshold change from the same issue.

Related to feast-dev#6856.

Signed-off-by: Chandler King <chandleroking@gmail.com>
Addresses review feedback on the GLIDE read path:

- teardown() now closes the native GLIDE client and clears the cached
  reference, so its Rust-side resources and file descriptors are released.
- The glide_sync module is imported once per store and cached on the
  instance instead of being looked up on every read.
- Allowlist the test password literals so detect-secrets stops failing
  lint-python.

Signed-off-by: Chandler King <chandleroking@gmail.com>
@ntkathole
ntkathole force-pushed the feat/redis-glide-client branch from b888c99 to 6dd260a Compare October 1, 2026 14:11
@chandlerok

Copy link
Copy Markdown
Contributor Author

@ntkathole would you mind taking another look when you have a chance?

Both of your suggestions from Sep 26 are addressed:

  • teardown() closes the GLIDE client and clears the cached reference, so a store that is torn down and re-created does not reuse a dead connection.
  • The lint failure CI flagged is fixed.

This is still the valkey-glide half of #6856 on its own; the HGETALL threshold change is deliberately not in here. Happy to split or drop anything if you would rather it landed differently.

@chandlerok

Copy link
Copy Markdown
Contributor Author

oh nvm I see you already reviewed! ty!

@ntkathole
ntkathole merged commit 6e9a532 into feast-dev:master Oct 1, 2026
24 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants