Skip to content

Conversation

@ei-grad
Copy link
Owner

@ei-grad ei-grad commented Feb 9, 2026

Summary

This PR modernizes the kill-timeout project by migrating to a pyproject.toml-based build system, refactoring the inter-process communication mechanism, and improving documentation and CI/CD setup.

Key Changes

  • Build System Migration: Replaced legacy setup.py and setup.cfg with modern pyproject.toml using setuptools backend
  • IPC Refactoring: Replaced multiprocessing.Manager() with multiprocessing.Pipe() for more efficient inter-process communication
    • Eliminates the need for a separate manager process
    • Simplifies result passing and error handling
    • Reduces resource overhead
  • CI/CD Setup: Added GitHub Actions workflow (.github/workflows/ci.yml) to test against Python 3.10-3.14
  • Project Metadata: Updated pyproject.toml with proper dependencies, classifiers, and project URLs
  • Documentation: Enhanced README with:
    • Better formatting and structure
    • Clearer code examples with f-strings
    • Added "How it works" section explaining the implementation
    • Added note about POSIX-only support
  • Code Quality: Updated Python requirement to 3.10+ (from 3.7+) and modernized string formatting throughout
  • Cleanup: Removed legacy requirements.txt and .gitignore now properly configured

Implementation Details

The refactored kill_timeout decorator now uses multiprocessing.Pipe() instead of Manager().dict():

  • Child process sends results through the pipe before closing
  • Parent process checks if data is available with poll() before attempting to receive
  • Cleaner error handling with f-string formatted timeout messages
  • Proper resource cleanup with explicit close() calls on pipe connections

https://claude.ai/code/session_01Qdue58yru1buXnhnEgMgvW

…e docs

- Replace setup.py/setup.cfg/pbr with pyproject.toml (PEP 621)
- Drop pbr build dependency, use plain setuptools
- Bump python_requires from >=3.7 to >=3.10 (3.7-3.9 are EOL)
- Replace multiprocessing.Manager with Pipe (no extra server process)
- Fix zombie process leak: call process.join() after process.kill()
- Fix id(object()) key reuse risk by using uuid4
- Add .gitignore
- Add GitHub Actions CI matrix (Python 3.10-3.14)
- Update README: fix typo, add POSIX note, add "How it works" section
- Use f-strings instead of % formatting
- Use SPDX license expression per PEP 639

https://claude.ai/code/session_01Qdue58yru1buXnhnEgMgvW
@ei-grad ei-grad requested a review from Copilot February 9, 2026 11:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Modernizes the project packaging and runtime behavior by moving to pyproject.toml, refactoring the IPC mechanism used by the kill_timeout decorator, and adding CI/documentation updates.

Changes:

  • Migrates packaging from setup.py/setup.cfg to pyproject.toml (setuptools backend)
  • Refactors IPC from multiprocessing.Manager().dict() to multiprocessing.Pipe()
  • Adds GitHub Actions CI and updates README formatting/usage docs

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
setup.py Removes legacy setup.py entrypoint in favor of pyproject.toml
setup.cfg Removes legacy setuptools metadata/config
pyproject.toml Adds modern project/build metadata + pytest config
kill_timeout.py Replaces Manager-based result passing with Pipe-based IPC
README.md Updates docs formatting, examples, and adds “How it works”
.github/workflows/ci.yml Adds CI matrix to run pytest across multiple Python versions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

kill_timeout.py Outdated
Comment on lines 27 to 33
parent_conn, child_conn = multiprocessing.Pipe()

def target():
child_conn.send(return_error(func)(*args, **kwargs))
child_conn.close()

process = multiprocessing.Process(target=target)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The target callable is a nested function/closure, which is not picklable under the spawn start method (notably the default on macOS). That contradicts the README’s “POSIX (Linux, macOS, etc.)” claim and will fail at runtime on macOS. Prefer a top-level target function (and pass func/args/kwargs/child_conn explicitly), or explicitly use a fork context where supported and documented.

Copilot uses AI. Check for mistakes.
Comment on lines 35 to +45
process.join(seconds)
if process.is_alive():
# NOTE: killing the process without getting its return code via
# .join() would cause the zombie process to stay until the next
# multiprocessing.Process() run, it is acceptable, since it
# shouldn't end up with many zombie processes, and the
# alternatives like creating the daemon threads to catch the
# zombies don't look like a good fit
process.kill()
if id(key) in results:
result = results.pop(id(key))
process.join()
parent_conn.close()
raise TimeoutError(
f"function {func.__name__} didn't complete in {seconds} seconds"
)
if parent_conn.poll():
result = parent_conn.recv()
parent_conn.close()
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

If the child process exits without sending anything (e.g., pickling failure, early crash before send, or IPC failure), the parent currently raises TimeoutError, which is inaccurate and makes debugging difficult. Consider checking process.exitcode after join() and/or handling EOFError from recv() to raise a more accurate error (e.g., ChildProcessError/RuntimeError) instead of reporting a timeout.

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 52
parent_conn.close()
raise TimeoutError(
f"function {func.__name__} didn't complete in {seconds} seconds"
)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

If the child process exits without sending anything (e.g., pickling failure, early crash before send, or IPC failure), the parent currently raises TimeoutError, which is inaccurate and makes debugging difficult. Consider checking process.exitcode after join() and/or handling EOFError from recv() to raise a more accurate error (e.g., ChildProcessError/RuntimeError) instead of reporting a timeout.

Copilot uses AI. Check for mistakes.
kill_timeout.py Outdated
Comment on lines 27 to 31
parent_conn, child_conn = multiprocessing.Pipe()

def target():
child_conn.send(return_error(func)(*args, **kwargs))
child_conn.close()
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

Pipe.send() can block if the payload is large enough to fill the OS pipe buffer, and the parent waits in join() before reading. This can deadlock or produce a false timeout (child stuck sending, parent waiting to join). A more robust approach is to receive while waiting (poll loop with remaining timeout), or use multiprocessing.Queue (which uses a feeder thread) to avoid blocking the child on large writes.

Copilot uses AI. Check for mistakes.

process = multiprocessing.Process(target=target)
process.start()
process.join(seconds)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

Pipe.send() can block if the payload is large enough to fill the OS pipe buffer, and the parent waits in join() before reading. This can deadlock or produce a false timeout (child stuck sending, parent waiting to join). A more robust approach is to receive while waiting (poll loop with remaining timeout), or use multiprocessing.Queue (which uses a feeder thread) to avoid blocking the child on large writes.

Copilot uses AI. Check for mistakes.
kill_timeout.py Outdated
Comment on lines 29 to 34
def target():
child_conn.send(return_error(func)(*args, **kwargs))
child_conn.close()

process = multiprocessing.Process(target=target)
process.start()
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The pipe endpoints aren’t fully closed in both processes. In the parent, child_conn should be closed promptly after starting the child; in the child, the inherited parent_conn should be closed before/after sending. Properly closing the unused ends avoids descriptor leaks and helps ensure EOF/cleanup semantics behave reliably.

Suggested change
def target():
child_conn.send(return_error(func)(*args, **kwargs))
child_conn.close()
process = multiprocessing.Process(target=target)
process.start()
def target():
parent_conn.close()
child_conn.send(return_error(func)(*args, **kwargs))
child_conn.close()
process = multiprocessing.Process(target=target)
process.start()
child_conn.close()

Copilot uses AI. Check for mistakes.
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install tblib pytest
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

CI is not installing the package itself, so it doesn’t validate that pyproject.toml packaging/install metadata works (nor that dependencies are resolved via the project metadata). Consider installing the project (e.g., pip install -e . or pip install .) before running tests, and only install pytest as a test dependency if it’s not declared elsewhere.

Suggested change
- run: pip install tblib pytest
- run: pip install tblib pytest
- run: pip install .

Copilot uses AI. Check for mistakes.
- Use explicit fork context (get_context("fork")) so the decorator works
  on macOS where the default start method is "spawn"
- Extract target() to a top-level _target() function (picklable)
- Close child_conn in parent after process.start(), close parent_conn
  in child via _target's finally block (fix descriptor leaks)
- Raise ChildProcessError instead of TimeoutError when child exits
  without sending data (crash, signal, pickling failure)
- Check process.exitcode to distinguish crashes from timeouts
- Document large-return-value pipe buffer limitation in docstring
- CI: install package via `pip install .` to validate pyproject.toml

https://claude.ai/code/session_01Qdue58yru1buXnhnEgMgvW
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