Skip to content

Conversation

@ko1
Copy link
Contributor

@ko1 ko1 commented Dec 5, 2025

  1. Introduce DATA to store all status.
  2. Store DATA instance to the Ractor local storage if possible
  3. Make GET_TIME (Method object) shareable if possible

3 is supporeted Ruby 4.0 and later, so the Rator support is works only on Ruby 4.0 and later.

Copy link
Member

@eregon eregon left a comment

Choose a reason for hiding this comment

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

I commented on some surface-level things.

In general its' not great we need extra indirections and hurt readability this way for Ractor :/
I'm also concerned about performance, could you run #15 (comment) before and after this change?

@eregon
Copy link
Member

eregon commented Dec 5, 2025

From a design POV it's not ideal to have potentially 1 Timeout thread per Ractor, especially with many Ractors, as that can double the number of threads if Timeout is used in every Ractor.
It would be probably better to have 1 Timeout thread per Ruby process.
OTOH I guess that's probably not really feasible, as we'd need to Thread#raise from a Thread in a different Ractor, and also the implementation would be complicated, so this approach of 1 Timeout thread per Ractor makes sense for now and is simpler.

@ko1
Copy link
Contributor Author

ko1 commented Dec 5, 2025

From a design POV it's not ideal to have potentially 1 Timeout thread per Ractor, especially with many Ractors, as that can double the number of threads if Timeout is used in every Ractor. It would be probably better to have 1 Timeout thread per Ruby process. OTOH I guess that's probably not really feasible, as we'd need to Thread#raise from a Thread in a different Ractor, and also the implementation would be complicated, so this approach of 1 Timeout thread per Ractor makes sense for now and is simpler.

Yes. See my slide: https://rubykaigi.org/2024/presentations/ko1.html (my trial and not matured on API)

@eregon
Copy link
Member

eregon commented Dec 5, 2025

Yes. See my slide: https://rubykaigi.org/2024/presentations/ko1.html (my trial and not matured on API)

Thanks, I hadn't seen those.
So this is solution 1.
Solution 2 would need something like Ractor#interrupt_exec but also be able to pass Thread references (of other Ractors) around, so yeah seems hard.
Solution 3 to make Timeout core would make sense, but it seems not enough time before Ruby 4.0.

So yeah, I think solution 1 makes sense for now.

@eregon
Copy link
Member

eregon commented Dec 5, 2025

I think after addressing my comments above and simplifying the changes now that everything can be defined as singleton methods and not module_function this PR will probably look good enough to me to merge and I'll re-review then.

lib/timeout.rb Outdated
TIMEOUT_THREAD_MUTEX = Mutex.new
@timeout_thread = nil
private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX
DATA = Struct.new(*%i{condvar queue queue_mutex timeout_thread_mutex timeout_thread})
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking this should be named State since it is the state of this library.
As a bonus it respects normal naming for classes (CamelCase) without conflicting with ::Data

@ko1
Copy link
Contributor Author

ko1 commented Dec 5, 2025

Thank you for the review and I'd merged your idea.

  • DATA -> State class
    • move create_timeout_thread and ensure_timeout_thread_created into State and we don't need to expose @timeout_thread and TIMEOUT_THREAD_MUTEX. We don't need to introduce private methods for Timeout module.
  • remove get_data and introduce State.instance to acquire the State instance.

I think it is much simpler.

For the performance, on my machine the measurement doesn't stable.

Maybe I think we clear all of concerns. If there is no big issue, we can merge it and apply another fixes in future.

lib/timeout.rb Outdated

# We keep a private reference so that time mocking libraries won't break
# Timeout.
GET_TIME =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can shorten them for example with

  m = Process.method(:clock_gettime)
  GET_TIME = (defined?(Ractor.make_shareable) && Ractor.make_shareable(m) rescue m) || m

but it is cosmetic issue (and difficult to understand by a glance).

Copy link
Member

Choose a reason for hiding this comment

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

This looks fine. It could also be:

      GET_TIME = Process.method(:clock_gettime)
      Ractor.make_shareable(GET_TIME) rescue nil # Only works on Ruby 4+

Copy link
Member

Choose a reason for hiding this comment

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

Actually, would just GET_TIME = Process.method(:clock_gettime).freeze work and be enough? That seems nicest.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, would just GET_TIME = Process.method(:clock_gettime).freeze work and be enough?

I tried but it does not work, it's not detected as shareable.
I thought it should work though for objects which use RUBY_TYPED_FROZEN_SHAREABLE (or RUBY_TYPED_FROZEN_SHAREABLE_NO_REC in this case).
Possibly a bug or is it my misunderstanding that.freeze should make such object shareable?

1. Introduce State to store all status.
2. Store State instance to the Ractor local storage if possible
3. Make `GET_TIME` (Method object) shareable if possible

3 is supporeted Ruby 4.0 and later, so the Rator support is works
only on Ruby 4.0 and later.
lib/timeout.rb Outdated

::Timeout::RACTOR_SUPPORT = true # for test
else
@GLOBAL_STATE = State.new
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@GLOBAL_STATE = State.new
GLOBAL_STATE = State.new

A constant should be faster (at least with JIT)

@eregon
Copy link
Member

eregon commented Dec 5, 2025

  • move create_timeout_thread and ensure_timeout_thread_created into State and we don't need to expose @timeout_thread and TIMEOUT_THREAD_MUTEX. We don't need to introduce private methods for Timeout module.

That's a great idea, it looks much cleaner now because these methods can then just access @ivars directly vs having to state.foo everywhere.

Copy link
Member

@eregon eregon left a comment

Choose a reason for hiding this comment

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

This looks good to me, I will just push some tiny changes on top and then I'll merge this.

@eregon
Copy link
Member

eregon commented Dec 5, 2025

All good now, there was a small bug which I fixed as well (see extra commits).

@eregon eregon merged commit 281b250 into ruby:master Dec 5, 2025
19 checks passed
@eregon eregon mentioned this pull request Dec 5, 2025
@eregon
Copy link
Member

eregon commented Dec 5, 2025

@hsbt I would like to make a release of timeout from current master, is that OK?
See also ruby/ruby#15424

Is the process just bumping VERSION in lib/timeout.rb, commit, tag it and push both commit & tag and the rest is automatic?

@luke-gruber
Copy link

As for having a single timeout thread, I think we would need Ractor.receive that takes an optional timeout argument, and to interrupt threads in different ractors, like you said. Maybe writing it in C and making it core would be better.

@hsbt
Copy link
Member

hsbt commented Dec 8, 2025

Released https://github.com/ruby/timeout/releases/tag/v0.5.0

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.

4 participants