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

Workflow/Activity Priorities #802

Merged
merged 16 commits into from
Mar 28, 2025
Merged

Workflow/Activity Priorities #802

merged 16 commits into from
Mar 28, 2025

Conversation

Sushisource
Copy link
Member

What was changed

Updated Core
Expose priority setting on workflow client & in child/activities launched from workflows

Why?

Priorities are cool!

Checklist

  1. Closes [Feature Request] Support Priority Annotations #760

  2. How was this tested:
    Added integ test

  3. Any docs updates needed?

@Sushisource Sushisource requested a review from a team as a code owner March 26, 2025 23:27

@activity.defn
async def check_priority_activity(should_have_priorty: int) -> str:
assert activity.info().priority.priority_key == should_have_priorty
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "priorty"

@@ -9,6 +10,7 @@ COPY ./ ./
RUN mkdir -p ./temporalio/api
RUN uv sync --all-extras
RUN uv add "protobuf<4"
RUN uv sync --all-extras
Copy link
Contributor

Choose a reason for hiding this comment

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

We can delete line 11 right?

Copy link
Member Author

@Sushisource Sushisource Mar 27, 2025

Choose a reason for hiding this comment

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

Probably Yes

@@ -456,6 +460,7 @@ async def start_workflow(
rpc_timeout: Optional[timedelta] = None,
request_eager_start: bool = False,
stack_level: int = 2,
priority: Optional[temporalio.common.Priority] = None,
Copy link
Member

Choose a reason for hiding this comment

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

Need to apply this to all execute_workflow calls too

@@ -456,6 +460,7 @@ async def start_workflow(
rpc_timeout: Optional[timedelta] = None,
request_eager_start: bool = False,
stack_level: int = 2,
priority: Optional[temporalio.common.Priority] = None,
Copy link
Member

Choose a reason for hiding this comment

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

Also probably need this on ScheduleActionStartWorkflow unless we're intentionally leaving off that for now

(more will be added here later)
"""

priority_key: int = 0
Copy link
Member

@cretz cretz Mar 27, 2025

Choose a reason for hiding this comment

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

I don't think this field should have a default value at this time, meaning the idea of creating Priority() without any params doesn't make sense at this time (but we can make it default-able later as more fields come about).

Also, even though proto and Go don't have the ability to model optional, we do, so we can treat zero as Optional here which may make sense. Of course we can still accept 0 or None to mean None, but when relaying to a user, I think None makes more sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I can agree with that

@@ -116,6 +116,7 @@ class Info:
workflow_namespace: str
workflow_run_id: str
workflow_type: str
priority: temporalio.common.Priority
Copy link
Member

Choose a reason for hiding this comment

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

Is priority always non-null in proto for activity task and workflow start event?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it's not, but we decided that just means default. Not personally a huge fan of that, but, the information conveyed is the same temporalio/sdk-java#2453 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

Works for me. When it comes to us passing it in, does server care whether we send a null priority or a non-null one with default values? The reason I ask about that direction is because if we want always output to be non-Optional, we might as well never accept Optional input and just default to a fixed immutable Priority.default. So there's no such thing as Optional[Priority] anywhere (kinda touched on in another comment).

inherit Priority from the workflow that created them, but may override fields when they are
started or modified. For each field of a Priority on an activity/workflow, not present or equal
to zero/empty string means to inherit the value from the calling workflow, or if there is no
calling workflow, then use the default (documented below).
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
calling workflow, then use the default (documented below).
calling workflow, then use the default (documented on the field).

"""

@staticmethod
def from_proto(proto: temporalio.api.common.v1.Priority) -> Priority:
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
def from_proto(proto: temporalio.api.common.v1.Priority) -> Priority:
def _from_proto(proto: temporalio.api.common.v1.Priority) -> Priority:

If _to_proto is private, this should be too IMO (I know some structures like RetryPolicy we chose to expose both, but I like the idea of not exposing until needed)

@@ -456,6 +460,7 @@ async def start_workflow(
rpc_timeout: Optional[timedelta] = None,
request_eager_start: bool = False,
stack_level: int = 2,
priority: Optional[temporalio.common.Priority] = None,
Copy link
Member

@cretz cretz Mar 27, 2025

Choose a reason for hiding this comment

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

Suggested change
priority: Optional[temporalio.common.Priority] = None,
priority: temporalio.common.Priority = temporalio.common.Priority.default,

If priority is never null when returned from the server, arguably it could be more idiomatic to do this from a user POV (i.e. make a default class var on the Priority with the default form)

Copy link
Member Author

Choose a reason for hiding this comment

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

I dunno, I like the explicit nature of either providing it or not. Plus just on a practical level I think it makes the code that is accepting it simpler to just check None and not set it at all.

Copy link
Member

Choose a reason for hiding this comment

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

I like the explicit nature of either providing it or not

Except when it comes to accessing it as a user, where we don't like that. So we say interceptor outbound input has this optional but interceptor inbound does not which loses symmetry a bit I think. Isn't it true that Priority() and None effectively mean the same thing?

Copy link
Member Author

Choose a reason for hiding this comment

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

But this is almost exactly contradictory with your suggestion to make the key itself optional. Now, either way, the user needs to see if that was set or not. Having it be optional here also means they have the most obvious way to determine if they explicitly set it, or not. The asymmetry is kind of a good thing I think, because on outbound they get to choose, but on inbound they don't, so it expresses that.

Copy link
Member

@cretz cretz Mar 28, 2025

Choose a reason for hiding this comment

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

But this is almost exactly contradictory with your suggestion to make the key itself optional

Not really. I support bidirectional optional-ality if you will. Basically if it is optional when they provide it it should be optional when they read it. Basically when we can (which is not always with server-side/calculated defaults), we either use the language construct to represent default-as-unset or we don't, but half of the time default being an empty object and the other half of the time it being None was a bit confusing to me while reading. The user only uses this parameter when they are changing the parameter, so the default is in effect more of a documentation thing (though there is the interceptor component). It is confusing that I can't expect the default on output be the default on input.

Having said that, my opinion is not strong here nor is it blocking, but I think it may affect a couple of other languages that can have smart parameter defaults like .NET and Ruby.

@@ -1878,6 +1883,7 @@ def start_activity(
cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL,
activity_id: Optional[str] = None,
versioning_intent: Optional[VersioningIntent] = None,
priority: Optional[temporalio.common.Priority] = None,
Copy link
Member

Choose a reason for hiding this comment

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

Don't forget start_activity_class, execute_activity_class, start_activity_method, and execute_activity_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.

Done. There's a lot of those!

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.

Nothing blocking, though would like default input to be same as default output if possible from a logic POV, but non-blocking.

@@ -711,6 +722,7 @@ async def execute_workflow(
rpc_metadata: Mapping[str, str] = {},
rpc_timeout: Optional[timedelta] = None,
request_eager_start: bool = False,
priority: Optional[temporalio.common.Priority] = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

execute_workflow isn't passing the new parameter on to start_workflow.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you

@@ -2439,6 +2451,7 @@ def __init__(
start_delay: Optional[timedelta] = None,
rpc_metadata: Mapping[str, str] = {},
rpc_timeout: Optional[timedelta] = None,
priority: Optional[temporalio.common.Priority] = None,
) -> None: ...

Copy link
Contributor

Choose a reason for hiding this comment

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

Missed an overload (l. 2484)

@@ -3965,6 +4025,7 @@ async def start_child_workflow(
UI/CLI. This can be in Temporal markdown format and can span multiple lines. This is
a fixed value on the workflow that cannot be updated. For details that can be
updated, use :py:meth:`Workflow.get_current_details` within the workflow.
priority: Priority to use for this workflow.
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not passing the parameter on to workflow_start_child_workflow here


The default priority is (min+max)/2. With the default max of 5 and min of 1, that comes out to
3.
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be useful to users to validate in Python, rather than waiting for an error from the server. Especially since they may think they can use floats, or 0, etc. Since this is a frozen data class it'll be sufficient to validate in __post_init__, e.g.

    def __post_init__(self):
        if self.priority_key is not None:
            if not isinstance(self.priority_key, int):
                raise TypeError("Priority key must be an integer")
            if self.priority_key < 1:
                raise ValueError("Priority key must be a positive integer")

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 that's cheap and easy, I will add

@Sushisource Sushisource enabled auto-merge (squash) March 28, 2025 18:20
@Sushisource Sushisource merged commit bf747f1 into main Mar 28, 2025
14 checks passed
@Sushisource Sushisource deleted the priority branch March 28, 2025 20:58
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] Support Priority Annotations
3 participants