diff --git a/docs/develop/task-queue-priority-fairness.mdx b/docs/develop/task-queue-priority-fairness.mdx new file mode 100644 index 0000000000..5c16d31f26 --- /dev/null +++ b/docs/develop/task-queue-priority-fairness.mdx @@ -0,0 +1,593 @@ +--- +id: task-queue-priority-fairness +title: Task Queue Priority and Fairness +sidebar_label: Task queue priority and fairness +description: How the Task Queue Priority and Fairness features can be used. +toc_max_heading_level: 4 +keywords: + - task queue + - task queue priority + - task queue fairness + - task queue priority and fairness +tags: + - Task Queues + - Priority and Fairness +--- + +import { EnlargeImage } from '@site/src/components'; +import SdkTabs from '@site/src/components'; + +[Task Queue Priority](#task-queue-priority) and [Task Queue Fairness](#task-queue-fairness) are two ways to manage the +distribution of work within Task Queues. Priority focuses on how Tasks are prioritized within a single Task Queue. +Fairness aims to prevent one set of Tasks from blocking others within the same priority level. + +You can use Priority and Fairness individually within your Task Queues or you can use them together for more complex +scenarios. + +:::tip Support, stability, and dependency info + +Priority and Fairness is currently in +[Public Preview](/evaluate/development-production-features/release-stages#public-preview). Fairness will be a paid +feature in Temporal Cloud and billing will be enabled in the near future. + +::: + +## Task Queue Priority + +**Task Queue Priority** lets you control the execution order of Workflows, Activities, and Child Workflows based on +assigned priority values within a _single_ Task Queue. Each priority level acts as a sub-queue that prioritizes Tasks +within a single Task Queue. + + + +### When to use Priority + +If you need a way to specify the order your Tasks execute in, you can use Priority to manage that. Priority lets you +differentiate between your Tasks, like batch and real-time Tasks, so that you can allocate resources more effectively. +You can also use this as a way to run urgent Tasks immediately and override others. For example, if you are running an +e-commerce platform, you may want to process payment related Tasks before less time-sensitive Tasks like internal +inventory management. + +### How to use Priority + +Priority is avaliable for both self-hosted Temporal instances and Temporal Cloud and it will be automatically enabled. +If you apply priority keys, the feature will be in use. + +You can select a priority level by setting the _priority key_ to a value within the integer range `[1,5]`. A lower value +implies higher priority, so `1` is the highest priority level. If you don't specify a Priority, a Task defaults to a +Priority of `3`. Activities will inherit the priority level of their Workflow if a separate Activity priority level +isn't set. + +When you set a priority level within your Task Queues, this means that they will **all** be processed in priority order. +For example, all of your priority level `1` Tasks will execute before your priority level `2` Tasks and so on. So your +lower priority Tasks will be blocked until the higher priority Tasks finish running. Tasks with the same priority level +are scheduled to run in first-in-first-out (FIFO) order. If you need more flexibility to allocate resources to Tasks of +the same type, like processing payments for multiple e-commerce platforms, check out +[the Fairness section](#task-queue-fairness). + +Choose your SDK below to see an example of setting priority for your Workflows: + + + +```go +workflowOptions := client.StartWorkflowOptions{ + ID: "my-workflow-id", + TaskQueue: "my-task-queue", + Priority: temporal.Priority{PriorityKey: 5}, +} +we, err := c.ExecuteWorkflow(context.Background(), workflowOptions, MyWorkflow) +``` + + +```java +WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("my-task-queue") + .setPriority(Priority.newBuilder().setPriorityKey(5).build()) + .build(); + +WorkflowClient client = WorkflowClient.newInstance(service); MyWorkflow workflow = +client.newWorkflowStub(MyWorkflow.class, options); workflow.run(); + +```` + + +```python +await client.start_workflow( + MyWorkflow.run, + args="hello", + id="my-workflow-id", + task_queue="my-task-queue", + priority=Priority(priority_key=1), +) +```` + + + +```csharp +var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync("hello"), + new StartWorkflowOptions( + id: "my-workflow-id", + taskQueue: "my-task-queue" + ) + { + Priority = new Priority(1), + } +); +``` + + + +Choose your SDK below to see an example of setting priority for your Activities: + + + +```go +ao := workflow.ActivityOptions{ + StartToCloseTimeout: time.Minute, + Priority: temporal.Priority{PriorityKey: 3}, +} +ctx := workflow.WithActivityOptions(ctx, ao) +err := workflow.ExecuteActivity(ctx, MyActivity).Get(ctx, nil) +``` + + +```java +ActivityOptions options = ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofMinutes(1)) + .setPriority(Priority.newBuilder().setPriorityKey(3).build()) + .build(); + +MyActivity activity = Workflow.newActivityStub(MyActivity.class, options); activity.perform(); + +```` + + +```python +await workflow.execute_activity( + say_hello, + "hi", + priority=Priority(priority_key=3), + start_to_close_timeout=timedelta(seconds=5), +) +```` + + + + + +```csharp +await Workflow.ExecuteActivityAsync( + () => SayHello("hi"), + new() + { + StartToCloseTimeout = TimeSpan.FromSeconds(5), + Priority = new(3), + } + ); +``` + + + +Choose your SDK below to see an example of setting priority for your Child Workflows: + + + +```go +cwo := workflow.ChildWorkflowOptions{ + WorkflowID: "child-workflow-id", + TaskQueue: "child-task-queue", + Priority: temporal.Priority{PriorityKey: 1}, +} +ctx := workflow.WithChildOptions(ctx, cwo) +err := workflow.ExecuteChildWorkflow(ctx, MyChildWorkflow).Get(ctx, nil) +``` + + +```java +ChildWorkflowOptions childOptions = ChildWorkflowOptions.newBuilder() + .setTaskQueue("child-task-queue") + .setWorkflowId("child-workflow-id") + .setPriority(Priority.newBuilder().setPriorityKey(1).build()) + .build(); + +MyChildWorkflow child = Workflow.newChildWorkflowStub(MyChildWorkflow.class, childOptions); child.run(); + +```` + + +```python +await workflow.execute_child_workflow( + MyChildWorkflow.run, + args="hello child", + priority=Priority(priority_key=1), +) +```` + + + +```csharp +await Workflow.ExecuteChildWorkflowAsync( + (MyChildWorkflow wf) => wf.RunAsync("hello child"), + new() { Priority = new(1) } +); +``` + + + +## Task Queue Fairness + +Task Queue Fairness lets you distribute Tasks based on _fairness keys_ and their _fairness weight_ within a Task Queue. +The fairness keys are used to describe your Task structure. Each fairness key creates its own "virtual queue" within a +Task Queue, allowing you to organize Tasks into logical groups like tenants, applications, or workload types. + +These virtual queues operate using a round-robin dispatch mechanism, meaning the system cycles through each fairness key +in turn when selecting the next Task to dispatch. The round-robin approach essentially prevents any single fairness key +from hogging Worker capacity. This ensures that Tasks from one fairness key can't completely block Tasks from other +keys, even if one key has a much larger backlog than the others. + +When Workers become available, each fairness key gets an opportunity to dispatch Tasks rather than processing all Tasks +from one key before moving to the next. This creates a fairer distribution where smaller tenants or lower-volume +workloads still receive regular processing time, rather than waiting behind higher volume operations. + +The Tasks associated with each fairness key can be dispatched based on the _fairness weight_ that has been assigned to +the key. There should only be one fairness weight assigned to each fairness key within a single Task Queue. Having +multiple fairness weights on a fairness key will result in unspecific behavior. + +Using the fairness keys and their corresponding fairness weights lets you define levels with weighted capacities. For +example, you can have free, basic, and premium levels and Fairness makes sure that an influx of premium Tasks don't +overwhelm your resources and block free and basic Tasks. + +:::info + +When you start using fairness keys, it switches your active Task Queues to fairness mode. That means new Tasks are +created with Fairness and the existing queued Tasks get processed before any of the new ones. + +::: + +### When to use Fairness + +Fairness is intended to address common situations like: + +- Multi-tenant applications with big and small tenants where small tenants shouldn't be blocked by big ones. +- Assigning Tasks to different capacity bands and then, for example, dispatching 80% from one band and 20% from another + without limiting overall capacity when one band is empty. + +It sequences Tasks in the Task Queue probabilistically using a weighted distribution based on: + +- Fairness weights you set +- The current backlog of Tasks +- A data structure that tracks how you've distributed Tasks for different fairness keys + +As an example, imagine a workload with three tenants, _tenant-big_, _tenant-mid_, _tenant-small_, that have varying +numbers of Tasks at all times. Your _tenant-big_ has a large number of Tasks that can overwhelm your Task Queue and +prevent _tenant-mid_ and _tenant-small_ from running their Tasks. With Fairness, you can give each tenant a different +fairness key to make sure _tenant-big_ doesn't use all of the Task Queue resources and block the others. + +In this case, _tenant-mid_ and _tenant-small_ will have Tasks run in between _tenant-big_ Tasks so that they are +executed "fairly". Although, if all your Tasks can be dispatched immediately, then you don't need to use fairness. + +This same scenario can apply to batch jobs where certain jobs run more often than others or processing orders from +multiple vendors where several vendors have the majority of the orders. + +Fairness applies at Task dispatch time based on information about the Tasks passing through the Task Queue and considers +each Task as having equal cost until dispatch. It doesn't consider any Task execution that is currently being done by +Workers. So if you look at Tasks being processed by Workers, you might not see "fairness" across tenants. For example, +if you already have Tasks from _tenant-big_ being processed, when Tasks for _tenant-small_ are dispatched it may still +look like _tenant-big_ is using the most resources. + +There are two ways to use Task Queue Fairness: without Priority and with Priority. + +### How Fairness works + +If you use Fairness without Priority, Tasks with different fairness keys will use a weighted distribution based on the +fairness weights to allocate resources in the Task Queue. For example, say you have three fairness keys to describe +customer tiers: _free-tier_, _basic-tier_, and _premium-tier_. You give _premium-tier_ a fairness weight of 5.0, +_basic-tier_ a fairness weight of 3.0, and _free-tier_ a fairness weight of 2.0. With Fairness, that means 50% of the +time _premium-tier_ Tasks dispatch, 30% of the time _basic-tier_ Tasks dispatch, and 20% of the time _free-tier_ Tasks +dispatch from the Task Queue backlog. + +If there are Tasks in the Task Queue backlog that have the same fairness key, then they're dispatched in +[FIFO order](/task-queue#task-ordering). + +This is how you are able to ensure that one tier doesn't use all the resources and block other Tasks in the Task Queue +backlog from dispatching. + +When you update fairness keys or fairness weights, the Task Queues will only reflect these changes for Tasks that +haven't dispatched yet. + + + +### Using Fairness and Priority together + +When you use Fairness and Priority together, Priority determines which sub-queue Tasks go into. A single Task Queue with +Priority implemented will have different sub-queues based on priority levels. Fairness will apply to the Tasks within +each priority level. + + + +### How to use Fairness + +Fairness is avaliable for both self-hosted Temporal instances and Temporal Cloud. + +If you start using _fairness keys_ in your API calls, it will automatically be enabled in Temporal Cloud. + +If you're self-hosting Temporal, use the latest pre-release development server and set `matching.useNewMatcher` and +`matching.enableFairness` to `true` in the +[dynamic config](https://github.com/temporalio/temporal/blob/a3a53266c002ae33b630a41977274f8b5b587031/common/dynamicconfig/constants.go#L1345-L1348) +on the relevant Task Queues or Namespaces. You'll also need to set `matching.enableMigration` to `true` in order to +support draining Tasks in existing backlogs after Fairness is enabled. + +Enabling `matching.useNewMatcher` and `matching.enableFairness` is only applicable for self-hosted Temporal instances. +There is a toggle coming to Temporal Cloud soon to enable Priority and Fairness at the Namespace level. + +:::info + +Fairness will be a paid feature in Temporal Cloud and billing will be enabled in the near future. You will be notified +before billing is enabled for your Namespaces. You will be able to enable or disable Fairness at the Namespace level and +billing will be disabled at the next calendar hour after it is disabled. + +::: + +Choose your SDK below to see an example of setting fairness for your Workflows: + + + +```go +workflowOptions := client.StartWorkflowOptions{ + ID: "my-workflow-id", + TaskQueue: "my-task-queue", + Priority: temporal.Priority{ + PriorityKey: 1, + FairnessKey: "a-key", + FairnessWeight: 3.14, + }, +} +we, err := c.ExecuteWorkflow(context.Background(), workflowOptions, MyWorkflow) +``` + + +```java +WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("my-task-queue") + .setPriority(Priority.newBuilder().setPriorityKey(5).setFairnessKey("a-key").setFairnessWeight(3.14).build()) + .build(); +WorkflowClient client = WorkflowClient.newInstance(service); +MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); +workflow.run(); +``` + + +```python +await client.start_workflow( + MyWorkflow.run, + args="hello", + id="my-workflow-id", + task_queue="my-task-queue", + priority=Priority(priority_key=3, fairness_key="a-key", fairness_weight=3.14), +) +``` + + +```ruby +client.start_workflow( + MyWorkflow, "input-arg", + id: "my-workflow-id", + task_queue: "my-task-queue", + priority: Temporalio::Priority.new( + priority_key: 3, + fairness_key: "a-key", + fairness_weight: 3.14 + ) +) +``` + + +```ts +const handle = await startWorkflow(workflows.priorityWorkflow, { + args: [false, 1], + priority: { priorityKey: 3, fairnessKey: 'a-key', fairnessWeight: 3.14 }, +}); +``` + + +```csharp +var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync("hello"), + new StartWorkflowOptions( + id: "my-workflow-id", + taskQueue: "my-task-queue" + ) + { + Priority = new Priority( + priorityKey: 3, + fairnessKey: "a-key", + fairnessWeight: 3.14 + ) + } +); +``` + + + +Choose your SDK below to see an example of setting fairness for your Activities: + + + +```go +ao := workflow.ActivityOptions{ + StartToCloseTimeout: time.Minute, + Priority: temporal.Priority{ + PriorityKey: 1, + FairnessKey: "a-key", + FairnessWeight: 3.14, + }, +} +ctx := workflow.WithActivityOptions(ctx, ao) +err := workflow.ExecuteActivity(ctx, MyActivity).Get(ctx, nil) +```` + + +```java +ActivityOptions options = ActivityOptions.newBuilder() + .setStartToCloseTimeout(Duration.ofMinutes(1)) + .setPriority(Priority.newBuilder().setPriorityKey(3).setFairnessKey("a-key").setFairnessWeight(3.14).build()) + .build(); +MyActivity activity = Workflow.newActivityStub(MyActivity.class, options); +activity.perform(); +```` + + + +```python +await workflow.execute_activity( + say_hello, + "hi", + priority=Priority(priority_key=3, fairness_key="a-key", fairness_weight=3.14), + start_to_close_timeout=timedelta(seconds=5), +) +``` + + +```ruby +client.start_activity( + MyActivity, "input-arg", + id: "my-workflow-id", + task_queue: "my-task-queue", + priority: Temporalio::Priority.new( + priority_key: 3, + fairness_key: "a-key", + fairness_weight: 3.14 + ) +) +``` + + +```ts +const handle = await startWorkflow(workflows.priorityWorkflow, { + args: [false, 1], + priority: { priorityKey: 3, fairnessKey: 'a-key', fairnessWeight: 3.14 }, +}); +``` + + +```csharp +var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync("hello"), + new StartWorkflowOptions( + id: "my-workflow-id", + taskQueue: "my-task-queue" + ) + { + Priority = new Priority( + priorityKey: 3, + fairnessKey: "a-key", + fairnessWeight: 3.14 + ) + } +); +``` + + + +Choose your SDK below to see an example of setting fairness for your Child Workflows: + + + +```go +cwo := workflow.ChildWorkflowOptions{ + WorkflowID: "child-workflow-id", + TaskQueue: "child-task-queue", + Priority: temporal.Priority{ + PriorityKey: 1, + FairnessKey: "a-key", + FairnessWeight: 3.14, + }, +} +ctx := workflow.WithChildOptions(ctx, cwo) +err := workflow.ExecuteChildWorkflow(ctx, MyChildWorkflow).Get(ctx, nil) +``` + + +```java +ChildWorkflowOptions childOptions = ChildWorkflowOptions.newBuilder() + .setTaskQueue("child-task-queue") + .setWorkflowId("child-workflow-id") + .setPriority(Priority.newBuilder().setPriorityKey(1).setFairnessKey("a-key").setFairnessWeight(3.14).build()) + .build(); +MyChildWorkflow child = Workflow.newChildWorkflowStub(MyChildWorkflow.class, childOptions); +child.run(); +``` + + +```python +await workflow.execute_child_workflow( + MyChildWorkflow.run, + args="hello child", + priority=Priority(priority_key=3, fairness_key="a-key", fairness_weight=3.14), +) +``` + + +```ruby +client.start_child_workflow( + MyChildWorkflow, "input-arg", + id: "my-child-workflow-id", + task_queue: "my-task-queue", + priority: Temporalio::Priority.new( + priority_key: 3, + fairness_key: "a-key", + fairness_weight: 3.14 + ) +) +``` + + +```ts +const handle = await startChildWorkflow(workflows.priorityWorkflow, { + args: [false, 1], + priority: { priorityKey: 3, fairnessKey: 'a-key', fairnessWeight: 3.14 }, +}); +``` + + +```csharp +var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync("hello"), + new StartWorkflowOptions( + id: "my-workflow-id", + taskQueue: "my-task-queue" + ) + { + Priority = new Priority( + priorityKey: 3, + fairnessKey: "a-key", + fairnessWeight: 3.14 + ) + } +); +``` + + + +### Limitations of Fairness + +- There isn't a limit on the number of fairness keys you can use, but their accuracy can degrade as you add more. +- Task Queues are internally [partitioned](/task-queue#task-ordering) and Tasks are distributed to partitions randomly. + This could interfer with fairnesss. Depending on your use case, you can reach out to Temporal Support to get your Task + Queues set to a single partition. +- When you use Worker Versioning and you're moving Workflows from one version to another, Priority will still apply + between versions. Fairness isn't guaranteed between versions. For example, you may have Tasks that were originally + queued on Worker version _alpha_, Tasks that were queued on Worker version _beta_, and some Tasks were moved from + _alpha_ to _beta_. Fairness is only guaranteed when Tasks are originally queued on the same Worker version. So there + might be some discrepancies on the Tasks moved from _alpha_ to _beta_. +- Fairness stats are not persisted, so server deployments can cause temporary lack of Fairness and misordered tasks. diff --git a/sidebars.js b/sidebars.js index 567a21a3fd..19efef9390 100644 --- a/sidebars.js +++ b/sidebars.js @@ -331,6 +331,7 @@ module.exports = { 'develop/worker-performance', 'develop/safe-deployments', 'develop/plugins-guide', + 'develop/task-queue-priority-fairness', ], }, { diff --git a/src/components/elements/SdkTabs/index.js b/src/components/elements/SdkTabs/index.js index 8c96491ec4..e9391abd34 100644 --- a/src/components/elements/SdkTabs/index.js +++ b/src/components/elements/SdkTabs/index.js @@ -47,7 +47,8 @@ const SdkTabs = ({ children, hideUnsupportedLanguages = false }) => { {tabLanguages.map(({ key, icon: Icon, label }) => ( }> {contentMap[key] || ( -
+ // Setting the text color to black for better readability on light yellow background in dark mode +
{label} example coming soon.
)} diff --git a/src/components/images/EnlargeImage.js b/src/components/images/EnlargeImage.js new file mode 100644 index 0000000000..5bace07f2c --- /dev/null +++ b/src/components/images/EnlargeImage.js @@ -0,0 +1,13 @@ +const EnlargeImage = ({ src, alt, ariaLabel }) => { + const label = ariaLabel || alt; + + return ( +
+ + {label} + +
+ ); +}; + +export default EnlargeImage; diff --git a/src/components/index.js b/src/components/index.js index 54dba7ea8e..b86e66e35b 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -13,6 +13,7 @@ export { default as JsonTable } from './formatting/JsonTable'; // Image components export { default as CaptionedImage } from './images/CaptionedImage'; export { default as ZoomingImage } from './images/ZoomingImage'; +export { default as EnlargeImage } from './images/EnlargeImage'; // Information components export { default as DiscoverableDisclosure } from './info/DiscoverableDisclosure'; diff --git a/static/img/develop/task-queue-priority-fairness/fairness-details.png b/static/img/develop/task-queue-priority-fairness/fairness-details.png new file mode 100644 index 0000000000..3444f75914 Binary files /dev/null and b/static/img/develop/task-queue-priority-fairness/fairness-details.png differ diff --git a/static/img/develop/task-queue-priority-fairness/priority-details.png b/static/img/develop/task-queue-priority-fairness/priority-details.png new file mode 100644 index 0000000000..d99a8f10f1 Binary files /dev/null and b/static/img/develop/task-queue-priority-fairness/priority-details.png differ diff --git a/static/img/develop/task-queue-priority-fairness/priority-fairness.png b/static/img/develop/task-queue-priority-fairness/priority-fairness.png new file mode 100644 index 0000000000..9c6d6ba704 Binary files /dev/null and b/static/img/develop/task-queue-priority-fairness/priority-fairness.png differ