feat: add endpoint to restart workers in a worker group (#8659)

* feat: add endpoint to restart workers in a worker group

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx query cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing modules field to RawCode in tests and regenerate sqlx cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update sqlx

* fix: use require_devops_role for restart worker group endpoint

Matches the permission level of the clean cache endpoint (update_config),
allowing both superadmin and devops role users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback for restart worker group

- Fix OpenAPI description to say "devops role" instead of "superadmin"
- Add dispatch('reload') after restart to refresh worker list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only dispatch reload on successful restart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-04-02 19:36:48 +02:00
committed by GitHub
parent d8edaad99c
commit f0437eba19
36 changed files with 183 additions and 35 deletions

View File

@@ -34,7 +34,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -76,7 +76,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -33,7 +33,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -43,7 +43,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -33,7 +33,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -44,7 +44,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -70,7 +70,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -34,7 +34,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -40,7 +40,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -148,7 +148,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -43,7 +43,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "78f7c4efd7be2aad03f0ec67ab882e1f9aaa7141ab75046a71ac789ab659bdc6"
}

View File

@@ -76,7 +76,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -41,7 +41,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -153,7 +153,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -118,7 +118,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -83,7 +83,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -133,7 +133,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -38,7 +38,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -153,7 +153,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -123,7 +123,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -43,7 +43,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -40,7 +40,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -77,7 +77,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -68,7 +68,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -77,7 +77,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -133,7 +133,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -40,7 +40,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -38,7 +38,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -84,7 +85,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -33,7 +33,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}

View File

@@ -1597,6 +1597,12 @@ async fn process_notify_event(
tracing::debug!("config changed but did not target this server/worker");
}
}
"restart_worker_group" => {
if worker_mode && payload == *WORKER_GROUP {
tracing::info!("Restart requested for worker group '{payload}'");
spawn_graceful_killpill(tx, db, 10, "worker group restart requested").await;
}
}
"notify_webhook_change" => {
tracing::info!(
"Webhook change detected, invalidating webhook cache: {}",

View File

@@ -1107,6 +1107,7 @@ main <- function(msg) {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.arg("msg", json!("world"))
.run_until_complete(&db, false, port)
@@ -1144,6 +1145,7 @@ main <- function() {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.run_until_complete(&db, false, port)
.await
@@ -1180,6 +1182,7 @@ main <- function() {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.run_until_complete(&db, false, port)
.await

View File

@@ -12,9 +12,7 @@ use std::{collections::HashMap, time::Duration};
mod ee;
pub mod ee_oss;
#[cfg(feature = "enterprise")]
use windmill_api_auth::require_devops_role;
use windmill_api_auth::{require_super_admin, ApiAuthed};
use windmill_api_auth::{require_devops_role, require_super_admin, ApiAuthed};
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
use windmill_common::DB;
@@ -102,6 +100,10 @@ pub fn global_service() -> Router {
.route(
"/sync_cached_resource_types",
post(sync_cached_resource_types),
)
.route(
"/restart_worker_group/{worker_group}",
post(restart_worker_group),
);
// Vault integration routes (EE only - requires both private and enterprise features)
@@ -662,6 +664,25 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
Ok("Sent stats".to_string())
}
async fn restart_worker_group(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Path(worker_group): Path<String>,
) -> error::Result<String> {
require_devops_role(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
worker_group
)
.execute(&db)
.await?;
Ok(format!(
"Restart signal sent to worker group '{worker_group}'"
))
}
#[derive(serde::Serialize)]
pub struct StatsDownload {
pub signature: String,

View File

@@ -1370,6 +1370,28 @@ paths:
schema:
type: string
/settings/restart_worker_group/{worker_group}:
post:
summary: restart worker group
description: Send a restart signal to all workers in the specified worker group. Workers will gracefully shut down and are expected to be restarted by their supervisor. Requires devops role.
operationId: restartWorkerGroup
tags:
- setting
parameters:
- name: worker_group
in: path
required: true
schema:
type: string
description: the name of the worker group to restart
responses:
"200":
description: restart signal sent
content:
text/plain:
schema:
type: string
/settings/get_stats:
get:
summary: get telemetry stats with HMAC signature (EE only)

View File

@@ -6,6 +6,7 @@
RotateCcw,
Settings,
Trash,
Power,
X,
ExternalLink,
FileCode
@@ -14,7 +15,13 @@
import Badge from './common/badge/Badge.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import { ConfigService, WorkspaceService, type WorkerPing, type Workspace } from '$lib/gen'
import {
ConfigService,
SettingService,
WorkspaceService,
type WorkerPing,
type Workspace
} from '$lib/gen'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
@@ -264,6 +271,7 @@
})
let openDelete = $state(false)
let openClean = $state(false)
let openRestart = $state(false)
// Compute hashed tags for display (actual tags used by the worker)
let hashedDedicatedTags: Map<string, string> = $state(new Map())
@@ -365,6 +373,32 @@
</div>
</ConfirmationModal>
<ConfirmationModal
open={openRestart}
title="Restart workers"
confirmationText="Restart"
on:canceled={() => {
openRestart = false
}}
on:confirmed={async () => {
try {
await SettingService.restartWorkerGroup({ workerGroup: name })
sendUserToast(`Restart signal sent to worker group '${name}'`)
dispatch('reload')
} catch (e) {
sendUserToast(`Failed to restart worker group: ${e}`, true)
}
openRestart = false
}}
>
<div class="flex flex-col w-full space-y-4">
<span
>Are you sure you want to restart all workers in worker group '{name}'? Workers will be
gracefully shut down and are expected to be restarted by their supervisor.</span
>
</div>
</ConfirmationModal>
<Drawer bind:this={drawer} size="800px">
<DrawerContent
on:close={() => drawer?.closeDrawer()}
@@ -1236,6 +1270,17 @@
>
Clean cache
</Button>
<Button
unifiedSize="sm"
variant="subtle"
on:click={() => {
openRestart = true
}}
startIcon={{ icon: Power }}
destructive
>
Restart workers
</Button>
{:else}
<Dropdown
items={[
@@ -1248,6 +1293,13 @@
disabled: !config,
type: 'delete'
},
{
displayName: 'Restart workers',
action: () => {
openRestart = true
},
type: 'delete'
},
{
displayName: 'Delete config',
action: () => {