Skip to content

Conversation

kirtimanmishrazipstack
Copy link
Contributor

@kirtimanmishrazipstack kirtimanmishrazipstack commented Sep 8, 2025

What

  • Sigterm Implementation in Sidecar Tool container

Why

  • Side car needs graceful shutdown

How

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)

Database Migrations

  • N/A

Env Config

  • N/A

Relevant Docs

Related Issues or PRs

Dependencies Versions

Notes on Testing

  • Yet to be tested on integration environment

Screenshots

Checklist

I have read and understood the Contribution Guidelines.

Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Summary by CodeRabbit

  • New Features
    • Graceful shutdown for log monitoring, ensuring a clean exit when the service is stopped.
    • Final log collection on shutdown to capture any remaining log lines and reduce data loss.
    • Support for termination signals (e.g., Ctrl+C, service stop) for responsive and predictable shutdown behavior.
    • Improved status messages reflecting shutdown progress and completion, aiding observability.

Walkthrough

Adds graceful shutdown to log monitoring: registers SIGINT/SIGTERM handlers to set a shutdown flag, updates the monitoring loop to observe this flag, performs a final log collection via a new helper, logs completion, and exits cleanly.

Changes

Cohort / File(s) Summary
Graceful shutdown and final log collection
tool-sidecar/src/unstract/tool_sidecar/log_processor.py
- Add global shutdown flag and signal handler for SIGINT/SIGTERM
- Register handlers in main()
- monitor_logs checks flag; on shutdown, calls _final_log_collection(...) then exits
- New _final_log_collection(file_handle) reads/processes remaining lines
- Logging updated to reflect shutdown flow

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant OS as OS
    participant Main as main()
    participant Sig as signal_handler
    participant Mon as monitor_logs()
    participant File as log file

    Note over Main: Startup
    Main->>Main: register SIGINT/SIGTERM handlers
    Main->>Mon: start monitoring loop

    loop monitor loop
      Mon->>File: read new lines
      File-->>Mon: lines (if any)
      Mon->>Mon: process lines
      alt shutdown not requested
        Mon->>Mon: sleep/continue
      end
    end

    Note over OS,Sig: Shutdown initiated
    OS-->>Sig: SIGINT/SIGTERM
    Sig->>Mon: set _shutdown_requested = True

    opt On next iteration
      Mon->>Mon: detect shutdown flag
      Mon->>File: _final_log_collection(file_handle)
      File-->>Mon: remaining lines
      Mon->>Mon: process remaining lines
      Mon-->>Main: exit monitoring loop
    end

    Main->>Main: log completion and exit
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The description includes the required section headings from the repository template but leaves critical sections blank—specifically the “How” implementation details, the assessment of potential breaking changes, and fields like Related Issues and Relevant Docs—hindering reviewers’ ability to understand the change and its impact. Please fill in the “How” section with a concise summary of the implementation, provide details on any possible breaking changes or confirm their absence, and complete other template fields such as Related Issues, Relevant Docs, and Dependencies Versions before requesting a merge.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly indicates the primary change of implementing SIGTERM handling in the Sidecar Tool container and includes the relevant ticket number and feature tag, making it specific and easily understood by reviewers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch UN-2717-Sigterm-handling-in-tool-sidecar

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.

Copy link

@kirtimanmishrazipstack kirtimanmishrazipstack marked this pull request as ready for review September 29, 2025 07:46
Copy link
Contributor

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}}$$

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

Caution

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

⚠️ Outside diff range comments (1)
tool-sidecar/src/unstract/tool_sidecar/log_processor.py (1)

98-113: Honor SIGTERM while waiting for the log file (otherwise can hang up to 300s).

If SIGTERM arrives before the file exists, the loop ignores it and can block until timeout, likely causing a forced SIGKILL in Kubernetes. Check the shutdown flag inside the wait loop.

Apply this diff:

 def wait_for_log_file(self, timeout: int = 300) -> bool:
     """Wait for the log file to be created by the tool container.
@@
-        start_time = time.time()
-        while not os.path.exists(self.log_path):
+        start_time = time.time()
+        while not os.path.exists(self.log_path):
+            if _shutdown_requested:
+                logger.info("Shutdown requested while waiting for log file; aborting wait.")
+                return False
             if time.time() - start_time > timeout:
                 return False
             time.sleep(0.1)
         return True
🧹 Nitpick comments (4)
tool-sidecar/src/unstract/tool_sidecar/log_processor.py (4)

227-227: Remove unnecessary global declaration.

monitor_logs only reads _shutdown_requested; global is not required.

Apply this diff:

-        global _shutdown_requested

266-290: Keep parsing behavior consistent: avoid .strip() here.

Normal path passes raw lines (with trailing newline) to process_log_line, which json.loads already handles. Stripping only in the final drain can introduce subtle differences (e.g., leading/trailing spaces are significant in some non‑JSON lines you log verbatim).

Apply this diff:

-            log_line = self.process_log_line(line.strip())
+            log_line = self.process_log_line(line)

230-230: Optional: shorten or customize the timeout exception message (Ruff TRY003).

Either shorten the message or introduce a small custom exception class to carry context.

Apply this minimal tweak:

-            raise TimeoutError("Log file was not created within timeout period")
+            raise TimeoutError("Log file not created within timeout")

296-300: Signal registration LGTM; confirm pod termination settings.

Handlers for SIGTERM/SIGINT are correctly registered in main().

  • Ensure your pod’s terminationGracePeriodSeconds comfortably exceeds the final-drain window (recommend ≥5–10s).
  • If you use a preStop hook, confirm it doesn’t block the sidecar from receiving SIGTERM promptly.
  • Verify in your manifests/Helm charts.

Would you like a follow-up PR checklist to validate this in the integration cluster?

📜 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 a9b090a and f14e7c7.

📒 Files selected for processing (1)
  • tool-sidecar/src/unstract/tool_sidecar/log_processor.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.13.1)
tool-sidecar/src/unstract/tool_sidecar/log_processor.py

33-33: Unused function argument: frame

(ARG001)


230-230: Avoid specifying long messages outside the exception class

(TRY003)

⏰ 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 (1)
tool-sidecar/src/unstract/tool_sidecar/log_processor.py (1)

235-240: Shutdown check and final drain look good.

Early exit with a brief final drain achieves graceful behavior without heavy work inside the signal handler.

Comment on lines +33 to +39
def signal_handler(signum, frame):
"""Handle shutdown signals gracefully."""
global _shutdown_requested
signal_name = signal.Signals(signum).name
logger.info(f"Received {signal_name}, initiating graceful shutdown...")
_shutdown_requested = True

Copy link
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix unused frame arg in signal handler (lint) and keep handler minimal.

Rename the unused parameter to underscore to satisfy Ruff ARG001 and keep the handler lean.

Apply this diff:

-def signal_handler(signum, frame):
+def signal_handler(signum, _frame):
     """Handle shutdown signals gracefully."""
     global _shutdown_requested
     signal_name = signal.Signals(signum).name
     logger.info(f"Received {signal_name}, initiating graceful shutdown...")
     _shutdown_requested = True

Optional: consider replacing the module‑level bool with a threading.Event for clearer intent and to avoid globals.

📝 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
def signal_handler(signum, frame):
"""Handle shutdown signals gracefully."""
global _shutdown_requested
signal_name = signal.Signals(signum).name
logger.info(f"Received {signal_name}, initiating graceful shutdown...")
_shutdown_requested = True
def signal_handler(signum, _frame):
"""Handle shutdown signals gracefully."""
global _shutdown_requested
signal_name = signal.Signals(signum).name
logger.info(f"Received {signal_name}, initiating graceful shutdown...")
_shutdown_requested = True
🧰 Tools
🪛 Ruff (0.13.1)

33-33: Unused function argument: frame

(ARG001)

🤖 Prompt for AI Agents
In tool-sidecar/src/unstract/tool_sidecar/log_processor.py around lines 33 to
39, the signal_handler currently accepts an unused parameter named frame which
triggers lint ARG001; rename the unused parameter to _frame (or just _ ) to
satisfy Ruff and keep the handler minimal by leaving only the logging and
setting of the shutdown flag (remove any extra comments or logic inside the
handler). Optionally, consider replacing the module-level boolean
_shutdown_requested with a threading.Event to avoid globals and make shutdown
signaling clearer.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kirtimanmishrazipstack can you address this comment

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 235 to 290
# Check for shutdown signal
if _shutdown_requested:
logger.info("Shutdown requested, performing final log collection...")
self._final_log_collection(f)
break

# Remember current position
where = f.tell()
line = f.readline()

if not line:
# No new data, check if tool container is done
if os.path.exists(
os.path.join(os.path.dirname(self.log_path), "completed")
):
break

# Sleep briefly to avoid CPU spinning
time.sleep(0.1)
# Go back to where we were
f.seek(where)
continue

# Process the log line
log_line = self.process_log_line(line)
if log_line.is_terminated:
logger.info("Completion signal received")
break

logger.info("Log monitoring completed")

def _final_log_collection(self, file_handle) -> None:
"""Perform final collection of any remaining logs before shutdown."""
logger.info("Performing final log collection...")

# Give main container brief moment to write final logs
time.sleep(0.2)

lines_collected = 0

# Read any remaining lines in the file
while True:
line = file_handle.readline()
if not line:
break

lines_collected += 1
log_line = self.process_log_line(line.strip())

if log_line.is_terminated:
logger.info("Found completion signal during final collection")

logger.info(
f"Final log collection completed, processed {lines_collected} remaining lines"
)

Copy link
Contributor

Choose a reason for hiding this comment

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

@kirtimanmishrazipstack I don't think this handling is required. Similar to what we did in the sdk, we just need to trap the signal. No need to do anything else for now.

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