Skip to content

Use broader exception to ensure things are properly deleted#48

Open
brucetony wants to merge 2 commits into
mainfrom
deploy-delete-fix
Open

Use broader exception to ensure things are properly deleted#48
brucetony wants to merge 2 commits into
mainfrom
deploy-delete-fix

Conversation

@brucetony

@brucetony brucetony commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

If there is an unhandled error (i.e. an error other than ApiException) during delete_deployment() then the rest of the resources fail to get deleted

Summary by CodeRabbit

  • Bug Fixes

    • More robust cleanup of Kubernetes resources with clearer warnings/errors and reduced failure noise.
    • Pod IP wait operations now time out predictably to avoid indefinite waits.
    • Improved handling of stuck startups so long-running "starting" states progress reliably.
  • New Features

    • Deployment lifecycle updates now persist status transitions and created pod identifiers for more accurate reporting.

@brucetony brucetony requested a review from antidodo June 12, 2026 05:16
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds bounded pod-IP waits and a shared _IP_WAIT_TIMEOUT, centralizes Kubernetes Deployment deletion and standardizes resource delete error handling, makes Analysis.start persist STARTING and mark FAILED on deployment errors, and adds a STARTING timeout with reconciler logic to detect stuck analyses.

Changes

Orchestration lifecycle updates

Layer / File(s) Summary
Kubernetes deletion helpers & IP wait time
src/k8s/kubernetes.py
Adds _IP_WAIT_TIMEOUT, centralizes deployment deletion in _delete_k8s_deployment(), updates _delete_service(), _delete_network_policy(), _delete_config_map() with consistent try/except handling, and makes pod-IP polling loops raise TimeoutError after the timeout.
Analysis.start persistence and failure handling
src/resources/analysis/entity.py
Persisted deployment status is now STARTING before creating the k8s deployment; creation is wrapped in try/except to set DB status to FAILED on error; on success pod IDs are stored and status set to STARTED.
STARTING timeout and reconciliation
src/status/constants.py, src/status/status.py
Introduces _STARTING_TIMEOUT = 360 and updates _get_analysis_status to keep STARTING within the timeout, otherwise force a safety transition (DB STARTED + internal FAILED) to trigger unstuck handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I waited for pods, then timed the chase,
Helpers tidy up every delete and race,
STARTING now watches the clock with care,
Deployments settle, logs crisp and fair,
Hops of code — clean, careful, and ace!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is partially related to the changeset but does not capture the main point. While the PR does broaden exception handling for delete operations, it also adds timeout handling in pod-IP polling, updates the Analysis.start() method with explicit status recording and error handling, and implements a safety mechanism for stuck STARTING deployments. Consider a more specific title that captures the primary objective. The current title addresses only the delete operations exception handling aspect; consider something like 'Fix deployment cleanup and add analysis startup timeout handling' to better reflect the scope of changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deploy-delete-fix

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

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)
src/k8s/kubernetes.py (1)

358-366: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pod IP wait loops in _create_nginx_config_map check pod existence instead of IP availability.

Both the message broker wait loop (Lines 358-366) and the pod orchestration wait loop (Lines 380-392) check while pod is None: when they should check while pod_ip is None:. The pod object can exist with status.pod_ip = None before the pod is scheduled or started. This causes both loops to exit before the IP is available, leading to None being used in the nginx configuration template (Lines 517-518), which breaks the configuration. The shared root cause is the incorrect loop condition in both waits; each must loop until the IP is available, not just until the pod object exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/k8s/kubernetes.py` around lines 358 - 366, In _create_nginx_config_map
the wait loops currently test the pod object existence (e.g., while
message_broker_pod is None / while pod is None) but must instead wait for the
pod IP to be populated; change the loops to check the IP variable (e.g., while
message_broker_ip is None and while pod_ip is None), call
core_client.read_namespaced_pod each iteration to refresh status, set
message_broker_ip = message_broker_pod.status.pod_ip (and pod_ip =
pod.status.pod_ip) after the read, and keep the same timeout logic so the loop
only exits when a non-None pod IP is available for use in the nginx template.
🧹 Nitpick comments (2)
src/k8s/kubernetes.py (1)

385-389: ⚡ Quick win

Replace bare except: with specific exception handling.

The bare except: clause catches all exceptions including SystemExit and KeyboardInterrupt, and masks the original exception details. The error message assumes a "not found" scenario, but other failures (network issues, API errors) would also be caught here with a misleading message.

♻️ Proposed fix
         try:
             pod_orchestration_pod = core_client.read_namespaced_pod(name=pod_orchestration_name,
                                                                     namespace=namespace)
-        except:
+        except client.exceptions.ApiException as e:
+            if e.reason == 'Not Found':
-            raise ValueError(f"Could not find pod orchestration pod with name {pod_orchestration_name} in namespace {namespace}. ")
+                raise ValueError(f"Could not find pod orchestration pod with name {pod_orchestration_name} in namespace {namespace}")
+            logger.error(f"API error reading pod orchestration pod: {repr(e)}")
+            raise
+        except Exception as e:
+            logger.error(f"Unexpected error reading pod orchestration pod: {repr(e)}")
+            raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/k8s/kubernetes.py` around lines 385 - 389, Replace the bare except in the
read_namespaced_pod call with specific exception handling: catch the Kubernetes
API exception (e.g., kubernetes.client.exceptions.ApiException) to handle 404
"not found" separately from other errors, include the original exception details
in the raised ValueError or re-raise non-404 exceptions; reference
core_client.read_namespaced_pod, pod_orchestration_pod, pod_orchestration_name
and namespace so you can return a clear "not found" message for status code 404
and propagate or log other ApiException/connection errors with the original
exception info.
src/resources/analysis/entity.py (1)

72-79: ⚡ Quick win

Consider logging the exception before re-raising.

The exception handler correctly marks the deployment as FAILED, but doesn't log the exception details. Adding a log statement would improve observability by capturing the failure at the point of detection.

📝 Proposed enhancement
         try:
             self.pod_ids = create_analysis_deployment(name=self.deployment_name,
                                                       image=self.image_url,
                                                       env=self.analysis_config,
                                                       namespace=namespace)
-        except Exception:
+        except Exception as e:
+            logger.error(f"Failed to create analysis deployment {self.deployment_name}: {repr(e)}")
             database.update_deployment_status(self.deployment_name, AnalysisStatus.FAILED.value)
             raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/resources/analysis/entity.py` around lines 72 - 79, Add an exception log
before re-raising in the create_analysis_deployment block: when
create_analysis_deployment(...) fails, call a logger (preferably
self.logger.exception(...) if the class has one, otherwise
logging.exception(...)) to record the exception and context (include
self.deployment_name, image_url, namespace) immediately before calling
database.update_deployment_status(self.deployment_name,
AnalysisStatus.FAILED.value) and re-raising the exception; this keeps the
existing status update and re-raise behavior but captures the stacktrace and
useful identifiers for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/status/status.py`:
- Around line 213-221: When handling AnalysisStatus.STARTING in
src/status/status.py, explicitly check if analysis.time_created is None before
computing elapsed; if None, log a warning including analysis_id about missing
time_created (data integrity), then treat it as stuck by setting db_status =
AnalysisStatus.STARTED.value and int_status = AnalysisStatus.FAILED.value (same
as the timeout branch); otherwise compute elapsed = time.time() -
analysis.time_created and keep the existing _STARTING_TIMEOUT logic.

---

Outside diff comments:
In `@src/k8s/kubernetes.py`:
- Around line 358-366: In _create_nginx_config_map the wait loops currently test
the pod object existence (e.g., while message_broker_pod is None / while pod is
None) but must instead wait for the pod IP to be populated; change the loops to
check the IP variable (e.g., while message_broker_ip is None and while pod_ip is
None), call core_client.read_namespaced_pod each iteration to refresh status,
set message_broker_ip = message_broker_pod.status.pod_ip (and pod_ip =
pod.status.pod_ip) after the read, and keep the same timeout logic so the loop
only exits when a non-None pod IP is available for use in the nginx template.

---

Nitpick comments:
In `@src/k8s/kubernetes.py`:
- Around line 385-389: Replace the bare except in the read_namespaced_pod call
with specific exception handling: catch the Kubernetes API exception (e.g.,
kubernetes.client.exceptions.ApiException) to handle 404 "not found" separately
from other errors, include the original exception details in the raised
ValueError or re-raise non-404 exceptions; reference
core_client.read_namespaced_pod, pod_orchestration_pod, pod_orchestration_name
and namespace so you can return a clear "not found" message for status code 404
and propagate or log other ApiException/connection errors with the original
exception info.

In `@src/resources/analysis/entity.py`:
- Around line 72-79: Add an exception log before re-raising in the
create_analysis_deployment block: when create_analysis_deployment(...) fails,
call a logger (preferably self.logger.exception(...) if the class has one,
otherwise logging.exception(...)) to record the exception and context (include
self.deployment_name, image_url, namespace) immediately before calling
database.update_deployment_status(self.deployment_name,
AnalysisStatus.FAILED.value) and re-raising the exception; this keeps the
existing status update and re-raise behavior but captures the stacktrace and
useful identifiers for debugging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec21e33e-bbf3-4726-a1c5-870b1adff11a

📥 Commits

Reviewing files that changed from the base of the PR and between 6f50d22 and 7f8c8ee.

📒 Files selected for processing (4)
  • src/k8s/kubernetes.py
  • src/resources/analysis/entity.py
  • src/status/constants.py
  • src/status/status.py
✅ Files skipped from review due to trivial changes (1)
  • src/status/constants.py

Comment thread src/status/status.py
Comment on lines +213 to +221
if db_status == AnalysisStatus.STARTING.value:
elapsed = time.time() - (analysis.time_created or 0)
if elapsed < _STARTING_TIMEOUT: # 360s / 6min
int_status = AnalysisStatus.STARTING.value
else:
# Safety net: creation took too long treat as STARTED+FAILED to trigger unstuck
logger.warning(f"Analysis {analysis_id} stuck in STARTING for {elapsed:.0f}s — triggering unstuck")
db_status = AnalysisStatus.STARTED.value
int_status = AnalysisStatus.FAILED.value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle None value for time_created more defensively.

The expression analysis.time_created or 0 uses 0 as a fallback if time_created is None. This would make elapsed = time.time() - 0 evaluate to the current Unix timestamp (~1.7 billion seconds), immediately triggering the timeout.

While the database creation code sets time_created=time.time(), so this should not happen in normal operation, a more defensive approach would explicitly check for None and either log a warning or treat it as a data integrity error.

🛡️ Proposed fix
         db_status = analysis.status
         if db_status == AnalysisStatus.STARTING.value:
-            elapsed = time.time() - (analysis.time_created or 0)
+            if analysis.time_created is None:
+                logger.error(f"Analysis {analysis_id} missing time_created — treating as timed out")
+                elapsed = _STARTING_TIMEOUT + 1  # Force timeout path
+            else:
+                elapsed = time.time() - analysis.time_created
             if elapsed < _STARTING_TIMEOUT:  # 360s / 6min
                 int_status = AnalysisStatus.STARTING.value
             else:
📝 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
if db_status == AnalysisStatus.STARTING.value:
elapsed = time.time() - (analysis.time_created or 0)
if elapsed < _STARTING_TIMEOUT: # 360s / 6min
int_status = AnalysisStatus.STARTING.value
else:
# Safety net: creation took too long treat as STARTED+FAILED to trigger unstuck
logger.warning(f"Analysis {analysis_id} stuck in STARTING for {elapsed:.0f}s — triggering unstuck")
db_status = AnalysisStatus.STARTED.value
int_status = AnalysisStatus.FAILED.value
if db_status == AnalysisStatus.STARTING.value:
if analysis.time_created is None:
logger.error(f"Analysis {analysis_id} missing time_created — treating as timed out")
elapsed = _STARTING_TIMEOUT + 1 # Force timeout path
else:
elapsed = time.time() - analysis.time_created
if elapsed < _STARTING_TIMEOUT: # 360s / 6min
int_status = AnalysisStatus.STARTING.value
else:
# Safety net: creation took too long treat as STARTED+FAILED to trigger unstuck
logger.warning(f"Analysis {analysis_id} stuck in STARTING for {elapsed:.0f}s — triggering unstuck")
db_status = AnalysisStatus.STARTED.value
int_status = AnalysisStatus.FAILED.value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/status/status.py` around lines 213 - 221, When handling
AnalysisStatus.STARTING in src/status/status.py, explicitly check if
analysis.time_created is None before computing elapsed; if None, log a warning
including analysis_id about missing time_created (data integrity), then treat it
as stuck by setting db_status = AnalysisStatus.STARTED.value and int_status =
AnalysisStatus.FAILED.value (same as the timeout branch); otherwise compute
elapsed = time.time() - analysis.time_created and keep the existing
_STARTING_TIMEOUT logic.

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.

1 participant