Skip to content

fix(odin): Shut down cleanly with running actions and orphaned extractor processes (EDG-1033) - #590

Draft
vikramlc-cognite wants to merge 1 commit into
EDG-1036-prompt-action-statusfrom
EDG-1033-clean-shutdown
Draft

vikramlc-cognite wants to merge 1 commit into
EDG-1036-prompt-action-statusfrom
EDG-1033-clean-shutdown

Conversation

@vikramlc-cognite

Copy link
Copy Markdown
Contributor

Summary

On Ctrl+C or a service stop, running actions now get a final status that is actually sent, and the extractor process shuts itself down if its runtime process is killed.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Refactor (no functional change)
  • Documentation update
  • Chore / tooling / CI

What changed

  • base.py __exit__: stop(), then cancel running action tokens and join their threads (bounded, 10 s), then queue canceled ("Extractor shut down while the action was running") for any action that did not stop, then the final flush with a fresh token and a 10 s bound. Previously the flush reused the already-cancelled token, so nothing was sent.
  • Action threads are tracked so they can be joined.
  • Parent watchdog: the extractor process polls its runtime parent (getppid on POSIX, psutil.pid_exists on Windows) every second and shuts down gracefully when it is gone. Only started when the runtime passes a parent_pid.
  • Tests in tests/test_unstable/test_clean_shutdown.py; CHANGELOG.

Why it changed

  • After SIGINT the process exited in about 3 s while the action stayed running at Odin until the next start (then failed with no message). After kill -9 of the runtime the extractor child kept running and checking in, so a restart gave two processes on one integration.
  • Related issue: EDG-1033 (parent: EDG-1031)
  • Related docs / discussion: actions bug bash findings Add test coverage and improve testing #5 and Add upload queues based on the cognite_uploader package #6. Stacked on EDG-1036. The Odin message for the startup fallback is in cognitedata/odin#325.

What to focus on during review

  • Shutdown ordering and the two 10 s bounds (worst case about 20 s plus state-store sync).
  • The abandoned-action path (an action stuck in a blocking call is reported canceled; a tiny race could report twice).
  • __exit__ now always does a final check-in over HTTP, even when idle. Check any test or tool that counts check-ins on exit.
  • Parent watchdog on Windows (untested).

Test evidence

  • tests/test_unstable/test_clean_shutdown.py: 7 tests (final canceled reaches the client on __exit__, stuck action reported with the shutdown message in under 5 s, bounded flush when the post hangs, watchdog cancels when the parent disappears, no watchdog without parent_pid)
  • uv run pytest tests/test_unstable: same 1 failed + 48 environment errors as master; passed count 279 -> 286
  • ruff, mypy, pre-commit run --all-files pass
  • 5 of 7 tests fail with the production change reverted
  • Manual: a spawned multiprocessing child detected its parent being SIGKILLed in 0.8 s on macOS. Earlier, with the File extractor, SIGINT during a running action left it running (the bug reproduced).

Risks and unknowns

  • Not run end to end: runtime plus extractor plus real SIGKILL; Linux and Windows.
  • prctl(PR_SET_PDEATHSIG) not used (SIGTERM would skip the graceful status report).
  • The 48 live-CDF tests could not run locally; CI should be the real check.

Rollout and rollback

  • No config changes. Revert the commit to roll back.

Checklist

  • Self-reviewed the diff
  • Tests added or updated (or N/A with reason)
  • Docs updated (or N/A): CHANGELOG
  • No secrets, credentials, or PII committed
  • Breaking changes called out above and communicated to affected teams: none

@vikramlc-cognite
vikramlc-cognite requested a review from a team as a code owner October 26, 2026 09:16

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a clean shutdown mechanism and a parent-process watchdog for the unstable extractor package. On shutdown, running actions are cancelled and given up to 10 seconds to finish, with any unfinished actions reported as canceled before a final check-in is sent. Additionally, a watchdog thread is introduced to monitor the parent process and shut down the extractor gracefully if the parent process dies. Feedback on these changes highlights a potential issue on Windows where psutil.pid_exists does not guard against PID reuse, which could prevent the watchdog from detecting that the parent process has terminated. It is recommended to instantiate and cache a psutil.Process object to reliably check if the original parent process is still running.

Comment on lines +147 to +156
def _parent_is_alive(parent_pid: int) -> bool:
"""
Check whether the process with the given PID, which spawned this process, is still running.

On POSIX an orphaned process is re-parented, so a changed parent PID means the original parent is gone. Windows
does not re-parent, so there the parent is looked up by PID (psutil also guards against PID reuse).
"""
if sys.platform != "win32":
return os.getppid() == parent_pid
return psutil.pid_exists(parent_pid) # pragma: no cover - Windows only

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

On Windows, psutil.pid_exists(parent_pid) only checks if any process with the given PID exists. It does not guard against PID reuse, which is a common occurrence on Windows. If the parent process dies and its PID is recycled, the watchdog will incorrectly assume the parent is still alive, leaving the orphan extractor process running indefinitely.

To reliably guard against PID reuse, we should instantiate a psutil.Process object once and use its is_running() method, which matches both the PID and the process creation time. We can cache this process object on the function itself to keep the implementation self-contained and efficient.

def _parent_is_alive(parent_pid: int) -> bool:
    """
    Check whether the process with the given PID, which spawned this process, is still running.

    On POSIX an orphaned process is re-parented, so a changed parent PID means the original parent is gone. Windows
    does not re-parent, so there the parent is looked up by PID (using a cached psutil.Process to guard against PID reuse).
    """
    if sys.platform != "win32":
        return os.getppid() == parent_pid
    
    parent_proc = getattr(_parent_is_alive, "_parent_proc", None)
    if parent_proc is None or parent_proc.pid != parent_pid:
        try:
            parent_proc = psutil.Process(parent_pid)
            _parent_is_alive._parent_proc = parent_proc
        except psutil.NoSuchProcess:
            return False
    return parent_proc.is_running()

@vikramlc-cognite
vikramlc-cognite marked this pull request as draft October 26, 2026 10:22
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.08197% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.93%. Comparing base (7e3937f) to head (e046cd1).

Files with missing lines Patch % Lines
cognite/extractorutils/unstable/core/base.py 94.91% 3 Missing ⚠️
Additional details and impacted files
@@                        Coverage Diff                        @@
##           EDG-1036-prompt-action-status     #590      +/-   ##
=================================================================
+ Coverage                          84.75%   84.93%   +0.18%     
=================================================================
  Files                                 46       46              
  Lines                               4761     4819      +58     
=================================================================
+ Hits                                4035     4093      +58     
  Misses                               726      726              
Files with missing lines Coverage Δ
cognite/extractorutils/unstable/core/runtime.py 76.59% <100.00%> (+0.16%) ⬆️
cognite/extractorutils/unstable/core/base.py 88.44% <94.91%> (+1.75%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…tor processes

- On exit, cancel running actions, wait a bounded time for them, report
  canceled for any still running, and flush with a fresh token and timeout
  so the final statuses are actually sent.
- Add a parent-process watchdog so the extractor process shuts down
  gracefully when the runtime process is killed.

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

This branch has not been deployed

No deployments
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