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

[NOMERGE] Rust SDK future proposal draft #550

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Sushisource
Copy link
Member

Just serves as a place we can discuss Rust SDK vision before turning it into an actual proposal.

For the community: This (unfortunately, sorry) is not meant to imply we'll be implementing this any time soon - rather it will serve to illustrate where we intend to go once we have decided to take on productionization of the Rust SDK.

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.

Initial thoughts, I may be way off on some but they are easily dismissable. They at least provide alternatives for the proposal to justify not using any them.

impl Workflow for MyWorkflow {
type Input = String;
type Output = u64;
const NAME: &'static str = "MyWorkflowType";
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 this would be better as a static getter, e.g. fn get_name() -> &str (or maybe String since we should only ask once anyways). If you do leave as const, might as well use the #[workflow] attribute.

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, it probably should be, if you need to dynamically construct workflow definitions for whatever reason

type Output = u64;
const NAME: &'static str = "MyWorkflowType";

fn new(input: Self::Input, _ctx: SafeWfContext) -> Self {
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
fn new(input: Self::Input, _ctx: SafeWfContext) -> Self {
fn new(input: Self::Input, _ctx: PureWorkflowContext) -> Self {

Two suggestions:

  • No more abbreviations (boo "Wf")
  • Something besides safe because I fear it's too generic of an adjective. I don't think "Pure" is that good either tbh, I'd have to think on it. But it's basically "CommandsDisallowedWorkflowContext".

Comment on lines +151 to +153
// TODO: The need to box here is slightly unfortunate, but it's either that or require
// users to depend on `async_trait` (which just hides the same thing). IMO this is the
// best option until more language features stabilize and this can go away.
Copy link
Member

Choose a reason for hiding this comment

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

Can you just require run be an async fn?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nah, that's the thing that can't work in a trait yet.

Copy link
Member

Choose a reason for hiding this comment

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

May make it worth doing it attribute style. PyO3 are heavy users of this approach over trait-based (e.g. https://pyo3.rs/v0.18.3/class and https://pyo3.rs/v0.18.3/function).

Copy link
Member Author

Choose a reason for hiding this comment

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

That's just async trait at that point, but if it ends up being useful for other reasons I'm definitely OK with a procmacro

Copy link
Member

@cretz cretz May 12, 2023

Choose a reason for hiding this comment

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

Maybe something like:

#[workflow]
struct MyWorkflow {}

impl MyWorkflow {
    #[workflow_run]
    async fn run(&mut self, some_arg: String) -> TemporalResult<String> {
        // ...
    }
    
    #[workflow_signal]
    async fn my_signal(&mut self, some_arg) {
        // ...
    }
}

Where it all gets interesting is what the caller side looks like in a type safe way which is why I know that traits requiring defined in/out types are helpful.

Copy link
Member

@cretz cretz May 12, 2023

Choose a reason for hiding this comment

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

(and a #[workflow_init] or #[workflow_new] or whatever named attribute on a func that accepts workflow args to create the struct (required if struct cannot be "default" created)...we built this attribute for .NET constructors to allow for readonly field initialization)

Copy link
Member Author

Choose a reason for hiding this comment

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

The #[workflow] attribute would just go over the impl block I think. There's no need for it on the struct definition, and that allows you to just require a new and a run method without them needing separate attributes.

Copy link
Member

@cretz cretz May 12, 2023

Choose a reason for hiding this comment

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

Nice. May be worth exploring at proposal PR time if there aren't too many drawbacks. If caught between two impls that both may kinda work, just list pros/cons of each and then start one of our classic design Zoom calls to pick a winner :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Mega necropost

Preparing for pres at offsite... I'm now pretty convinced #[workflow] attribute macro is the best way to go for sure. Fixes async problem, makes signals/queries automatically work, enforces new & run... It's the way to go. People who want to could still do it all by hand if they hate it for whatever reason

Copy link
Member

Choose a reason for hiding this comment

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

And it's so pretty!


fn run(
&mut self,
ctx: WfContext,
Copy link
Member

@cretz cretz May 12, 2023

Choose a reason for hiding this comment

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

When you do get around to proposal time, I think it'd be worth a discussion of the tradeoff of making this statically accessible via async-local or similar. What I've found in the last two SDKs is that I can just grab the current coroutine scheduler.

Calls like https://docs.rs/tokio/latest/tokio/task/fn.spawn.html#panics are ok assuming the runtime or panicking, I wonder if we can be too. But also I could understand from a function-grouping perspective that having it on a context may be clearer.

One benefit of not having a manually propagated context is that users aren't tempted to store it to call from a query handler or something.

Hrmm, now I wonder if we can have the trait have fn context(&mut self) -> WorkflowContext and fn safe_context(&self) -> SafeWorkflowContext on it. But that doesn't help the new call though.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could. I would be OK with it I think. This feels somewhat more obvious to me, and I like that you don't have these error paths for "called in some context where it doesn't make sense".

fn run(
&mut self,
ctx: WfContext,
) -> BoxFuture<Result<WfExitValue<Self::Output>, anyhow::Error>>;
Copy link
Member

Choose a reason for hiding this comment

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

Is there any way we can avoid forcing anyhow on users here? Can we just have TemporalFailure enum here and some into() or whatever-similar-to-anyhow helper that's clear it converts to ApplicationFailure?

Copy link
Member Author

@Sushisource Sushisource May 12, 2023

Choose a reason for hiding this comment

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

It can certainly be our own thing yes, this is actually addressed briefly in a comment near the top

impl MyWorkflow {
// Attrib commented out since it's nonexistent for now, but that's what it'd look like.
// #[signal]
pub fn my_signal(&mut self, arg: String) {
Copy link
Member

Choose a reason for hiding this comment

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

Can I have the context here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep

self.bar.insert(arg, 1);
}
// #[query]
pub fn my_query(&self, arg: String) -> Option<u64> {
Copy link
Member

Choose a reason for hiding this comment

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

Can I have the "safe" context here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Also yep

ctx.timer(Duration::from_secs(1)).await;
self.foo = 1;
// See activity definitions file
ctx.my_act_fn("Hi!").await.unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

Concern here is how to pass in options. One reason that Java's approach of reusing actual activity signature was so rough is that you were forced to make options a separate thing.

Comment on lines +144 to +147
// The into() is unfortunately unavoidable without making C-A-N and confirm cancel
// be errors instead. Personally, I don't love that and I think it's not idiomatic
// Rust, whereas needing to `into()` something is. Other way would be macros, but
// it's slightly too much magic I think.
Copy link
Member

Choose a reason for hiding this comment

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

Concerning the into(), I wonder if maybe instead of Result<WfExitValue<Self::Output>, anyhow::Error> you have Result<Self::Output, TemporalNonSuccess> of some form. That way Ok only represents literal success, not cancellation or continue as new.

I think it's worth optimizing for the normal/happy path of Ok being the literal result and all else being the Err.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem is CAN is a form of success. The semantics of it are my real issue. TemporalNonSuccess as a name is a perfect example. CAN isn't "NonSuccess".

Now, to be fair, currently I have WillRespondAsync as an error in activities. My rationale is that's less common than CAN. I get optimizing for the happy path, but into is easy and the compiler error will make it super obvious if the user hasn't done it.

I could live with it, for sure, but it just doesn't feel correct to me or particularly idiomatic for Rust

Copy link
Member

@cretz cretz May 12, 2023

Choose a reason for hiding this comment

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

Since continue as new has to be invoked top level to use this type anyways, I wonder if you can make it a macro that returns (so they don't have to ? or Err it), e.g. continue_as_new!(). That can kinda hide it. Otherwise, I agree that continue as new and cancel aren't technically errors, it's just a bit rough on the simple path. I can see both arguments here.

bar: HashMap<String, u64>,
}

impl Workflow for MyWorkflow {
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
impl Workflow for MyWorkflow {
#[workflow]
impl Workflow {

I wonder if there's value in not requiring trait impl and instead marking the run method with #[workflow_run] to extract input/output types. Only reason I was thinking this is because I kinda like that I can put my signal/query/update functions in the same impl block as my run stuff. But otherwise, I see how it hurts new() (unless you put the attribute there too).

Copy link
Member Author

Choose a reason for hiding this comment

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

People are pretty used to multiple impl blocks in Rust. That said, I can get behind a procmacro like this too if it's providing value like auto-naming. It just needs to not do too much magic, because messing up IDEs etc is where things get really bad.

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