Skip to content

Replace helper library with argparse CLI and pydantic config models#56

Merged
gmr merged 6 commits into
mainfrom
remove-helper
Apr 1, 2026
Merged

Replace helper library with argparse CLI and pydantic config models#56
gmr merged 6 commits into
mainfrom
remove-helper

Conversation

@gmr
Copy link
Copy Markdown
Owner

@gmr gmr commented Apr 1, 2026

Summary

Removes the helper library dependency and replaces it with native Python tooling: argparse for the CLI and pydantic for configuration validation. Also removes the InfluxDB integration (sprockets_influxdb) and switches __version__ to use importlib.metadata.

Problem

The helper library was a decade-old internal utility that provided daemonization, config loading, and a base controller class. It carried a lot of daemon-era baggage (pidfiles, user/group dropping, foreground flags) that no longer fits how rejected is deployed (containers, systemd, always foreground). It also made the config structure opaque — a dict-like Data object with no schema.

sprockets_influxdb was an optional instrumentation backend that added significant code complexity for a feature not widely used.

Solution

rejected/config.py (new) — Pydantic v2 models for the full config schema:

  • Config — top-level application config (connections, consumers, stats, logging)
  • ConnectionConfig — RabbitMQ connection params (validates pass alias → password)
  • ConsumerConfig — per-consumer settings with typed fields
  • StatsdConfig — statsd client settings
  • StatsConfig — stats aggregator
  • ConnectionRef — typed connection reference in consumer connection lists
  • Settings — dict wrapper for consumer-facing settings (preserves .get() / attribute access API)
  • load(path) — loads YAML or TOML, validates with pydantic, returns a Config

rejected/controller.py (rewritten) — standalone Controller class with:

  • argparse CLI: -c/--config, -o/--only, -q/--qty, -P/--profile, -p/--prepend-path, --version
  • -f/--foreground and all daemonization removed (always runs in foreground)
  • Signal handling (SIGHUP, SIGTERM) without the helper state machine
  • Logging configured via logging.config.dictConfig from the config file

rejected/process.py — removed helper_config.LoggingConfig; subprocess logging now calls dictConfig directly. Connection and consumer config access updated to use pydantic model attributes.

rejected/mcp.py — updated to work with Config pydantic model directly instead of helper.config.Config.

rejected/testing.pyconfig.Data(...)config.Settings(...).

__version__ — replaced hardcoded string with importlib.metadata.version('rejected').

InfluxDB removedsprockets_influxdb import, InfluxDBConfig, setup_influxdb(), submit_influxdb_measurement(), and influxdb_measurement consumer config key all removed. Instrumentation is now statsd-only.

Impact

Breaking changes:

  • -f/--foreground flag removed — rejected always runs in the foreground now
  • Daemon: config section ignored (no longer parsed)
  • influxdb_measurement consumer config key removed
  • helper.config.Data no longer available; consumers using self.settings are unaffected (the Settings wrapper preserves the same interface)
  • sprockets_influxdb no longer installed as a side-effect

Config files require no changes beyond removing the Daemon: section and any influxdb_measurement keys. The Application:, Connections:, Consumers:, and Logging: structure is unchanged.

All 225 tests pass.

Summary by CodeRabbit

  • New Features

    • Typed, validated configuration loader for safer parsing and clearer errors.
  • Removals

    • InfluxDB metrics backend and related measurement/daemonization settings removed.
  • Chores

    • Dependency updates: added pydantic and removed legacy helper; streamlined package requirements.
  • Bug Fixes

    • Safer version reporting with fallback and corrected Sentry DSN formatting.

gmr and others added 3 commits April 1, 2026 13:01
Remove the `helper` dependency entirely. Add `rejected/config.py` with
pydantic v2 models (Config, ConsumerConfig, ConnectionConfig, StatsConfig,
StatsdConfig, InfluxDBConfig, ConnectionRef, Settings) and a `load()`
function that reads YAML/TOML files. Rewrite `controller.py` with a
standalone argparse CLI and signal-handling Controller class. Update
`mcp.py` and `process.py` to consume pydantic Config objects instead of
helper's dict-like Data objects. Update `testing.py` to use the new
Settings class. Update tests to build pydantic Config fixtures directly.
Remove the Daemon section from example.yaml.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Removes the hardcoded version string; the installed package version is
read at import time. Falls back to 'unknown' if the package metadata
is not available (e.g. running directly from source without install).

Also fixes two pre-existing lint issues in config.py introduced by the
remove-helper commit: removes the obsolete Python version guard around
tomllib (always available in 3.11+) and adds `from None` to the
AttributeError raise in Settings.__getattr__.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
sprockets_influxdb was an optional dependency that added complexity
without being widely used. Removing it simplifies the instrumentation
path to statsd-only.

- Remove InfluxDBConfig model from config.py
- Remove influxdb_measurement from ConsumerConfig
- Remove sprockets_influxdb import from process.py
- Remove self.influxdb, setup_influxdb(), submit_influxdb_measurement()
- Simplify setup_instrumentation() to statsd only
- Clean up example.yaml and test fixtures

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

Warning

Rate limit exceeded

@gmr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 20 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 20 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab9caaa6-ccd4-484d-9291-bbe0c1b1eed5

📥 Commits

Reviewing files that changed from the base of the PR and between e4fad2b and 60ca1d2.

📒 Files selected for processing (1)
  • rejected/controller.py
📝 Walkthrough

Walkthrough

Replaces the external helper config with an internal Pydantic rejected.config loader and models; updates CLI/controller to load and validate typed config, shifts runtime modules to attribute-based config access, removes InfluxDB and daemonization config, and updates tests and packaging to match.

Changes

Cohort / File(s) Summary
Configuration & packaging
rejected/config.py, pyproject.toml, example.yaml
Adds Pydantic config models (Config, ConsumerConfig, ConnectionConfig, Stats*, Settings) and load(); removes Application.stats.influxdb and Daemon sections from example YAML; replaces helper dependency with pydantic in pyproject.toml.
Controller / CLI
rejected/controller.py
Reworks CLI to _build_parser(); main() now parses args, loads rejected.config.load(), configures logging, instantiates Controller(args, cfg), and calls run(); Controller no longer subclasses helper.controller.Controller and installs signal handlers directly.
Master control & process logic
rejected/mcp.py, rejected/process.py
Migrates from dict-based to typed attribute access (config.consumers, config.connections, config.stats); passes typed config into child processes; removes InfluxDB instrumentation and related setup/submit logic; updates statsd and logging initialization to use typed models and logging.config.dictConfig.
Package metadata & docs
rejected/__init__.py, rejected/data.py
__version__ now loaded via importlib.metadata.version(...) with PackageNotFoundError fallback 'unknown'; docstring updated to remove InfluxDB mention.
Testing
rejected/testing.py, tests/test_mcp.py, tests/test_process.py
Tests updated to import and instantiate rejected.config.Config/ConsumerConfig, pass Settings wrapper to consumers, and assert using typed config accessors; test fixtures and helpers rebuilt to produce typed configs.

Sequence Diagram

sequenceDiagram
    participant User
    participant Main as rejected.controller.main()
    participant Parser as ArgumentParser
    participant Loader as rejected.config.load()
    participant Validator as Config.model_validate()
    participant Controller as rejected.controller.Controller
    participant MCP as MasterControlProgram
    participant Process as Process

    User->>Main: invoke CLI
    Main->>Parser: parse args
    Parser-->>Main: args
    Main->>Loader: load(args.config)
    Loader->>Loader: read YAML/TOML
    Loader->>Validator: validate -> Config instance
    Validator-->>Loader: validated Config
    Loader-->>Main: Config
    Main->>Controller: Controller(args, Config)
    Controller->>MCP: instantiate/run MCP
    MCP->>MCP: install signal handlers
    MCP->>Process: new_process(typed Config, consumer)
    Process->>Process: access config.connections / config.consumers / config.stats
    Process->>MCP: start child process
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nudged the YAML, tidy and spry,
Pydantic burrows where settings now lie,
Influx hopped off, statsd stays near,
Helper waved gentle, the path now clear,
Typed configs guide each process' cheer.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main architectural change: replacing the helper library with argparse and Pydantic models, which is the primary focus across multiple files (controller.py, config.py, process.py, mcp.py).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch remove-helper

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
Copy Markdown

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
rejected/controller.py (1)

58-69: ⚠️ Potential issue | 🟠 Major

Stop spawned workers in a finally block.

If self._mcp.run() raises unexpectedly, Line 67 re-raises before the cleanup at Line 68 runs. That can leave consumer subprocesses alive after the controller exits.

💡 Suggested fix
         try:
             self._mcp.run()
         except KeyboardInterrupt:
             LOGGER.info('Caught CTRL-C, shutting down')
         except Exception:
             exc_info = sys.exc_info()
             if self._sentry_client:
                 LOGGER.debug('Sending exception to sentry')
                 sentry_sdk.capture_exception(exc_info)
             raise
-        if self._mcp and self._mcp.is_running:
-            self._mcp.stop_processes()
+        finally:
+            if self._mcp and self._mcp.is_running:
+                self._mcp.stop_processes()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rejected/controller.py` around lines 58 - 69, Move the worker-cleanup into a
finally block so spawned consumers are always stopped even if self._mcp.run()
raises; specifically, wrap the run/exception handling (self._mcp.run(), the
KeyboardInterrupt except, and the generic except that calls
sentry_sdk.capture_exception(exc_info)) in a try...finally and in the finally
call self._mcp.stop_processes() only if self._mcp and self._mcp.is_running are
true; ensure you still re-raise the original exception after sending to Sentry
(capture exc_info with sentry_sdk.capture_exception) before the finally
completes.
🧹 Nitpick comments (2)
tests/test_process.py (2)

167-170: Avoid calling get_consumer twice in the same assertion path.

Lines 168-169 invoke the method twice; one call is enough and keeps this test
more deterministic.

Suggested diff
         new_process = self.new_process()
-        new_process.get_consumer(cfg)
-        self.assertIsNone(new_process.get_consumer(cfg))
+        result = new_process.get_consumer(cfg)
+        self.assertIsNone(result)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_process.py` around lines 167 - 170, The test calls
new_process.get_consumer(cfg) twice; instead call it once, store the result in a
local variable (e.g., consumer = new_process.get_consumer(cfg)) and
assertIsNone(consumer) to avoid repeated invocation and make the test
deterministic; update the test around self.new_process() / get_consumer(cfg)
accordingly.

79-81: Use an explicit None check in _make_config.

Line 81 currently treats empty dict input like “no input”. Prefer explicit
raw is None to avoid accidental fallback behavior.

Suggested diff
 def _make_config(raw=None) -> config_module.Config:
     """Build a Config pydantic object from the raw dict."""
-    return config_module.Config.model_validate(raw or _CONFIG_RAW)
+    source = _CONFIG_RAW if raw is None else raw
+    return config_module.Config.model_validate(source)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_process.py` around lines 79 - 81, The helper _make_config treats
any falsy raw (e.g., empty dict) as missing input by using "raw or _CONFIG_RAW";
change this to check explicitly for None so empty dicts are validated as
provided. Update the return in _make_config to call Config.model_validate with
raw when raw is not None, otherwise pass _CONFIG_RAW (i.e., use an explicit "raw
is None" conditional), referencing the _make_config function and
Config.model_validate to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rejected/config.py`:
- Around line 157-158: The code assumes `raw` is a mapping and directly calls
`.get()` which will raise AttributeError for non-mapping YAML results; update
the start of the config parsing to validate that `raw` is a mapping (e.g.,
isinstance(raw, Mapping)) and if not raise a ValueError with a clear message (so
controller.main() handles it), then safely compute app_raw and logging_raw from
`raw` using `.get()`; reference the variables `raw`, `app_raw`, and
`logging_raw` in config.py and perform the type check before any `.get()` calls.

In `@rejected/controller.py`:
- Around line 143-156: Validate args.only and args.qty before constructing the
Controller/MCP: check that if args.only is provided its value maps to a known
consumer set (reject unknown values) and that args.qty is not provided without
args.only; on invalid cases call parser.error(...) with a clear message so the
program exits immediately instead of creating Controller(args, cfg) and idling;
add these checks just after config loading and logging setup and before
instantiating Controller (referencing args, args.only, args.qty, parser.error,
and Controller).

In `@rejected/process.py`:
- Around line 1282-1283: The queue_name() method incorrectly falls back to
self.name (the per-worker name) causing each worker to use a different queue;
change the fallback to use the logical consumer identifier by returning
self.consumer_config.queue or self.consumer_name instead of self.name (update
the queue_name method accordingly so it references self.consumer_name as the
implicit queue).
- Around line 604-615: The code currently passes a plain dict as settings
(settings = dict(cfg.config)) but consumers expect a rejected.config.Settings
instance; import Settings from rejected.config and instantiate it from
cfg.config (e.g. settings = Settings(cfg.config) or Settings(**cfg.config)
depending on constructor), then set settings._import_module =
'.'.join(cfg.consumer.split('.')[0:-1]) and pass that Settings instance in
kwargs (the variable names to update are settings, cfg, and kwargs in this
block). Ensure the change mirrors how rejected/testing.py constructs consumers
so attribute-style access works in production and tests.

In `@tests/test_process.py`:
- Around line 48-50: The test fixture's ssl_options dict contains a typo key
'prototcol' which should be 'protocol'; update the ssl_options fixture in
tests/test_process.py (the dict literal containing 'ssl_options') to use the
correct key name 'protocol' so SSL option mapping tests exercise the real option
and don't mask mapping bugs.

---

Outside diff comments:
In `@rejected/controller.py`:
- Around line 58-69: Move the worker-cleanup into a finally block so spawned
consumers are always stopped even if self._mcp.run() raises; specifically, wrap
the run/exception handling (self._mcp.run(), the KeyboardInterrupt except, and
the generic except that calls sentry_sdk.capture_exception(exc_info)) in a
try...finally and in the finally call self._mcp.stop_processes() only if
self._mcp and self._mcp.is_running are true; ensure you still re-raise the
original exception after sending to Sentry (capture exc_info with
sentry_sdk.capture_exception) before the finally completes.

---

Nitpick comments:
In `@tests/test_process.py`:
- Around line 167-170: The test calls new_process.get_consumer(cfg) twice;
instead call it once, store the result in a local variable (e.g., consumer =
new_process.get_consumer(cfg)) and assertIsNone(consumer) to avoid repeated
invocation and make the test deterministic; update the test around
self.new_process() / get_consumer(cfg) accordingly.
- Around line 79-81: The helper _make_config treats any falsy raw (e.g., empty
dict) as missing input by using "raw or _CONFIG_RAW"; change this to check
explicitly for None so empty dicts are validated as provided. Update the return
in _make_config to call Config.model_validate with raw when raw is not None,
otherwise pass _CONFIG_RAW (i.e., use an explicit "raw is None" conditional),
referencing the _make_config function and Config.model_validate to locate the
change.
🪄 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: f5f00da6-c383-4fd4-816b-858a915ece5d

📥 Commits

Reviewing files that changed from the base of the PR and between 0be9ba8 and 8c5940a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • example.yaml
  • pyproject.toml
  • rejected/__init__.py
  • rejected/config.py
  • rejected/controller.py
  • rejected/data.py
  • rejected/mcp.py
  • rejected/process.py
  • rejected/testing.py
  • tests/test_mcp.py
  • tests/test_process.py

Comment thread rejected/config.py
Comment thread rejected/controller.py
Comment thread rejected/process.py
Comment thread rejected/process.py Outdated
Comment thread tests/test_process.py
- config.py: raise ValueError when YAML root is not a mapping, preventing
  AttributeError from bypassing controller error handling
- controller.py: validate --only and --qty before constructing the MCP,
  so unknown consumer names and --qty-without-only fail immediately
- process.py: pass config_module.Settings instance to consumers instead
  of a plain dict, matching the contract documented in config.py and
  testing.py
- process.py: queue_name falls back to self.consumer_name (logical name)
  not self.name (per-worker name) to avoid each worker consuming from a
  different queue
- tests/test_process.py: fix typo 'prototcol' -> 'protocol' in SSL
  fixture

Co-Authored-By: Gavin M. Roy <gavin@aweber.com>
@gmr
Copy link
Copy Markdown
Owner Author

gmr commented Apr 1, 2026

🤖 This comment was posted by Claude on behalf of @gmr

PR Monitor Summary

All CI checks were passing. Addressed 5 CodeRabbit review comments (commit 91a19ca):

Fixes applied

  • rejected/config.py:158 - Added isinstance(raw, dict) guard before calling .get() so a non-mapping YAML root raises a clean ValueError instead of an AttributeError traceback.
  • rejected/controller.py:156 - Added validation after config load: unknown --only value calls parser.error() immediately; --qty without --only also calls parser.error().
  • rejected/process.py:615 - Wrapped settings dict in config_module.Settings(settings) before passing to consumers, consistent with config.py and testing.py.
  • rejected/process.py:1283 - Changed queue_name fallback from self.name (per-worker name) to self.consumer_name (logical consumer name) so all workers share the correct queue.
  • tests/test_process.py:50 - Fixed typo prototcol -> protocol in SSL options fixture.

All 225 tests pass locally. All 5 review threads resolved.

Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
rejected/config.py (1)

149-161: ⚠️ Potential issue | 🟠 Major

The or {} coercions still let malformed config values through.

Line 151 turns falsy YAML roots like [] or false into {}, so the root-mapping guard never runs. Lines 160-161 do the same for Application and Logging, which means values like Application: [] are silently treated as empty config instead of failing fast. Treat only None as “empty” and reject any present non-mapping section.

Possible fix
         elif path.suffix in ('.yaml', '.yml'):
             with open(path) as f:
-                raw = yaml.safe_load(f) or {}
+                raw = yaml.safe_load(f)
+                if raw is None:
+                    raw = {}
@@
-    app_raw = raw.get('Application', raw.get('application', {})) or {}
-    logging_raw = raw.get('Logging', raw.get('logging', {})) or {}
+    app_raw = raw.get('Application', raw.get('application'))
+    logging_raw = raw.get('Logging', raw.get('logging'))
+
+    if app_raw is None:
+        app_raw = {}
+    elif not isinstance(app_raw, dict):
+        raise ValueError('Application section must be a mapping')
+
+    if logging_raw is None:
+        logging_raw = {}
+    elif not isinstance(logging_raw, dict):
+        raise ValueError('Logging section must be a mapping')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rejected/config.py` around lines 149 - 161, The current code coerces any
falsy YAML values into {} which hides malformed configs; change the logic so
that raw = yaml.safe_load(f) (leave it as-is), then set raw = {} only if raw is
None, and keep the existing check raise ValueError('Configuration root must be a
mapping') if raw is not a dict; similarly, for Application and Logging use
raw.get('Application', raw.get('application', None')) and treat None as empty
(app_raw = {} only when the section is absent or None) but raise ValueError if
the section is present and not a mapping (i.e. if app_raw is not None and not
isinstance(app_raw, dict) raise ValueError), and do the same for
logging_raw—this preserves path.suffix-based parsing while ensuring app_raw and
logging_raw are validated rather than silently coerced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rejected/controller.py`:
- Around line 153-159: The logging.dictConfig call can raise uncaught
exceptions; wrap the cfg.logging branch so dictConfig failures are caught and
routed through the same CLI error path used for config_module.load() in main():
surround logging.config.dictConfig(cfg.logging) with a try/except Exception as
e, and on exception invoke the same error handling/exit logic (same error
message format "Error: ...", same exception/exit code) that main() uses for
config_module.load() failures so malformed logging dicts produce the normalized
CLI error instead of a raw traceback.

---

Duplicate comments:
In `@rejected/config.py`:
- Around line 149-161: The current code coerces any falsy YAML values into {}
which hides malformed configs; change the logic so that raw = yaml.safe_load(f)
(leave it as-is), then set raw = {} only if raw is None, and keep the existing
check raise ValueError('Configuration root must be a mapping') if raw is not a
dict; similarly, for Application and Logging use raw.get('Application',
raw.get('application', None')) and treat None as empty (app_raw = {} only when
the section is absent or None) but raise ValueError if the section is present
and not a mapping (i.e. if app_raw is not None and not isinstance(app_raw, dict)
raise ValueError), and do the same for logging_raw—this preserves
path.suffix-based parsing while ensuring app_raw and logging_raw are validated
rather than silently coerced.
🪄 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: fd81742e-599f-4bc7-853b-ee509000546f

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5940a and 91a19ca.

📒 Files selected for processing (4)
  • rejected/config.py
  • rejected/controller.py
  • rejected/process.py
  • tests/test_process.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_process.py
  • rejected/process.py

Comment thread rejected/controller.py Outdated
A malformed logging dict in the config previously escaped as a raw
traceback. Catch ValueError, TypeError, AttributeError, and ImportError
from logging.config.dictConfig and route them through sys.exit with the
same "Error: ..." format used for config_module.load() failures.

Co-Authored-By: Gavin M. Roy <gavin@aweber.com>
@gmr
Copy link
Copy Markdown
Owner Author

gmr commented Apr 1, 2026

🤖 This comment was posted by Claude on behalf of @gmr

PR Monitor Update (cycle 2)

CodeRabbit reviewed the new HEAD and left one additional comment. Fixed and resolved:

  • rejected/controller.py - Wrapped logging.config.dictConfig() in a try/except block catching ValueError, TypeError, AttributeError, ImportError, routing failures through sys.exit('Error: ...') consistent with the config loading error path. Commit: e4fad2b.

All 225 tests still pass. Waiting for CI and CodeRabbit to review the new HEAD.

Copy link
Copy Markdown

@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)
rejected/controller.py (1)

58-69: ⚠️ Potential issue | 🟠 Major

Run stop_processes() in a finally block.

Line 67 re-raises before the cleanup block on Lines 68-69, so an unexpected failure in self._mcp.run() can leave active consumer processes running without their controller.

💡 Suggested fix
         try:
             self._mcp.run()
         except KeyboardInterrupt:
             LOGGER.info('Caught CTRL-C, shutting down')
         except Exception:
             exc_info = sys.exc_info()
             if self._sentry_client:
                 LOGGER.debug('Sending exception to sentry')
                 sentry_sdk.capture_exception(exc_info)
             raise
-        if self._mcp and self._mcp.is_running:
-            self._mcp.stop_processes()
+        finally:
+            if self._mcp and self._mcp.is_running:
+                self._mcp.stop_processes()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rejected/controller.py` around lines 58 - 69, The current try/except
re-raises exceptions before calling self._mcp.stop_processes(), risking leftover
processes; move the cleanup into a finally block so stop_processes() always
runs. Specifically, wrap the run/exception handling around self._mcp.run() (keep
the KeyboardInterrupt handling and sentry capture using self._sentry_client and
sentry_sdk.capture_exception(exc_info) in the except clauses), then in a finally
block check if self._mcp and self._mcp.is_running and call
self._mcp.stop_processes(); re-raise the exception after ensuring the finally
block runs (i.e., don’t rely on code after the try/except for cleanup).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rejected/controller.py`:
- Around line 27-31: The startup window can ignore a SIGTERM because the handler
is installed while self._mcp is still being constructed; modify the init/startup
flow so SIGTERM is honored: either install the SIGTERM handler before
constructing self._mcp in __init__ or make the handler (e.g., _sigterm_handler)
only set a durable flag/attribute like self._terminate_requested (instead of
just logging) and then immediately check that flag right after constructing
self._mcp (and before proceeding) to abort startup/clean up; ensure any places
that continue boot (the code that currently follows creation of self._mcp and
use of self._sentry_client) check self._terminate_requested and perform an
orderly shutdown.

---

Outside diff comments:
In `@rejected/controller.py`:
- Around line 58-69: The current try/except re-raises exceptions before calling
self._mcp.stop_processes(), risking leftover processes; move the cleanup into a
finally block so stop_processes() always runs. Specifically, wrap the
run/exception handling around self._mcp.run() (keep the KeyboardInterrupt
handling and sentry capture using self._sentry_client and
sentry_sdk.capture_exception(exc_info) in the except clauses), then in a finally
block check if self._mcp and self._mcp.is_running and call
self._mcp.stop_processes(); re-raise the exception after ensuring the finally
block runs (i.e., don’t rely on code after the try/except for cleanup).
🪄 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: 5ce32eac-c94d-48f8-bb59-b8d3b45cb46b

📥 Commits

Reviewing files that changed from the base of the PR and between 91a19ca and e4fad2b.

📒 Files selected for processing (1)
  • rejected/controller.py

Comment thread rejected/controller.py
A SIGTERM arriving between _setup_signals() and the end of MCP
construction was only logged and then ignored, allowing the process to
continue booting after a shutdown request.

Add a _shutdown_requested flag set by _on_sigterm, and check it at each
startup checkpoint in run(): after signal setup and after MCP
construction, returning immediately if shutdown was requested.

Co-Authored-By: Gavin M. Roy <gavin@aweber.com>
@gmr
Copy link
Copy Markdown
Owner Author

gmr commented Apr 1, 2026

🤖 This comment was posted by Claude on behalf of @gmr

PR Monitor Update (cycle 3)

CodeRabbit reviewed HEAD e4fad2b and left one additional comment. Fixed and resolved:

  • rejected/controller.py - Added _shutdown_requested flag to honor SIGTERM during the startup window. The flag is set in _on_sigterm and checked after _setup_signals() and after MCP construction, so a SIGTERM delivered before the process finishes booting now causes an orderly early return instead of being silently ignored. Commit: 60ca1d2.

All 225 tests still pass. Waiting for CI and CodeRabbit to review the new HEAD.

@gmr gmr merged commit 401ceb0 into main Apr 1, 2026
5 checks passed
@gmr gmr deleted the remove-helper branch April 1, 2026 18:25
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