Skip to content

[FIX] Fix Redis Sentinel crash in MetricsMixin#1856

Merged
hari-kuriakose merged 4 commits intomainfrom
fix/redis-sentinel-metrics-mixin
Mar 15, 2026
Merged

[FIX] Fix Redis Sentinel crash in MetricsMixin#1856
hari-kuriakose merged 4 commits intomainfrom
fix/redis-sentinel-metrics-mixin

Conversation

@chandrasekharan-zipstack
Copy link
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack commented Mar 15, 2026

What

  • Fix MetricsMixin in sdk1 to use Sentinel-aware Redis client factory instead of raw StrictRedis
  • Add unstract-core as a dependency of sdk1
  • Add missing COPY unstract/core in tool Dockerfiles

Why

  • In Sentinel HA mode, REDIS_HOST/PORT point to the Sentinel process (port 26379), not a Redis master
  • MetricsMixin was connecting directly to Sentinel and issuing SELECT 1, which Sentinel doesn't support
  • This caused redis.exceptions.ResponseError: unknown command 'SELECT' → 500 errors on /answer-prompt in onprem-ha

How

  • Replace StrictRedis(host, port, db=1) with create_redis_client(db=1) from unstract.core.cache.redis_client, which handles Sentinel discovery via master_for()
  • Add unstract-core to sdk1's pyproject.toml dependencies (all services using sdk1 already depend on core — zero new transitive deps)
  • Wrap set_start_time() and collect_metrics() in try/except so Redis failures degrade gracefully (time_taken=None) instead of crashing requests
  • Add COPY ${BUILD_PACKAGES_PATH}/core /unstract/core to classifier, structure, and text_extractor Dockerfiles since sdk1 now depends on core at build time

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. create_redis_client() already handles both standalone and Sentinel modes transparently. In standalone mode, behavior is identical to the old StrictRedis constructor. The added error handling only makes metrics more resilient — worst case, time_taken reports None instead of crashing.

Database Migrations

  • None

Env Config

  • No new env vars. Uses existing REDIS_SENTINEL_MODE, REDIS_HOST, REDIS_PORT, REDIS_SENTINEL_MASTER_NAME

Relevant Docs

  • N/A

Related Issues or PRs

Dependencies Versions

  • Added unstract-core as dependency of unstract-sdk1 (internal package, no new external deps)

Notes on Testing

  • Verify uv.lock files get auto-updated by CI for all affected services
  • Build prompt-service and confirm it starts without Redis errors
  • Test in Sentinel HA environment — /answer-prompt should work without SELECT errors
  • Verify tool containers (classifier, structure, text_extractor) build successfully
  • Confirm standalone (non-Sentinel) Redis mode still works

Screenshots

N/A

Checklist

I have read and understood the Contribution Guidelines.

MetricsMixin was creating a raw StrictRedis client using REDIS_HOST/PORT
directly, which in Sentinel HA mode points to the Sentinel process (port
26379). Sentinel doesn't support the SELECT command, causing
ResponseError on every /answer-prompt call.

- Replace StrictRedis with create_redis_client() from unstract-core
  which handles Sentinel discovery via master_for()
- Add unstract-core as sdk1 dependency (all services using sdk1 already
  depend on core, so zero new transitive deps)
- Add error handling around set_start_time() and collect_metrics() so
  Redis failures degrade gracefully instead of crashing requests
- Add missing COPY for unstract/core in tool Dockerfiles (classifier,
  structure, text_extractor) since sdk1 now depends on core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Summary by CodeRabbit

  • Chores

    • Docker builds now include core package components alongside existing packages.
    • SDK now declares the core module as a dependency and supports editable local source linking.
    • Tool dependency lists adjusted to favor the core package over previous editable references.
  • Refactor

    • Redis client initialization unified behind a factory and simplified across services.
    • Cache initialization logging and state handling streamlined.
  • Bug Fixes

    • Metrics collection hardened with safer Redis error handling.

Walkthrough

Adds unstract-core to builds and editable tool requirements; updates SDK pyproject to depend on unstract-core; refactors Redis client creation to a centralized factory with SSL support and defensive error handling, and updates worker/cache initialization to use the factory.

Changes

Cohort / File(s) Summary
Docker Build Updates
tools/classifier/Dockerfile, tools/structure/Dockerfile, tools/text_extractor/Dockerfile
Each Dockerfile now copies ${BUILD_PACKAGES_PATH}/core into /unstract/core in addition to existing sdk1 and flags copies.
Tool requirements (editable installs)
tools/classifier/requirements.txt, tools/structure/requirements.txt, tools/text_extractor/requirements.txt
Replace editable installs of sdk1[aws] (and removed flags where applicable) with an editable install for /unstract/core.
SDK Dependency Configuration
unstract/sdk1/pyproject.toml
Adds unstract-core to dependencies and registers it under [tool.uv.sources] with path = "../core", editable = true.
Redis client core changes
unstract/core/src/unstract/core/cache/redis_client.py
Expanded env parsing to include SSL and ssl_cert_reqs; introduced _build_connection_kwargs usage; refactored RedisClient to be a wrapper exposing redis_client and to delegate creation to create_redis_client(); unified connection kwargs handling for standalone/sentinel.
SDK metrics usage
unstract/sdk1/src/unstract/sdk1/utils/metrics_mixin.py
Replaced direct StrictRedis usage with create_redis_client(db=1), added try/except around Redis operations, log on failures and nullify the client to fail gracefully.
Worker/cache initialization
workers/shared/cache/cache_backends.py, workers/shared/infrastructure/caching/cache_utils.py
Removed sentinel-vs-standalone branching; always obtain raw client via create_redis_client and wrap with RedisClient; simplified logging and ensured clean None state on init failure.
Minor manifest adjustments
various Dockerfile/requirements/pyproject files
Small line additions/removals to add core and update editable paths across tools and builds.

Sequence Diagram(s)

sequenceDiagram
  participant Metrics as MetricsMixin
  participant Factory as create_redis_client
  participant Redis as RedisServer
  participant Logger as Logger

  Metrics->>Factory: request redis client (db=1)
  Factory-->>Metrics: Redis client (or None)
  alt client available
    Metrics->>Redis: GET/SET/LPUSH (metric ops)
    Redis-->>Metrics: responses
    Metrics->>Logger: log success
  else client None or error
    Metrics->>Logger: log error and nullify client
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title '[FIX] Fix Redis Sentinel crash in MetricsMixin' directly addresses the primary change—fixing Redis Sentinel compatibility in MetricsMixin—which is a core issue resolved across multiple files in this changeset.
Description check ✅ Passed The description provides comprehensive coverage of all required template sections: What (three specific changes), Why (root cause in Sentinel mode), How (detailed implementation approach), backwards compatibility assessment, database migrations (none), environment config (existing vars only), testing notes, and related issues/PRs.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/redis-sentinel-metrics-mixin
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR fixes a Redis Sentinel crash in MetricsMixin by replacing the raw StrictRedis constructor (which connected directly to the Sentinel process on port 26379 and issued an unsupported SELECT 1 command) with the Sentinel-aware create_redis_client(db=1) factory from unstract.core. The same simplification is applied to RedisCacheBackend and WorkerCacheManager, and the three tool Dockerfiles are updated to COPY the unstract/core package now that sdk1 depends on it.

Key changes:

  • MetricsMixin now uses create_redis_client(db=1) and wraps both set_start_time() and collect_metrics() in try/except for graceful degradation when Redis is unavailable.
  • RedisCacheBackend.__init__ and WorkerCacheManager._initialize_redis_client remove manual Sentinel-vs-standalone branching in favour of the shared factory. This also silently fixes a pre-existing bug in the standalone path of RedisCacheBackend where RedisClient(host=..., port=...) was called even though RedisClient has no such constructor.
  • unstract-core is added as an explicit dependency in sdk1/pyproject.toml with an editable local source path; no new external transitive dependencies are introduced.
  • Classifier, structure, and text_extractor Dockerfiles gain a COPY ${BUILD_PACKAGES_PATH}/core /unstract/core layer.
  • Performance concern in WorkerCacheManager: the is_available property pings Redis on every invocation, so every cache read/write incurs an extra round-trip. This same property can also trigger _clear_all_cache() which uses the blocking KEYS command — all from the hot path of routine cache operations.
  • CacheDecorator in cache_utils.py is an incomplete no-op wrapper that adds no actual caching.

Confidence Score: 3/5

  • The core Sentinel fix in MetricsMixin is correct and safe; the new worker cache infrastructure has performance issues that should be addressed before heavy production load.
  • The primary fix (MetricsMixin + Dockerfile + pyproject.toml) is well-scoped and correct. However, the new WorkerCacheManager implementation in cache_utils.py has a real performance regression: is_available pings Redis synchronously on every cache method call, and the same property can trigger a blocking KEYS scan. In high-throughput worker scenarios these could become meaningful bottlenecks.
  • workers/shared/infrastructure/caching/cache_utils.py — the is_available property and _clear_all_cache method need attention before this code sees production traffic.

Important Files Changed

Filename Overview
unstract/sdk1/src/unstract/sdk1/utils/metrics_mixin.py Core fix: replaces raw StrictRedis(host, port, db=1) with create_redis_client(db=1) so Sentinel mode is handled correctly; adds try/except blocks around both set_start_time and collect_metrics for graceful degradation.
workers/shared/infrastructure/caching/cache_utils.py Switches to create_redis_client factory (removing manual Sentinel-vs-standalone branching), but the is_available property pings Redis on every cache method call, and _clear_all_cache issues a blocking KEYS command from within that hot path.
workers/shared/cache/cache_backends.py Simplifies RedisCacheBackend.__init__ to always use create_redis_client(env_prefix="CACHE_REDIS_", ...) — also fixes a pre-existing bug where the standalone path called RedisClient(host=..., port=...) even though RedisClient has no such constructor.
unstract/core/src/unstract/core/cache/redis_client.py Pre-existing Sentinel-aware factory; no functional changes in this PR. Correctly passes db override into master_kwargs so SELECT db is issued against the Redis master, not the Sentinel process.
tools/classifier/Dockerfile Adds COPY ${BUILD_PACKAGES_PATH}/core /unstract/core so unstract-core is available at build time after sdk1 declares it as a dependency.
tools/structure/Dockerfile Same COPY unstract/core addition as classifier Dockerfile; structure tool Dockerfile already uses uv for installation, so the build chain is consistent.
tools/text_extractor/Dockerfile Same COPY unstract/core addition as classifier; no other changes.
unstract/sdk1/pyproject.toml Adds unstract-core as a runtime dependency with an editable local source path; no new external transitive dependencies since all consuming services already depend on unstract-core.

Sequence Diagram

sequenceDiagram
    participant AP as /answer-prompt
    participant MM as MetricsMixin.__init__
    participant CRC as create_redis_client(db=1)
    participant S as Redis Sentinel :26379
    participant RM as Redis Master :6379

    AP->>MM: instantiate(run_id)
    MM->>CRC: create_redis_client(db=1)
    alt Sentinel mode (REDIS_SENTINEL_MODE=true)
        CRC->>S: discover master
        S-->>CRC: master address
        CRC->>RM: PING (validates connection)
        RM-->>CRC: PONG
        CRC-->>MM: Sentinel-backed redis.Redis client
    else Standalone mode
        CRC->>RM: connect (lazy)
        CRC-->>MM: redis.Redis client
    end
    MM->>RM: SET metrics:run_id:op_id time.time() EX 86400
    RM-->>MM: OK
    Note over MM: (previously: SET issued to Sentinel port → SELECT 1 error)

    AP->>MM: collect_metrics()
    MM->>RM: EXISTS / GET / DELETE metrics:run_id:op_id
    RM-->>MM: time_taken
    MM-->>AP: {time_taken(s): X.XXX}
Loading

Comments Outside Diff (2)

  1. workers/shared/infrastructure/caching/cache_utils.py, line 61-82 (link)

    is_available issues a ping() on every invocation — and can trigger a blocking KEYS scan

    is_available is a property called at the top of every cache method (get_execution_status, set_execution_status, increment_callback_attempt, etc.). Each call unconditionally issues self._redis_client.ping(), so every single cache read/write incurs an extra Redis round-trip. On top of that, when a connection gap is detected (line 71–78), it calls _clear_all_cache() which uses the blocking KEYS command against all six patterns — all within this property.

    The health_check_interval=30 parameter already passed to create_redis_client tells redis-py to do periodic background health checks; an explicit ping() on every property access duplicates that work.

    Consider checking availability lazily (e.g. a flag that's invalidated by the except branch) and using a dedicated _is_connected() helper only for the reconnection-gap detection logic, separate from the hot path:

    @property
    def is_available(self) -> bool:
        return self._redis_client is not None and self._available
    
    def _check_connection(self) -> bool:
        """Called infrequently to detect reconnections and clear stale state."""
        try:
            self._redis_client.ping()
            ...
            return True
        except Exception:
            self._available = False
            return False
  2. workers/shared/infrastructure/caching/cache_utils.py, line 524-534 (link)

    CacheDecorator never applies any caching

    The wrapper function either short-circuits early (if not self.cache_manager.is_available) or calls func(*args, **kwargs) directly. Neither branch actually reads from or writes to the cache — the decorator is currently a no-op that adds overhead (one is_available ping per call) without providing any caching benefit.

    Either implement the cache lookup/store logic or remove the class to avoid misleading callers. The comment # can be enhanced with specific caching logic suggests it's intentionally incomplete, but shipping an exported decorator that silently does nothing can be confusing.

    def wrapper(*args, **kwargs):
        if not self.cache_manager.is_available:
            return func(*args, **kwargs)
        # TODO: implement key derivation + cache read/write
        return func(*args, **kwargs)
Prompt To Fix All With AI
This is a comment left during a code review.
Path: workers/shared/infrastructure/caching/cache_utils.py
Line: 61-82

Comment:
**`is_available` issues a `ping()` on every invocation — and can trigger a blocking `KEYS` scan**

`is_available` is a property called at the top of every cache method (`get_execution_status`, `set_execution_status`, `increment_callback_attempt`, etc.). Each call unconditionally issues `self._redis_client.ping()`, so every single cache read/write incurs an extra Redis round-trip. On top of that, when a connection gap is detected (line 71–78), it calls `_clear_all_cache()` which uses the blocking `KEYS` command against all six patterns — all within this property.

The `health_check_interval=30` parameter already passed to `create_redis_client` tells `redis-py` to do periodic background health checks; an explicit `ping()` on every property access duplicates that work.

Consider checking availability lazily (e.g. a flag that's invalidated by the `except` branch) and using a dedicated `_is_connected()` helper only for the reconnection-gap detection logic, separate from the hot path:

```python
@property
def is_available(self) -> bool:
    return self._redis_client is not None and self._available

def _check_connection(self) -> bool:
    """Called infrequently to detect reconnections and clear stale state."""
    try:
        self._redis_client.ping()
        ...
        return True
    except Exception:
        self._available = False
        return False
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: workers/shared/infrastructure/caching/cache_utils.py
Line: 524-534

Comment:
**`CacheDecorator` never applies any caching**

The `wrapper` function either short-circuits early (`if not self.cache_manager.is_available`) or calls `func(*args, **kwargs)` directly. Neither branch actually reads from or writes to the cache — the decorator is currently a no-op that adds overhead (one `is_available` ping per call) without providing any caching benefit.

Either implement the cache lookup/store logic or remove the class to avoid misleading callers. The comment `# can be enhanced with specific caching logic` suggests it's intentionally incomplete, but shipping an exported decorator that silently does nothing can be confusing.

```python
def wrapper(*args, **kwargs):
    if not self.cache_manager.is_available:
        return func(*args, **kwargs)
    # TODO: implement key derivation + cache read/write
    return func(*args, **kwargs)
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: workers/shared/infrastructure/caching/cache_utils.py
Line: 84-110

Comment:
**Blocking `KEYS` in `_clear_all_cache` — prefer `SCAN`**

`_clear_all_cache` iterates through six pattern strings with `self._redis_client.keys(pattern)`, which is the blocking `KEYS` command. For large keyspaces this can stall the Redis event loop and affect all other clients.

`RedisCacheBackend.scan_keys` in the same codebase already shows the recommended pattern. Consider replacing `keys()` with a cursor-based scan loop here too:

```python
cursor = 0
while True:
    cursor, batch = self._redis_client.scan(cursor=cursor, match=pattern, count=100)
    keys_to_delete.extend(batch)
    if cursor == 0:
        break
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 9486aea

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
unstract/sdk1/src/unstract/sdk1/utils/metrics_mixin.py (1)

54-60: Avoid exists() + get() race and extra Redis roundtrip.

exists() followed by get() can fail if the key expires/deletes between calls; reading once and handling None is simpler and quieter.

♻️ Proposed change
-            if not self.redis_client.exists(self.redis_key):
-                return {self.TIME_TAKEN_KEY: None}
-
-            start_time = float(self.redis_client.get(self.redis_key))
-            time_taken = round(time.time() - start_time, 3)
-
-            self.redis_client.delete(self.redis_key)
+            start_time_raw = self.redis_client.get(self.redis_key)
+            if start_time_raw is None:
+                return {self.TIME_TAKEN_KEY: None}
+
+            time_taken = round(time.time() - float(start_time_raw), 3)
+            self.redis_client.delete(self.redis_key)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/sdk1/src/unstract/sdk1/utils/metrics_mixin.py` around lines 54 - 60,
Replace the exists()+get() pattern with a single read and a None check: call
self.redis_client.get(self.redis_key) once, return {self.TIME_TAKEN_KEY: None}
if the result is None, otherwise convert the value to float, compute time_taken
= round(time.time() - start_time, 3), then delete the key with
self.redis_client.delete(self.redis_key); reference the existing symbols
self.redis_client, self.redis_key, and self.TIME_TAKEN_KEY when making this
change to avoid the race and extra Redis roundtrip.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@unstract/sdk1/pyproject.toml`:
- Around line 79-80: The pyproject override [tool.uv.sources] only affects
uv-based installs and pip-based builds will fail to resolve the local
unstract-core dependency; fix by adding an explicit editable local install of
the core package to the pip requirements used by the failing Docker builds: add
"-e file:/unstract/core" to tools/text_extractor/requirements.txt and
tools/classifier/requirements.txt (or alternatively change those Dockerfiles to
use uv pip install like tools/structure/Dockerfile does), ensuring the
dependency referenced in pyproject.toml (unstract-core) is available to
pip-based installs.

---

Nitpick comments:
In `@unstract/sdk1/src/unstract/sdk1/utils/metrics_mixin.py`:
- Around line 54-60: Replace the exists()+get() pattern with a single read and a
None check: call self.redis_client.get(self.redis_key) once, return
{self.TIME_TAKEN_KEY: None} if the result is None, otherwise convert the value
to float, compute time_taken = round(time.time() - start_time, 3), then delete
the key with self.redis_client.delete(self.redis_key); reference the existing
symbols self.redis_client, self.redis_key, and self.TIME_TAKEN_KEY when making
this change to avoid the race and extra Redis roundtrip.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 037505e2-002b-4d73-9b91-10bf2ad8c741

📥 Commits

Reviewing files that changed from the base of the PR and between 23cdfb9 and 4d725b4.

⛔ Files ignored due to path filters (5)
  • backend/uv.lock is excluded by !**/*.lock
  • platform-service/uv.lock is excluded by !**/*.lock
  • prompt-service/uv.lock is excluded by !**/*.lock
  • unstract/filesystem/uv.lock is excluded by !**/*.lock
  • workers/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • tools/classifier/Dockerfile
  • tools/structure/Dockerfile
  • tools/text_extractor/Dockerfile
  • unstract/sdk1/pyproject.toml
  • unstract/sdk1/src/unstract/sdk1/utils/metrics_mixin.py

Tool Dockerfiles (classifier, text_extractor, structure) use pip, not
uv. Since sdk1 now depends on unstract-core (which isn't on PyPI),
pip can't resolve it via [tool.uv.sources]. Adding explicit editable
install of core before sdk1 ensures pip finds it locally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Worker cache modules (cache_backends.py, cache_utils.py) duplicated
sentinel/standalone branching logic for Redis client creation. Both
now use create_redis_client() from unstract-core as the single entry
point, which already handles mode detection internally.

- Add SSL support (ssl, ssl_cert_reqs) to create_redis_client() via
  env vars ({prefix}SSL, {prefix}SSL_CERT_REQS), disabled by default
- Remove if-sentinel-else-standalone branching in cache_backends.py
  and cache_utils.py — replaced with single create_redis_client() call
- Remove unused os and redis imports from simplified modules

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 63 passed, 0 failed (63 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{63}}$$ $$\textcolor{#23d18b}{\tt{63}}$$

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
unstract/core/src/unstract/core/cache/redis_client.py (1)

300-308: Consider adding an __init__ method for safer instantiation.

The class declares redis_client: redis.Redis as a type hint without a default, and relies on from_env() or manual __new__ + attribute assignment. This works but is fragile—calling methods on an improperly constructed instance will raise AttributeError.

Adding an explicit __init__ would make the expected construction pattern clearer and prevent misuse.

💡 Optional: Add __init__ for explicit construction
 class RedisClient:
     """Wrapper around redis.Redis providing a consistent interface.

     Always instantiate via RedisClient.from_env(), which delegates to
     create_redis_client() and handles both Sentinel and standalone modes.
     """

-    redis_client: redis.Redis
+    def __init__(self, redis_client: redis.Redis):
+        """Initialize with an existing redis.Redis client.
+        
+        Prefer using from_env() for environment-based construction.
+        """
+        self.redis_client = redis_client

     # ... existing methods ...

     `@classmethod`
     def from_env(cls, env_prefix: str = "REDIS_") -> "RedisClient":
         """Create client from environment variables."""
-        instance = cls.__new__(cls)
-        instance.redis_client = create_redis_client(
+        return cls(create_redis_client(
             env_prefix=env_prefix, decode_responses=True
-        )
-        return instance
+        ))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unstract/core/src/unstract/core/cache/redis_client.py` around lines 300 -
308, Add an explicit __init__ to RedisClient that accepts a redis_client:
redis.Redis (or Optional[redis.Redis] with validation) and assigns it to
self.redis_client, and raise a clear ValueError if None or not an instance of
redis.Redis; update the class docstring to note the preferred construction via
RedisClient.from_env() or RedisClient(redis_client=...) so callers don't
accidentally create instances without the required attribute. Ensure references
to create_redis_client() and from_env() remain compatible with the new
constructor.
workers/shared/cache/cache_backends.py (1)

102-111: Consider using RedisClient.from_env() or a proper constructor.

The manual __new__ + attribute assignment pattern works but bypasses any future __init__ logic. Since this code needs custom connection parameters that from_env() doesn't support, consider either:

  1. Extending from_env() to accept additional kwargs, or
  2. Adding a proper __init__ to RedisClient (as suggested in the other file)

For now, adding a comment explaining why this pattern is necessary would help maintainability.

💡 Option 1: If RedisClient gets an __init__, use it directly
             raw_client = create_redis_client(
                 env_prefix="CACHE_REDIS_",
                 decode_responses=True,
                 socket_timeout=5,
                 socket_connect_timeout=5,
                 health_check_interval=30,
             )
-            # Wrap in RedisClient for interface compatibility
-            self.redis_client = RedisClient.__new__(RedisClient)
-            self.redis_client.redis_client = raw_client
+            # Wrap in RedisClient for interface compatibility
+            self.redis_client = RedisClient(raw_client)
💡 Option 2: If keeping __new__ pattern, add explanatory comment
             raw_client = create_redis_client(
                 env_prefix="CACHE_REDIS_",
                 decode_responses=True,
                 socket_timeout=5,
                 socket_connect_timeout=5,
                 health_check_interval=30,
             )
-            # Wrap in RedisClient for interface compatibility
+            # Wrap raw client in RedisClient for interface compatibility.
+            # Using __new__ because RedisClient has no __init__ and from_env()
+            # doesn't support custom connection parameters.
             self.redis_client = RedisClient.__new__(RedisClient)
             self.redis_client.redis_client = raw_client
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workers/shared/cache/cache_backends.py` around lines 102 - 111, The current
code instantiates RedisClient via RedisClient.__new__ and assigns redis_client
directly after create_redis_client; update this to either extend
RedisClient.from_env to accept the custom kwargs from create_redis_client or add
a proper RedisClient.__init__ that accepts (and uses)
decode_responses/socket_timeout/socket_connect_timeout/health_check_interval and
call that instead of using __new__; if you choose to keep the __new__ pattern
for now, add a clear explanatory comment above the RedisClient.__new__ usage
referencing create_redis_client and why __init__ is bypassed (mention
RedisClient, RedisClient.from_env, and the redis_client attribute) so future
maintainers know this is intentional and should be revisited when RedisClient
gains an __init__ or extended from_env.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@unstract/core/src/unstract/core/cache/redis_client.py`:
- Around line 300-308: Add an explicit __init__ to RedisClient that accepts a
redis_client: redis.Redis (or Optional[redis.Redis] with validation) and assigns
it to self.redis_client, and raise a clear ValueError if None or not an instance
of redis.Redis; update the class docstring to note the preferred construction
via RedisClient.from_env() or RedisClient(redis_client=...) so callers don't
accidentally create instances without the required attribute. Ensure references
to create_redis_client() and from_env() remain compatible with the new
constructor.

In `@workers/shared/cache/cache_backends.py`:
- Around line 102-111: The current code instantiates RedisClient via
RedisClient.__new__ and assigns redis_client directly after create_redis_client;
update this to either extend RedisClient.from_env to accept the custom kwargs
from create_redis_client or add a proper RedisClient.__init__ that accepts (and
uses)
decode_responses/socket_timeout/socket_connect_timeout/health_check_interval and
call that instead of using __new__; if you choose to keep the __new__ pattern
for now, add a clear explanatory comment above the RedisClient.__new__ usage
referencing create_redis_client and why __init__ is bypassed (mention
RedisClient, RedisClient.from_env, and the redis_client attribute) so future
maintainers know this is intentional and should be revisited when RedisClient
gains an __init__ or extended from_env.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cad729f8-4faf-4da5-8f3f-de5d6e768698

📥 Commits

Reviewing files that changed from the base of the PR and between da46bae and 9486aea.

📒 Files selected for processing (3)
  • unstract/core/src/unstract/core/cache/redis_client.py
  • workers/shared/cache/cache_backends.py
  • workers/shared/infrastructure/caching/cache_utils.py

@hari-kuriakose hari-kuriakose merged commit 7eaa879 into main Mar 15, 2026
9 checks passed
@hari-kuriakose hari-kuriakose deleted the fix/redis-sentinel-metrics-mixin branch March 15, 2026 07:07
@hari-kuriakose
Copy link
Contributor

All reviewer agents comments are addressed, hence proceeding for now.

@chandrasekharan-zipstack
Copy link
Contributor Author

Skipped reviews on this due to urgency, can be done and addressed later. This PR also includes changes from a minor refactor in #1857

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants