Skip to content

fix: make network rescans non-blocking - #65

Merged
superdav42 merged 2 commits into
mainfrom
feature/auto-20260809-144405
Aug 10, 2026
Merged

fix: make network rescans non-blocking#65
superdav42 merged 2 commits into
mainfrom
feature/auto-20260809-144405

Conversation

@superdav42

@superdav42 superdav42 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • run full-network cron discovery in a subprocess so periodic rescans no longer block the worker event loop
  • poll scanner output asynchronously with overlap prevention, malformed-output handling, timeouts, and shutdown cleanup
  • scan ordinary multisite blogs plus nested isolated/sovereign registries
  • bound in-memory timer scheduling with a configurable horizon instead of scheduling every distant event immediately

Configuration

  • QUEUE_WORKER_SCAN_TIMEOUT defaults to 300 seconds
  • QUEUE_WORKER_SCHEDULING_HORIZON defaults to 3600 seconds and is clamped to at least the rescan interval

Verification

  • composer test:regression
  • php -l src/class-worker-process.php
  • php -l src/class-config.php
  • php -l bin/scan-cron.php
  • php -l tests/regression.php
  • composer validate --strict

Regression coverage includes scheduling-horizon filtering, asynchronous scan startup, overlap prevention, successful polling, malformed output, timeouts, and process cleanup.


aidevops.sh v3.32.245 plugin for OpenCode v1.18.9 with gpt-5.5 spent 3h 35m and 1,547,684 tokens on this with the user in an interactive session.

Summary by CodeRabbit

  • New Features

    • Added configurable scheduling horizons and scan timeouts.
    • Added full-network scanning, including isolated network and sovereign site scans.
    • Improved scan handling with asynchronous execution, timeout protection, and shutdown cleanup.
    • Jobs outside the configured scheduling horizon are now excluded from scheduling.
  • Documentation

    • Documented the new scheduling horizon and scan timeout settings, including defaults and behavior.

@superdav42 superdav42 added the origin:interactive Created by interactive user session label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f1769c8-6c47-4699-b47e-48607ea900ef

📥 Commits

Reviewing files that changed from the base of the PR and between 245da27 and 914ffa7.

📒 Files selected for processing (3)
  • bin/scan-cron.php
  • src/class-worker-process.php
  • tests/regression.php
📝 Walkthrough

Walkthrough

The change adds scheduling-horizon and scan-timeout configuration, horizon-aware job filtering, registry-driven full-network scans, asynchronous subprocess handling, shutdown cleanup, and regression coverage.

Changes

Full-network scanning

Layer / File(s) Summary
Configuration contracts
src/class-config.php, src/class-worker-process.php
Adds bounded scheduling-horizon and scan-timeout settings and loads them into the worker.
Scanner entry and horizon filtering
bin/scan-cron.php
Accepts scan settings, propagates the scheduling horizon, and filters cron and Action Scheduler jobs by deadline.
Registry-driven isolated scanning
bin/scan-cron.php
Reads active registry entries, resolves URLs, excludes selected sites from local scans, and validates isolated scanner subprocess output.
Asynchronous worker scan lifecycle
src/class-worker-process.php, bin/worker.php
Runs full-network scans asynchronously, prevents overlap, polls output, handles timeouts, schedules validated payloads, and terminates active scans during shutdown.
Regression coverage and documentation
tests/regression.php, README.md
Tests horizon filtering, subprocess lifecycle behavior, shutdown cleanup, configuration bounds, and documents the new settings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Worker_Process
  participant scan_cron.php
  participant RegistrySites
  participant IsolatedScanner
  Worker_Process->>scan_cron.php: start full-network scan
  scan_cron.php->>RegistrySites: load active registry entries
  scan_cron.php->>IsolatedScanner: send URL, horizon, and timeout
  IsolatedScanner-->>scan_cron.php: return validated JSON jobs
  scan_cron.php-->>Worker_Process: provide jobs for scheduling
  Worker_Process->>Worker_Process: poll, schedule, or terminate scan
Loading

Possibly related PRs

Suggested labels: review-feedback-scanned

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 describes the primary change: making full-network rescans non-blocking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/auto-20260809-144405

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

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
tests/regression.php (1)

814-818: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add scan-timeout expiry coverage.

This fixture only verifies shutdown cleanup. It does not set scan_timeout below the subprocess duration or poll after its deadline. Add a case that sets scan_timeout to one second, polls the five-second fixture after expiry, and asserts that the process is terminated and reaped.

🤖 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 `@tests/regression.php` around lines 814 - 818, Extend the regression coverage
around Worker_Process::run_full_rescan and scan polling by configuring
scan_timeout to one second for the five-second async_scan_fixture, waiting or
polling past that deadline, and then asserting active_scan_process is null to
confirm the scanner subprocess was terminated and reaped. Keep the existing
shutdown cleanup assertion intact.
src/class-worker-process.php (1)

1134-1146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider skipping invalid payloads instead of discarding the whole batch.

One invalid entry causes schedule_scan_output to return 0. The worker then discards every valid job from every site in the network scan. Those jobs wait until the next rescan. In bin/scan-cron.php, qw_scan_registry_jobs applies the same all-or-nothing rule, but its blast radius is one registry entry. Here it is the whole network.

Skipping the bad entry and logging it keeps the remaining jobs schedulable.

♻️ Proposed change to skip invalid entries
     $validated_payloads = [];
+    $invalid = 0;
     foreach ($payloads as $payload_data) {
         if (!is_array($payload_data)) {
-            Worker::log(sprintf('[W%d][RESCAN][FAIL] Full-network scanner returned an invalid payload shape.', $worker_id));
-            return 0;
+            $invalid++;
+            continue;
         }
         try {
             $validated_payloads[] = new Job_Payload($payload_data);
         } catch (\Throwable $e) {
-            Worker::log(sprintf('[W%d][RESCAN][FAIL] Full-network scanner returned invalid routing metadata (%s).', $worker_id, $e->getMessage()));
-            return 0;
+            $invalid++;
+            Worker::log(sprintf('[W%d][RESCAN][WARN] Skipping payload with invalid routing metadata (%s).', $worker_id, $e->getMessage()));
         }
     }
+
+    if ($invalid > 0) {
+        Worker::log(sprintf('[W%d][RESCAN][WARN] Skipped %d invalid payloads.', $worker_id, $invalid));
+    }
🤖 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/class-worker-process.php` around lines 1134 - 1146, Update the
payload-validation loop in schedule_scan_output to log and skip invalid
non-array payloads and Job_Payload construction failures instead of returning 0.
Continue appending valid payloads to validated_payloads so they remain
schedulable, while preserving the existing failure messages for each rejected
entry.
bin/scan-cron.php (4)

174-194: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Accumulate payloads with a nested array instead of array_merge in the loop.

array_merge copies the whole accumulated array on every iteration. On a network with many sites this is quadratic in the total payload count. Collecting per-site results and flattening once removes the repeated copying.

♻️ Proposed change
+    $payload_groups = [];
     foreach ($site_ids as $site_id) {
@@
         $switched = $site_id !== get_current_blog_id();
         if ($switched) {
             switch_to_blog($site_id);
         }
-        $payloads = array_merge($payloads, qw_scan_current_site_jobs($scheduling_horizon));
+        $payload_groups[] = qw_scan_current_site_jobs($scheduling_horizon);
         if ($switched) {
             restore_current_blog();
         }
     }
+    $payloads = array_merge($payloads, ...$payload_groups);
🤖 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 `@bin/scan-cron.php` around lines 174 - 194, Update the site iteration around
qw_scan_current_site_jobs to collect each site’s payload array as a nested
element instead of repeatedly calling array_merge on $payloads. After the loop,
flatten the collected per-site payloads once while preserving the existing
skipped-site, blog-switching, and payload ordering behavior.

396-416: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider stream_select instead of a 10 ms polling loop.

The loop wakes 100 times per second for the whole child scan. stream_select on the two pipes with a timeout blocks until output arrives and removes the spin. The current form is correct; this is an efficiency point only.

Note on the static analysis hint for lines 369-373: proc_open receives an array, so no shell is involved and the arguments are PHP_BINARY and __FILE__. The command-injection finding is a false positive.

🤖 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 `@bin/scan-cron.php` around lines 396 - 416, Replace the fixed 10 ms usleep
polling in the scan process loop with stream_select on the stdout and stderr
pipes, using the remaining scan timeout as its wait limit. Continue reading
available output, checking proc_get_status, and terminating on timeout while
preserving the existing stdout/stderr handling and process behavior.

Source: Linters/SAST tools


281-345: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

These registry readers duplicate the Worker_Process versions and already diverge.

qw_scan_sovereign_site_entries and qw_scan_isolated_network_entries repeat sovereign_site_entries() and isolated_network_entries() in src/class-worker-process.php. The predicates are not identical. The worker version requires a non-empty domains array. This version accepts an entry that only sets domain, because it tests the resolved URL instead. The two components can therefore select different sovereign sites from the same registry file.

Move the registry parsing into one shared class under src/ and call it from both places.

As per coding guidelines: "Use the QueueWorker\ namespace prefix for classes in src/."

🤖 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 `@bin/scan-cron.php` around lines 281 - 345, Create a shared namespaced
registry-reader class under src/ using the QueueWorker\ namespace, and move the
common sovereign-site and isolated-network parsing logic into it. Replace
qw_scan_sovereign_site_entries() and qw_scan_isolated_network_entries() in
bin/scan-cron.php and the corresponding sovereign_site_entries() and
isolated_network_entries() methods in Worker_Process with calls to that class,
preserving one consistent domains-array validation and registry filtering
behavior.

Source: Coding guidelines


239-242: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Move the deadline check out of the events loop.

$timestamp does not change inside foreach ($events as $event). The check repeats for every event of the same timestamp. Testing it at the foreach ($crons as $timestamp => $hooks) level skips the whole timestamp bucket in one step.

🤖 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 `@bin/scan-cron.php` around lines 239 - 242, Move the `$timestamp > $deadline`
guard from inside the `foreach ($events as $event)` loop to the enclosing
`foreach ($crons as $timestamp => $hooks)` level, before events are processed,
so an expired timestamp skips its entire bucket once.
🤖 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.

Inline comments:
In `@bin/scan-cron.php`:
- Around line 200-216: Update the registry-entry loops around
qw_scan_registry_jobs to share the overall scan deadline: track the remaining
total budget and derive a per-entry timeout from it before each call, rather
than passing the full $scan_timeout for every entry. Apply the same deadline
logic to both $sovereign_sites and $isolated_networks, while preserving the
existing payload aggregation and ensuring the timeout never becomes invalid.

In `@tests/regression.php`:
- Around line 601-623: Make the beyond_horizon_hook fixture in the
schedule_timer test deterministic by placing its timestamp safely beyond the
60-second scheduling_horizon, rather than only one second past it. Keep the
within_horizon_hook timestamp and pending_timers assertion unchanged.

---

Nitpick comments:
In `@bin/scan-cron.php`:
- Around line 174-194: Update the site iteration around
qw_scan_current_site_jobs to collect each site’s payload array as a nested
element instead of repeatedly calling array_merge on $payloads. After the loop,
flatten the collected per-site payloads once while preserving the existing
skipped-site, blog-switching, and payload ordering behavior.
- Around line 396-416: Replace the fixed 10 ms usleep polling in the scan
process loop with stream_select on the stdout and stderr pipes, using the
remaining scan timeout as its wait limit. Continue reading available output,
checking proc_get_status, and terminating on timeout while preserving the
existing stdout/stderr handling and process behavior.
- Around line 281-345: Create a shared namespaced registry-reader class under
src/ using the QueueWorker\ namespace, and move the common sovereign-site and
isolated-network parsing logic into it. Replace qw_scan_sovereign_site_entries()
and qw_scan_isolated_network_entries() in bin/scan-cron.php and the
corresponding sovereign_site_entries() and isolated_network_entries() methods in
Worker_Process with calls to that class, preserving one consistent domains-array
validation and registry filtering behavior.
- Around line 239-242: Move the `$timestamp > $deadline` guard from inside the
`foreach ($events as $event)` loop to the enclosing `foreach ($crons as
$timestamp => $hooks)` level, before events are processed, so an expired
timestamp skips its entire bucket once.

In `@src/class-worker-process.php`:
- Around line 1134-1146: Update the payload-validation loop in
schedule_scan_output to log and skip invalid non-array payloads and Job_Payload
construction failures instead of returning 0. Continue appending valid payloads
to validated_payloads so they remain schedulable, while preserving the existing
failure messages for each rejected entry.

In `@tests/regression.php`:
- Around line 814-818: Extend the regression coverage around
Worker_Process::run_full_rescan and scan polling by configuring scan_timeout to
one second for the five-second async_scan_fixture, waiting or polling past that
deadline, and then asserting active_scan_process is null to confirm the scanner
subprocess was terminated and reaped. Keep the existing shutdown cleanup
assertion intact.
🪄 Autofix

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 Plus

Run ID: e6051e1a-dc2f-440f-ad70-32683e5b2e8b

📥 Commits

Reviewing files that changed from the base of the PR and between 6d71761 and 245da27.

📒 Files selected for processing (6)
  • README.md
  • bin/scan-cron.php
  • bin/worker.php
  • src/class-config.php
  • src/class-worker-process.php
  • tests/regression.php

Comment thread bin/scan-cron.php
Comment thread tests/regression.php
@superdav42
superdav42 merged commit 414637c into main Aug 10, 2026
2 checks passed
@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

origin:interactive Created by interactive user session review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant