diff --git a/docs/develop/priority-fairness-walkthrough.mdx b/docs/develop/priority-fairness-walkthrough.mdx new file mode 100644 index 0000000000..f51c37dcaf --- /dev/null +++ b/docs/develop/priority-fairness-walkthrough.mdx @@ -0,0 +1,20 @@ +--- +id: priority-fairness-walkthrough +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 + - priority key + - fairness key + - interactive demo +tags: + - Task Queues + - Priority and Fairness +--- + +import { PriorityFairnessWalkthrough } from '@site/src/components'; + + diff --git a/sidebars.js b/sidebars.js index d5944d13fb..4ef019afaa 100644 --- a/sidebars.js +++ b/sidebars.js @@ -890,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/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/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`} +
+ +
+

+ Next steps +

+ +
+
+ ); +} 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/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..7e2cc96cb0 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,5 +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';