Skip to content

fix(operator): Enable request timeout after "300s"#881

Merged
8R0WNI3 merged 2 commits into
open-component-model:masterfrom
8R0WNI3:watch-timeout
Jul 7, 2026
Merged

fix(operator): Enable request timeout after "300s"#881
8R0WNI3 merged 2 commits into
open-component-model:masterfrom
8R0WNI3:watch-timeout

Conversation

@8R0WNI3

@8R0WNI3 8R0WNI3 commented Jul 7, 2026

Copy link
Copy Markdown
Member

What this PR does / why we need it:
The timeout is necessary to prevent keeping an unnoticed stale connection which blocks further events from being processed. If the idle timeout is reached, the watch stream breaks (without error) and will be re-instantiated by the outer while True loop. Already processed events will not be processed again because the last processed resource_version is kept.

Which issue(s) this PR fixes:
Fixes open-component-model/open-delivery-gear#147

Special notes for your reviewer:

Release note:

The backlog-controller and odg-operator now specify a request timeout > 0s to prevent stale connections from blocking incoming events
The ODG operator now properly handles common Kubernetes API watch related exceptions

@8R0WNI3 8R0WNI3 self-assigned this Jul 7, 2026
@8R0WNI3 8R0WNI3 requested a review from a team as a code owner July 7, 2026 10:17
@8R0WNI3 8R0WNI3 added kind/bugfix Bug area/ipcei Important Project of Common European Interest labels Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@8R0WNI3, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94de3f38-1239-4059-8d9f-d293f767a8e4

📥 Commits

Reviewing files that changed from the base of the PR and between e28017c and ab7e5e9.

📒 Files selected for processing (2)
  • src/backlog_controller.py
  • src/odg_operator/__main__.py
📝 Walkthrough

Walkthrough

Two Kubernetes watch streams now use a 300-second timeout, and the operator’s top-level loop now owns watch restart handling by tracking resource_version and retrying after watch expiry or retryable protocol errors.

Changes

Watch Timeout and Restart Handling

Layer / File(s) Summary
Backlog controller watch timeout
src/backlog_controller.py
main() sets the Kubernetes watch stream timeout_seconds to 300 instead of 0.
ODG operator watch and restart flow
src/odg_operator/__main__.py
reconcile() uses timeout_seconds=300, removes watch error handling from that scope, and the outer loop now manages resource_version resets and watch retries on HTTPStatus.GONE, ProtocolError, and MaxRetryError.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MainLoop
  participant Reconcile
  participant KubernetesWatch

  MainLoop->>Reconcile: call reconcile(resource_version)
  Reconcile->>KubernetesWatch: stream(..., timeout_seconds=300)
  KubernetesWatch-->>Reconcile: watch events or timeout
  Reconcile-->>MainLoop: return / raise watch-related error
  MainLoop->>MainLoop: reset resource_version on GONE or ProtocolError
  MainLoop->>Reconcile: retry with updated resource_version
Loading

Poem

Hoppity watch, five minutes pass,
then back we bounce through event grass.
A tiny timeout, a careful retry,
and sleepy pods wake up and fly. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The timeout and watch-restart changes address the stalled backlog-controller behavior described in issue #147.
Out of Scope Changes check ✅ Passed All code changes stay focused on watch timeout and restart handling for backlog-controller and odg-operator.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title matches the main change: enabling a nonzero watch request timeout for the operator.
Description check ✅ Passed The description follows the template and covers purpose, linked issue, reviewer notes, and release note.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/odg_operator/__main__.py (1)

571-596: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate resource_version across watch restarts. reconcile() updates it locally, but the outer while True calls reconcile() again with the default '', so each 300s timeout restarts the watch from the initial state instead of resuming from the last seen event. Return the latest RV or thread it through the caller.

🤖 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/odg_operator/__main__.py` around lines 571 - 596, The watch restart in
reconcile() is losing the latest resource_version because the outer caller
starts each new reconcile() invocation with the default empty value, so the
stream resumes from the beginning instead of the last seen event. Update the
control flow around reconcile() and the watch loop to carry the latest
resource_version forward across timeout restarts, using the reconcile() function
and its caller to persist and pass the returned RV into the next stream
invocation.
🧹 Nitpick comments (1)
src/odg_operator/__main__.py (1)

595-595: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the shared 300 timeout to a named constant.

Same literal 300 is duplicated in src/backlog_controller.py and here for the same underlying reason (avoid stale idle watch connections). A shared, named constant (e.g. WATCH_TIMEOUT_SECONDS) would document intent and avoid divergence if the value needs tuning later.

🤖 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/odg_operator/__main__.py` at line 595, The watch timeout literal is
duplicated and should be centralized to avoid drift. Extract the shared
300-second value into a named constant such as WATCH_TIMEOUT_SECONDS in the
shared module or a common constants location, then update both __main__. and
backlog_controller to use that constant for the idle watch connection timeout.
Keep the existing timeout behavior unchanged while making the intent explicit
through the shared symbol.
🤖 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.

Outside diff comments:
In `@src/odg_operator/__main__.py`:
- Around line 571-596: The watch restart in reconcile() is losing the latest
resource_version because the outer caller starts each new reconcile() invocation
with the default empty value, so the stream resumes from the beginning instead
of the last seen event. Update the control flow around reconcile() and the watch
loop to carry the latest resource_version forward across timeout restarts, using
the reconcile() function and its caller to persist and pass the returned RV into
the next stream invocation.

---

Nitpick comments:
In `@src/odg_operator/__main__.py`:
- Line 595: The watch timeout literal is duplicated and should be centralized to
avoid drift. Extract the shared 300-second value into a named constant such as
WATCH_TIMEOUT_SECONDS in the shared module or a common constants location, then
update both __main__. and backlog_controller to use that constant for the idle
watch connection timeout. Keep the existing timeout behavior unchanged while
making the intent explicit through the shared symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac2f9759-263b-4cd0-95ea-567d207cf2cd

📥 Commits

Reviewing files that changed from the base of the PR and between 698bfb1 and 6cb374c.

📒 Files selected for processing (2)
  • src/backlog_controller.py
  • src/odg_operator/__main__.py

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/odg_operator/__main__.py (1)

580-581: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

resource_version update inside reconcile() never reaches the caller — watch relists on every idle timeout.

reconcile() mutates its local resource_version parameter (Line 646) but never returns it. Since strings are passed by value, the __main__ loop's resource_version (Lines 977-989) is only ever updated inside the except branches (GONE/ProtocolError/MaxRetryError). On the expected/common path — a clean 300s timeout with no exception — reconcile() simply returns None, and the next iteration re-calls reconcile() with the stale resource_version. This means every idle timeout now causes a full relist/reprocessing of all ODG resources, which is exactly what the PR states it intends to avoid ("the last processed resource_version is preserved so already processed events are not reprocessed").

🐛 Proposed fix: return and capture the updated resource_version
 def reconcile(
     extension_definitions: collections.abc.Iterable[odgm.ExtensionDefinition],
     required_extensions: list[str],
     cluster_identity: str,
     component_descriptor_lookup: ocm.ComponentDescriptorLookup,
     kubernetes_api: k8s.util.KubernetesApi,
     oci_client: oci.client.Client,
     group: str = odgm.ODGMeta.group,
     plural: str = odgm.ODGMeta.plural,
     resource_version: str = '',
-):
+) -> str:
     """
     watches for events of ODG custom-resource
     creates, updates and deletes ODG installations using managed-resources
     """
     ...
             logger.error(e)
+
+    return resource_version
     while True:
         try:
-            reconcile(
+            resource_version = reconcile(
                 extension_definitions=extension_definitions,
                 required_extensions=odg_operator_cfg.required_extension_names,
                 cluster_identity=odg_operator_cfg.cluster_identity,
                 component_descriptor_lookup=component_descriptor_lookup,
                 oci_client=oci_client,
                 kubernetes_api=kubernetes_api,
                 resource_version=resource_version,
             )

Also applies to: 646-646, 806-806, 977-989

🤖 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/odg_operator/__main__.py` around lines 580 - 581, The updated
resource_version is being lost because reconcile() only mutates its local
parameter and does not pass the final value back to the caller. Update
reconcile() to return the latest resource_version on the normal timeout path,
then capture that return value in the __main__ loop where reconcile() is invoked
so the next iteration reuses it instead of relisting. Keep the existing
exception handling for GONE, ProtocolError, and MaxRetryError, but ensure the
common no-error path preserves the last processed resource_version.
🤖 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.

Outside diff comments:
In `@src/odg_operator/__main__.py`:
- Around line 580-581: The updated resource_version is being lost because
reconcile() only mutates its local parameter and does not pass the final value
back to the caller. Update reconcile() to return the latest resource_version on
the normal timeout path, then capture that return value in the __main__ loop
where reconcile() is invoked so the next iteration reuses it instead of
relisting. Keep the existing exception handling for GONE, ProtocolError, and
MaxRetryError, but ensure the common no-error path preserves the last processed
resource_version.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdb714eb-963c-4169-9aa5-d9e34335844c

📥 Commits

Reviewing files that changed from the base of the PR and between 6cb374c and e28017c.

📒 Files selected for processing (2)
  • src/backlog_controller.py
  • src/odg_operator/__main__.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/backlog_controller.py

8R0WNI3 added 2 commits July 7, 2026 13:27
The timeout is necessary to prevent keeping an unnoticed stale connection which
blocks further events from being processed. If the idle timeout is reached, the
watch stream breaks (without error) and will be re-instantiated by the outer
`while True` loop. Already processed events will not be processed again because
the last processed `resource_version` is kept.

Signed-off-by: Jonas Brand (8R0WNI3) <j.brand@sap.com>
The handled exceptions here are all related to the Kubernetes API watch,
however, previously they were handled within the watch loop itself
(which is not working). Therefore, moved the error handling and added
an extra exception to align behaviour with the backlog-controller.

Signed-off-by: Jonas Brand (8R0WNI3) <j.brand@sap.com>

@zkdev zkdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

wow, I am surprised we even have the handling for the backlog controller in place already

/lgtm

@8R0WNI3 8R0WNI3 merged commit 6d57b32 into open-component-model:master Jul 7, 2026
19 checks passed
@8R0WNI3 8R0WNI3 deleted the watch-timeout branch July 7, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ipcei Important Project of Common European Interest kind/bugfix Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix stalled (backlog-controller) pods

2 participants