Skip to content

Allow concurrent-ruby 1.3.x - #473

Merged
adamruzicka merged 3 commits into
Dynflow:masterfrom
log0u7:relax-concurrent-ruby-pin
Sep 25, 2026
Merged

adamruzicka merged 3 commits into
Dynflow:masterfrom
log0u7:relax-concurrent-ruby-pin

Conversation

@log0u7

@log0u7 log0u7 commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The ~> 1.1.3 pessimistic pin on concurrent-ruby (and the ~> 0.6.0 pin on concurrent-ruby-edge, which itself pins concurrent-ruby ~> 1.1.6) blocks every dependent from resolving the security-fixed concurrent-ruby releases. 1.3.7+ fixes CVE-2026-54906 and CVE-2026-54904; Foreman and the smart-proxy plugins (which pull dynflow) therefore ship a vulnerable concurrent-ruby with no resolver path to the fixed release.

Fixes #474

Changes

  • dynflow.gemspec: concurrent-ruby '~> 1.1.3' -> '>= 1.1.3', '< 2.0'
  • dynflow.gemspec: concurrent-ruby-edge '~> 0.6.0' -> '~> 0.7.0' (0.7.x supports concurrent-ruby ~> 1.3; latest 0.7.2 released 2025-01)
  • Gemfile: concurrent-ruby-ext '~> 1.1.3' -> '~> 1.3.0' (ext releases follow the main gem, 1.3.8 available)
  • lib/dynflow.rb: require 'logger' - concurrent-ruby 1.1 loaded logger as a side effect, 1.3 no longer does, so the global logger wiring crashed with NameError on 1.3.x

Test evidence

ruby 3.3, concurrent-ruby 1.3.8 + edge 0.7.2 + ext 1.3.8:

  • bundle resolves cleanly with the relaxed constraints
  • 397 tests of the suite pass, including the executor, dispatcher and polling tests
  • the full suite later hangs in the multi-executor dispatcher tests in our container environment; the same hang reproduces on master with concurrent-ruby 1.1.10 (progressing through fewer tests), so it is pre-existing and unrelated to this change

@jakduch

jakduch commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

This also blocks https://projects.theforeman.org/issues/39353. Once the relaxed concurrent-ruby constraint is released, Foreman can align its lock with the rest of the ecosystem without carrying a local workaround.

Comment thread dynflow.gemspec Outdated
@log0u7

log0u7 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both for the review!

Pushed two follow-ups:

  • Sorted the dependencies in the gemspec (rubocop Gemspec/OrderedDependencies).
  • Widened the bats termination window (60s -> 180s) for the pg goes away for good test: under concurrent-ruby 1.3 the persistence retry chain surfaces the fatal error after ~58s (10 x retry delay plus the new timer/polling cadence), which raced the old window - the world does terminate, just later. Assertion unchanged.

Note on concurrent-ruby-edge ~> 0.7.0: it is forced by bundler resolution, edge 0.6 pins concurrent-ruby ~> 1.1.6 which excludes 1.3.x. dynflow does not actually require edge at runtime (nothing in lib/), so if you'd rather drop that vestigial dependency from the gemspec instead of bumping it, happy to do that in this PR.

The new CI run will need a workflow approval since it comes from a fork.

@adamruzicka

Copy link
Copy Markdown
Contributor

dynflow does not actually require edge at runtime (nothing in lib/)

It sort of does. concurrent-ruby-edge doesn't always honor its own namespace. We rely on Concurrent::Actor which is defined in the edge gem

@log0u7

log0u7 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up from CI on 75880da - and thanks @adamruzicka for the correction on edge, understood: Concurrent::Actor comes from the edge gem, so the ~> 0.7.0 bump is indeed required.

Summary of where the bats failure actually stands (test 5, pg goes away for good):

  • The number of MAX_RETRIES exceeded -> FatalPersistenceError fires (13:28:38)
  • PersistenceError in executor + FATAL Terminating are logged, so the core.tell([:handle_persistence_error, ...]) actor message delivers and world.terminate runs
  • shutting down Core ... logged at 13:29:03 (25s after Terminating - the actor delivery itself is very slow under edge 0.7?)
  • but none of the subsequent start terminating ... progress lines appear, and World terminated, exiting. never shows, even with a 180s window

So under concurrent-ruby 1.3 + edge 0.7 the termination sequence stalls inside start_termination (before the first start terminating delayed_executor... log, i.e. around run_before_termination_hooks), and the actor tell latency is ~25s. That is a real porting task in the Actor/Promises path, well above the scope of a pin relaxation.

Question for you: is this porting something you'd want to handle on dynflow's side (given edge 0.7 rewrote the Actor machinery), or should this PR stay pinned-realying on that port landing separately? Either way we can keep the PR scope as-is and I'm happy to test patches. Also FYI the two unit-test failures in the 3.0/3.2 postgresql jobs are timing-sensitive (WithPollingSubPlans resume) plus one json-3.0.2 parse issue on Ruby 3.0 - happy to dig further if useful.

@adamruzicka

Copy link
Copy Markdown
Contributor

I'm afraid I won't really be able to do any sort of porting in the immediate future.

@jakduch

jakduch commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

@adamruzicka I took care of the porting in #475 (Redmine #39799).

The hang was not in Actor delivery itself: the termination sequence progressed through all components, but both the timeout watchdog and its asynchronous completion callback were scheduled on the saturated global IO executor. The separate PR keeps the future-based API, exposes a resolvable future, and runs only the bounded watchdog on a dedicated short-lived thread.

I reproduced the PostgreSQL outage with concurrent-ruby 1.3.8 / concurrent-ruby-edge 0.7.2; the world exited after 80 seconds, and the Ruby 3.3/PostgreSQL suite passed with 402 runs and 1488 assertions.

@log0u7

log0u7 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Perfect - #475 fixes exactly the node my CI tracing pointed at (start_termination waiting forever after shutting down Core ..., termination watchdog + completion callback scheduled on the saturated global IO executor). Your root cause is much more precise than my 'actor delivery is slow' guess, and a dedicated short-lived thread for the bounded watchdog is the right shape for it.

With #475 in place, this PR is scope-complete as a pure pin relaxation:

  • gemspec: concurrent-ruby >= 1.1.3, < 2.0 + edge ~> 0.7.0 (+ logger, deps sorted)
  • the logger addition became needed since the gem no longer autoloads it under newer rubies (unrelated to 1.3 semantics)
  • unit suites pass on 8/10 matrix jobs; the two failures (Ruby 3.0/3.2 postgresql, WithPollingSubPlans resume + a json-3.0.2 arg issue on 3.0) are on master too, so not regressions from this branch
  • bats test 5 needs Ensure world termination completes under IO saturation #475 to pass - once that lands, this should go green

Happy to rebase here once #475 merges if you'd like the bats run re-verified against it. Thanks both for the quick turnaround!

@jakduch

jakduch commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Follow-up: I added the two unit-CI fixes to #475 in aef3e41 as part of the concurrent-ruby 1.3 port:

  • Ruby < 3.2 now stays on JSON 2, because the multi_json release with the JSON 3 keyword-argument fix requires Ruby >= 3.2.
  • The WithPollingSubPlans tests wait for the parent poll to be scheduled before advancing the managed clock.

Verified on the combined #473 + #475 tree: Ruby 3.2/PostgreSQL full suite passes (402 runs, 1483 assertions), Ruby 3.0 and 3.2 targeted action/future-execution tests pass (41 runs, 131 assertions on each), and RuboCop is clean. Together with the PostgreSQL-outage verification already reported, rebasing this PR after #475 should address all three currently failing jobs. I did not open another helper PR.

@log0u7

log0u7 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Sounds good - json 2 for Ruby < 3.2 plus the polling-clock ordering explains both matrix failures nicely. I'll rebase this branch once #475 lands so the bats + full matrix get validated on top of it.

@adamruzicka

Copy link
Copy Markdown
Contributor

Could you please rebase?

The ~> 1.1.3 pessimistic pin blocks every dependent (Foreman, smart-proxy
plugins) from resolving the security-fixed concurrent-ruby releases:
1.3.7+ fixes CVE-2026-54906 and CVE-2026-54904.

Relax the three related constraints:

- dynflow.gemspec: concurrent-ruby '>= 1.1.3', '< 2.0'
- dynflow.gemspec: concurrent-ruby-edge '~> 0.7.0' (0.7.x supports
  concurrent-ruby ~> 1.3, 0.6.x pins ~> 1.1.6)
- Gemfile: concurrent-ruby-ext '~> 1.3.0' (ext releases follow the main
  gem: 1.3.8 is available)

Code change: lib/dynflow.rb requires 'logger' explicitly. concurrent-ruby
1.1 loaded it as a side effect; 1.3 no longer does, so the global logger
wiring in lib/dynflow.rb crashes with NameError on 1.3.x.

Test evidence (ruby 3.3, concurrent-ruby 1.3.8 + edge 0.7.2 + ext 1.3.8):
397 tests of the suite pass. The full suite later hangs in the
multi-executor dispatcher tests in our environment; the same hang
reproduces on master with concurrent-ruby 1.1.10 (progressing through
fewer tests), so it is pre-existing and unrelated to this change.

Signed-off-by: log0u7 <70974447+log0u7@users.noreply.github.com>
Under concurrent-ruby 1.3 the persistence retry chain surfaces the
fatal error after ~58s of polling rounds, past the previous 60s
window. Keep the assertion, widen the window.
@log0u7
log0u7 force-pushed the relax-concurrent-ruby-pin branch from 75880da to c8603f1 Compare September 25, 2026 09:49
@log0u7

log0u7 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on top of #475 (master 012510a) - three commits, no conflicts. CI run will need the usual fork approval.

@adamruzicka

Copy link
Copy Markdown
Contributor

Is the timeout increase in bats still needed?

@log0u7

log0u7 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Yes, I believe so - even with #475 in, the fatal-persistence detection chain is slower under concurrent-ruby 1.3: jakduch measured the world exiting after ~80s (in the #475 verification note), and the original 60s window raced that even without the stall. The widened window is what let the bats job pass on this rebased branch. Happy to trim it to 120s if 180 feels generous - the assertion itself is unchanged.

@jakduch

jakduch commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Yes. #475 fixes the deadlock, but it does not shorten the persistence retry budget. In my PostgreSQL outage reproduction with concurrent-ruby 1.3.8 and concurrent-ruby-edge 0.7.2, World terminated, exiting. appeared after about 80 seconds, so the original 60-second deadline would still be flaky. 120 seconds should be enough; 180 just leaves more CI headroom.

@adamruzicka
adamruzicka merged commit fc5d1c3 into Dynflow:master Sep 25, 2026
21 of 22 checks passed
@adamruzicka

Copy link
Copy Markdown
Contributor

Thank you @log0u7 & @jakduch !

@log0u7

log0u7 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both for the fast review and the port work - great collaboration. One small note for the record: keeping the 180s window (jakduch's 120s floor + CI headroom, your call if you prefer trimming it in a follow-up).

Looking forward to the gem release on rubygems so downstream (foreman #39353) can align its lock.

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.

Allow concurrent-ruby 1.3.x (CVE-2026-54906/54904 fixes blocked by ~> 1.1 pins)

3 participants