Skip to content

REVERSE MERGE - v0.138.15-hotfix#1625

Merged
ritwik-g merged 2 commits into
mainfrom
v0.138.15-hotfix
Oct 31, 2025
Merged

REVERSE MERGE - v0.138.15-hotfix#1625
ritwik-g merged 2 commits into
mainfrom
v0.138.15-hotfix

Conversation

@muhammad-ali-e

Copy link
Copy Markdown
Contributor

What

  • Hotfix -reverse merge
  • Implemented HeartbeatKeeper as a Celery bootstep to maintain RabbitMQ heartbeats during warm shutdown
  • Added configurable environment variables for HeartbeatKeeper behavior (HEARTBEAT_KEEPER_ENABLED, HEARTBEAT_KEEPER_SHUTDOWN_INTERVAL, HEARTBEAT_KEEPER_CHECK_INTERVAL, HEARTBEAT_KEEPER_MAX_ERRORS)
  • HeartbeatKeeper remains dormant during normal operation, only activates during warm shutdown
  • Uses existing CELERY_BROKER_HEARTBEAT configuration for smart defaults

Why

  • During Kubernetes rolling updates, Celery workers receive SIGTERM and enter warm shutdown
  • Celery's internal heartbeat thread stops immediately on warm shutdown
  • RabbitMQ detects missing heartbeats after 2 × CELERY_BROKER_HEARTBEAT timeout and closes the connection
  • When connection closes, RabbitMQ requeues unacknowledged tasks to other workers
  • This causes the same task to be processed twice - once by the shutting-down worker and once by another worker
  • Task duplication can lead to data corruption, duplicate processing, and inconsistent state
  • Critical issue for production deployments with frequent updates or pod restarts

How

  • Created HeartbeatKeeper class that extends Celery's bootsteps.StartStopStep
  • During normal operation: Thread sleeps and checks periodically if shutdown has started (minimal overhead)
  • During warm shutdown: Activates and sends heartbeats every CELERY_BROKER_HEARTBEAT/5 seconds (configurable) to RabbitMQ
  • Uses heartbeat_check(rate=0) to force immediate heartbeat sends without validation
  • Gracefully handles "Too many heartbeats missed" errors that occur due to historical misses
  • Continues attempting heartbeats even when connection appears degraded
  • Only stops when task completes or terminate() is called
  • Configuration via environment variables with smart defaults based on CELERY_BROKER_HEARTBEAT

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, this PR will not break existing features:

  1. Backward Compatible: HeartbeatKeeper is enabled by default but can be disabled via HEARTBEAT_KEEPER_ENABLED=false
  2. Minimal Overhead: During normal operation, HeartbeatKeeper just sleeps - Celery's built-in heartbeat handles everything
  3. Non-Invasive: Uses Celery's official bootstep extension mechanism - doesn't modify core Celery code
  4. Isolated: HeartbeatKeeper only activates during warm shutdown and doesn't interfere with task execution
  5. Fail-Safe: Even if HeartbeatKeeper fails, it logs errors but doesn't crash the worker
  6. Tested: Thoroughly tested with long-running tasks (60-180 seconds) and warm shutdown scenarios
  7. Configurable: All intervals configurable via environment variables - can be tuned per deployment

Potential edge cases (not breaking, but worth noting):

  • Warning logs during shutdown ("Connection degraded") - expected behavior, connection stays alive despite warnings
  • If CELERY_BROKER_HEARTBEAT is set very high (>60s), shutdown intervals may need adjustment

Database Migrations

  • None required

Env Config

New Environment Variables (all optional, have defaults):

  • HEARTBEAT_KEEPER_ENABLED (default: true)

    • Enable/disable HeartbeatKeeper feature
    • Values: true (enabled), any other value (disabled)
  • HEARTBEAT_KEEPER_SHUTDOWN_INTERVAL (default: CELERY_BROKER_HEARTBEAT / 5)

    • How often to send heartbeats during warm shutdown (seconds)
    • Must be less than CELERY_BROKER_HEARTBEAT / 2
    • Example: For CELERY_BROKER_HEARTBEAT=10, default is 2s
  • HEARTBEAT_KEEPER_CHECK_INTERVAL (default: CELERY_BROKER_HEARTBEAT)

    • How often to check if shutdown started during normal operation (seconds)
    • Doesn't affect performance, just determines responsiveness to shutdown signal
  • HEARTBEAT_KEEPER_MAX_ERRORS (default: 10)

    • Maximum consecutive errors before logging "connection appears closed"
    • Worker continues trying regardless

Existing Config Used:

  • CELERY_BROKER_HEARTBEAT (default: 10) - Used to calculate smart defaults and determines RabbitMQ connection timeout

Relevant Docs

Related Issues or PRs

  • Jira Ticket: UN-2910
  • Related to Kubernetes rolling update stability
  • Addresses race condition in distributed file processing

Dependencies Versions

  • No new dependencies added
  • Uses existing: celery, kombu, socket (stdlib)

Notes on Testing

Manual Testing Performed:

  1. Normal Operation Test:

    • Start worker, process multiple tasks
    • Verify HeartbeatKeeper logs "In standby mode"
    • No extra heartbeats sent during normal operation
    • Celery's built-in heartbeat handles everything
  2. Warm Shutdown Test (Task Completes):

    • Start worker with 60-second task
    • Press Ctrl+C after 5 seconds
    • Observe heartbeats every 2 seconds during shutdown (CELERY_BROKER_HEARTBEAT=10)
    • Task completes successfully after 60 seconds
    • Worker exits cleanly
    • No task duplication
  3. Warm Shutdown Test (Connection Degraded):

    • Start worker with 180-second task
    • Press Ctrl+C after 5 seconds
    • Heartbeats sent successfully for first 2 × CELERY_BROKER_HEARTBEAT period
    • "Connection degraded" warnings appear after that (expected)
    • Worker continues maintaining connection
    • RabbitMQ does NOT close connection
    • Task completes without duplication
  4. Disable Feature Test:

    • Set HEARTBEAT_KEEPER_ENABLED=false
    • Start worker
    • Verify HeartbeatKeeper logs "DISABLED"
    • Original Celery behavior maintained
  5. Configuration Test:

    • Test with different CELERY_BROKER_HEARTBEAT values (10, 30, 60)
    • Verify shutdown intervals auto-calculated correctly
    • Test manual override of HEARTBEAT_KEEPER_SHUTDOWN_INTERVAL

RabbitMQ Logs Verified:

  • No "missed heartbeats from client" errors during warm shutdown
  • Connections remain open until task completion
  • Clean disconnection after task completes

Expected Behavior:

  • ✅ No task duplication during warm shutdown
  • ✅ Heartbeats maintained until task completes
  • ✅ Warning logs during shutdown (expected, not errors)
  • ✅ Clean worker exit after task completion

Screenshots

Not applicable (backend infrastructure change)

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

…tkeeper and resolve sigsegv issues (#1616)

* UN-2910 [FIX] Prevent task duplication during Kubernetes warm shutdown with HeartbeatKeeper

Implemented HeartbeatKeeper bootstep to maintain RabbitMQ connection during
Celery warm shutdown, preventing task requeuing and duplication during
Kubernetes rolling updates and graceful pod terminations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* structuretool-for-testing

* handling envs

* sonar issue addressing

* forksafe gcs and gdrive connectors

* updated grpc imports

* fork safty in shared internal client session

* reverted forksafty fropm baseclient, addressed coderabbitai comments

* minor changes

* reverted structure tool changes that was changed for tetsing

* plugin registry changes for .so file comaptibility

* added default value for heartbeat

---------

Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added Celery pool type configuration option for worker customization
    • Added support for Nuitka-compiled worker modules in plugin discovery
    • Introduced RabbitMQ heartbeat management system for improved worker reliability
  • Improvements

    • Enhanced fork-safety for Google Cloud connectors (Storage, BigQuery, Drive)
    • Changed API client singleton to disabled by default for better prefork reliability
    • Added gRPC state reinitialization after process fork

Walkthrough

This pull request introduces fork-safety improvements for Google Cloud integrations and Celery worker infrastructure. Changes include lazy initialization patterns for Google API clients to defer gRPC state creation after process fork, thread-safe locking mechanisms to prevent deadlocks in prefork scenarios, a new heartbeat keeper bootstep for warm shutdown, Celery pool type configuration support, and disabling API client singleton by default in prefork environments.

Changes

Cohort / File(s) Summary
Google Cloud lazy initialization patterns
unstract/connectors/src/unstract/connectors/databases/bigquery/bigquery.py, unstract/connectors/src/unstract/connectors/filesystems/google_cloud_storage/google_cloud_storage.py, unstract/connectors/src/unstract/connectors/filesystems/google_drive/google_drive.py, unstract/connectors/src/unstract/connectors/gcs_helper.py
Defers Google API client and credential initialization to after process fork to avoid gRPC state issues. Introduces thread-safe locking (module-level locks) and postpones imports/client creation to method scope. BigQuery moves google.api_core.exceptions import inside execute_query. GCS filesystem replaces eager initialization with get_fsspec_fs() lazy pattern. Google Drive adds _get_client_secrets() lazy loader with per-instance lock. GCS helper adds _get_credentials() and _get_storage_client() lazy helpers.
Celery worker pool and heartbeat management
workers/run-worker.sh, workers/worker.py, workers/shared/api/internal_client.py
Adds --pool option to configure Celery pool type via command-line flag. Worker.py introduces HeartbeatKeeper bootstep for RabbitMQ heartbeat management during warm shutdown and on_worker_process_init signal handler to reinitialize gRPC state after fork. Enables gRPC fork support and polling strategy. Internal client now instantiates an additional WorkflowAPIClient.
Worker configuration defaults
workers/shared/infrastructure/config/worker_config.py, workers/sample.env
Changes enable_api_client_singleton default from true to false to prevent SIGSEGV in prefork workers. Each worker now creates its own HTTP session after fork. Comments clarify fork-safety rationale and conditions for reenabling singleton mode.
Worker plugin module discovery
workers/client_plugin_registry.py
Reworkes plugin loading to search for Python source (.py) before compiled Nuitka modules (.so). Adds glob-based discovery and debug logging for compiled module detection.
Docker base image dependencies
docker/dockerfiles/worker-unified.Dockerfile
Adds build-essential to apt-get install list.

Sequence Diagram(s)

sequenceDiagram
    actor Worker as Celery Worker
    participant Init as __init__
    participant Fork as fork()
    participant Method as get_fsspec_fs()
    participant Lock as Lock
    participant Client as GCS Client
    
    Worker->>Init: Create connector instance
    Note over Init: Store credentials only,<br/>no client creation
    Init-->>Worker: Return
    
    Worker->>Fork: Fork process
    Note over Fork: Child process spawned
    
    Worker->>Method: Call get_fsspec_fs()
    Method->>Lock: Acquire lock
    Lock-->>Method: Lock acquired
    
    alt Client already initialized
        Method-->>Lock: Skip creation
    else First call after fork
        Method->>Client: Initialize credentials
        Method->>Client: Create GCS client
        Client-->>Method: Client ready
    end
    
    Method->>Lock: Release lock
    Lock-->>Method: Lock released
    Method-->>Worker: Return filesystem
Loading
sequenceDiagram
    actor Start as Worker Startup
    participant Bootstep as HeartbeatKeeper Bootstep
    participant Signal as on_worker_process_init Signal
    participant Task as gRPC State
    participant Thread as Heartbeat Thread
    
    Start->>Bootstep: Worker initialized
    Bootstep->>Signal: Register process_init signal handler
    Signal-->>Bootstep: Handler registered
    Bootstep->>Thread: Create heartbeat thread
    Note over Thread: Monitors warm shutdown<br/>standby during normal ops
    Thread-->>Bootstep: Thread started
    
    Note over Bootstep: When fork occurs
    Signal->>Signal: on_worker_process_init triggered
    Signal->>Task: Reinitialize gRPC state
    Task->>Task: Clear gRPC channels
    Task-->>Signal: State reinitialized
    Signal->>Thread: Resume with clean state
    Thread-->>Worker: Ready
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25-30 minutes

  • Areas requiring extra attention:
    • google_drive.py: Most complex file with significant refactoring of initialization flow, multiple locking patterns, and new _get_client_secrets() lazy loader; verify thread-safety of double-checked locking and error handling paths for Secret Manager access failures
    • gcs_helper.py: Verify that lazy initialization with per-instance and global locks correctly prevents deadlocks in Celery pool scenarios; check timeout handling on access_secret_version calls
    • worker.py: New HeartbeatKeeper bootstep and on_worker_process_init signal handler require careful review of lifecycle management, signal ordering, and gRPC state reinitialization correctness
    • run-worker.sh: Verify pool_type parameter propagation through all invocation paths (single worker vs. all-workers) and command-line argument placement

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title references the main objectives of the changeset: fixing UN-2910 to prevent task duplication during warm shutdown through HeartbeatKeeper and resolving SIGSEGV issues. The title is directly related to the actual changes in the PR, which include HeartbeatKeeper implementation, fork-safety improvements for Google API clients, and configuration changes for worker infrastructure. While the title is somewhat lengthy and includes "Fix/un 2910 fix" phrasing that could be clearer, it accurately conveys that this is a hotfix addressing multiple interconnected issues related to warm shutdown and process forking.
Description Check ✅ Passed The pull request description comprehensively covers all required sections of the template with substantial detail and clarity. It includes well-structured "What," "Why," and "How" sections explaining the HeartbeatKeeper implementation and SIGSEGV fixes; a thorough explanation of why the PR will not break existing features; complete environment configuration documentation; relevant documentation links; related issue references; dependency information; and detailed manual testing notes. The author also provided a checklist confirmation at the end, demonstrating full compliance with template requirements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v0.138.15-hotfix

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workers/shared/api/internal_client.py (1)

149-172: Pass the shared WorkerConfig into WorkflowAPIClient

In both _initialize_core_clients_optimized and _initialize_core_clients_traditional the new WorkflowAPIClient() instances are created without self.config. If a caller provides a customized WorkerConfig (different base URL, API key, timeouts, etc.), every other client respects it, but this new client will silently fall back to defaults, breaking those overrides and potentially sending requests to the wrong host. Please wire the shared config through so the workflow client stays consistent.

-        self.workflow_client = WorkflowAPIClient()
+        self.workflow_client = WorkflowAPIClient(self.config)
         self.workflow_client.session.close()
         self.workflow_client.session = InternalAPIClient._shared_session
@@
-        self.workflow_client = WorkflowAPIClient()
+        self.workflow_client = WorkflowAPIClient(self.config)

Also applies to: 177-186

🧹 Nitpick comments (1)
workers/worker.py (1)

138-226: Use typing.Any in type hints

The new helper methods return/accept any, which references the builtin function rather than a type, so these annotations won’t pass type checking. Please import Any from typing and swap the hints to keep static analysis green.

-import logging
-import os
-import sys
-import threading
-import time
+import logging
+import os
+import sys
+import threading
+import time
+from typing import Any
@@
-    def _is_connection_ready(self) -> tuple[bool, any]:
+    def _is_connection_ready(self) -> tuple[bool, Any]:
@@
-        self, consumer: any, consecutive_errors: int, last_heartbeat: float
+        self, consumer: Any, consecutive_errors: int, last_heartbeat: float
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between b5a389e and 88c4bda.

📒 Files selected for processing (11)
  • docker/dockerfiles/worker-unified.Dockerfile (1 hunks)
  • unstract/connectors/src/unstract/connectors/databases/bigquery/bigquery.py (2 hunks)
  • unstract/connectors/src/unstract/connectors/filesystems/google_cloud_storage/google_cloud_storage.py (4 hunks)
  • unstract/connectors/src/unstract/connectors/filesystems/google_drive/google_drive.py (3 hunks)
  • unstract/connectors/src/unstract/connectors/gcs_helper.py (4 hunks)
  • workers/client_plugin_registry.py (2 hunks)
  • workers/run-worker.sh (9 hunks)
  • workers/sample.env (1 hunks)
  • workers/shared/api/internal_client.py (1 hunks)
  • workers/shared/infrastructure/config/worker_config.py (1 hunks)
  • workers/worker.py (3 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
workers/worker.py

20-20: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


26-26: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


27-27: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


28-28: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


29-29: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)


71-71: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


73-73: Unused method argument: kwargs

(ARG002)


80-80: Unused method argument: worker

(ARG002)


121-121: Unused method argument: worker

(ARG002)


130-130: Unused method argument: worker

(ARG002)


174-174: Consider moving this statement to an else block

(TRY300)


176-176: Do not catch blind exception: Exception

(BLE001)


222-222: Do not catch blind exception: Exception

(BLE001)


342-342: Unused function argument: kwargs

(ARG001)

unstract/connectors/src/unstract/connectors/filesystems/google_cloud_storage/google_cloud_storage.py

137-137: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


137-137: Use explicit conversion flag

Replace with conversion flag

(RUF010)


141-141: Use explicit conversion flag

Replace with conversion flag

(RUF010)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
docker/dockerfiles/worker-unified.Dockerfile (1)

24-30: Adding build-essential improves reproducibility
Shipping the whole toolchain up front prevents missing headers during native builds. Looks good to me.

unstract/connectors/src/unstract/connectors/databases/bigquery/bigquery.py (1)

9-235: Deferred import pattern looks solid
Thanks for moving the exceptions import into execute_query; this keeps the handler logic untouched while avoiding pre-fork gRPC init.

unstract/connectors/src/unstract/connectors/filesystems/google_cloud_storage/google_cloud_storage.py (1)

16-145: Fork-safe lazy initialization implemented well
Double-checked locking plus JSON validation keeps the GCS filesystem creation both safe and deterministic. Nice work.

unstract/connectors/src/unstract/connectors/filesystems/google_drive/google_drive.py (1)

14-210: Lazy secret loading + Drive init look correct
Appreciate the thorough locking and error mapping to keep Google Drive fork-safe without regressing UX.

workers/shared/infrastructure/config/worker_config.py (1)

245-252: Defaulting the singleton off is a good call

Thanks for making the fork-safety stance explicit in both the comment and default value. This keeps prefork workers stable by default while still allowing explicit opt-in when needed. Nice alignment with the broader hotfix goals.

workers/client_plugin_registry.py (1)

137-193: Nice enhancement for compiled plugins

Appreciate the dual lookup for .py and Nuitka-produced .so artifacts with graceful fallback. This widens deployment options without disturbing the existing import flow.

workers/sample.env (1)

92-101: Clear guidance in sample env

The expanded comment spells out exactly why the singleton is off by default and when it’s safe to re-enable it. That will save operators a lot of guesswork—thanks for keeping the docs in lockstep with the config.

workers/run-worker.sh (4)

330-333: Pool type option added to Celery command correctly.

The conditional check for pool_type properly gates the --pool=$pool_type argument addition to the Celery command. The argument is inserted at the right place in the command structure—after logging and queue configuration, before concurrency settings—maintaining logical grouping of options.


89-89: Help text and argument parsing comprehensive and well‑documented.

The usage documentation clearly describes the new -P/--pool option with supported pool types and practical examples. The argument parsing correctly handles the option with shift 2, and the examples (lines 115-117) demonstrate realistic usage patterns for operators.

Also applies to: 115-117, 460-463


429-429: Pool type initialization aligns with other configuration variables.

The POOL_TYPE="" initialization follows the same pattern as other optional configuration variables (CUSTOM_QUEUES, HEALTH_PORT, CUSTOM_HOSTNAME), maintaining consistency in the script's variable setup.


269-270: Pool type parameter implementation is complete and consistent.

Verification confirms both internal invocations of run_worker() at lines 403 and 530 correctly pass all 8 parameters in the proper order. No external callers exist outside run-worker.sh (run-worker-docker.sh is an independent script with its own run_worker function). The signature change from 7 to 8 parameters is self-contained and poses no backward compatibility risks. The implementation is correct and complete.

Comment on lines +48 to +109
# Lazy initialization - credentials created only when needed (after fork)
self._google_credentials: Any | None = None
self._credentials_lock = threading.Lock()

def _get_credentials(self) -> Any:
"""Lazily create credentials object (fork-safe).

This method is called after fork, ensuring gRPC state is created
in the child process, not inherited from parent.
"""
if self._google_credentials is None:
with self._credentials_lock:
if self._google_credentials is None:
# Import inside method to avoid module-level initialization
from google.oauth2 import service_account

logger.debug("Creating Google credentials (lazy init after fork)")
self._google_credentials = (
service_account.Credentials.from_service_account_info(
json.loads(self.google_service_json)
)
)
return self._google_credentials

def get_google_credentials(self) -> Any:
"""Get Google credentials, creating them lazily if needed."""
return self._get_credentials()

def get_secret(self, secret_name: str) -> str:
from google.cloud import secretmanager
"""Get secret from Google Secret Manager (lazy init after fork).

Thread-safe: Uses global lock to prevent gRPC deadlock when multiple
threads create Secret Manager clients simultaneously.
"""
# Use global lock to serialize Secret Manager client creation ONLY
# This prevents deadlock when multiple threads hit this simultaneously
with _GOOGLE_API_INIT_LOCK:
from google.cloud import secretmanager

credentials = self._get_credentials()
google_secrets_client = secretmanager.SecretManagerServiceClient(
credentials=credentials
)

google_secrets_client = secretmanager.SecretManagerServiceClient(
credentials=self.google_credentials
)
# Network I/O happens OUTSIDE the lock for better concurrency
s = google_secrets_client.access_secret_version(
request={
"name": f"projects/{self.google_project_id}/secrets/{secret_name}/versions/latest" # noqa: E501
}
"name": f"projects/{self.google_project_id}/secrets/{secret_name}/versions/latest"
},
timeout=30.0, # Add reasonable timeout to prevent indefinite hangs
)
return s.payload.data.decode("UTF-8")

def _get_storage_client(self) -> Any:
"""Lazily create GCS client (fork-safe and thread-safe)."""
# Use global lock to serialize GCS client creation
with _GOOGLE_API_INIT_LOCK:
from google.cloud.storage import Client

credentials = self._get_credentials()
return Client(credentials=credentials)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cache the storage client to avoid gRPC leaks
As written, every call to _get_storage_client() instantiates a brand-new storage.Client. Those gRPC clients spin up background threads and open channels that are never closed, so repeated uploads/downloads will steadily leak threads/file descriptors. We still need lazy, post-fork creation, but once created the client should be cached on the helper. Please store it on the instance and reuse it, e.g.:

@@
-        self._google_credentials: Any | None = None
-        self._credentials_lock = threading.Lock()
+        self._google_credentials: Any | None = None
+        self._credentials_lock = threading.Lock()
+        self._storage_client: Any | None = None
@@
-    def _get_storage_client(self) -> Any:
-        """Lazily create GCS client (fork-safe and thread-safe)."""
-        # Use global lock to serialize GCS client creation
-        with _GOOGLE_API_INIT_LOCK:
-            from google.cloud.storage import Client
-
-            credentials = self._get_credentials()
-            return Client(credentials=credentials)
+    def _get_storage_client(self) -> Any:
+        """Lazily create and cache the GCS client (fork-safe and thread-safe)."""
+        if self._storage_client is None:
+            with _GOOGLE_API_INIT_LOCK:
+                if self._storage_client is None:
+                    from google.cloud.storage import Client
+
+                    credentials = self._get_credentials()
+                    self._storage_client = Client(credentials=credentials)
+        return self._storage_client

This keeps the initialization fork-safe while preventing the runaway resource growth.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Lazy initialization - credentials created only when needed (after fork)
self._google_credentials: Any | None = None
self._credentials_lock = threading.Lock()
def _get_credentials(self) -> Any:
"""Lazily create credentials object (fork-safe).
This method is called after fork, ensuring gRPC state is created
in the child process, not inherited from parent.
"""
if self._google_credentials is None:
with self._credentials_lock:
if self._google_credentials is None:
# Import inside method to avoid module-level initialization
from google.oauth2 import service_account
logger.debug("Creating Google credentials (lazy init after fork)")
self._google_credentials = (
service_account.Credentials.from_service_account_info(
json.loads(self.google_service_json)
)
)
return self._google_credentials
def get_google_credentials(self) -> Any:
"""Get Google credentials, creating them lazily if needed."""
return self._get_credentials()
def get_secret(self, secret_name: str) -> str:
from google.cloud import secretmanager
"""Get secret from Google Secret Manager (lazy init after fork).
Thread-safe: Uses global lock to prevent gRPC deadlock when multiple
threads create Secret Manager clients simultaneously.
"""
# Use global lock to serialize Secret Manager client creation ONLY
# This prevents deadlock when multiple threads hit this simultaneously
with _GOOGLE_API_INIT_LOCK:
from google.cloud import secretmanager
credentials = self._get_credentials()
google_secrets_client = secretmanager.SecretManagerServiceClient(
credentials=credentials
)
google_secrets_client = secretmanager.SecretManagerServiceClient(
credentials=self.google_credentials
)
# Network I/O happens OUTSIDE the lock for better concurrency
s = google_secrets_client.access_secret_version(
request={
"name": f"projects/{self.google_project_id}/secrets/{secret_name}/versions/latest" # noqa: E501
}
"name": f"projects/{self.google_project_id}/secrets/{secret_name}/versions/latest"
},
timeout=30.0, # Add reasonable timeout to prevent indefinite hangs
)
return s.payload.data.decode("UTF-8")
def _get_storage_client(self) -> Any:
"""Lazily create GCS client (fork-safe and thread-safe)."""
# Use global lock to serialize GCS client creation
with _GOOGLE_API_INIT_LOCK:
from google.cloud.storage import Client
credentials = self._get_credentials()
return Client(credentials=credentials)
# Lazy initialization - credentials created only when needed (after fork)
self._google_credentials: Any | None = None
self._credentials_lock = threading.Lock()
self._storage_client: Any | None = None
def _get_credentials(self) -> Any:
"""Lazily create credentials object (fork-safe).
This method is called after fork, ensuring gRPC state is created
in the child process, not inherited from parent.
"""
if self._google_credentials is None:
with self._credentials_lock:
if self._google_credentials is None:
# Import inside method to avoid module-level initialization
from google.oauth2 import service_account
logger.debug("Creating Google credentials (lazy init after fork)")
self._google_credentials = (
service_account.Credentials.from_service_account_info(
json.loads(self.google_service_json)
)
)
return self._google_credentials
def get_google_credentials(self) -> Any:
"""Get Google credentials, creating them lazily if needed."""
return self._get_credentials()
def get_secret(self, secret_name: str) -> str:
"""Get secret from Google Secret Manager (lazy init after fork).
Thread-safe: Uses global lock to prevent gRPC deadlock when multiple
threads create Secret Manager clients simultaneously.
"""
# Use global lock to serialize Secret Manager client creation ONLY
# This prevents deadlock when multiple threads hit this simultaneously
with _GOOGLE_API_INIT_LOCK:
from google.cloud import secretmanager
credentials = self._get_credentials()
google_secrets_client = secretmanager.SecretManagerServiceClient(
credentials=credentials
)
# Network I/O happens OUTSIDE the lock for better concurrency
s = google_secrets_client.access_secret_version(
request={
"name": f"projects/{self.google_project_id}/secrets/{secret_name}/versions/latest"
},
timeout=30.0, # Add reasonable timeout to prevent indefinite hangs
)
return s.payload.data.decode("UTF-8")
def _get_storage_client(self) -> Any:
"""Lazily create and cache the GCS client (fork-safe and thread-safe)."""
if self._storage_client is None:
with _GOOGLE_API_INIT_LOCK:
if self._storage_client is None:
from google.cloud.storage import Client
credentials = self._get_credentials()
self._storage_client = Client(credentials=credentials)
return self._storage_client
🤖 Prompt for AI Agents
In unstract/connectors/src/unstract/connectors/gcs_helper.py around lines 48 to
109, _get_storage_client() currently creates a new google.cloud.storage.Client
on every call which leaks gRPC threads; change it to lazily create and cache a
single client on the instance (e.g. self._google_storage_client = None at init),
use the existing _GOOGLE_API_INIT_LOCK to guard creation, return the cached
client on subsequent calls, and ensure you import and construct the Client only
once after the lock so the client is fork-safe and reused across calls.

@ritwik-g ritwik-g changed the title Fix/un 2910 fix prevent task duplication while warm shutdown heartbeatkeeper and resolve sigsegv issues - hotfix REVERSE MERGE - v0.138.15-hotfix Oct 31, 2025
@sonarqubecloud

Copy link
Copy Markdown

@ritwik-g ritwik-g merged commit f3e6b4c into main Oct 31, 2025
7 checks passed
@ritwik-g ritwik-g deleted the v0.138.15-hotfix branch October 31, 2025 14:00
@github-actions

Copy link
Copy Markdown
Contributor

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 66 passed, 0 failed (66 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\_max\_time\_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{TestRetryWithExponentialBackoff.test\_delay\_would\_exceed\_max\_time}}$$ $$\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\_max\_time}}$$ $$\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{66}}$$ $$\textcolor{#23d18b}{\tt{66}}$$

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.

3 participants