Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions flowfile_core/flowfile_core/kernel/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,48 @@ async def stop_kernel(self, kernel_id: str) -> None:
kernel.container_id = None
logger.info("Stopped kernel '%s'", kernel_id)

async def restart_kernel(self, kernel_id: str) -> KernelInfo:
"""Stop and restart a kernel container, preserving configuration.

All in-memory artifacts are cleared (they do not survive a restart).
The kernel configuration (packages, memory/CPU limits) is preserved.
If the restart fails, the error message includes container logs.
"""
kernel = self._get_kernel_or_raise(kernel_id)
kernel.state = KernelState.RESTARTING
kernel.error_message = None

try:
# Stop and remove the existing container
self._cleanup_container(kernel_id)
kernel.container_id = None

# Re-allocate port if needed (local mode only)
if kernel.port is None and not self._kernel_volume:
kernel.port = self._allocate_port()

# Start a fresh container with the same configuration
env = self._build_kernel_env(kernel_id, kernel)
run_kwargs = self._build_run_kwargs(kernel_id, kernel, env)
container = self._docker.containers.run(_KERNEL_IMAGE, **run_kwargs)
kernel.container_id = container.id
await self._wait_for_healthy(kernel_id, timeout=kernel.health_timeout)
kernel.state = KernelState.IDLE
logger.info("Restarted kernel '%s' (container %s)", kernel_id, container.short_id)
except (docker.errors.DockerException, httpx.HTTPError, TimeoutError, OSError) as exc:
kernel.state = KernelState.ERROR
# Try to capture container logs for diagnostics
logs = self._get_container_logs(kernel_id)
if logs:
kernel.error_message = f"Restart failed: {exc}\n\nContainer logs:\n{logs}"
else:
kernel.error_message = f"Restart failed: {exc}"
logger.error("Failed to restart kernel '%s': %s", kernel_id, exc)
self._cleanup_container(kernel_id)
raise RuntimeError(kernel.error_message) from exc

return kernel

async def delete_kernel(self, kernel_id: str) -> None:
kernel = self._get_kernel_or_raise(kernel_id)
if kernel.state in (KernelState.IDLE, KernelState.EXECUTING):
Expand Down Expand Up @@ -818,6 +860,18 @@ def _cleanup_container(self, kernel_id: str) -> None:
except (docker.errors.APIError, docker.errors.DockerException) as exc:
logger.warning("Error cleaning up container for kernel '%s': %s", kernel_id, exc)

def _get_container_logs(self, kernel_id: str, tail: int = 50) -> str:
"""Retrieve the last *tail* lines of logs from the kernel container."""
kernel = self._kernels.get(kernel_id)
if kernel is None or kernel.container_id is None:
return ""
try:
container = self._docker.containers.get(kernel.container_id)
return container.logs(tail=tail).decode("utf-8", errors="replace")
except Exception as exc:
logger.debug("Could not retrieve logs for kernel '%s': %s", kernel_id, exc)
return ""

async def _wait_for_healthy(self, kernel_id: str, timeout: int = _HEALTH_TIMEOUT) -> None:
kernel = self._get_kernel_or_raise(kernel_id)
url = f"{self._kernel_url(kernel)}/health"
Expand Down
1 change: 1 addition & 0 deletions flowfile_core/flowfile_core/kernel/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
class KernelState(str, Enum):
STOPPED = "stopped"
STARTING = "starting"
RESTARTING = "restarting"
IDLE = "idle"
EXECUTING = "executing"
ERROR = "error"
Expand Down
15 changes: 15 additions & 0 deletions flowfile_core/flowfile_core/kernel/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ async def stop_kernel(kernel_id: str, current_user=Depends(get_current_active_us
raise HTTPException(status_code=404, detail=str(exc))


@router.post("/{kernel_id}/restart", response_model=KernelInfo)
async def restart_kernel(kernel_id: str, current_user=Depends(get_current_active_user)):
"""Restart a kernel container, preserving configuration but clearing all in-memory artifacts."""
manager = _get_manager()
kernel = await manager.get_kernel(kernel_id)
if kernel is None:
raise HTTPException(status_code=404, detail=f"Kernel '{kernel_id}' not found")
if manager.get_kernel_owner(kernel_id) != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized to access this kernel")
try:
return await manager.restart_kernel(kernel_id)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc


@router.post("/{kernel_id}/execute", response_model=ExecuteResult)
async def execute_code(kernel_id: str, request: ExecuteRequest, current_user=Depends(get_current_active_user)):
manager = _get_manager()
Expand Down
13 changes: 13 additions & 0 deletions flowfile_frontend/src/renderer/app/api/kernel.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ export class KernelApi {
}
}

static async restart(kernelId: string): Promise<KernelInfo> {
try {
const response = await axios.post<KernelInfo>(
`${API_BASE_URL}/${encodeURIComponent(kernelId)}/restart`,
);
return response.data;
} catch (error) {
console.error("API Error: Failed to restart kernel:", error);
const errorMsg = (error as any).response?.data?.detail || "Failed to restart kernel";
throw new Error(errorMsg);
}
}

static async getArtifacts(kernelId: string): Promise<Record<string, any>> {
try {
const response = await axios.get<Record<string, any>>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
</span>
</el-option>
</el-select>
<button
v-if="selectedKernelId && (selectedKernelState === 'idle' || selectedKernelState === 'executing' || selectedKernelState === 'error')"
class="restart-kernel-btn"
:disabled="isRestarting"
title="Restart Kernel"
@click="handleRestartKernel"
>
<i class="fa-solid fa-arrows-rotate" :class="{ 'fa-spin': isRestarting }"></i>
</button>
<router-link :to="{ name: 'kernelManager' }" class="manage-kernels-link">
Manage Kernels
</router-link>
Expand All @@ -50,6 +59,7 @@
<template v-if="selectedKernelState === 'stopped'">Start it from the Kernel Manager to execute code.</template>
<template v-else-if="selectedKernelState === 'error'">Check the Kernel Manager for details.</template>
<template v-else-if="selectedKernelState === 'starting'">Please wait for it to become idle.</template>
<template v-else-if="selectedKernelState === 'restarting'">Please wait for the restart to complete.</template>
<template v-else-if="selectedKernelState === 'executing'">Please wait for the current execution to finish.</template>
</div>
</div>
Expand Down Expand Up @@ -247,13 +257,31 @@ const stopKernelPolling = () => {
}
};

const isRestarting = ref(false);

const handleKernelChange = (kernelId: string | null) => {
if (nodePythonScript.value) {
nodePythonScript.value.python_script_input.kernel_id = kernelId ?? null;
}
loadArtifacts();
};

const handleRestartKernel = async () => {
if (!selectedKernelId.value || isRestarting.value) return;
isRestarting.value = true;
try {
await KernelApi.restart(selectedKernelId.value);
await loadKernels();
loadArtifacts();
} catch (error: any) {
const msg = error.message || "Failed to restart kernel.";
console.error("Kernel restart failed:", msg);
alert(`Kernel restart failed: ${msg}`);
} finally {
isRestarting.value = false;
}
};

// ─── Artifact helpers ───────────────────────────────────────────────────────

const loadArtifacts = async () => {
Expand Down Expand Up @@ -472,6 +500,33 @@ defineExpose({ loadNodeData, pushNodeData, saveSettings });
flex: 1;
}

.restart-kernel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid var(--el-border-color, #dcdfe6);
border-radius: 4px;
background: var(--el-fill-color-blank, #fff);
color: var(--el-text-color-regular, #606266);
cursor: pointer;
font-size: 0.8rem;
flex-shrink: 0;
transition: all 0.15s;
}

.restart-kernel-btn:hover:not(:disabled) {
color: var(--el-color-warning, #e6a23c);
border-color: var(--el-color-warning, #e6a23c);
}

.restart-kernel-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.manage-kernels-link {
font-size: 0.8rem;
color: var(--el-color-primary);
Expand Down Expand Up @@ -516,6 +571,10 @@ defineExpose({ loadNodeData, pushNodeData, saveSettings });
background-color: #f56c6c;
}

.kernel-state-dot--restarting {
background-color: #e6a23c;
}

.kernel-state-label {
font-size: 0.8rem;
color: var(--el-text-color-secondary);
Expand Down
2 changes: 1 addition & 1 deletion flowfile_frontend/src/renderer/app/types/kernel.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Kernel management related TypeScript interfaces and types

export type KernelState = "stopped" | "starting" | "idle" | "executing" | "error";
export type KernelState = "stopped" | "starting" | "restarting" | "idle" | "executing" | "error";

export interface KernelConfig {
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@
>
<i class="fa-solid fa-play"></i> Start
</button>
<button
v-if="kernel.state === 'idle' || kernel.state === 'executing' || kernel.state === 'error'"
class="btn btn-warning btn-sm"
:disabled="busy"
@click="$emit('restart', kernel.id)"
>
<i class="fa-solid fa-arrows-rotate"></i> Restart
</button>
<button
v-if="kernel.state === 'idle' || kernel.state === 'executing'"
class="btn btn-secondary btn-sm"
Expand All @@ -65,7 +73,7 @@
</button>
<button
class="btn btn-danger btn-sm"
:disabled="busy || kernel.state === 'starting'"
:disabled="busy || kernel.state === 'starting' || kernel.state === 'restarting'"
@click="$emit('delete', kernel.id, kernel.name)"
>
<i class="fa-solid fa-trash-alt"></i> Delete
Expand All @@ -87,6 +95,7 @@ const props = defineProps<{
defineEmits<{
start: [id: string];
stop: [id: string];
restart: [id: string];
delete: [id: string, name: string];
}>();

Expand Down Expand Up @@ -244,4 +253,22 @@ const displayedPackages = computed(() => props.kernel.packages.slice(0, maxPacka
padding: var(--spacing-1) var(--spacing-2-5);
font-size: var(--font-size-xs);
}

.btn-warning {
background-color: var(--color-warning, #e6a23c);
color: #fff;
border: 1px solid var(--color-warning, #e6a23c);
border-radius: var(--border-radius-md);
cursor: pointer;
transition: opacity var(--transition-base) var(--transition-timing);
}

.btn-warning:hover:not(:disabled) {
opacity: 0.85;
}

.btn-warning:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
:busy="isActionInProgress(kernel.id)"
@start="handleStart"
@stop="handleStop"
@restart="handleRestart"
@delete="confirmDelete"
/>
</div>
Expand Down Expand Up @@ -114,6 +115,7 @@ const {
createKernel,
startKernel,
stopKernel,
restartKernel,
deleteKernel,
isActionInProgress,
} = useKernelManager();
Expand Down Expand Up @@ -150,6 +152,14 @@ const handleStop = async (kernelId: string) => {
}
};

const handleRestart = async (kernelId: string) => {
try {
await restartKernel(kernelId);
} catch (error: any) {
alert(`Error: ${error.message || "Failed to restart kernel."}`);
}
};

const confirmDelete = (kernelId: string, kernelName: string) => {
deleteTarget.value = { id: kernelId, name: kernelName };
showDeleteModal.value = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const config = computed(() => {
const map: Record<KernelState, { icon: string; label: string }> = {
stopped: { icon: "fa-solid fa-circle-stop", label: "Stopped" },
starting: { icon: "fa-solid fa-spinner fa-spin", label: "Starting" },
restarting: { icon: "fa-solid fa-arrows-rotate fa-spin", label: "Restarting" },
idle: { icon: "fa-solid fa-circle-check", label: "Ready" },
executing: { icon: "fa-solid fa-gear fa-spin", label: "Executing" },
error: { icon: "fa-solid fa-circle-exclamation", label: "Error" },
Expand Down Expand Up @@ -51,6 +52,11 @@ const label = computed(() => config.value.label);
color: var(--color-warning-dark);
}

.status-restarting {
background-color: var(--color-warning-light);
color: var(--color-warning-dark);
}

.status-idle {
background-color: var(--color-success-light);
color: var(--color-success-hover);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export function useKernelManager() {
}
};

const restartKernel = async (kernelId: string) => {
actionInProgress.value[kernelId] = true;
try {
await KernelApi.restart(kernelId);
await loadKernels();
} finally {
actionInProgress.value[kernelId] = false;
}
};

const deleteKernel = async (kernelId: string) => {
actionInProgress.value[kernelId] = true;
try {
Expand Down Expand Up @@ -112,6 +122,7 @@ export function useKernelManager() {
createKernel,
startKernel,
stopKernel,
restartKernel,
deleteKernel,
isActionInProgress,
};
Expand Down