Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom slot suplier #372

Merged
merged 8 commits into from
Dec 5, 2024
Merged

Custom slot suplier #372

merged 8 commits into from
Dec 5, 2024

Conversation

Sushisource
Copy link
Member

@Sushisource Sushisource commented Nov 25, 2024

What was changed

Add user-implementable slot suppliers

Why?

Parity. Cool feature.

Checklist

  1. Closes

  2. How was this tested:
    Added tests

  3. Any docs updates needed?
    Will need to follow up with docs for custom interface in all langs

@Sushisource Sushisource force-pushed the custom-slot-suplier branch 9 times, most recently from 8f822c1 to 0a9de76 Compare November 28, 2024 01:44
@Sushisource Sushisource force-pushed the custom-slot-suplier branch 5 times, most recently from ff2f074 to 23d874b Compare December 3, 2024 23:00
@Sushisource Sushisource marked this pull request as ready for review December 3, 2024 23:00
@Sushisource Sushisource changed the title [Draft] Custom slot suplier Custom slot suplier Dec 3, 2024
@Sushisource Sushisource mentioned this pull request Dec 4, 2024
@Sushisource
Copy link
Member Author

Let's merge #376 first then I will rebase

src/Temporalio/Bridge/CustomSlotSupplier.cs Outdated Show resolved Hide resolved
cancelTokenSrc.Dispose();
return;
}
catch (OperationCanceledException)
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
catch (OperationCanceledException)
catch (OperationCanceledException) when (cancelTokenSrc.IsCancellationRequested)

I assume there will be a missed reservation if a user raised this exception for whatever reason unrelated to our cancellation. In fact, should we either 1) always call complete_async_reserve before returning, even on cancel, and/or 2) always avoid calling complete_async_reserve if cancellation is requested (user can choose to ignore cancellation token)

Copy link
Member Author

Choose a reason for hiding this comment

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

It won't matter - because if the token is cancelled Rust has already given up waiting for anything to come back through the completion channel, so if something is sent or not, either way it's ignored.

Copy link
Member

Choose a reason for hiding this comment

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

I am saying OperationCanceledException can be thrown from ReserveSlotAsync without it being the result of this cancellation token, hence the when suggestion here.

But in addition to that, remember that Box::into_raw(Box::new(tx)) means that Rust will never drop the tx side of the oneshot until it's Box::from_raw'd in complete_async_reserve. So to not have memory leaks, you probably need to call complete_async_reserve even on cancel, but just give it a user permit of 0 or something.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yeah I get what you mean now

src/Temporalio/Bridge/CustomSlotSupplier.cs Outdated Show resolved Hide resolved
}
}

private unsafe void Reserve(Interop.SlotReserveCtx ctx, void* sender)
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
private unsafe void Reserve(Interop.SlotReserveCtx ctx, void* sender)
private unsafe void Reserve(Interop.SlotReserveCtx* ctx, void* sender)

I would have expected these all to be pointers so they aren't copied (so *const SlotReserveCtx on the Rust side). I think it's clearer that these short-lived Rust-owned things be passed by reference to Rust, so you don't need to re-capture a fixed pointer later.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I was thinking about doing that, makes sense.

{
try
{
var handle = GCHandle.Alloc(cancelTokenSrc);
Copy link
Member

Choose a reason for hiding this comment

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

Is this only freed if CancelReserve is called? Consider inlining the logic of this method with Reserve and freeing this in a finally. Once the try/catch is removed and the need for a fixed pointer is removed (see other comments), it's a short method anyways.

EDIT: Looking at Rust code, it appears CancelReserve is always called even if cancellation doesn't need to occur. Unsure if it's worth adding that comment somewhere on the .NET side. Regardless, so this is technically fine to free inside of callback, though there is still probably clarity in inlining this method.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added comment

/// <param name="ctx">The bridge version of the slot reserve context.</param>
internal SlotReserveContext(Temporalio.Bridge.Interop.SlotReserveCtx ctx)
{
this.SlotType = ctx.slot_type switch
Copy link
Member

Choose a reason for hiding this comment

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

A bit surprised a switch expression works in older .NET (or maybe it doesn't, didn't check CI)

/// <remarks>
/// WARNING: Custom slot suppliers are currently experimental.
/// </remarks>
public interface ICustomSlotSupplier : ISlotSupplier
Copy link
Member

Choose a reason for hiding this comment

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

Could be discussed whether this is better as an abstract class, but I am fine with this too

/// to cancel the operation.</param>
/// <returns>A permit to use the slot which may be populated with your own data.</returns>
/// <exception cref="OperationCanceledException">Cancellation requested.</exception>
public Task<ISlotPermit> ReserveSlotAsync(SlotReserveContext ctx, CancellationToken cancellationToken);
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
public Task<ISlotPermit> ReserveSlotAsync(SlotReserveContext ctx, CancellationToken cancellationToken);
Task<ISlotPermit> ReserveSlotAsync(SlotReserveContext ctx, CancellationToken cancellationToken);

Implied

Copy link
Member

Choose a reason for hiding this comment

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

Arguably this could just not exist and we could accept any object from the user. Alternatively we could just have a sealed SlotPermit that has user data inside it like Java has. I think either would be better than this, because with this there is no easy/default implementation for people who don't care about user data.

Copy link
Member Author

@Sushisource Sushisource Dec 4, 2024

Choose a reason for hiding this comment

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

Yeah, good point about not being able to create something easily. I will go with the Java style way because I don't like the idea of just returning any old object if for no other reason than that it's nice to know what is a permit and what isn't.

There was some reason I had to change this to an interface (it was a class before) that I've forgotten now and I'm sure is going to bite me again, but, I'm sure it's not blocking.

/// <see cref="FixedSizeSlotSupplier"/> and <see cref="ResourceBasedSlotSupplier"/>.
///
/// In order to implement your own slot supplier, you can implement the
/// <see cref="ICustomSlotSupplier"/> interface.
/// </summary>
public interface ISlotSupplier
Copy link
Member

Choose a reason for hiding this comment

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

If you do change ICustomSlotSupplier to an abstract class, this could also be an abstract class with an internal constructor thereby making it where people can't accidentally provide an invalid instance.

Copy link
Member Author

@Sushisource Sushisource Dec 4, 2024

Choose a reason for hiding this comment

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

This one doesn't work so much because the built-in impls are records and thus can't extend from a class, only interfaces.

We could change them to classes, but that's technically breaking, though that's fine at this point and I think is probably worth it.

Copy link
Member

Choose a reason for hiding this comment

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

👍 I don't have a strong opinion. I'm ok leaving this as an interface or doing the compat break on the fixed/resource suppliers.

@Sushisource Sushisource force-pushed the custom-slot-suplier branch 5 times, most recently from 4e356fc to f97d26c Compare December 4, 2024 19:15
Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

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

LGTM, only minor things

{
try
{
ConfiguredTaskAwaitable<Temporalio.Worker.Tuning.SlotPermit> reserveTask;
Copy link
Member

Choose a reason for hiding this comment

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

Never seen this type used. Docs say it's an internal compiler thing. I think it should be Task<.

Copy link
Member Author

@Sushisource Sushisource Dec 5, 2024

Choose a reason for hiding this comment

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

It won't compile as just Task, but I can move the configure await to where it's awaited

Copy link
Member

@cretz cretz Dec 5, 2024

Choose a reason for hiding this comment

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

Yeah, assuming it's because of the unsafe block, I'd say something like this should work:

SlotReserveContext ctx;
unsafe { ctx = ReserveCtxFromBridge((Interop.SlotReserveCtx*)ctx.ToPointer()); }
var permit = await userSupplier.ReserveSlotAsync(ctx, cancelTokenSrc.Token).ConfigureAwait(false);

private unsafe void CancelReserve(void* tokenSrc)
{
var handle = GCHandle.FromIntPtr(new IntPtr(tokenSrc));
var cancelTokenSrc = (CancellationTokenSource)handle.Target!;
Copy link
Member

Choose a reason for hiding this comment

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

To confirm, this can never be called before set_reserve_cancel_target returns and never called after complete_async_reserve returns right?

Copy link
Member Author

Choose a reason for hiding this comment

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

It shouldn't be possible, no

else
{
throw new ArgumentException(
"ISlotSupplier must be one of the types provided by the library");
throw new ArgumentException("ISlotSupplier is an invalid type");
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
throw new ArgumentException("ISlotSupplier is an invalid type");
throw new ArgumentException("Unknown slot supplier");

namespace Temporalio.Worker.Tuning
{
/// <summary>
/// A permit to use a slot for a workflow/activity/local activity task.
Copy link
Member

Choose a reason for hiding this comment

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

Can tell users they can extend this class if they want (unlike what we allowed in Java)

Comment on lines +14 to +17
public SlotPermit()
{
UserData = null;
}
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
public SlotPermit()
{
UserData = null;
}
public SlotPermit() => UserData = null;

Just an FYI, lots of these single-line methods can be shortened. But this is pedantic/unimportant.

Comment on lines 23 to 25
bool IsSticky)
{
}
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
bool IsSticky)
{
}
bool IsSticky);

/// <remarks>
/// WARNING: Custom slot suppliers are currently experimental.
/// </remarks>
public abstract class CustomSlotSupplier : SlotSupplier
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if you have to have a public, parameterless constructor here for accessibility outside the project. The tests package doesn't do a good job of testing this because we set System.Runtime.CompilerServices.InternalsVisibleTo to the test project so it can access some visible-to-testing only things (I forget what). May need just a quick confirmation this can be extended outside the project. Can just be some scratch code in tests/Temporalio.SmokeTests, doesn't have to be committed.

Copy link
Member Author

Choose a reason for hiding this comment

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

It appears to work just fine without it. The bench project is easier to use since it doesn't require nupkg mumbojumbo but also doesn't do internals visible.

@Sushisource Sushisource merged commit 15a3142 into main Dec 5, 2024
8 checks passed
@Sushisource Sushisource deleted the custom-slot-suplier branch December 5, 2024 22:24
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.

2 participants