fix: hash long dedicated worker tags (#7914)

* fix: hash long dedicated worker tags

* Update frontend/src/lib/components/dedicated_worker.ts

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
hugocasa
2026-02-11 20:29:44 +01:00
committed by GitHub
parent 22f22c2661
commit aaa1b92300
9 changed files with 225 additions and 82 deletions
@@ -8,6 +8,7 @@
import { SvelteMap } from 'svelte/reactivity'
import { untrack } from 'svelte'
import BarsStaggered from './icons/BarsStaggered.svelte'
import { parseTag } from './dedicated_worker'
// A "Runnable" is a script or flow with dedicated_worker=true
interface Runnable {
@@ -62,21 +63,6 @@
// Languages that support dedicated workers
const DEDICATED_WORKER_LANGUAGES = ['python3', 'bun', 'deno']
// Parse a tag to extract workspace, type (script/flow), and path
function parseTag(tag: string): { workspace: string; type: 'script' | 'flow'; path: string } | null {
const colonIndex = tag.indexOf(':')
if (colonIndex === -1) return null
const workspace = tag.substring(0, colonIndex)
const rest = tag.substring(colonIndex + 1)
if (rest.startsWith('flow/')) {
return { workspace, type: 'flow', path: rest.substring(5) }
} else {
return { workspace, type: 'script', path: rest }
}
}
// Resolve workspace script languages and filter to supported languages
async function resolveAndFilterRunners(
workspace: string,
@@ -438,25 +424,27 @@
{:else}
<div class="w-7"></div>
{/if}
<div class="flex-1 flex items-center gap-2 px-2 py-1.5 min-w-0">
{#if info}
{#if info.type === 'flow'}
<BarsStaggered size={14} class="flex-shrink-0 text-secondary" />
<div class="flex-1 flex flex-col gap-0.5 px-2 py-1.5 min-w-0">
<div class="flex items-center gap-2 min-w-0">
{#if info}
{#if info.type === 'flow'}
<BarsStaggered size={14} class="flex-shrink-0 text-secondary" />
{:else}
<CodeXml size={14} class="flex-shrink-0 text-secondary" />
{/if}
<span class="text-xs truncate flex-1 min-w-0">{info.path}</span>
<span class="text-xs text-tertiary flex-shrink-0">({info.workspace})</span>
{#if info.type === 'flow' && info.runners}
<Badge color="indigo" small>
{info.runners.length} runner{info.runners.length !== 1 ? 's' : ''}
</Badge>
{:else if info.type === 'script'}
<Badge color="blue" small>1 runner</Badge>
{/if}
{:else}
<CodeXml size={14} class="flex-shrink-0 text-secondary" />
<span class="text-xs text-tertiary truncate">{tag}</span>
{/if}
<span class="text-xs truncate flex-1">{info.path}</span>
<span class="text-xs text-tertiary flex-shrink-0">({info.workspace})</span>
{#if info.type === 'flow' && info.runners}
<Badge color="indigo" small>
{info.runners.length} runner{info.runners.length !== 1 ? 's' : ''}
</Badge>
{:else if info.type === 'script'}
<Badge color="blue" small>1 runner</Badge>
{/if}
{:else}
<span class="text-xs text-tertiary truncate">{tag}</span>
{/if}
</div>
</div>
{#if !disabled}
<button
@@ -474,10 +462,14 @@
{#if info?.type === 'flow' && info.expanded && info.runners}
<div class="bg-surface-secondary border-t">
{#each info.runners as runner (runner.stepId)}
<div class="flex items-center gap-2 px-9 py-1 text-xs border-t first:border-t-0">
<span class="font-mono text-tertiary">{runner.stepId}</span>
<div
class="flex items-center gap-2 px-9 py-1 text-xs border-t first:border-t-0 min-w-0"
>
<span class="font-mono text-tertiary flex-shrink-0">{runner.stepId}</span>
{#if runner.stepSummary}
<span class="text-secondary truncate flex-1">{runner.stepSummary}</span>
<span class="text-secondary truncate flex-1 min-w-0"
>{runner.stepSummary}</span
>
{/if}
<Badge color="gray" small>
{runner.isInline ? runner.language : runner.scriptPath}
@@ -577,7 +569,7 @@
<div class="w-7"></div>
{/if}
<button
class="flex-1 flex items-center gap-2 px-2 py-1.5 hover:bg-surface-hover transition-colors text-left"
class="flex-1 flex items-center gap-2 px-2 py-1.5 hover:bg-surface-hover transition-colors text-left min-w-0"
onclick={(e) => {
e.stopPropagation()
if (!disabled) toggleRunnable(runnable)
@@ -593,9 +585,11 @@
<Check class="h-3 w-3 text-white" />
{/if}
</div>
<span class="flex-1 text-xs truncate">{runnable.displayName}</span>
<span class="flex-1 text-xs truncate min-w-0"
>{runnable.displayName}</span
>
{#if runnable.type === 'flow' && runnable.runners}
<span class="text-xs text-tertiary">
<span class="text-xs text-tertiary flex-shrink-0">
{runnable.runners.length}
</span>
{/if}
@@ -614,11 +608,13 @@
{:else}
{#each runnable.runners as runner (runner.stepId)}
<div
class="flex items-center gap-2 px-9 py-1 text-xs border-t first:border-t-0"
class="flex items-center gap-2 px-9 py-1 text-xs border-t first:border-t-0 min-w-0"
>
<span class="font-mono text-tertiary">{runner.stepId}</span>
<span class="font-mono text-tertiary flex-shrink-0"
>{runner.stepId}</span
>
{#if runner.stepSummary}
<span class="text-secondary truncate flex-1">
<span class="text-secondary truncate flex-1 min-w-0">
{runner.stepSummary}
</span>
{/if}
@@ -1443,8 +1443,8 @@
{#if script.dedicated_worker}
<div class="py-2">
<Alert type="info" title="Require dedicated workers">
One worker in a worker group needs to be configured with dedicated worker
set to: <pre>{$workspaceStore}:{script.path}</pre>
A worker group needs to be configured to listen to this script. Select
it in the dedicated workers section of the worker group configuration.
</Alert>
</div>
{/if}
+31 -9
View File
@@ -43,6 +43,7 @@
import Dropdown from './DropdownV2.svelte'
import TagList from './TagList.svelte'
import DedicatedWorkersSelector from './DedicatedWorkersSelector.svelte'
import { computeHashedTag } from './dedicated_worker'
function computeVCpuAndMemory(workers: [string, WorkerPing[]][]) {
let vcpus = 0
@@ -252,6 +253,21 @@
let openDelete = $state(false)
let openClean = $state(false)
// Compute hashed tags for display (actual tags used by the worker)
let hashedDedicatedTags: Map<string, string> = $state(new Map())
$effect(() => {
const dws = config?.dedicated_workers ?? (config?.dedicated_worker ? [config.dedicated_worker] : [])
if (dws.length > 0) {
Promise.all(dws.map(async (dw) => [dw, await computeHashedTag(dw)] as const)).then(
(entries) => {
hashedDedicatedTags = new Map(entries)
}
)
} else {
hashedDedicatedTags = new Map()
}
})
let drawer: Drawer | undefined = $state()
let vcpus_memory = $derived(computeVCpuAndMemory(workers))
let selected = $derived(
@@ -1166,21 +1182,27 @@
<TagList tags={config.worker_tags} maxVisible={25} class="flex-wrap" />
</div>
{:else if config?.dedicated_workers && config.dedicated_workers.length > 0}
<div class="flex flex-row items-start gap-2 w-full">
<div class="text-secondary text-xs mt-1">Dedicated to:</div>
<div class="flex flex-wrap gap-1">
<div class="flex flex-row items-start gap-2 w-full min-w-0">
<div class="text-secondary text-xs mt-1 flex-shrink-0">Dedicated to:</div>
<div class="flex flex-wrap gap-1 min-w-0">
{#each config.dedicated_workers as dw}
<div class="text-xs bg-surface-secondary px-2 py-1 rounded text-primary font-mono">
{dw}
<div
class="text-xs bg-surface-secondary px-2 py-1 rounded text-primary font-mono truncate max-w-xs"
title={hashedDedicatedTags.get(dw) ?? dw}
>
{hashedDedicatedTags.get(dw) ?? dw}
</div>
{/each}
</div>
</div>
{:else if config?.dedicated_worker}
<div class="flex flex-row items-start gap-2 w-full">
<div class="text-secondary text-xs mt-1">Dedicated to:</div>
<div class="text-xs bg-surface-secondary px-2 py-1 rounded text-primary font-mono">
{config.dedicated_worker}
<div class="flex flex-row items-start gap-2 w-full min-w-0">
<div class="text-secondary text-xs mt-1 flex-shrink-0">Dedicated to:</div>
<div
class="text-xs bg-surface-secondary px-2 py-1 rounded text-primary font-mono truncate max-w-xs"
title={hashedDedicatedTags.get(config.dedicated_worker) ?? config.dedicated_worker}
>
{hashedDedicatedTags.get(config.dedicated_worker) ?? config.dedicated_worker}
</div>
</div>
{/if}
@@ -0,0 +1,41 @@
const MAX_TAG_LEN = 50
const HASH_SUFFIX_LEN = 16
// IMPORTANT: This implementation must stay in sync with the Rust backend
// `dedicated_worker_tag()` in backend/windmill-common/src/worker.rs.
// The frontend version is used for display; the backend version is authoritative.
export async function dedicatedWorkerTag(workspaceId: string, path: string): Promise<string> {
const fullTag = `${workspaceId}:${path}`
if (fullTag.length <= MAX_TAG_LEN) return fullTag
const encoded = new TextEncoder().encode(fullTag)
const hashBuffer = await crypto.subtle.digest('SHA-256', encoded)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hexHash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
const prefixLen = MAX_TAG_LEN - 1 - HASH_SUFFIX_LEN
return `${fullTag.substring(0, prefixLen)}#${hexHash.substring(0, HASH_SUFFIX_LEN)}`
}
export function parseTag(
tag: string
): { workspace: string; type: 'script' | 'flow'; path: string } | null {
const colonIndex = tag.indexOf(':')
if (colonIndex === -1) return null
const workspace = tag.substring(0, colonIndex)
const rest = tag.substring(colonIndex + 1)
if (rest.startsWith('flow/')) {
return { workspace, type: 'flow', path: rest.substring(5) }
} else {
return { workspace, type: 'script', path: rest }
}
}
export async function computeHashedTag(rawTag: string): Promise<string> {
const parsed = parseTag(rawTag)
if (!parsed) return rawTag
const fullPath = parsed.type === 'flow' ? `flow/${parsed.path}` : parsed.path
return dedicatedWorkerTag(parsed.workspace, fullPath)
}
@@ -8,7 +8,7 @@
import { Alert, Button, SecondsInput } from '$lib/components/common'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { enterpriseLicense, userStore } from '$lib/stores'
import { isCloudHosted } from '$lib/cloud'
import Tooltip from '$lib/components/Tooltip.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
@@ -554,9 +554,8 @@
{#if flowStore.val.dedicated_worker}
<div class="mt-2">
<Alert type="info" title="Require dedicated workers">
One worker in a worker group needs to be configured with dedicated worker set to: <pre
>{$workspaceStore}:flow/{$pathStore}</pre
>
A worker group needs to be configured to listen to this flow. Select it in the dedicated
workers section of the worker group configuration.
</Alert>
</div>
{/if}