From 54bbbba85e89b5ef0e028c9ed4e3e96e67a88818 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Fri, 27 Mar 2026 18:11:15 -0500 Subject: [PATCH 1/2] Add Priority and Fairness interactive walkthrough Co-Authored-By: Claude Sonnet 4.6 --- .../develop/priority-fairness-walkthrough.mdx | 240 ++++++ sidebars.js | 30 +- .../elements/PriorityFairnessSimulator.js | 702 ++++++++++++++++++ .../priority-fairness-simulator.module.css | 495 ++++++++++++ src/components/index.js | 1 + 5 files changed, 1457 insertions(+), 11 deletions(-) create mode 100644 docs/develop/priority-fairness-walkthrough.mdx create mode 100644 src/components/elements/PriorityFairnessSimulator.js create mode 100644 src/components/elements/priority-fairness-simulator.module.css diff --git a/docs/develop/priority-fairness-walkthrough.mdx b/docs/develop/priority-fairness-walkthrough.mdx new file mode 100644 index 0000000000..bbc5b0ea81 --- /dev/null +++ b/docs/develop/priority-fairness-walkthrough.mdx @@ -0,0 +1,240 @@ +--- +id: priority-fairness-walkthrough +title: Task Queue Priority and Fairness — Interactive Demo +sidebar_label: Priority and Fairness (Interactive) +description: Interactively explore how Task Queue Priority and Fairness control dispatch order. Load a preset scenario, step through dispatches, and see exactly which task gets picked next — and why. +toc_max_heading_level: 3 +keywords: + - task queue priority + - task queue fairness + - priority key + - fairness key + - interactive demo +tags: + - Task Queues + - Priority and Fairness +--- + +import { PriorityFairnessSimulator } from '@site/src/components'; +import SdkTabs from '@site/src/components'; + +When multiple workloads compete for Workers, Task Queue Priority determines which tasks get picked first, while Task Queue Fairness ensures no single workload can crowd out the rest. + +
+ +
+
+ + + + +Priority +
+ +Every task carries a `priorityKey` from `1` (critical) to `5` (batch), with `3` as the default. When a Worker polls, it always picks the lowest-numbered task first regardless of arrival time. This lets you share a single Worker pool across very different workloads and guarantee that time-sensitive work never waits behind low-urgency jobs. + +
+ +
+
+ + + + +Fairness +
+ +Without Fairness, tasks at the same priority dispatch strictly FIFO, so a backlog-heavy tenant can block everyone else at that level indefinitely. Fairness groups tasks by `fairnessKey` and dispatches proportionally by `fairnessWeight`. A key with weight `5` gets roughly 5x more dispatches than a key with weight `1`, but no key is ever completely locked out. + +
+ +
+ +Use both together when you need SLA ordering across workload types and fair distribution across tenants within each tier. + +
+ {[['P1','Critical','#ef4444','#fff'],['P2','High','#f97316','#fff'],['P3','Normal (default)','#3b82f6','#fff'],['P4','Low','#22c55e','#000'],['P5','Batch','#94a3b8','#fff']].map(([p,l,bg,fg]) => ( +
+ {p} · {l} +
+ ))} +
+ +## Try it + +Load a preset or build your own queue. Use **Step ->** to dispatch one task at a time and watch which task gets picked next, or **Dispatch All** to see the full order at once. + + + +--- + +## How it works + +When a Worker polls for the next task, Temporal applies two rules in sequence: + +1. **Priority first** - the task with the lowest `priorityKey` wins. If there are tasks at priority `1`, none of the `2`s will dispatch until all `1`s are gone. +2. **Fairness within a tier** - when multiple tasks share the same priority level but carry different `fairnessKey` values, Temporal tracks how many tasks each key has received relative to its weight and dispatches from the key that is furthest behind its expected share. + +If no `fairnessKey` is set, tasks within a priority level dispatch in FIFO order. + +### When to use Priority vs Fairness + +| Scenario | Use | +|---|---| +| Payments should never wait behind inventory syncs | **Priority** | +| Premium users shouldn't be blocked by a single large tenant | **Fairness** | +| SLAs differ across customer tiers and tenants vary in volume | **Both** | + +--- + +## Setup + +Priority and Fairness are enabled automatically - no configuration required. Just set `priorityKey`, `fairnessKey`, or both when starting Workflows or Activities. + +For self-hosted Temporal, set `matching.useNewMatcher` to `true` in dynamic config. To enable Fairness, also set `matching.enableFairness: true`. + +--- + +## SDK examples + +### Workflow - Priority only + + + +```go +workflowOptions := client.StartWorkflowOptions{ + ID: "my-workflow-id", + TaskQueue: "my-task-queue", + Priority: temporal.Priority{PriorityKey: 1}, +} +we, err := c.ExecuteWorkflow(ctx, workflowOptions, MyWorkflow) +``` + + +```java +WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("my-task-queue") + .setPriority(Priority.newBuilder().setPriorityKey(1).build()) + .build(); +MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); +workflow.run(); +``` + + +```python +await client.start_workflow( + MyWorkflow.run, + id="my-workflow-id", + task_queue="my-task-queue", + priority=Priority(priority_key=1), +) +``` + + +```ts +const handle = await client.workflow.start(MyWorkflow, { + workflowId: "my-workflow-id", + taskQueue: "my-task-queue", + priority: { priorityKey: 1 }, +}); +``` + + +```csharp +var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync(), + new StartWorkflowOptions("my-workflow-id", "my-task-queue") + { + Priority = new Priority(priorityKey: 1), + } +); +``` + + + +### Workflow - Priority + Fairness + + + +```go +workflowOptions := client.StartWorkflowOptions{ + ID: "my-workflow-id", + TaskQueue: "my-task-queue", + Priority: temporal.Priority{ + PriorityKey: 1, + FairnessKey: "tenant-acme", + FairnessWeight: 3.0, + }, +} +we, err := c.ExecuteWorkflow(ctx, workflowOptions, MyWorkflow) +``` + + +```java +WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("my-task-queue") + .setPriority(Priority.newBuilder() + .setPriorityKey(1) + .setFairnessKey("tenant-acme") + .setFairnessWeight(3.0) + .build()) + .build(); +MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); +workflow.run(); +``` + + +```python +await client.start_workflow( + MyWorkflow.run, + id="my-workflow-id", + task_queue="my-task-queue", + priority=Priority(priority_key=1, fairness_key="tenant-acme", fairness_weight=3.0), +) +``` + + +```ts +const handle = await client.workflow.start(MyWorkflow, { + workflowId: "my-workflow-id", + taskQueue: "my-task-queue", + priority: { priorityKey: 1, fairnessKey: "tenant-acme", fairnessWeight: 3.0 }, +}); +``` + + +```csharp +var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync(), + new StartWorkflowOptions("my-workflow-id", "my-task-queue") + { + Priority = new Priority( + priorityKey: 1, + fairnessKey: "tenant-acme", + fairnessWeight: 3.0 + ), + } +); +``` + + + +### Temporal CLI + +```bash +temporal workflow start \ + --type MyWorkflow \ + --task-queue my-task-queue \ + --workflow-id my-workflow-id \ + --priority-key 1 \ + --fairness-key tenant-acme \ + --fairness-weight 3.0 +``` + +--- + +## Next steps + +- [Task Queue Priority and Fairness - full reference](/develop/task-queue-priority-fairness) +- [Task Queue overview](/task-queue) +- [Worker performance tuning](/develop/worker-performance) diff --git a/sidebars.js b/sidebars.js index 4e601b9b3b..12c05ad4f9 100644 --- a/sidebars.js +++ b/sidebars.js @@ -333,7 +333,15 @@ module.exports = { ], }, 'develop/environment-configuration', - 'develop/activity-retry-simulator', + { + type: 'category', + label: 'Interactive Demos', + collapsed: false, + items: [ + 'develop/activity-retry-simulator', + 'develop/priority-fairness-walkthrough', + ], + }, 'develop/worker-performance', 'develop/worker-tuning-reference', 'develop/safe-deployments', @@ -840,18 +848,18 @@ module.exports = { collapsed: true, link: { type: 'doc', - id: 'encyclopedia/nexus', + id: 'encyclopedia/nexus/nexus', }, items: [ - 'encyclopedia/nexus-endpoints', - 'encyclopedia/nexus-registry', - 'encyclopedia/nexus-services', - 'encyclopedia/nexus-operations', - 'encyclopedia/nexus-use-cases', - 'encyclopedia/nexus-execution-debugging', - 'encyclopedia/nexus-error-handling', - 'encyclopedia/nexus-metrics', - 'encyclopedia/nexus-security', + 'encyclopedia/nexus/nexus-services', + 'encyclopedia/nexus/nexus-operations', + 'encyclopedia/nexus/nexus-endpoints', + 'encyclopedia/nexus/nexus-registry', + 'encyclopedia/nexus/nexus-patterns', + 'encyclopedia/nexus/nexus-security', + 'encyclopedia/nexus/nexus-execution-debugging', + 'encyclopedia/nexus/nexus-error-handling', + 'encyclopedia/nexus/nexus-metrics', ], }, { diff --git a/src/components/elements/PriorityFairnessSimulator.js b/src/components/elements/PriorityFairnessSimulator.js new file mode 100644 index 0000000000..1f9019d955 --- /dev/null +++ b/src/components/elements/PriorityFairnessSimulator.js @@ -0,0 +1,702 @@ +import Chart from "chart.js/auto"; +import React, { useState, useEffect, useRef } from "react"; +import { useColorMode } from "@docusaurus/theme-common"; +import styles from "./priority-fairness-simulator.module.css"; + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const PRIORITY_META = { + 1: { color: "#ef4444", textColor: "#fff", label: "Critical" }, + 2: { color: "#f97316", textColor: "#fff", label: "High" }, + 3: { color: "#3b82f6", textColor: "#fff", label: "Normal" }, + 4: { color: "#22c55e", textColor: "#000", label: "Low" }, + 5: { color: "#94a3b8", textColor: "#fff", label: "Batch" }, +}; + +const FAIRNESS_PALETTE = [ + "#6366f1", "#ec4899", "#14b8a6", "#f59e0b", "#8b5cf6", "#06b6d4", +]; + +const PRESETS = [ + { + label: "Choose a scenario…", + queue: [], + fairnessKeys: [], + }, + { + label: "Priority only — Payments vs Inventory", + queue: [ + { priorityKey: 5, fairnessKey: "" }, + { priorityKey: 5, fairnessKey: "" }, + { priorityKey: 5, fairnessKey: "" }, + { priorityKey: 3, fairnessKey: "" }, + { priorityKey: 3, fairnessKey: "" }, + { priorityKey: 1, fairnessKey: "" }, + { priorityKey: 5, fairnessKey: "" }, + { priorityKey: 1, fairnessKey: "" }, + { priorityKey: 2, fairnessKey: "" }, + { priorityKey: 3, fairnessKey: "" }, + ], + fairnessKeys: [], + }, + { + label: "Fairness only — Multi-tenant (equal weights)", + queue: [ + { priorityKey: 3, fairnessKey: "tenant-big" }, + { priorityKey: 3, fairnessKey: "tenant-big" }, + { priorityKey: 3, fairnessKey: "tenant-big" }, + { priorityKey: 3, fairnessKey: "tenant-big" }, + { priorityKey: 3, fairnessKey: "tenant-big" }, + { priorityKey: 3, fairnessKey: "tenant-big" }, + { priorityKey: 3, fairnessKey: "tenant-mid" }, + { priorityKey: 3, fairnessKey: "tenant-mid" }, + { priorityKey: 3, fairnessKey: "tenant-small" }, + { priorityKey: 3, fairnessKey: "tenant-small" }, + ], + fairnessKeys: [ + { key: "tenant-big", weight: 1 }, + { key: "tenant-mid", weight: 1 }, + { key: "tenant-small", weight: 1 }, + ], + }, + { + label: "Fairness only — Tiered weights (Premium 5×, Basic 3×, Free 1×)", + queue: [ + { priorityKey: 3, fairnessKey: "premium" }, + { priorityKey: 3, fairnessKey: "premium" }, + { priorityKey: 3, fairnessKey: "premium" }, + { priorityKey: 3, fairnessKey: "premium" }, + { priorityKey: 3, fairnessKey: "basic" }, + { priorityKey: 3, fairnessKey: "basic" }, + { priorityKey: 3, fairnessKey: "basic" }, + { priorityKey: 3, fairnessKey: "free" }, + { priorityKey: 3, fairnessKey: "free" }, + { priorityKey: 3, fairnessKey: "free" }, + ], + fairnessKeys: [ + { key: "premium", weight: 5 }, + { key: "basic", weight: 3 }, + { key: "free", weight: 1 }, + ], + }, + { + label: "Priority + Fairness — E-commerce platform", + queue: [ + { priorityKey: 1, fairnessKey: "vendor-a" }, + { priorityKey: 1, fairnessKey: "vendor-a" }, + { priorityKey: 1, fairnessKey: "vendor-b" }, + { priorityKey: 2, fairnessKey: "vendor-a" }, + { priorityKey: 2, fairnessKey: "vendor-b" }, + { priorityKey: 2, fairnessKey: "vendor-c" }, + { priorityKey: 3, fairnessKey: "vendor-a" }, + { priorityKey: 3, fairnessKey: "vendor-a" }, + { priorityKey: 3, fairnessKey: "vendor-b" }, + { priorityKey: 3, fairnessKey: "vendor-c" }, + { priorityKey: 5, fairnessKey: "vendor-a" }, + { priorityKey: 5, fairnessKey: "vendor-c" }, + ], + fairnessKeys: [ + { key: "vendor-a", weight: 3 }, + { key: "vendor-b", weight: 2 }, + { key: "vendor-c", weight: 1 }, + ], + }, +]; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +let _seed = 0; +function makeTask(priorityKey, fairnessKey) { + return { id: ++_seed, priorityKey: +priorityKey, fairnessKey: (fairnessKey || "").trim() }; +} + +/** + * Weighted Fair Queue dispatch simulation. + * Returns tasks in dispatch order respecting priority (lower = first), + * then within each priority tier uses WFQ (lowest dispatched/weight wins). + */ +function simulateDispatches(initialQueue, fairnessKeys) { + const order = []; + let queue = [...initialQueue]; + const counters = {}; + + const weightOf = (key) => { + const fk = fairnessKeys.find((f) => f.key === key); + return fk ? fk.weight : 1; + }; + + while (queue.length > 0) { + const minPriority = Math.min(...queue.map((t) => t.priorityKey)); + const tier = queue.filter((t) => t.priorityKey === minPriority); + const rest = queue.filter((t) => t.priorityKey !== minPriority); + + // Group tier by fairness key + const groups = {}; + tier.forEach((t) => { + const k = t.fairnessKey || "__none__"; + if (!groups[k]) groups[k] = []; + groups[k].push(t); + }); + + const keys = Object.keys(groups); + let chosenKey = keys[0]; + + // If multiple fairness keys exist within this priority tier, use WFQ + const hasRealFairness = keys.some((k) => k !== "__none__"); + if (keys.length > 1 && hasRealFairness) { + let lowestRatio = Infinity; + for (const k of keys) { + const ratio = (counters[k] || 0) / weightOf(k === "__none__" ? "" : k); + if (ratio < lowestRatio) { + lowestRatio = ratio; + chosenKey = k; + } + } + } + + const task = groups[chosenKey][0]; + counters[chosenKey] = (counters[chosenKey] || 0) + 1; + order.push(task); + queue = [...tier.filter((t) => t !== task), ...rest]; + } + + return order; +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +export default function PriorityFairnessSimulator() { + const { colorMode } = useColorMode(); + const isDark = colorMode === "dark"; + const chartRef = useRef(null); + + // Config state + const [queue, setQueue] = useState([]); + const [fairnessKeys, setFairnessKeys] = useState([]); + const [newTaskPriority, setNewTaskPriority] = useState(3); + const [newTaskFairnessKey, setNewTaskFairnessKey] = useState(""); + const [newFkName, setNewFkName] = useState(""); + const [newFkWeight, setNewFkWeight] = useState(1); + + // Simulation state + const [dispatchOrder, setDispatchOrder] = useState([]); + const [stepIndex, setStepIndex] = useState(0); + const [simStarted, setSimStarted] = useState(false); + + const dispatched = dispatchOrder.slice(0, stepIndex); + const remainingIds = new Set(dispatchOrder.slice(stepIndex).map((t) => t.id)); + + // ── Derived fairness key color map + const fkColorMap = {}; + fairnessKeys.forEach((fk, i) => { + fkColorMap[fk.key] = FAIRNESS_PALETTE[i % FAIRNESS_PALETTE.length]; + }); + + // ── Preset loader + function loadPreset(idx) { + if (!+idx) return; + const p = PRESETS[+idx]; + const newQueue = p.queue.map((t) => makeTask(t.priorityKey, t.fairnessKey)); + setQueue(newQueue); + setFairnessKeys(p.fairnessKeys.map((fk) => ({ ...fk }))); + resetSim(newQueue, p.fairnessKeys); + } + + // ── Task management + function addTask() { + const task = makeTask(newTaskPriority, newTaskFairnessKey); + const newQ = [...queue, task]; + setQueue(newQ); + resetSim(newQ, fairnessKeys); + } + + function removeTask(id) { + const newQ = queue.filter((t) => t.id !== id); + setQueue(newQ); + resetSim(newQ, fairnessKeys); + } + + // ── Fairness key management + function addFairnessKey() { + if (!newFkName.trim() || fairnessKeys.find((f) => f.key === newFkName.trim())) return; + const updated = [...fairnessKeys, { key: newFkName.trim(), weight: +newFkWeight || 1 }]; + setFairnessKeys(updated); + setNewFkName(""); + setNewFkWeight(1); + resetSim(queue, updated); + } + + function removeFairnessKey(key) { + const updated = fairnessKeys.filter((f) => f.key !== key); + setFairnessKeys(updated); + resetSim(queue, updated); + } + + function updateWeight(key, weight) { + const updated = fairnessKeys.map((f) => (f.key === key ? { ...f, weight: +weight || 1 } : f)); + setFairnessKeys(updated); + resetSim(queue, updated); + } + + // ── Simulation controls + function resetSim(q = queue, fk = fairnessKeys) { + setDispatchOrder([]); + setStepIndex(0); + setSimStarted(false); + } + + function runSim() { + if (!queue.length) return; + const order = simulateDispatches(queue, fairnessKeys); + setDispatchOrder(order); + setStepIndex(0); + setSimStarted(true); + } + + function stepForward() { + setStepIndex((i) => Math.min(i + 1, dispatchOrder.length)); + } + + function dispatchAll() { + setStepIndex(dispatchOrder.length); + } + + function restart() { + setStepIndex(0); + } + + // ── Chart + useEffect(() => { + if (!chartRef.current) return; + const hasFairness = fairnessKeys.length > 0; + if (!hasFairness || dispatched.length === 0) { + if (chartRef.current._chart) { + chartRef.current._chart.destroy(); + chartRef.current._chart = null; + } + return; + } + + const totalWeight = fairnessKeys.reduce((s, f) => s + f.weight, 0); + const labels = fairnessKeys.map((f) => f.key); + const actualCounts = {}; + dispatched.forEach((t) => { + if (t.fairnessKey) actualCounts[t.fairnessKey] = (actualCounts[t.fairnessKey] || 0) + 1; + }); + + const actualPct = labels.map((k) => (((actualCounts[k] || 0) / dispatched.length) * 100).toFixed(1)); + const expectedPct = labels.map((f) => { + const fk = fairnessKeys.find((x) => x.key === f); + return totalWeight > 0 ? ((fk.weight / totalWeight) * 100).toFixed(1) : 0; + }); + + const gridColor = isDark ? "#333" : "#e5e7eb"; + const labelColor = isDark ? "#d1d5db" : "#374151"; + + if (chartRef.current._chart) { + const chart = chartRef.current._chart; + chart.data.labels = labels; + chart.data.datasets[0].data = actualPct; + chart.data.datasets[1].data = expectedPct; + chart.options.scales.y.grid.color = gridColor; + chart.options.scales.x.grid.color = gridColor; + chart.options.scales.y.ticks.color = labelColor; + chart.options.scales.x.ticks.color = labelColor; + chart.options.plugins.legend.labels.color = labelColor; + chart.update(); + } else { + chartRef.current._chart = new Chart(chartRef.current, { + type: "bar", + data: { + labels, + datasets: [ + { + label: "Actual dispatch %", + backgroundColor: "#3b82f6", + borderRadius: 4, + data: actualPct, + }, + { + label: "Target weight %", + backgroundColor: isDark ? "#374151" : "#d1d5db", + borderRadius: 4, + data: expectedPct, + }, + ], + }, + options: { + responsive: true, + animation: { duration: 250 }, + plugins: { + legend: { + position: "top", + labels: { color: labelColor }, + }, + }, + scales: { + y: { + max: 100, + title: { display: true, text: "% of dispatches", color: labelColor }, + grid: { color: gridColor }, + ticks: { color: labelColor }, + }, + x: { + grid: { color: gridColor }, + ticks: { color: labelColor }, + }, + }, + }, + }); + } + }, [dispatched, fairnessKeys, isDark]); + + // Cleanup chart on unmount + useEffect(() => { + return () => { + if (chartRef.current && chartRef.current._chart) { + chartRef.current._chart.destroy(); + } + }; + }, []); + + const hasFairnessKeys = fairnessKeys.length > 0; + const canStep = simStarted && stepIndex < dispatchOrder.length; + const canDispatchAll = simStarted && stepIndex < dispatchOrder.length; + const canRestart = simStarted && stepIndex > 0; + const showChart = hasFairnessKeys && dispatched.length > 0; + + return ( +
+ {/* ── Top bar ── */} +
+
+ +
+
+ {!simStarted ? ( + + ) : ( + <> + + + + + + )} +
+
+ + {/* ── Main layout ── */} +
+ {/* ── Left: Config panel ── */} +
+ {/* Fairness Keys */} +
+
+

Fairness Keys

+ {!hasFairnessKeys && ( + Priority only + )} + {hasFairnessKeys && ( + + Fairness active + + )} +
+ + {!hasFairnessKeys && ( +

+ No fairness keys — tasks dispatch in strict priority order (FIFO within same + priority). +

+ )} + + {fairnessKeys.map((fk) => ( +
+ + {fk.key} + + updateWeight(fk.key, e.target.value)} + /> + +
+ ))} + + {!simStarted && ( +
+ setNewFkName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addFairnessKey()} + /> + setNewFkWeight(e.target.value)} + /> + +
+ )} +
+ + {/* Add Task */} + {!simStarted && ( +
+

Add Task

+
+
+ + +
+ {hasFairnessKeys && ( +
+ + +
+ )} + +
+
+ )} + + {/* Priority legend */} +
+

Priority Key Legend

+
+ {[1, 2, 3, 4, 5].map((p) => ( +
+ + P{p} + + {PRIORITY_META[p].label} + {p === 3 && default} +
+ ))} +
+
+
+ + {/* ── Right: Visualizer ── */} +
+ {/* Task Queue Backlog */} +
+
+

Task Queue

+
+ + {simStarted ? remainingIds.size : queue.length} waiting + + {simStarted && ( + + {dispatched.length} dispatched + + )} +
+
+ + {queue.length === 0 ? ( +

+ Load a scenario from the dropdown above, or add tasks manually. +

+ ) : ( +
+ {queue.map((t) => { + const isDispatched = simStarted && !remainingIds.has(t.id); + return ( +
+ + P{t.priorityKey} + + {t.fairnessKey && ( + + {t.fairnessKey} + + )} + {isDispatched && } + {!simStarted && ( + + )} +
+ ); + })} +
+ )} +
+ + {/* Dispatch Log */} + {simStarted && ( +
+
+

Dispatch Order

+ + {stepIndex} / {dispatchOrder.length} + +
+ + {dispatched.length === 0 ? ( +

+ Click Step to dispatch one task at a time, or{" "} + Dispatch All to run the full simulation. +

+ ) : ( +
+ {dispatched.map((t, i) => ( +
+ #{i + 1} + + P{t.priorityKey} + + {t.fairnessKey ? ( + + {t.fairnessKey} + + ) : ( + + {PRIORITY_META[t.priorityKey].label} + + )} + task #{t.id} +
+ ))} +
+ )} +
+ )} + + {/* Fairness Distribution Chart */} + {showChart && ( +
+

Fairness Distribution

+

+ Actual dispatch percentage vs. target weight percentage per fairness key. +

+ +
+ )} + + {/* Placeholder canvas (hidden) when chart not showing but ref needed */} + {!showChart && } +
+
+
+ ); +} diff --git a/src/components/elements/priority-fairness-simulator.module.css b/src/components/elements/priority-fairness-simulator.module.css new file mode 100644 index 0000000000..878ad4528f --- /dev/null +++ b/src/components/elements/priority-fairness-simulator.module.css @@ -0,0 +1,495 @@ +/* ── Root ──────────────────────────────────────────────────────────────────── */ +.root { + font-size: 0.9rem; + margin: 1.5rem 0; +} + +/* ── Top Bar ─────────────────────────────────────────────────────────────────*/ +.topBar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 1rem; + padding: 0.75rem 1rem; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + background: var(--ifm-background-surface-color); +} + +.topBarLeft { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.topBarRight { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* ── Layout ──────────────────────────────────────────────────────────────────*/ +.layout { + display: flex; + gap: 1rem; + align-items: flex-start; +} + +.configPanel { + flex: 0 0 300px; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.vizPanel { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +@media (max-width: 900px) { + .layout { + flex-direction: column; + } + .configPanel { + flex: 1 1 auto; + } +} + +/* ── Card ────────────────────────────────────────────────────────────────────*/ +.card { + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + padding: 0.85rem 1rem; + background: var(--ifm-background-surface-color); +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.6rem; + gap: 0.5rem; +} + +.cardTitle { + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--ifm-color-emphasis-700); + margin: 0 0 0.6rem 0; +} + +.cardHeader .cardTitle { + margin-bottom: 0; +} + +/* ── Mode Tag ────────────────────────────────────────────────────────────────*/ +.modeTag { + font-size: 0.7rem; + font-weight: 600; + padding: 0.15em 0.55em; + border-radius: 999px; + background: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-700); + white-space: nowrap; +} + +.modeTagFairness { + background: #dbeafe; + color: #1d4ed8; +} + +[data-theme="dark"] .modeTagFairness { + background: #1e3a5f; + color: #93c5fd; +} + +/* ── Hint text ───────────────────────────────────────────────────────────────*/ +.hint { + font-size: 0.8rem; + color: var(--ifm-color-emphasis-500); + margin: 0 0 0.5rem 0; + line-height: 1.5; +} + +/* ── Fairness key rows ───────────────────────────────────────────────────────*/ +.fkRow { + display: flex; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.4rem; +} + +.fkDot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.fkName { + flex: 1; + font-size: 0.82rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.weightLabel { + font-size: 0.72rem; + color: var(--ifm-color-emphasis-500); + white-space: nowrap; +} + +.weightInput { + width: 56px; + padding: 0.2em 0.4em; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 4px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.82rem; + text-align: right; +} + +/* ── Add row ─────────────────────────────────────────────────────────────────*/ +.addRow { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.6rem; +} + +.textInput { + flex: 1; + padding: 0.3em 0.5em; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 4px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.82rem; + min-width: 0; +} + +.textInput::placeholder { + color: var(--ifm-color-emphasis-400); +} + +/* ── Add task row ────────────────────────────────────────────────────────────*/ +.addTaskRow { + display: flex; + align-items: flex-end; + gap: 0.5rem; + flex-wrap: wrap; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.2rem; + flex: 1; + min-width: 100px; +} + +.fieldLabel { + font-size: 0.72rem; + color: var(--ifm-color-emphasis-600); + font-weight: 600; +} + +/* ── Legend ──────────────────────────────────────────────────────────────────*/ +.legendGrid { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.legendRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.legendLabel { + font-size: 0.82rem; + flex: 1; +} + +.defaultTag { + font-size: 0.68rem; + color: var(--ifm-color-emphasis-500); + font-style: italic; +} + +/* ── Queue stats ─────────────────────────────────────────────────────────────*/ +.queueStats { + display: flex; + gap: 0.4rem; +} + +.statBadge { + font-size: 0.72rem; + font-weight: 600; + padding: 0.15em 0.55em; + border-radius: 999px; + background: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-700); +} + +.statBadgeDone { + background: #dcfce7; + color: #15803d; +} + +[data-theme="dark"] .statBadgeDone { + background: #14532d; + color: #86efac; +} + +/* ── Queue grid ──────────────────────────────────────────────────────────────*/ +.queueGrid { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +/* ── Task chip ───────────────────────────────────────────────────────────────*/ +.taskChip { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.25em 0.5em; + border-radius: 6px; + border: 2px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-color); + font-size: 0.78rem; + transition: opacity 0.2s; + position: relative; +} + +.taskChipDone { + opacity: 0.35; + border-color: var(--ifm-color-emphasis-200) !important; + filter: grayscale(1); +} + +.doneCheck { + font-size: 0.7rem; + color: #16a34a; +} + +.chipRemove { + background: none; + border: none; + cursor: pointer; + color: var(--ifm-color-emphasis-400); + font-size: 0.9rem; + line-height: 1; + padding: 0 0 0 0.1rem; + margin: 0; +} + +.chipRemove:hover { + color: #ef4444; +} + +/* ── Priority chip ───────────────────────────────────────────────────────────*/ +.priorityChip { + display: inline-block; + padding: 0.1em 0.45em; + border-radius: 4px; + font-size: 0.72rem; + font-weight: 700; + line-height: 1.4; + background: var(--ifm-color-emphasis-200); + color: var(--ifm-font-color-base); + white-space: nowrap; +} + +/* ── Fairness key chip ───────────────────────────────────────────────────────*/ +.fkChip { + display: inline-block; + padding: 0.1em 0.45em; + border-radius: 4px; + font-size: 0.72rem; + font-weight: 600; + line-height: 1.4; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100px; +} + +/* ── Dispatch log ────────────────────────────────────────────────────────────*/ +.progressText { + font-size: 0.75rem; + color: var(--ifm-color-emphasis-500); + font-variant-numeric: tabular-nums; +} + +.logList { + display: flex; + flex-direction: column; + gap: 0.3rem; + max-height: 280px; + overflow-y: auto; +} + +.logRow { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.25em 0.4em; + border-radius: 6px; + background: var(--ifm-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + transition: background 0.25s; +} + +.logRowLatest { + background: #eff6ff; + border-color: #bfdbfe; +} + +[data-theme="dark"] .logRowLatest { + background: #1e3a5f; + border-color: #2563eb44; +} + +.logNum { + font-size: 0.72rem; + font-variant-numeric: tabular-nums; + color: var(--ifm-color-emphasis-500); + width: 2rem; + text-align: right; + flex-shrink: 0; +} + +.logLabel { + font-size: 0.78rem; + color: var(--ifm-color-emphasis-600); +} + +.taskRef { + font-size: 0.7rem; + color: var(--ifm-color-emphasis-400); + margin-left: auto; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +/* ── Chart ───────────────────────────────────────────────────────────────────*/ +.chartCaption { + font-size: 0.78rem; + color: var(--ifm-color-emphasis-500); + margin: -0.2rem 0 0.6rem 0; +} + +/* ── Buttons ─────────────────────────────────────────────────────────────────*/ +.btn, +.btnPrimary, +.btnOutline, +.btnSmall, +.removeBtn { + display: inline-flex; + align-items: center; + gap: 0.25em; + border-radius: 6px; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.15s, background 0.15s; + white-space: nowrap; +} + +.btn { + padding: 0.35em 0.75em; + border: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); +} + +.btn:hover:not(:disabled) { + background: var(--ifm-color-emphasis-100); +} + +.btnPrimary { + padding: 0.35em 0.85em; + border: none; + background: var(--ifm-color-primary); + color: #fff; +} + +.btnPrimary:hover:not(:disabled) { + opacity: 0.88; +} + +.btnOutline { + padding: 0.35em 0.75em; + border: 1px solid var(--ifm-color-emphasis-300); + background: transparent; + color: var(--ifm-color-emphasis-600); +} + +.btnOutline:hover:not(:disabled) { + border-color: var(--ifm-color-emphasis-500); + color: var(--ifm-font-color-base); +} + +.btnSmall { + padding: 0.25em 0.6em; + border: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.78rem; +} + +.btnSmall:hover:not(:disabled) { + background: var(--ifm-color-emphasis-100); +} + +.removeBtn { + padding: 0.1em 0.35em; + border: 1px solid transparent; + background: transparent; + color: var(--ifm-color-emphasis-400); + font-size: 1rem; +} + +.removeBtn:hover:not(:disabled) { + color: #ef4444; + border-color: #fca5a5; + background: #fef2f2; +} + +.btn:disabled, +.btnPrimary:disabled, +.btnOutline:disabled, +.btnSmall:disabled, +.removeBtn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* ── Select ──────────────────────────────────────────────────────────────────*/ +.select { + padding: 0.3em 0.55em; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.82rem; + cursor: pointer; + max-width: 100%; +} diff --git a/src/components/index.js b/src/components/index.js index 07b4ad9ef6..b03dc88a96 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,5 +1,6 @@ // Website components export { default as RetrySimulator } from './elements/RetrySimulator'; +export { default as PriorityFairnessSimulator } from './elements/PriorityFairnessSimulator'; export { default as HomePageHero } from './elements/HomePageHero'; export { SdkLogos } from './elements/SdkLogos'; export { SdkLogosAsBlocks } from './elements/SdkLogosAsBlocks'; From 256cdad0306e224779bc9bcb3e44445c96d20936 Mon Sep 17 00:00:00 2001 From: Jwahir Sundai Date: Fri, 27 Mar 2026 18:47:40 -0500 Subject: [PATCH 2/2] Refactor Priority & Fairness walkthrough into tabbed demo - Replace inline MDX with a tabbed PriorityFairnessWalkthrough component (Overview, Try It, How It Works, SDK Examples) - Move Interactive Demos sidebar section after glossary; restore activity-retry-simulator to standalone position in Develop - Remove unicode symbols from simulator buttons Co-Authored-By: Claude Sonnet 4.6 --- .../develop/priority-fairness-walkthrough.mdx | 232 +----------- sidebars.js | 19 +- .../PriorityFairnessWalkthrough/HowItWorks.js | 79 +++++ .../PriorityFairnessWalkthrough/Overview.js | 90 +++++ .../PriorityFairnessWalkthrough/SDK.js | 178 ++++++++++ .../PriorityFairnessWalkthrough/TryIt.js | 23 ++ .../PriorityFairnessWalkthrough/index.js | 45 +++ .../walkthrough.module.css | 331 ++++++++++++++++++ src/components/index.js | 1 + 9 files changed, 763 insertions(+), 235 deletions(-) create mode 100644 src/components/elements/PriorityFairnessWalkthrough/HowItWorks.js create mode 100644 src/components/elements/PriorityFairnessWalkthrough/Overview.js create mode 100644 src/components/elements/PriorityFairnessWalkthrough/SDK.js create mode 100644 src/components/elements/PriorityFairnessWalkthrough/TryIt.js create mode 100644 src/components/elements/PriorityFairnessWalkthrough/index.js create mode 100644 src/components/elements/PriorityFairnessWalkthrough/walkthrough.module.css diff --git a/docs/develop/priority-fairness-walkthrough.mdx b/docs/develop/priority-fairness-walkthrough.mdx index bbc5b0ea81..f51c37dcaf 100644 --- a/docs/develop/priority-fairness-walkthrough.mdx +++ b/docs/develop/priority-fairness-walkthrough.mdx @@ -1,9 +1,9 @@ --- id: priority-fairness-walkthrough -title: Task Queue Priority and Fairness — Interactive Demo -sidebar_label: Priority and Fairness (Interactive) -description: Interactively explore how Task Queue Priority and Fairness control dispatch order. Load a preset scenario, step through dispatches, and see exactly which task gets picked next — and why. -toc_max_heading_level: 3 +title: Task Queue Priority and Fairness - Interactive Walkthrough +sidebar_label: Priority and Fairness +description: Interactively explore how Task Queue Priority and Fairness control dispatch order. Step through scenarios, see which task gets picked next and why, then grab the SDK code. +toc_max_heading_level: 2 keywords: - task queue priority - task queue fairness @@ -15,226 +15,6 @@ tags: - Priority and Fairness --- -import { PriorityFairnessSimulator } from '@site/src/components'; -import SdkTabs from '@site/src/components'; +import { PriorityFairnessWalkthrough } from '@site/src/components'; -When multiple workloads compete for Workers, Task Queue Priority determines which tasks get picked first, while Task Queue Fairness ensures no single workload can crowd out the rest. - -
- -
-
- - - - -Priority -
- -Every task carries a `priorityKey` from `1` (critical) to `5` (batch), with `3` as the default. When a Worker polls, it always picks the lowest-numbered task first regardless of arrival time. This lets you share a single Worker pool across very different workloads and guarantee that time-sensitive work never waits behind low-urgency jobs. - -
- -
-
- - - - -Fairness -
- -Without Fairness, tasks at the same priority dispatch strictly FIFO, so a backlog-heavy tenant can block everyone else at that level indefinitely. Fairness groups tasks by `fairnessKey` and dispatches proportionally by `fairnessWeight`. A key with weight `5` gets roughly 5x more dispatches than a key with weight `1`, but no key is ever completely locked out. - -
- -
- -Use both together when you need SLA ordering across workload types and fair distribution across tenants within each tier. - -
- {[['P1','Critical','#ef4444','#fff'],['P2','High','#f97316','#fff'],['P3','Normal (default)','#3b82f6','#fff'],['P4','Low','#22c55e','#000'],['P5','Batch','#94a3b8','#fff']].map(([p,l,bg,fg]) => ( -
- {p} · {l} -
- ))} -
- -## Try it - -Load a preset or build your own queue. Use **Step ->** to dispatch one task at a time and watch which task gets picked next, or **Dispatch All** to see the full order at once. - - - ---- - -## How it works - -When a Worker polls for the next task, Temporal applies two rules in sequence: - -1. **Priority first** - the task with the lowest `priorityKey` wins. If there are tasks at priority `1`, none of the `2`s will dispatch until all `1`s are gone. -2. **Fairness within a tier** - when multiple tasks share the same priority level but carry different `fairnessKey` values, Temporal tracks how many tasks each key has received relative to its weight and dispatches from the key that is furthest behind its expected share. - -If no `fairnessKey` is set, tasks within a priority level dispatch in FIFO order. - -### When to use Priority vs Fairness - -| Scenario | Use | -|---|---| -| Payments should never wait behind inventory syncs | **Priority** | -| Premium users shouldn't be blocked by a single large tenant | **Fairness** | -| SLAs differ across customer tiers and tenants vary in volume | **Both** | - ---- - -## Setup - -Priority and Fairness are enabled automatically - no configuration required. Just set `priorityKey`, `fairnessKey`, or both when starting Workflows or Activities. - -For self-hosted Temporal, set `matching.useNewMatcher` to `true` in dynamic config. To enable Fairness, also set `matching.enableFairness: true`. - ---- - -## SDK examples - -### Workflow - Priority only - - - -```go -workflowOptions := client.StartWorkflowOptions{ - ID: "my-workflow-id", - TaskQueue: "my-task-queue", - Priority: temporal.Priority{PriorityKey: 1}, -} -we, err := c.ExecuteWorkflow(ctx, workflowOptions, MyWorkflow) -``` - - -```java -WorkflowOptions options = WorkflowOptions.newBuilder() - .setTaskQueue("my-task-queue") - .setPriority(Priority.newBuilder().setPriorityKey(1).build()) - .build(); -MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); -workflow.run(); -``` - - -```python -await client.start_workflow( - MyWorkflow.run, - id="my-workflow-id", - task_queue="my-task-queue", - priority=Priority(priority_key=1), -) -``` - - -```ts -const handle = await client.workflow.start(MyWorkflow, { - workflowId: "my-workflow-id", - taskQueue: "my-task-queue", - priority: { priorityKey: 1 }, -}); -``` - - -```csharp -var handle = await Client.StartWorkflowAsync( - (MyWorkflow wf) => wf.RunAsync(), - new StartWorkflowOptions("my-workflow-id", "my-task-queue") - { - Priority = new Priority(priorityKey: 1), - } -); -``` - - - -### Workflow - Priority + Fairness - - - -```go -workflowOptions := client.StartWorkflowOptions{ - ID: "my-workflow-id", - TaskQueue: "my-task-queue", - Priority: temporal.Priority{ - PriorityKey: 1, - FairnessKey: "tenant-acme", - FairnessWeight: 3.0, - }, -} -we, err := c.ExecuteWorkflow(ctx, workflowOptions, MyWorkflow) -``` - - -```java -WorkflowOptions options = WorkflowOptions.newBuilder() - .setTaskQueue("my-task-queue") - .setPriority(Priority.newBuilder() - .setPriorityKey(1) - .setFairnessKey("tenant-acme") - .setFairnessWeight(3.0) - .build()) - .build(); -MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); -workflow.run(); -``` - - -```python -await client.start_workflow( - MyWorkflow.run, - id="my-workflow-id", - task_queue="my-task-queue", - priority=Priority(priority_key=1, fairness_key="tenant-acme", fairness_weight=3.0), -) -``` - - -```ts -const handle = await client.workflow.start(MyWorkflow, { - workflowId: "my-workflow-id", - taskQueue: "my-task-queue", - priority: { priorityKey: 1, fairnessKey: "tenant-acme", fairnessWeight: 3.0 }, -}); -``` - - -```csharp -var handle = await Client.StartWorkflowAsync( - (MyWorkflow wf) => wf.RunAsync(), - new StartWorkflowOptions("my-workflow-id", "my-task-queue") - { - Priority = new Priority( - priorityKey: 1, - fairnessKey: "tenant-acme", - fairnessWeight: 3.0 - ), - } -); -``` - - - -### Temporal CLI - -```bash -temporal workflow start \ - --type MyWorkflow \ - --task-queue my-task-queue \ - --workflow-id my-workflow-id \ - --priority-key 1 \ - --fairness-key tenant-acme \ - --fairness-weight 3.0 -``` - ---- - -## Next steps - -- [Task Queue Priority and Fairness - full reference](/develop/task-queue-priority-fairness) -- [Task Queue overview](/task-queue) -- [Worker performance tuning](/develop/worker-performance) + diff --git a/sidebars.js b/sidebars.js index 12c05ad4f9..4ef019afaa 100644 --- a/sidebars.js +++ b/sidebars.js @@ -333,15 +333,7 @@ module.exports = { ], }, 'develop/environment-configuration', - { - type: 'category', - label: 'Interactive Demos', - collapsed: false, - items: [ - 'develop/activity-retry-simulator', - 'develop/priority-fairness-walkthrough', - ], - }, + 'develop/activity-retry-simulator', 'develop/worker-performance', 'develop/worker-tuning-reference', 'develop/safe-deployments', @@ -898,6 +890,15 @@ module.exports = { ], }, 'glossary', + { + type: 'category', + label: 'Interactive Demos', + collapsed: false, + items: [ + 'develop/activity-retry-simulator', + 'develop/priority-fairness-walkthrough', + ], + }, 'with-ai', // { // type: "autogenerated", diff --git a/src/components/elements/PriorityFairnessWalkthrough/HowItWorks.js b/src/components/elements/PriorityFairnessWalkthrough/HowItWorks.js new file mode 100644 index 0000000000..f0a5020ee7 --- /dev/null +++ b/src/components/elements/PriorityFairnessWalkthrough/HowItWorks.js @@ -0,0 +1,79 @@ +import React from 'react'; +import styles from './walkthrough.module.css'; + +const STEPS = [ + { + title: 'Worker polls the Task Queue', + body: 'When a Worker is ready, it sends a poll request to the Task Queue. Temporal evaluates all waiting tasks and applies Priority and Fairness rules to decide which one to return.', + }, + { + title: 'Priority tier is selected first', + body: 'Temporal finds the lowest priorityKey among all waiting tasks. Every task at priority 1 will be dispatched before any task at priority 2 moves, and so on. Tasks at the same level compete under Fairness rules.', + }, + { + title: 'Fairness distributes capacity within the tier', + body: 'Within a priority tier, Temporal tracks how many tasks each fairnessKey has received relative to its fairnessWeight. The key that is furthest behind its expected share gets the next dispatch. This prevents any single tenant from consuming disproportionate capacity, even if they have a deep backlog.', + }, + { + title: 'No fairnessKey means strict FIFO within the tier', + body: 'If you set priorityKey but omit fairnessKey, tasks at the same priority level are dispatched in arrival order. Fairness only applies when at least one task in the tier carries a fairnessKey.', + }, + { + title: 'Priority and Fairness are per Task Queue', + body: 'The rules apply independently per Task Queue. Workers on the same Task Queue share the same dispatch ordering. Workers on separate Task Queues are unaffected by each other.', + }, +]; + +const WHEN_ROWS = [ + { scenario: 'Payments should never wait behind inventory syncs', use: 'Priority', badge: 'badgePriority' }, + { scenario: 'Premium users should not be blocked by a large free-tier tenant', use: 'Fairness', badge: 'badgeFairness' }, + { scenario: 'SLAs differ across customer tiers and tenants vary in volume', use: 'Both', badge: 'badgeBoth' }, +]; + +export default function HowItWorks({ onNext }) { + return ( +
+

+ When a Worker polls for the next task, Temporal applies two rules in sequence: Priority + determines which tier goes first, and Fairness distributes capacity among tenants within + each tier. +

+ +
+ {STEPS.map((step, i) => ( +
+
{i + 1}
+
+

{step.title}

+

{step.body}

+
+
+ ))} +
+ +

When to use Priority vs Fairness

+ + + + + + + + + {WHEN_ROWS.map((row, i) => ( + + + + + ))} + +
ScenarioUse
{row.scenario} + {row.use} +
+ + +
+ ); +} diff --git a/src/components/elements/PriorityFairnessWalkthrough/Overview.js b/src/components/elements/PriorityFairnessWalkthrough/Overview.js new file mode 100644 index 0000000000..e28598a75d --- /dev/null +++ b/src/components/elements/PriorityFairnessWalkthrough/Overview.js @@ -0,0 +1,90 @@ +import React from 'react'; +import styles from './walkthrough.module.css'; + +const PILLS = [ + { label: 'P1', desc: 'Critical', bg: '#ef4444', fg: '#fff' }, + { label: 'P2', desc: 'High', bg: '#f97316', fg: '#fff' }, + { label: 'P3', desc: 'Normal (default)', bg: '#3b82f6', fg: '#fff' }, + { label: 'P4', desc: 'Low', bg: '#22c55e', fg: '#000' }, + { label: 'P5', desc: 'Batch', bg: '#94a3b8', fg: '#fff' }, +]; + +export default function Overview({ onNext }) { + return ( +
+

+ The core idea of Task Queue Priority and Fairness is that when tasks from different workloads + compete for the same Workers, Priority controls which ones get picked first, and Fairness + ensures no single tenant can run away with all the capacity. +

+ +
+ {/* Priority card */} +
+
+ + + + + + + + + + Priority +
+

+ Every task carries a priorityKey from 1 (critical) to{' '} + 5 (batch), with 3 as the default. When a Worker polls, it + always picks the lowest-numbered task first regardless of arrival time. This lets you + share a single Worker pool across very different workloads and guarantee that + time-sensitive work never waits behind low-urgency jobs. +

+
+ + {/* Fairness card */} +
+
+ + + + + + + + + + Fairness +
+

+ Without Fairness, tasks at the same priority dispatch strictly FIFO, so a backlog-heavy + tenant can block everyone else at that level indefinitely. Fairness groups tasks by{' '} + fairnessKey and dispatches proportionally by fairnessWeight. A + key with weight 5 gets roughly 5x more dispatches than a key with weight{' '} + 1, but no key is ever completely locked out. +

+
+
+ +

+ Use both together when you need SLA ordering across workload types and fair distribution + across tenants within each tier. +

+ +
+ {PILLS.map(({ label, desc, bg, fg }) => ( +
+ {label} · {desc} +
+ ))} +
+ + +
+ ); +} diff --git a/src/components/elements/PriorityFairnessWalkthrough/SDK.js b/src/components/elements/PriorityFairnessWalkthrough/SDK.js new file mode 100644 index 0000000000..93f48e6c4a --- /dev/null +++ b/src/components/elements/PriorityFairnessWalkthrough/SDK.js @@ -0,0 +1,178 @@ +import CodeBlock from '@theme/CodeBlock'; +import React, { useState } from 'react'; +import styles from './walkthrough.module.css'; + +const LANGS = ['Go', 'Java', 'Python', 'TypeScript', '.NET']; + +const EXAMPLES = { + priorityOnly: { + title: 'Workflow - Priority only', + Go: `workflowOptions := client.StartWorkflowOptions{ + ID: "my-workflow-id", + TaskQueue: "my-task-queue", + Priority: temporal.Priority{PriorityKey: 1}, +} +we, err := c.ExecuteWorkflow(ctx, workflowOptions, MyWorkflow)`, + Java: `WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("my-task-queue") + .setPriority(Priority.newBuilder().setPriorityKey(1).build()) + .build(); +MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); +workflow.run();`, + Python: `await client.start_workflow( + MyWorkflow.run, + id="my-workflow-id", + task_queue="my-task-queue", + priority=Priority(priority_key=1), +)`, + TypeScript: `const handle = await client.workflow.start(MyWorkflow, { + workflowId: "my-workflow-id", + taskQueue: "my-task-queue", + priority: { priorityKey: 1 }, +});`, + '.NET': `var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync(), + new StartWorkflowOptions("my-workflow-id", "my-task-queue") + { + Priority = new Priority(priorityKey: 1), + } +);`, + }, + priorityAndFairness: { + title: 'Workflow - Priority + Fairness', + Go: `workflowOptions := client.StartWorkflowOptions{ + ID: "my-workflow-id", + TaskQueue: "my-task-queue", + Priority: temporal.Priority{ + PriorityKey: 1, + FairnessKey: "tenant-acme", + FairnessWeight: 3.0, + }, +} +we, err := c.ExecuteWorkflow(ctx, workflowOptions, MyWorkflow)`, + Java: `WorkflowOptions options = WorkflowOptions.newBuilder() + .setTaskQueue("my-task-queue") + .setPriority(Priority.newBuilder() + .setPriorityKey(1) + .setFairnessKey("tenant-acme") + .setFairnessWeight(3.0) + .build()) + .build(); +MyWorkflow workflow = client.newWorkflowStub(MyWorkflow.class, options); +workflow.run();`, + Python: `await client.start_workflow( + MyWorkflow.run, + id="my-workflow-id", + task_queue="my-task-queue", + priority=Priority(priority_key=1, fairness_key="tenant-acme", fairness_weight=3.0), +)`, + TypeScript: `const handle = await client.workflow.start(MyWorkflow, { + workflowId: "my-workflow-id", + taskQueue: "my-task-queue", + priority: { priorityKey: 1, fairnessKey: "tenant-acme", fairnessWeight: 3.0 }, +});`, + '.NET': `var handle = await Client.StartWorkflowAsync( + (MyWorkflow wf) => wf.RunAsync(), + new StartWorkflowOptions("my-workflow-id", "my-task-queue") + { + Priority = new Priority( + priorityKey: 1, + fairnessKey: "tenant-acme", + fairnessWeight: 3.0 + ), + } +);`, + }, +}; + +const LANG_META = { + Go: 'go', + Java: 'java', + Python: 'python', + TypeScript: 'typescript', + '.NET': 'csharp', +}; + +export default function SDK() { + const [activeLang, setActiveLang] = useState('Go'); + + return ( +
+

+ Set priorityKey, fairnessKey, or both when starting Workflows or + scheduling Activities. Priority and Fairness are enabled by default - no configuration + required for Temporal Cloud or self-hosted clusters running Temporal 1.26+. +

+ + {/* Language picker */} +
+ {LANGS.map((lang) => ( + + ))} +
+ + {/* Priority only */} +
+
Priority only
+ + {EXAMPLES.priorityOnly[activeLang]} + +
+ + {/* Priority + Fairness */} +
+
Priority + Fairness
+ + {EXAMPLES.priorityAndFairness[activeLang]} + +
+ + {/* CLI */} +
+
Temporal CLI
+ {`temporal workflow start \\ + --type MyWorkflow \\ + --task-queue my-task-queue \\ + --workflow-id my-workflow-id \\ + --priority-key 1 \\ + --fairness-key tenant-acme \\ + --fairness-weight 3.0`} +
+ + +
+ ); +} diff --git a/src/components/elements/PriorityFairnessWalkthrough/TryIt.js b/src/components/elements/PriorityFairnessWalkthrough/TryIt.js new file mode 100644 index 0000000000..e59ec78822 --- /dev/null +++ b/src/components/elements/PriorityFairnessWalkthrough/TryIt.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PriorityFairnessSimulator from '../PriorityFairnessSimulator'; +import styles from './walkthrough.module.css'; + +export default function TryIt({ onNext }) { + return ( +
+
+

+ Load a preset or build your own queue. Use Step to dispatch one task at a + time and watch which task gets picked next, or Dispatch All to see the + full order at once. +

+
+ +
+ +
+
+ ); +} diff --git a/src/components/elements/PriorityFairnessWalkthrough/index.js b/src/components/elements/PriorityFairnessWalkthrough/index.js new file mode 100644 index 0000000000..7b2d5e7d80 --- /dev/null +++ b/src/components/elements/PriorityFairnessWalkthrough/index.js @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import HowItWorks from './HowItWorks'; +import Overview from './Overview'; +import SDK from './SDK'; +import TryIt from './TryIt'; +import styles from './walkthrough.module.css'; + +const NAV = [ + { id: 'overview', label: 'Overview' }, + { id: 'tryit', label: 'Try It' }, + { id: 'howitworks', label: 'How It Works' }, + { id: 'sdk', label: 'SDK Examples' }, +]; + +export default function PriorityFairnessWalkthrough() { + const [active, setActive] = useState('overview'); + + function next(current) { + const idx = NAV.findIndex((n) => n.id === current); + if (idx < NAV.length - 1) setActive(NAV[idx + 1].id); + } + + return ( +
+ + + {active === 'overview' && next('overview')} />} + {active === 'tryit' && next('tryit')} />} + {active === 'howitworks' && next('howitworks')} />} + {active === 'sdk' && } +
+ ); +} diff --git a/src/components/elements/PriorityFairnessWalkthrough/walkthrough.module.css b/src/components/elements/PriorityFairnessWalkthrough/walkthrough.module.css new file mode 100644 index 0000000000..6175c0a2e9 --- /dev/null +++ b/src/components/elements/PriorityFairnessWalkthrough/walkthrough.module.css @@ -0,0 +1,331 @@ +/* + * PriorityFairnessWalkthrough.module.css + * Adapted from NexusDemo CSS vars for light/dark mode support. + */ + +/* ── Per-theme accent vars ─────────────────────────────── */ +:global([data-theme='dark']) { + --pfw-surface: #1a1a2e; + --pfw-surface2: #252540; + --pfw-border: rgba(255, 255, 255, 0.08); + --pfw-muted: #94a3b8; + --pfw-nav-inactive: #94a3b8; + --pfw-nav-hover: #d4d7ff; + --pfw-nav-active: #7f86f1; + --pfw-uv: #444ce7; + --pfw-teal: #1ff1a5; + --pfw-lime: #c3ff62; + --pfw-badge-bg: rgba(68, 76, 231, 0.2); + --pfw-step-num-bg: #7f86f1; + --pfw-step-num-text: #fff; + --pfw-card-bg: rgba(255, 255, 255, 0.03); + --pfw-card-border: rgba(127, 134, 241, 0.2); + --pfw-priority-card-border: linear-gradient(255deg, #444ce7 0%, #b664ff 100%); +} + +:global([data-theme='light']) { + --pfw-surface: #f4f6f9; + --pfw-surface2: #ffffff; + --pfw-border: rgba(0, 0, 0, 0.08); + --pfw-muted: #64748b; + --pfw-nav-inactive: #64748b; + --pfw-nav-hover: #3a41cc; + --pfw-nav-active: #444ce7; + --pfw-uv: #3a41cc; + --pfw-teal: #059669; + --pfw-lime: #65a30d; + --pfw-badge-bg: rgba(58, 65, 204, 0.1); + --pfw-step-num-bg: #444ce7; + --pfw-step-num-text: #fff; + --pfw-card-bg: #ffffff; + --pfw-card-border: rgba(0, 0, 0, 0.09); + --pfw-priority-card-border: linear-gradient(255deg, #444ce7 0%, #b664ff 100%); +} + +/* ── Shell ─────────────────────────────────────────────── */ +.shell { + font-family: var(--ifm-font-family-base); + color: var(--ifm-font-color-base); + background: var(--ifm-background-color); + min-height: 60vh; +} + +/* ── Nav ───────────────────────────────────────────────── */ +.nav { + position: sticky; + top: var(--ifm-navbar-height); + z-index: 50; + background: var(--ifm-background-color); + border-bottom: 1px solid var(--pfw-border); + display: flex; + align-items: center; + gap: 4px; + padding: 0 24px; + overflow-x: auto; + scrollbar-width: none; +} + +.nav::-webkit-scrollbar { + display: none; +} + +.navLogo { + font-weight: 700; + font-size: 13px; + color: var(--ifm-color-primary); + margin-right: 16px; + white-space: nowrap; + flex-shrink: 0; + letter-spacing: 0.01em; +} + +.navBtn { + background: none; + border: none; + cursor: pointer; + font-size: 13px; + font-weight: 500; + color: var(--pfw-nav-inactive); + padding: 14px 12px; + white-space: nowrap; + border-bottom: 2px solid transparent; + transition: color 0.15s, border-color 0.15s; + margin-bottom: -1px; + font-family: var(--ifm-font-family-base); +} + +.navBtn:hover { + color: var(--pfw-nav-hover); +} + +.navBtnActive { + color: var(--pfw-nav-active); + border-bottom-color: var(--pfw-nav-active); +} + +/* ── Section container ─────────────────────────────────── */ +.section { + max-width: 860px; + margin: 0 auto; + padding: 40px 24px 64px; +} + +.sectionHeading { + font-size: 1.15rem; + font-weight: 600; + margin: 2rem 0 1rem; + color: var(--ifm-font-color-base); +} + +.lead { + font-size: 1.05rem; + line-height: 1.7; + color: var(--ifm-font-color-base); + margin-bottom: 1.5rem; +} + +/* ── Feature cards (gradient border, no rounded corners) ── */ +.featureGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin: 1.75rem 0 1.25rem; +} + +@media (max-width: 640px) { + .featureGrid { + grid-template-columns: 1fr; + } +} + +.featureCard { + border: 1px solid transparent; + background-image: + linear-gradient(var(--ifm-background-color, #13111b), var(--ifm-background-color, #13111b)), + linear-gradient(255deg, #444ce7 0%, #b664ff 100%); + background-origin: padding-box, border-box; + background-clip: padding-box, border-box; + border-radius: 0; + padding: 1.25rem 1.25rem 1.25rem 1rem; +} + +.featureCardHeader { + display: flex; + align-items: flex-start; + gap: 0.875rem; + margin-bottom: 0.75rem; +} + +.featureCardTitle { + font-size: 1.05rem; + font-weight: 300; + letter-spacing: 0.01em; + margin: 0; + line-height: 1.5; +} + +.featureCardBody { + font-size: 0.9rem; + line-height: 1.7; + margin: 0; + color: var(--ifm-font-color-base); +} + +/* ── Priority pills ────────────────────────────────────── */ +.pillStrip { + display: flex; + gap: 6px; + margin: 1.25rem 0 0.5rem; + flex-wrap: wrap; +} + +.pill { + border-radius: 0; + padding: 5px 14px; + font-size: 12px; + font-weight: 600; + font-family: var(--ifm-font-family-monospace); + white-space: nowrap; +} + +/* ── Use-both callout ──────────────────────────────────── */ +.useBoth { + font-size: 0.9rem; + line-height: 1.7; + color: var(--pfw-muted); + margin: 1.25rem 0 0; +} + +/* ── How It Works steps ────────────────────────────────── */ +.stepList { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 1.5rem 0; +} + +.step { + display: flex; + gap: 1rem; + align-items: flex-start; + background: var(--pfw-card-bg); + border: 1px solid var(--pfw-card-border); + border-radius: 0; + padding: 1.25rem; +} + +.stepNum { + flex-shrink: 0; + width: 28px; + height: 28px; + background: var(--pfw-step-num-bg); + color: var(--pfw-step-num-text); + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + margin-top: 1px; +} + +.stepContent { + flex: 1; +} + +.stepTitle { + font-weight: 600; + font-size: 0.95rem; + margin: 0 0 0.35rem; +} + +.stepBody { + font-size: 0.875rem; + line-height: 1.65; + margin: 0; + color: var(--ifm-font-color-base); +} + +/* ── When-to-use table ─────────────────────────────────── */ +.whenTable { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; + margin: 1rem 0 2rem; +} + +.whenTable th, +.whenTable td { + border: 1px solid var(--pfw-border); + padding: 0.65rem 1rem; + text-align: left; +} + +.whenTable th { + background: var(--pfw-card-bg); + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--pfw-muted); +} + +.badge { + display: inline-block; + padding: 2px 8px; + font-size: 11px; + font-weight: 600; + border-radius: 0; + font-family: var(--ifm-font-family-monospace); +} + +.badgePriority { + background: var(--pfw-badge-bg); + color: var(--pfw-nav-active); +} + +.badgeFairness { + background: rgba(31, 241, 165, 0.12); + color: var(--pfw-teal); +} + +.badgeBoth { + background: rgba(182, 100, 255, 0.12); + color: #b664ff; +} + +/* ── SDK section ───────────────────────────────────────── */ +.sdkGroup { + margin-bottom: 2.5rem; +} + +.sdkGroupTitle { + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--pfw-muted); + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--pfw-border); +} + +/* ── Next button ───────────────────────────────────────── */ +.nextBtn { + margin-top: 2rem; + display: inline-block; + background: var(--pfw-uv); + color: #fff; + border: none; + padding: 10px 22px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + border-radius: 0; + font-family: var(--ifm-font-family-base); + transition: opacity 0.15s; +} + +.nextBtn:hover { + opacity: 0.85; +} diff --git a/src/components/index.js b/src/components/index.js index b03dc88a96..7e2cc96cb0 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,6 +1,7 @@ // Website components export { default as RetrySimulator } from './elements/RetrySimulator'; export { default as PriorityFairnessSimulator } from './elements/PriorityFairnessSimulator'; +export { default as PriorityFairnessWalkthrough } from './elements/PriorityFairnessWalkthrough'; export { default as HomePageHero } from './elements/HomePageHero'; export { SdkLogos } from './elements/SdkLogos'; export { SdkLogosAsBlocks } from './elements/SdkLogosAsBlocks';