Skip to content

Update fork to upstream solid_queue v1.4.0 - #2

Merged
Micaherb merged 90 commits into
mainfrom
update-upstream-to-v1.4.0
May 19, 2026
Merged

Update fork to upstream solid_queue v1.4.0#2
Micaherb merged 90 commits into
mainfrom
update-upstream-to-v1.4.0

Conversation

@nate-wise

@nate-wise nate-wise commented Apr 8, 2026

Copy link
Copy Markdown

Linear: SRE-138

Summary

Brings our fork up to date with rails/solid_queue v1.4.0. We were pinned at v1.2.2 (90 commits behind).

Notable upstream changes (v1.2.3 → v1.4.0):

  • Async mode reintroduction — non-standalone supervisor runs in a separate thread, configurable via env var
  • Dynamic recurring tasks — opt-in feature for managing recurring tasks at runtime (not just via YAML)
  • Bug fixes:
    • Avoid unintended FOR UPDATE query execution
    • Fix crash when recording a duplicate failed execution
    • Guard against nil pid in Puma plugin stop
    • Handle nil process in heartbeat
    • Skip concurrency controls for jobs whose class has been removed
    • Detect and warn when schedules generate multiple CRONs
    • Clean up dead threads from process_instances when replacing
  • Performance: Index hint for releasing blocked executions
  • Stability: Numerous flaky test fixes, CI timeout guards

Why now: Our batch-rc-2 branch (used by crm-web) is based on v1.2.2. Updating main first lets us rebase the batch work onto v1.4.0. Trial rebase showed zero conflicts.

Next steps after merge:

  1. Rebase batch-rc-2 onto updated main
  2. Verify crm-web tests pass against rebased branch
  3. Configure dependabot to also target batch-rc-2

🤖 Generated with Claude Code

faraquet and others added 30 commits October 27, 2025 14:52
- Updated Appraisals to include a new entry for Rails 8.1 with the appropriate railties version.
- Modified GitHub Actions workflow to include the new Rails 8.1 gemfile in the CI process.
- Created a new gemfile for Rails 8.1 to manage dependencies.

This ensures compatibility with the latest Rails version and enhances CI testing.
Remove deprecated loading of `statistics.rake` from Rakefile
Process#register and #degister for the supervisor to start up
properly.

RecurringSchedule#reload_tasks resolves all the records immediately to
avoid deferred resolution outside the executor block.

Supervisor#handle_claimed_jobs_by wraps its code in the executor.

A second attempt at rails#655 without causing the issues from rails#670
Default logging levels changed in rails/rails@308594b5
…rails#661)

* print warning on startup if path to configuration file does not exist

* change `assert_predicate` to `assert`

Co-authored-by: Rosa Gutierrez <rosa.ge@gmail.com>

---------

Co-authored-by: Rosa Gutierrez <rosa.ge@gmail.com>
Generally in the README, it uses "we" and makes statements like
"we recommend". Changing the instances of "I'd" to "we" seems to
better match the rest of the document and feels like better phrasing
for a project README.

[skip ci]
…onsistency

Improve Consistency of README
This PR notes that `SKIP LOCKED` is supported on MariaDB 10.6 and newer, and that one may need to set `config.solid_queue.use_skip_locked = false` for MariaDB versions < 10.6. I ran into this when setting up `solid_queue` on Slackware64 15.0, which is shipping MariaDB 10.5 series still.
For some reason this stopped working in CI and we don't really need it.
I'm not sure why we did it in this way instead of using `mocha` ^_^U

Rails still requires it, so let's make sure it's loaded in the
test_helper.
And minitest 6.x requires Ruby 3.2+
…on_commit`

See rails/rails#55788

This setting has gone through different stages, and we must account for
them:
- Rails 7.1: Method not available (no deferred enqueue)
- Rails 7.2: :default (defers to adapter's method, which returns true in
  Solid Queue's case)
- Rails 8.0-8.1: false (no deferred enqueue, the setting was deprecated)
- Rails 8.2: true (deferred enqueue enabled by default)
cupatea and others added 17 commits February 24, 2026 21:58
Remove extra blank lines, column-aligned hashes, and inline comments
on test assertions. Revert unrelated Gemfile.lock platform addition.
Filter :static from schedule_task options since dynamic tasks are
always non-static by definition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dynamic tasks require explicit `dynamic_tasks: true` in the scheduler
config. Without it, the scheduler behaves as before — no extra DB
queries, no polling for dynamic changes. Default polling interval
changed from 1s to 5s.

Rename schedule_task/unschedule_task to schedule_recurring_task/
unschedule_recurring_task so the API clearly communicates these are
recurring tasks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tasks_enabled

Have from_configuration read static from options instead of
hardcoding it. Callers that create static tasks (YAML config) pass
static: true via reverse_merge. schedule_recurring_task passes
static: false directly.

Rename the config key from dynamic_tasks to dynamic_tasks_enabled
for clarity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep the longer sleep interval when dynamic tasks are disabled.

Add tests for `dynamic_tasks_enabled` opt-in in configuration and for
verifying dynamic tasks are ignored when not enabled.
We don't need to track dynamic task changes, we can just reload the
metadata in every case. If it hasn't changed, Rails won't issue any new
update to the process record.

Also, we can just use `dynamic_tasks` everywhere, as an empty AR
relation if dynamic tasks are disabled, avoiding extra queries but
keeping the code simpler.
We need the dynamic task keys there and were doing a new query every
time. We only need to do a query when explicitly reloading them.
We want to set that value overriding whatever we get in options, so it
should be merged into options, not the other way around.

Co-Authored-By: Vladyslav Davydenko <vladyslav@hey.com>
Without `#to_a`, `select_candidates` returns an `ActiveRecord::Relation`.
Consequently, `executeions.none?` inside the `lock_candidates` method triggers an unintended query:

```
SELECT 1 AS one FROM `solid_queue_ready_executions` LIMIT 1 FOR UPDATE SKIP LOCKED;
```
@nate-wise nate-wise self-assigned this Apr 8, 2026
@nate-wise
nate-wise requested review from a team, Micaherb, jpcamara, mike-leone-wealthbox, nathanstpierre-wealthbox and treiff and removed request for a team April 8, 2026 17:40

@mike-leone-wealthbox mike-leone-wealthbox 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.

Not sure if we should really be reviewing this line by line, but given that this isn't any code we're writing ourselves and an upstream update, I'm approving it.

Looks like we won't need this fork after SQ 2.0!

@nathanstpierre-wealthbox nathanstpierre-wealthbox 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.

Threw claude at it, and I think it mostly makes sense. Here's that for posterity:

Here's my analysis of the PR. It's a big upstream jump (v1.2.2 → v1.4.0, 63 files changed) but the PR author did a trial rebase with zero conflicts. That said, there
are a few things worth verifying when batch-rc-2 gets rebased:

Things to Watch

  1. Supervisor kind name changed — potential query breakage

The supervisor kind field changed from "Supervisor" to "Supervisor(fork)" / "Supervisor(async)". Your custom ProcessCrashRecovery
(app/lib/solid_queue/process_crash_recovery.rb) interacts with SolidQueue::Process records. If it queries by kind, it'll need updating. Same for any admin panels or
monitoring that filter by supervisor kind.

  1. Supervisor class hierarchy refactored

Supervisor was split into ForkSupervisor and AsyncSupervisor subclasses. The @forks hash is now @process_instances, and methods like reap_and_replace_terminated_forks
are now check_and_replace_terminated_processes. If the batch branch patched or extended Supervisor internals, those customizations may need reworking even if the
rebase itself doesn't conflict textually.

  1. FailedExecution recording changed

create_or_find_by! was replaced with create! inside a nested transaction + rescue ActiveRecord::RecordNotUnique. Your ProcessCrashRecovery auto-retries failed jobs —
make sure the retry flow still works correctly with the new error-recording path, since the before_create callback became before_save.

  1. Semaphore now acquires a row lock on wait

Semaphore.find_by → Semaphore.lock.find_by. This is a correctness fix for a race condition in concurrency controls. Since crm-web uses ProcessQueue::ConcurrencyKey and
limits_concurrency, this is a good fix but worth a quick sanity check under load — the added FOR UPDATE could increase lock contention on high-throughput queues.

  1. ReadyExecution.select_candidates forces .to_a

This prevents an unintended FOR UPDATE on the full query chain. It's a fix, but changes query materialization timing. If the batch branch modified this query path,
watch for interaction.

  1. Recurring task schedule validation is stricter

Fugit.parse(schedule) → Fugit.parse(schedule, multi: :fail). Schedules that silently generated multiple crons will now raise. Your four recurring tasks in
config/recurring.yml use every hour at minute 12, every 5 minutes, etc. — these are all single-cron expressions, so you should be fine.

  1. use_index optimizer hints (MySQL-specific)

The blocked execution release query now includes a MySQL index hint via optimizer_hints. Since crm-web uses PostgreSQL, this is a no-op (PG ignores MySQL hint
comments). No concern here.

What Looks Safe

  • Async mode: New feature, opt-in only. crm-web defaults to fork mode via ${SOLID_QUEUE_SUPERVISOR_MODE:-fork} — no behavioral change.
  • Dynamic recurring tasks: Also opt-in via dynamic_tasks_enabled: true. Not enabled by default.
  • Puma plugin refactoring: crm-web runs solid_queue as separate processes (Procfile), not via the Puma plugin. No impact.
  • enqueue_after_transaction_commit skip logic: crm-web sets :never on SolidQueueApplicationJob, so the changed test skip conditions don't affect you.
  • Nil-safe heartbeat (process&.heartbeat): Strictly more robust.
  • Guard against nil pid in Puma plugin stop: Now checks return unless solid_queue_pid before Process.waitpid. Good defensive fix.

Recommendation

Items 1 and 2 are the most likely to bite you after the rebase onto batch-rc-2. I'd specifically:

  • Grep batch-rc-2 for any references to "Supervisor" as a kind string or any direct patches to Supervisor's internal methods (@forks,
    reap_and_replace_terminated_forks, handle_claimed_jobs_by, etc.)
  • Run the ProcessCrashRecovery tests after the rebase
  • Do a production smoke test of concurrency-controlled jobs to validate the semaphore locking change under real load

Overall the upstream changes are solid bug fixes and good architectural improvements. Nothing that should destabilize your fork as long as the rebase touchpoints above
are checked.

@treiff
treiff removed their request for review May 7, 2026 13:19
@Micaherb
Micaherb merged commit 5d0c28c into main May 19, 2026
133 checks passed
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.