-
Notifications
You must be signed in to change notification settings - Fork 33
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
Custom slot suplier #372
Conversation
8f822c1
to
0a9de76
Compare
ff2f074
to
23d874b
Compare
Let's merge #376 first then I will rebase |
cancelTokenSrc.Dispose(); | ||
return; | ||
} | ||
catch (OperationCanceledException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
} | ||
} | ||
|
||
private unsafe void Reserve(Interop.SlotReserveCtx ctx, void* sender) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public Task<ISlotPermit> ReserveSlotAsync(SlotReserveContext ctx, CancellationToken cancellationToken); | |
Task<ISlotPermit> ReserveSlotAsync(SlotReserveContext ctx, CancellationToken cancellationToken); |
Implied
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Everything working except cancelling reserves
4e356fc
to
f97d26c
Compare
f97d26c
to
008aa55
Compare
008aa55
to
14c12af
Compare
There was a problem hiding this 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; |
There was a problem hiding this comment.
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<
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
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)
public SlotPermit() | ||
{ | ||
UserData = null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
bool IsSticky) | ||
{ | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool IsSticky) | |
{ | |
} | |
bool IsSticky); |
/// <remarks> | ||
/// WARNING: Custom slot suppliers are currently experimental. | ||
/// </remarks> | ||
public abstract class CustomSlotSupplier : SlotSupplier |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
What was changed
Add user-implementable slot suppliers
Why?
Parity. Cool feature.
Checklist
Closes
How was this tested:
Added tests
Any docs updates needed?
Will need to follow up with docs for custom interface in all langs