Run python nodes out of process, with modules of their own
User code no longer execs in the engine. A pool of persistent worker subprocesses speaks one JSON object per line; the controller installs a proxy as the node's function, so every execution path funnels through it and the pipeline is untouched. A crash costs one subprocess, a per-node timeout is a kill, and cancelling from the canvas is that same kill. The workers run a venv of the user's own on the data volume, filled from a pip manifest versioned beside the flows. Applying it retires the workers and rebuilds, so a package lands without restarting the engine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
This commit is contained in:
@@ -15,12 +15,13 @@ import {
|
||||
Radio,
|
||||
Shuffle,
|
||||
Split,
|
||||
Square,
|
||||
Terminal,
|
||||
Timer,
|
||||
} from "lucide-react"
|
||||
import { memo } from "react"
|
||||
|
||||
import type { MessageSpec, NodeDef_Input } from "@/client"
|
||||
import { FlowsService, type MessageSpec, type NodeDef_Input } from "@/client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -157,6 +158,32 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
{typeLabel}
|
||||
</span>
|
||||
</span>
|
||||
{status === "running" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
aria-label="Stop this node"
|
||||
data-testid="node-cancel"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
// Best effort by nature: it may well have finished between
|
||||
// the render and the click, which is the outcome asked for.
|
||||
FlowsService.cancelNode({
|
||||
name: flow,
|
||||
nodeId: definition.id,
|
||||
}).catch(() => {})
|
||||
}}
|
||||
>
|
||||
<Square />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop this node</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{status === "error" && onShowLogs ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -934,6 +934,32 @@ function PanelBody({
|
||||
onChange={(params) => onChange({ ...node, params })}
|
||||
/>
|
||||
) : null}
|
||||
{hasSource ? (
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="node-timeout" className={SECTION}>
|
||||
Timeout
|
||||
</Label>
|
||||
<Input
|
||||
id="node-timeout"
|
||||
type="number"
|
||||
min={0}
|
||||
step="any"
|
||||
className="h-8 text-sm"
|
||||
placeholder="30 (default)"
|
||||
value={node.timeout ? String(node.timeout) : ""}
|
||||
onChange={(event) =>
|
||||
onChange({
|
||||
...node,
|
||||
timeout: Number(event.target.value) || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Seconds this code may run before it is stopped. Above 60 the
|
||||
engine may deliver the same work again while it is still running.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{hasSource ? (
|
||||
<SharingSection flow={flow} node={node} onShared={onShared} />
|
||||
) : null}
|
||||
|
||||
@@ -26,6 +26,7 @@ type FlowEvent =
|
||||
ts: number
|
||||
source?: ValueSource
|
||||
}
|
||||
| { type: "node_started"; node: string }
|
||||
| { type: "node_executed"; node: string; outputs: number }
|
||||
| { type: "node_error"; node: string; error: string }
|
||||
| { type: "node_status"; node: string; status: string; error?: string | null }
|
||||
@@ -97,6 +98,9 @@ export function useFlowSocket(onAuthFailure?: () => void): void {
|
||||
source: message.source,
|
||||
})
|
||||
break
|
||||
case "node_started":
|
||||
liveStore.setStatus(message.node, { status: "running" })
|
||||
break
|
||||
case "node_executed":
|
||||
liveStore.setStatus(message.node, { status: "success" })
|
||||
if (message.outputs > 0) liveStore.recordEmit(message.node)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
KeyRound,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Package,
|
||||
Settings,
|
||||
Users,
|
||||
Workflow,
|
||||
@@ -28,6 +29,7 @@ const baseItems: Item[] = [
|
||||
// Both are engine-wide operator settings rather than personal ones, so they
|
||||
// sit here and not among the per-user tabs under Settings.
|
||||
{ icon: KeyRound, title: "Secrets", path: "/secrets" },
|
||||
{ icon: Package, title: "Modules", path: "/modules" },
|
||||
{ icon: Bell, title: "Alerts", path: "/alerts" },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user