Skip to content

Reduce idle polling CPU for 0.13.1 - #39

Merged
cardmagic merged 1 commit into
mainfrom
agent/idle-polling-backoff
Aug 16, 2026
Merged

Reduce idle polling CPU for 0.13.1#39
cardmagic merged 1 commit into
mainfrom
agent/idle-polling-backoff

Conversation

@cardmagic

@cardmagic cardmagic commented Aug 16, 2026

Copy link
Copy Markdown
Owner

What changed

Coordinated Node parity: solid-objects-js#5

  • back actor, effect, reminder, and broadcast polling off exponentially from the configured fast interval to a one-second idle ceiling
  • reset the interval on processed work and wake-up notifications, while clamping actor waits to the lease-renewal interval
  • expose the current interval and emit transition instrumentation
  • warn once when live processes share the database without a configured cross-process wake-up adapter
  • make the in-process wake-up generation-aware and extend the PostgreSQL and Redis adapters with the same watch-before-check contract
  • add a reproducible four-role SQLite idle benchmark, repair the benchmark schema setup, and prepare version 0.13.1

Why

Each runtime role previously polled at the configured interval even when the application had no work. Four default roles therefore turned a 100 ms setting into roughly 40 SQLite passes per second, and tighter application settings multiplied the idle CPU cost.

The default wake-up already interrupts waits inside one Ruby process, so a fixed short interval buys no local delivery latency. Separate processes still need PostgreSQL notifications or optional Redis Pub/Sub for prompt delivery. The new warning and documentation make that topology visible.

Measured impact

Apple M5, Ruby 4.0.6, SQLite 3.53.2, four roles, three-second warmup and ten-second sample:

Fast interval Before After
20 ms 165.340 polls/s, 8.401% CPU 3.998 polls/s, 0.947% CPU
100 ms 38.396 polls/s, 2.925% CPU 3.999 polls/s, 0.482% CPU
500 ms 7.998 polls/s, 2.061% CPU 3.996 polls/s, 0.283% CPU

Five post-idle SQLite samples kept one-process p50 latency in the same range: 43.360 ms before and 50.339 ms after. A deliberately worst-case polling-only second process moved from 117.787 ms to 1,028.006 ms p50; this is the documented reason to configure a cross-process adapter when low latency matters.

Compatibility

polling_interval is now the fast interval after activity. Existing explicit values back off to idle_polling_interval, which defaults to one second. Set both values equal to retain a fixed cadence. Legacy custom wake-up adapters that return nil remain at the fast cadence.

Validation

  • bundle exec rake — 518 tests, 1,733 assertions, 15 adapter-dependent skips; Standard, RuboCop, RBS, Steep, and Brakeman passed
  • PostgreSQL matrix — 516 tests, 1,711 assertions, 15 skips
  • MySQL matrix — 516 tests, 1,692 assertions, 23 skips
  • Redis integration — 11 passed
  • gem build and contents inspection — solid_objects 0.13.1

Idle runtime roles currently query the database at a fixed cadence even when no work exists. Back empty passes off to a bounded ceiling while keeping work and wake-up paths prompt, observable, and lease-safe.

Warn when separate processes rely on polling alone, document the resulting latency tradeoff, add reproducible measurements, and prepare 0.13.1.
@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown

Greptile Summary

The PR introduces generation-aware wake-up snapshots and adaptive polling backoff for actor, effect, reminder, and broadcast roles, reducing idle database polling while retaining prompt local notifications.

  • Adds configurable idle polling ceilings and transition instrumentation.
  • Adds PostgreSQL and Redis watch-before-check behavior.
  • Warns about polling-only multi-process deployments.
  • Adds integration coverage, operational documentation, and an SQLite idle benchmark.
  • Bumps the package version to 0.13.1.

Confidence Score: 5/5

The PR appears safe to merge; no concrete blocking or independently actionable non-blocking defect remains.

The adaptive waits remain bounded, actor lease renewal is preserved, built-in adapters close the check-before-wait notification race, and legacy adapters retain the prior wait contract.

Important Files Changed

Filename Overview
lib/solid_objects/polling_backoff.rb Adds a small reusable exponential-backoff state object with transition callbacks and bounded intervals.
lib/solid_objects/worker.rb Applies adaptive polling to actor work while clamping waits to the lease-renewal interval.
lib/solid_objects/wake_up.rb Adds generation snapshots so notifications between the durable check and blocking wait are retained.
lib/solid_objects/wake_up_adapters/postgresql.rb Establishes LISTEN before checking durable work to close the notification race.
lib/solid_objects/wake_up_adapters/redis.rb Adds generation-aware watch objects around the shared subscriber counter.
lib/solid_objects/process_registry.rb Adds a process-wide, once-only diagnostic for multi-process deployments lacking a cross-process wake-up adapter.
test/integration/polling_test.rb Covers backoff progression, resets, watch snapshots, role behavior, lease clamping, and topology warnings.

Sequence Diagram

sequenceDiagram
    participant Role
    participant Wake as Wake-up adapter
    participant DB as Durable database
    Role->>Wake: watch()
    Role->>DB: claim/check work
    alt Work processed
        Role->>Role: reset to fast interval
    else No work
        Role->>Wake: wait(current interval)
        alt Notification received
            Wake-->>Role: true
            Role->>Role: reset to fast interval
        else Timeout
            Wake-->>Role: false
            Role->>Role: double interval up to ceiling
        end
    end
Loading

Reviews (1): Last reviewed commit: "fix: back off idle polling" | Re-trigger Greptile

@cardmagic
cardmagic marked this pull request as ready for review August 16, 2026 20:38
@cardmagic
cardmagic merged commit 00415eb into main Aug 16, 2026
29 checks passed
@cardmagic
cardmagic deleted the agent/idle-polling-backoff branch August 16, 2026 20:38
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