Skip to content

Conversation

cretz
Copy link
Member

@cretz cretz commented Sep 23, 2025

What was changed

  • Added ability to provide Temporalio::Worker::Tuner::SlotSupplier::Custom instances for slot suppliers
  • Added bridge/Rust code to support custom slot suppliers

Checklist

  1. Closes [Feature Request] Custom slot supplier #225

@cretz cretz requested a review from a team as a code owner September 23, 2025 21:24

fn build_tuner(options: Struct) -> Result<TunerHolder, Error> {
fn extract_poller_behavior(poller_behavior: Struct) -> Result<PollerBehavior, Error> {
Ok(if poller_behavior.member::<usize>(id!("initial")).is_ok() {
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we should use some explicit discrimination tag or something for these. More than one issue came up in Python because of this kind of implicit discrimination. EX: A new variant comes along later that also has an initial field and all of a sudden things go wrong.

I won't block on it but IMO it's worth doing the small amount of legwork for the safety guarantee

Copy link
Member Author

Choose a reason for hiding this comment

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

This is your code from #275 that I just moved from below build_tuner to above

Copy link
Member

Choose a reason for hiding this comment

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

Haha fair enough, I think I've learned more now about how I don't love this, now. Might be good to improve it

loop {
match self.attempt_reserve_slot(ctx.clone()).await {
Ok(permit) => {
return SlotSupplierPermit::with_user_data(Arc::new(permit));
Copy link
Member

Choose a reason for hiding this comment

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

It would make sense, if reasonable, for the underlying magnus object to be some kind of clonable ref rather than Arc here, as there semantically is only one "permit", and this way we can't accidentally dupe it Rust side. Not really hugely important but IIRC in Python we are cloning the py object ref rather than the permit itself when we need to hand it back and forth.

Copy link
Member Author

@cretz cretz Sep 26, 2025

Choose a reason for hiding this comment

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

You can't accidentally dupe Rust side because you don't accept it as cloneable. The actual Ruby object is not cloneable, it has a single reference (https://docs.rs/magnus/latest/magnus/value/struct.BoxValue.html). I have to clone this because I have to extract the value from a Ruby context in another thread later. So I need some kind of send/sync/clone holder that will drop the value when the last holder goes away. I think Arc fits this.

Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines +760 to +761
let (result_tx, mut result_rx) = unbounded_channel();
let (cancel_proc_ready_tx, mut cancel_proc_ready_rx) = unbounded_channel();
Copy link
Member

Choose a reason for hiding this comment

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

Ideally these would be oneshot channels which would probably be more accurate semantics. In principle you can have just one actually, and use a Result | CancelReady enum too I think.

In practice you have these closures that need to own them separately so I can see why that didn't really work out. From my reading it doesn't look like you can change them to be not 'static, if possible that would solve it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally these would be oneshot channels which would probably be more accurate semantic

Not exactly. The oneshot sender does not allow you to clone it and it consumes it on send, but the result may get an error on Ruby response or Ruby callback (or technically both, but we only care about the first). Ruby callbacks where these are called from are "procs" passed to the user, and even though we know they are FnOnce, to Ruby they are actually Fn and technically users could call them multiple times.

In principle you can have just one actually, and use a Result | CancelReady enum too I think

No, cancel ready is sent at a different time and is an out of band thing (sometimes before response, sometimes after)

&self,
method: &'static LazyId,
ctx: impl TryIntoValue + Send + 'static,
on_success: impl Fn(&Ruby, Value) + Send + 'static,
Copy link
Member

Choose a reason for hiding this comment

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

The semantics of this are FnOnce, which would allow you to avoid the whole option dance later. It's hard for me just reading to see if there's some obvious reason that doesn't work, but we should use it if it does.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is moved into an FnMut as part of https://docs.rs/magnus/latest/magnus/struct.Ruby.html#method.proc_from_fn. These are Ruby procs that we want to only be called once, but from a system POV technically they could be called multiple times. It was clearer to make it harmless to call multiple times than it was to try to move this FnOnce into an FnMut by wrapping it in some kind of thing.

ruby.get_inner(&SLOT_INFO_WORKFLOW_CLASS).funcall(
id!("new"),
(
// Workflow type
Copy link
Member

Choose a reason for hiding this comment

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

These comments seem unnecessary

Copy link
Member Author

@cretz cretz Sep 26, 2025

Choose a reason for hiding this comment

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

For every parameter I am using from these holder objects to tie to Ruby parameters, I clarify the Ruby parameter name it maps to since the class/method is defined in a far away place. I am just being consistent with myself here, even if some of them are redundant.

@cretz cretz merged commit 9b641d7 into temporalio:main Sep 29, 2025
10 of 11 checks passed
@cretz cretz deleted the custom-slot-supplier branch September 29, 2025 18:11
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.

[Feature Request] Custom slot supplier
2 participants