mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: workspace dedicated workers (#7741)
* feat: workspace dedicated workers * ref * chore: update ee-repo-ref to a18ac31062ac092cb9a5fc87629e217d97f4911d This commit updates the EE repository reference after PR #398 was merged in windmill-ee-private. Previous ee-repo-ref: 98cfe3fef764d9d815d326d5056c734a03689d33 New ee-repo-ref: a18ac31062ac092cb9a5fc87629e217d97f4911d Automated by sync-ee-ref workflow. * fix(frontend): workspace script in flow steps --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
c05572e93739e2697ab310d87efe2744cd0e1aaf
|
||||
a18ac31062ac092cb9a5fc87629e217d97f4911d
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE worker_ping DROP COLUMN IF EXISTS dedicated_workers;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE worker_ping ADD COLUMN IF NOT EXISTS dedicated_workers TEXT[];
|
||||
+38
-28
@@ -56,8 +56,8 @@ use windmill_common::{
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
|
||||
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
@@ -84,10 +84,10 @@ use windmill_common::{
|
||||
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
|
||||
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
|
||||
use windmill_worker::{
|
||||
handle_job_error, JobCompletedSender, OtelTracingProxySettings, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
|
||||
INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS,
|
||||
OTEL_TRACING_PROXY_SETTINGS, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NUGET_CONFIG, PIP_EXTRA_INDEX_URL,
|
||||
PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL,
|
||||
handle_job_error, JobCompletedSender, OtelTracingProxySettings, SameWorkerSender,
|
||||
BUNFIG_INSTALL_SCOPES, INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS,
|
||||
NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS,
|
||||
PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -246,6 +246,7 @@ pub async fn initial_load(
|
||||
),
|
||||
priority_tags_sorted: vec![],
|
||||
dedicated_worker: None,
|
||||
dedicated_workers: None,
|
||||
init_bash: load_init_bash_from_env(),
|
||||
periodic_script_bash: load_periodic_bash_script_from_env(),
|
||||
periodic_script_interval_seconds: load_periodic_bash_script_interval_from_env(),
|
||||
@@ -784,26 +785,24 @@ pub async fn load_keep_job_dir(conn: &Connection) {
|
||||
|
||||
pub async fn reload_otel_tracing_proxy_setting(conn: &Connection) {
|
||||
match load_value_from_global_settings_with_conn(conn, OTEL_TRACING_PROXY_SETTING, true).await {
|
||||
Ok(Some(settings)) => {
|
||||
match serde_json::from_value::<OtelTracingProxySettings>(settings) {
|
||||
Ok(new_settings) => {
|
||||
let mut current = OTEL_TRACING_PROXY_SETTINGS.write().await;
|
||||
if current.enabled != new_settings.enabled
|
||||
|| current.enabled_languages != new_settings.enabled_languages
|
||||
{
|
||||
tracing::info!(
|
||||
"OTEL tracing proxy settings changed: enabled={}, languages={:?}",
|
||||
new_settings.enabled,
|
||||
new_settings.enabled_languages
|
||||
);
|
||||
*current = new_settings;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error parsing OTEL tracing proxy settings: {e:#}");
|
||||
Ok(Some(settings)) => match serde_json::from_value::<OtelTracingProxySettings>(settings) {
|
||||
Ok(new_settings) => {
|
||||
let mut current = OTEL_TRACING_PROXY_SETTINGS.write().await;
|
||||
if current.enabled != new_settings.enabled
|
||||
|| current.enabled_languages != new_settings.enabled_languages
|
||||
{
|
||||
tracing::info!(
|
||||
"OTEL tracing proxy settings changed: enabled={}, languages={:?}",
|
||||
new_settings.enabled,
|
||||
new_settings.enabled_languages
|
||||
);
|
||||
*current = new_settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error parsing OTEL tracing proxy settings: {e:#}");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Error loading OTEL tracing proxy setting: {e:#}");
|
||||
}
|
||||
@@ -985,7 +984,10 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting expired MCP OAuth authorization codes: {:?}", e),
|
||||
Err(e) => tracing::error!(
|
||||
"Error deleting expired MCP OAuth authorization codes: {:?}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
let job_retention_secs = *JOB_RETENTION_SECS.read().await;
|
||||
@@ -1866,7 +1868,13 @@ pub async fn monitor_db(
|
||||
|
||||
let update_min_worker_version_f = async {
|
||||
#[cfg(not(feature = "test_job_debouncing"))]
|
||||
windmill_common::min_version::update_min_version(conn, _worker_mode, WORKERS_NAMES.read().await.clone(), initial_load).await;
|
||||
windmill_common::min_version::update_min_version(
|
||||
conn,
|
||||
_worker_mode,
|
||||
WORKERS_NAMES.read().await.clone(),
|
||||
initial_load,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
// Run every 5 minutes (10 iterations * 30s = 5 minutes)
|
||||
@@ -2060,10 +2068,12 @@ pub async fn reload_worker_config(db: &DB, tx: KillpillSender, kill_if_change: b
|
||||
} else {
|
||||
let wc = WORKER_CONFIG.read().await;
|
||||
let config = config.unwrap();
|
||||
if *wc != config || config.dedicated_worker.is_some() {
|
||||
let has_dedicated = config.dedicated_worker.is_some() || config.dedicated_workers.as_ref().is_some_and(|dws| !dws.is_empty());
|
||||
if *wc != config || has_dedicated {
|
||||
if kill_if_change {
|
||||
if config.dedicated_worker.is_some()
|
||||
if has_dedicated
|
||||
|| (*wc).dedicated_worker != config.dedicated_worker
|
||||
|| (*wc).dedicated_workers != config.dedicated_workers
|
||||
{
|
||||
tracing::info!("Dedicated worker config changed, sending killpill. Expecting to be restarted by supervisor.");
|
||||
let _ = tx.send();
|
||||
|
||||
@@ -995,6 +995,7 @@ TABLE: worker_ping
|
||||
- custom_tags (text[])
|
||||
- worker_group (character)
|
||||
- dedicated_worker (character)
|
||||
- dedicated_workers (text[])
|
||||
- wm_version (character)
|
||||
- current_job_id (uuid)
|
||||
- current_job_workspace_id (character)
|
||||
|
||||
@@ -5751,6 +5751,14 @@ paths:
|
||||
If true, the description field will be omitted from the response.
|
||||
schema:
|
||||
type: boolean
|
||||
- name: dedicated_worker
|
||||
in: query
|
||||
description: |
|
||||
(default regardless)
|
||||
If true, show only scripts with dedicated_worker enabled.
|
||||
If false, show only scripts with dedicated_worker disabled.
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
responses:
|
||||
"200":
|
||||
@@ -7178,6 +7186,14 @@ paths:
|
||||
If true, the description field will be omitted from the response.
|
||||
schema:
|
||||
type: boolean
|
||||
- name: dedicated_worker
|
||||
in: query
|
||||
description: |
|
||||
(default regardless)
|
||||
If true, show only flows with dedicated_worker enabled.
|
||||
If false, show only flows with dedicated_worker disabled.
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All flow
|
||||
|
||||
@@ -191,6 +191,9 @@ async fn list_flows(
|
||||
if !lq.include_draft_only.unwrap_or(false) || authed.is_operator {
|
||||
sqlb.and_where("o.draft_only IS NOT TRUE");
|
||||
}
|
||||
if let Some(dw) = &lq.dedicated_worker {
|
||||
sqlb.and_where_eq("dedicated_worker", dw);
|
||||
}
|
||||
|
||||
if lq.with_deployment_msg.unwrap_or(false) {
|
||||
sqlb.join("deployment_metadata dm")
|
||||
|
||||
@@ -371,6 +371,9 @@ async fn list_scripts(
|
||||
if let Some(it) = &lq.is_template {
|
||||
sqlb.and_where_eq("is_template", it);
|
||||
}
|
||||
if let Some(dw) = &lq.dedicated_worker {
|
||||
sqlb.and_where_eq("dedicated_worker", dw);
|
||||
}
|
||||
if authed.is_operator {
|
||||
sqlb.and_where_eq("kind", quote("script"));
|
||||
} else if let Some(lowercased_kinds) = lowercased_kinds {
|
||||
|
||||
@@ -1146,6 +1146,7 @@ pub struct ListFlowQuery {
|
||||
pub starred_only: Option<bool>,
|
||||
pub include_draft_only: Option<bool>,
|
||||
pub with_deployment_msg: Option<bool>,
|
||||
pub dedicated_worker: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn add_virtual_items_if_necessary(modules: &mut Vec<FlowModule>) {
|
||||
|
||||
@@ -687,6 +687,7 @@ pub struct ListScriptQuery {
|
||||
pub with_deployment_msg: Option<bool>,
|
||||
#[serde(default, deserialize_with = "from_seq")]
|
||||
pub languages: Option<Vec<ScriptLang>>,
|
||||
pub dedicated_worker: Option<bool>,
|
||||
}
|
||||
|
||||
fn from_seq<'de, D>(deserializer: D) -> Result<Option<Vec<ScriptLang>>, D::Error>
|
||||
|
||||
@@ -219,6 +219,7 @@ lazy_static::lazy_static! {
|
||||
worker_tags: Default::default(),
|
||||
priority_tags_sorted: Default::default(),
|
||||
dedicated_worker: Default::default(),
|
||||
dedicated_workers: Default::default(),
|
||||
cache_clear: Default::default(),
|
||||
init_bash: Default::default(),
|
||||
periodic_script_bash: Default::default(),
|
||||
@@ -1243,6 +1244,7 @@ pub struct Ping {
|
||||
pub ip: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub dw: Option<String>,
|
||||
pub dws: Option<Vec<String>>,
|
||||
pub version: Option<String>,
|
||||
pub vcpus: Option<i64>,
|
||||
pub memory: Option<i64>,
|
||||
@@ -1298,6 +1300,7 @@ pub async fn update_ping_http(
|
||||
&insert_ping.ip.unwrap(),
|
||||
insert_ping.tags.unwrap_or_default().as_slice(),
|
||||
insert_ping.dw,
|
||||
insert_ping.dws.as_deref(),
|
||||
&insert_ping.version.unwrap(),
|
||||
insert_ping.vcpus,
|
||||
insert_ping.memory,
|
||||
@@ -1428,6 +1431,7 @@ pub async fn insert_ping_query(
|
||||
ip: &str,
|
||||
tags: &[String],
|
||||
dw: Option<String>,
|
||||
dws: Option<&[String]>,
|
||||
version: &str,
|
||||
vcpus: Option<i64>,
|
||||
memory: Option<i64>,
|
||||
@@ -1435,14 +1439,15 @@ pub async fn insert_ping_query(
|
||||
db: &DB,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, wm_version, vcpus, memory, job_isolation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (worker)
|
||||
DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group",
|
||||
"INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, dedicated_workers, wm_version, vcpus, memory, job_isolation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (worker)
|
||||
DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group, dedicated_workers = EXCLUDED.dedicated_workers",
|
||||
worker_instance,
|
||||
worker_name,
|
||||
ip,
|
||||
tags,
|
||||
worker_group,
|
||||
dw,
|
||||
dws,
|
||||
version,
|
||||
vcpus,
|
||||
memory,
|
||||
@@ -1614,6 +1619,30 @@ pub async fn load_worker_config(
|
||||
}
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Parse dedicated_workers (multiple dedicated workers)
|
||||
let dedicated_workers = config
|
||||
.dedicated_workers
|
||||
.map(|workers| {
|
||||
workers
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let splitted = x.split(':').to_owned().collect_vec();
|
||||
if splitted.len() != 2 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid dedicated_workers format. Got {x}, expects <workspace_id>:<path>"
|
||||
));
|
||||
}
|
||||
let workspace = splitted[0];
|
||||
let script_path = splitted[1];
|
||||
Ok(WorkspacedPath {
|
||||
workspace_id: workspace.to_string(),
|
||||
path: script_path.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
.transpose()?;
|
||||
if *WORKER_GROUP == "default" && dedicated_worker.is_none() {
|
||||
let mut all_tags = config
|
||||
.worker_tags
|
||||
@@ -1647,7 +1676,18 @@ pub async fn load_worker_config(
|
||||
let worker_tags = config
|
||||
.worker_tags
|
||||
.or_else(|| {
|
||||
if let Some(ref dedicated_worker) = dedicated_worker.as_ref() {
|
||||
// Check for multiple dedicated workers first
|
||||
if let Some(ref dws) = dedicated_workers.as_ref() {
|
||||
let mut dedi_tags: Vec<String> = dws
|
||||
.iter()
|
||||
.map(|dw| format!("{}:{}", dw.workspace_id, dw.path))
|
||||
.collect();
|
||||
if std::env::var("ADD_FLOW_TAG").is_ok() {
|
||||
dedi_tags.push("flow".to_string());
|
||||
}
|
||||
Some(dedi_tags)
|
||||
} else if let Some(ref dedicated_worker) = dedicated_worker.as_ref() {
|
||||
// Fallback to single dedicated worker for backward compatibility
|
||||
let mut dedi_tags = vec![format!(
|
||||
"{}:{}",
|
||||
dedicated_worker.workspace_id, dedicated_worker.path
|
||||
@@ -1743,6 +1783,7 @@ pub async fn load_worker_config(
|
||||
worker_tags,
|
||||
priority_tags_sorted,
|
||||
dedicated_worker,
|
||||
dedicated_workers,
|
||||
init_bash: config
|
||||
.init_bash
|
||||
.or_else(|| load_init_bash_from_env())
|
||||
@@ -1836,6 +1877,7 @@ pub struct WorkerConfigOpt {
|
||||
pub worker_tags: Option<Vec<String>>,
|
||||
pub priority_tags: Option<HashMap<String, u8>>,
|
||||
pub dedicated_worker: Option<String>,
|
||||
pub dedicated_workers: Option<Vec<String>>,
|
||||
pub init_bash: Option<String>,
|
||||
pub periodic_script_bash: Option<String>,
|
||||
pub periodic_script_interval_seconds: Option<u64>,
|
||||
@@ -1852,6 +1894,7 @@ impl Default for WorkerConfigOpt {
|
||||
worker_tags: Default::default(),
|
||||
priority_tags: Default::default(),
|
||||
dedicated_worker: Default::default(),
|
||||
dedicated_workers: Default::default(),
|
||||
init_bash: Default::default(),
|
||||
periodic_script_bash: Default::default(),
|
||||
periodic_script_interval_seconds: Default::default(),
|
||||
@@ -1869,6 +1912,7 @@ pub struct WorkerConfig {
|
||||
pub worker_tags: Vec<String>,
|
||||
pub priority_tags_sorted: Vec<PriorityTags>,
|
||||
pub dedicated_worker: Option<WorkspacedPath>,
|
||||
pub dedicated_workers: Option<Vec<WorkspacedPath>>,
|
||||
pub init_bash: Option<String>,
|
||||
pub periodic_script_bash: Option<String>,
|
||||
pub periodic_script_interval_seconds: Option<u64>,
|
||||
@@ -1880,8 +1924,8 @@ pub struct WorkerConfig {
|
||||
|
||||
impl std::fmt::Debug for WorkerConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, init_bash: {:?}, periodic_script_bash: {:?}, periodic_script_interval_seconds: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
|
||||
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.init_bash, self.periodic_script_bash, self.periodic_script_interval_seconds, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
|
||||
write!(f, "WorkerConfig {{ worker_tags: {:?}, priority_tags_sorted: {:?}, dedicated_worker: {:?}, dedicated_workers: {:?}, init_bash: {:?}, periodic_script_bash: {:?}, periodic_script_interval_seconds: {:?}, cache_clear: {:?}, additional_python_paths: {:?}, pip_local_dependencies: {:?}, env_vars: {:?} }}",
|
||||
self.worker_tags, self.priority_tags_sorted, self.dedicated_worker, self.dedicated_workers, self.init_bash, self.periodic_script_bash, self.periodic_script_interval_seconds, self.cache_clear, self.additional_python_paths, self.pip_local_dependencies, self.env_vars.iter().map(|(k, v)| format!("{}: {}{} ({} chars)", k, &v[..3.min(v.len())], "***", v.len())).collect::<Vec<String>>().join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -533,6 +533,7 @@ pub async fn update_worker_ping_for_failed_init_script(
|
||||
ip: None,
|
||||
tags: None,
|
||||
dw: None,
|
||||
dws: None,
|
||||
jobs_executed: None,
|
||||
occupancy_rate: None,
|
||||
occupancy_rate_15s: None,
|
||||
|
||||
@@ -1529,7 +1529,10 @@ pub async fn run_worker(
|
||||
let mut occupancy_metrics = OccupancyMetrics::new(start_time);
|
||||
let mut jobs_executed = 0;
|
||||
|
||||
let is_dedicated_worker: bool = WORKER_CONFIG.read().await.dedicated_worker.is_some();
|
||||
let is_dedicated_worker: bool = {
|
||||
let config = WORKER_CONFIG.read().await;
|
||||
config.dedicated_worker.is_some() || config.dedicated_workers.as_ref().is_some_and(|dws| !dws.is_empty())
|
||||
};
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
let benchmark_jobs: i32 = std::env::var("BENCHMARK_JOBS")
|
||||
@@ -1631,9 +1634,9 @@ pub async fn run_worker(
|
||||
// Option<JoinHandle<()>>,
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
let (dedicated_workers, is_flow_worker, dedicated_handles): (
|
||||
let (dedicated_workers, dedicated_flow_paths, dedicated_handles): (
|
||||
HashMap<String, Sender<DedicatedWorkerJob>>,
|
||||
bool,
|
||||
HashSet<String>,
|
||||
Vec<JoinHandle<()>>,
|
||||
) = match conn {
|
||||
Connection::Sql(pool) => {
|
||||
@@ -1648,15 +1651,15 @@ pub async fn run_worker(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Connection::Http(_) => (HashMap::new(), false, vec![]),
|
||||
Connection::Http(_) => (HashMap::new(), HashSet::new(), vec![]),
|
||||
};
|
||||
|
||||
#[cfg(any(not(feature = "private"), not(feature = "enterprise")))]
|
||||
let (dedicated_workers, is_flow_worker, dedicated_handles): (
|
||||
let (dedicated_workers, dedicated_flow_paths, dedicated_handles): (
|
||||
HashMap<String, Sender<DedicatedWorkerJob>>,
|
||||
bool,
|
||||
HashSet<String>,
|
||||
Vec<JoinHandle<()>>,
|
||||
) = (HashMap::new(), false, vec![]);
|
||||
) = (HashMap::new(), HashSet::new(), vec![]);
|
||||
|
||||
if i_worker == 1 {
|
||||
if let Err(e) = queue_init_bash_maybe(conn, same_worker_tx.clone(), &worker_name).await {
|
||||
@@ -2030,30 +2033,32 @@ pub async fn run_worker(
|
||||
JobKind::Script | JobKind::Preview | JobKind::FlowScript
|
||||
) {
|
||||
if !dedicated_workers.is_empty() {
|
||||
let key_o = if is_flow_worker {
|
||||
job.flow_step_id.as_ref().map(|x| x.to_string())
|
||||
// Try flow path + step_id combinations for flow jobs, otherwise use runnable_path
|
||||
let dedicated_worker_tx = if let Some(step_id) = job.flow_step_id.as_ref() {
|
||||
dedicated_flow_paths.iter().find_map(|flow_path| {
|
||||
let key = format!("{}:{}", flow_path, step_id);
|
||||
dedicated_workers.get(&key)
|
||||
})
|
||||
} else {
|
||||
job.runnable_path.as_ref().map(|x| x.to_string())
|
||||
job.runnable_path.as_ref().and_then(|path| dedicated_workers.get(path))
|
||||
};
|
||||
if let Some(key) = key_o {
|
||||
if let Some(dedicated_worker_tx) = dedicated_workers.get(&key) {
|
||||
let dedicated_job = DedicatedWorkerJob {
|
||||
job: Arc::new(job.job()),
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
};
|
||||
if let Err(e) = dedicated_worker_tx.send(dedicated_job).await {
|
||||
tracing::info!("failed to send jobs to dedicated workers. Likely dedicated worker has been shut down. This is normal: {e:?}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
{
|
||||
add_time!(bench, "sent to dedicated worker");
|
||||
infos.add_iter(bench, true);
|
||||
}
|
||||
|
||||
continue;
|
||||
if let Some(dedicated_worker_tx) = dedicated_worker_tx {
|
||||
let dedicated_job = DedicatedWorkerJob {
|
||||
job: Arc::new(job.job()),
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
};
|
||||
if let Err(e) = dedicated_worker_tx.send(dedicated_job).await {
|
||||
tracing::info!("failed to send jobs to dedicated workers. Likely dedicated worker has been shut down. This is normal: {e:?}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
{
|
||||
add_time!(bench, "sent to dedicated worker");
|
||||
infos.add_iter(bench, true);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ async fn update_worker_ping_full_inner(
|
||||
ip: None,
|
||||
tags: Some(tags.to_vec()),
|
||||
dw: None,
|
||||
dws: None,
|
||||
jobs_executed: Some(jobs_executed),
|
||||
occupancy_rate: Some(occupancy_rate),
|
||||
occupancy_rate_15s: Some(occupancy_rate_15s.unwrap_or(0.0)),
|
||||
@@ -159,13 +160,19 @@ pub async fn insert_ping(
|
||||
ip: &str,
|
||||
db: &Connection,
|
||||
) -> anyhow::Result<()> {
|
||||
let (tags, dw) = {
|
||||
let (tags, dw, dws) = {
|
||||
let wc = WORKER_CONFIG.read().await.clone();
|
||||
(
|
||||
wc.worker_tags,
|
||||
wc.dedicated_worker
|
||||
.as_ref()
|
||||
.map(|x| format!("{}:{}", x.workspace_id, x.path)),
|
||||
wc.dedicated_workers.as_ref().map(|workers| {
|
||||
workers
|
||||
.iter()
|
||||
.map(|x| format!("{}:{}", x.workspace_id, x.path))
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -190,6 +197,7 @@ pub async fn insert_ping(
|
||||
ip,
|
||||
tags.as_slice(),
|
||||
dw,
|
||||
dws.as_deref(),
|
||||
windmill_common::utils::GIT_VERSION,
|
||||
vcpus,
|
||||
memory,
|
||||
@@ -210,6 +218,7 @@ pub async fn insert_ping(
|
||||
ip: Some(ip.to_string()),
|
||||
tags: Some(tags.to_vec()),
|
||||
dw: dw,
|
||||
dws: dws,
|
||||
jobs_executed: None,
|
||||
occupancy_rate: None,
|
||||
occupancy_rate_15s: None,
|
||||
@@ -282,6 +291,7 @@ pub async fn update_worker_ping_from_job(
|
||||
ip: None,
|
||||
tags: None,
|
||||
dw: None,
|
||||
dws: None,
|
||||
version: None,
|
||||
vcpus: None,
|
||||
memory: None,
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
<script lang="ts">
|
||||
import { ScriptService, FlowService, WorkspaceService, type FlowModule } from '$lib/gen'
|
||||
import { Check, X, RefreshCcw, ChevronDown, ChevronRight, CodeXml } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
import Select from './select/Select.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
import { SvelteMap } from 'svelte/reactivity'
|
||||
import { untrack } from 'svelte'
|
||||
import BarsStaggered from './icons/BarsStaggered.svelte'
|
||||
|
||||
// A "Runnable" is a script or flow with dedicated_worker=true
|
||||
interface Runnable {
|
||||
tag: string // workspace:path or workspace:flow/path
|
||||
displayName: string
|
||||
language: string
|
||||
type: 'script' | 'flow'
|
||||
path: string
|
||||
selected: boolean
|
||||
// For flows, the actual runners (steps) that will be spawned
|
||||
runners?: FlowRunner[]
|
||||
loadingRunners?: boolean
|
||||
expanded?: boolean
|
||||
}
|
||||
|
||||
// A "FlowRunner" is an individual step within a flow that will get a dedicated worker
|
||||
interface FlowRunner {
|
||||
stepId: string
|
||||
stepSummary?: string
|
||||
language?: string
|
||||
scriptPath?: string
|
||||
isInline: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedTags: string[]
|
||||
disabled?: boolean
|
||||
onchange?: (tags: string[]) => void
|
||||
}
|
||||
|
||||
let { selectedTags = $bindable([]), disabled = false, onchange }: Props = $props()
|
||||
|
||||
let selectedWorkspace: string | undefined = $state(undefined)
|
||||
let runnables: Runnable[] = $state([])
|
||||
let loading = $state(false)
|
||||
let workspaces: { id: string; name: string }[] = $state([])
|
||||
let workspacesLoading = $state(true)
|
||||
let selectorExpanded = $state(false)
|
||||
|
||||
// Track detailed info for each selected tag (for displaying in summary)
|
||||
interface SelectedTagInfo {
|
||||
tag: string
|
||||
workspace: string
|
||||
type: 'script' | 'flow'
|
||||
path: string
|
||||
runners?: FlowRunner[]
|
||||
expanded?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
let selectedTagsInfo: SvelteMap<string, SelectedTagInfo> = $state(new SvelteMap())
|
||||
|
||||
// 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,
|
||||
preliminaryRunners: FlowRunner[]
|
||||
): Promise<FlowRunner[]> {
|
||||
const runnersWithLanguage = await Promise.all(
|
||||
preliminaryRunners.map(async (runner) => {
|
||||
if (!runner.isInline && runner.scriptPath) {
|
||||
try {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace,
|
||||
path: runner.scriptPath
|
||||
})
|
||||
return { ...runner, language: script.language }
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch script ${runner.scriptPath}`, e)
|
||||
return { ...runner, language: undefined }
|
||||
}
|
||||
}
|
||||
return runner
|
||||
})
|
||||
)
|
||||
|
||||
// Filter to only supported languages
|
||||
return runnersWithLanguage.filter(
|
||||
(runner) => runner.language && DEDICATED_WORKER_LANGUAGES.includes(runner.language)
|
||||
)
|
||||
}
|
||||
|
||||
// Load detailed info for all selected tags
|
||||
async function loadSelectedTagsInfo(tags: string[]) {
|
||||
if (tags.length === 0) {
|
||||
selectedTagsInfo = new SvelteMap()
|
||||
return
|
||||
}
|
||||
|
||||
// Capture current state without tracking to avoid infinite loops
|
||||
const currentInfo = untrack(() => selectedTagsInfo)
|
||||
const currentRunnables = untrack(() => runnables)
|
||||
|
||||
const newInfo = new SvelteMap<string, SelectedTagInfo>()
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
tags.map(async (tag) => {
|
||||
// Check if we already have this info cached
|
||||
const existing = currentInfo.get(tag)
|
||||
if (existing && (existing.type === 'script' || existing.runners !== undefined)) {
|
||||
newInfo.set(tag, existing)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have it loaded in runnables
|
||||
const existingRunnable = currentRunnables.find((r) => r.tag === tag)
|
||||
if (existingRunnable) {
|
||||
newInfo.set(tag, {
|
||||
tag,
|
||||
workspace: tag.substring(0, tag.indexOf(':')),
|
||||
type: existingRunnable.type,
|
||||
path: existingRunnable.path,
|
||||
runners: existingRunnable.runners,
|
||||
expanded: existing?.expanded ?? false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and fetch
|
||||
const parsed = parseTag(tag)
|
||||
if (!parsed) return
|
||||
|
||||
if (parsed.type === 'script') {
|
||||
newInfo.set(tag, {
|
||||
tag,
|
||||
workspace: parsed.workspace,
|
||||
type: 'script',
|
||||
path: parsed.path
|
||||
})
|
||||
} else {
|
||||
// Flows need to fetch to get runners
|
||||
try {
|
||||
const flow = await FlowService.getFlowByPath({
|
||||
workspace: parsed.workspace,
|
||||
path: parsed.path
|
||||
})
|
||||
const preliminaryRunners = flow.value?.modules
|
||||
? extractRunnersFromModules(flow.value.modules)
|
||||
: []
|
||||
const runners = await resolveAndFilterRunners(parsed.workspace, preliminaryRunners)
|
||||
newInfo.set(tag, {
|
||||
tag,
|
||||
workspace: parsed.workspace,
|
||||
type: 'flow',
|
||||
path: parsed.path,
|
||||
runners,
|
||||
expanded: existing?.expanded ?? false
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(`Failed to load flow ${parsed.path}`, e)
|
||||
newInfo.set(tag, {
|
||||
tag,
|
||||
workspace: parsed.workspace,
|
||||
type: 'flow',
|
||||
path: parsed.path,
|
||||
runners: []
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} finally {
|
||||
selectedTagsInfo = newInfo
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectedTagExpanded(tag: string) {
|
||||
const info = selectedTagsInfo.get(tag)
|
||||
if (info) {
|
||||
// Need to set the whole object to trigger reactivity
|
||||
selectedTagsInfo.set(tag, { ...info, expanded: !info.expanded })
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-expand selector if no tags selected
|
||||
$effect(() => {
|
||||
if (selectedTags.length === 0) {
|
||||
selectorExpanded = true
|
||||
}
|
||||
})
|
||||
|
||||
// Load selected tags info when selectedTags change
|
||||
$effect(() => {
|
||||
loadSelectedTagsInfo(selectedTags)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
loadWorkspaces()
|
||||
})
|
||||
|
||||
async function loadWorkspaces() {
|
||||
try {
|
||||
workspacesLoading = true
|
||||
const ws = await WorkspaceService.listWorkspaces()
|
||||
workspaces = ws.map((w) => ({ id: w.id, name: w.name }))
|
||||
} catch (e) {
|
||||
console.error('Failed to load workspaces', e)
|
||||
sendUserToast('Failed to load workspaces', true)
|
||||
} finally {
|
||||
workspacesLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Extract runners from flow modules recursively
|
||||
// Returns runners with language info for inline scripts, and scriptPath for workspace scripts
|
||||
function extractRunnersFromModules(modules: FlowModule[]): FlowRunner[] {
|
||||
const runners: FlowRunner[] = []
|
||||
|
||||
for (const module of modules) {
|
||||
const value = module.value
|
||||
switch (value.type) {
|
||||
case 'rawscript':
|
||||
if (DEDICATED_WORKER_LANGUAGES.includes(value.language)) {
|
||||
runners.push({
|
||||
stepId: module.id,
|
||||
stepSummary: module.summary,
|
||||
language: value.language,
|
||||
scriptPath: value.path,
|
||||
isInline: true
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'script':
|
||||
// For workspace script references, we'll resolve the language later
|
||||
runners.push({
|
||||
stepId: module.id,
|
||||
stepSummary: module.summary,
|
||||
language: undefined, // Will be resolved by fetching the script
|
||||
scriptPath: value.path,
|
||||
isInline: false
|
||||
})
|
||||
break
|
||||
case 'forloopflow':
|
||||
runners.push(...extractRunnersFromModules(value.modules))
|
||||
break
|
||||
case 'whileloopflow':
|
||||
runners.push(...extractRunnersFromModules(value.modules))
|
||||
break
|
||||
case 'branchone':
|
||||
for (const branch of value.branches) {
|
||||
runners.push(...extractRunnersFromModules(branch.modules))
|
||||
}
|
||||
runners.push(...extractRunnersFromModules(value.default))
|
||||
break
|
||||
case 'branchall':
|
||||
for (const branch of value.branches) {
|
||||
runners.push(...extractRunnersFromModules(branch.modules))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return runners
|
||||
}
|
||||
|
||||
async function loadFlowRunners(runnable: Runnable) {
|
||||
if (!selectedWorkspace || runnable.type !== 'flow') return
|
||||
|
||||
try {
|
||||
runnable.loadingRunners = true
|
||||
const flow = await FlowService.getFlowByPath({
|
||||
workspace: selectedWorkspace,
|
||||
path: runnable.path
|
||||
})
|
||||
|
||||
if (flow.value?.modules) {
|
||||
const preliminaryRunners = extractRunnersFromModules(flow.value.modules)
|
||||
runnable.runners = await resolveAndFilterRunners(selectedWorkspace, preliminaryRunners)
|
||||
} else {
|
||||
runnable.runners = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load flow runners', e)
|
||||
runnable.runners = []
|
||||
} finally {
|
||||
runnable.loadingRunners = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRunnables(workspaceId: string) {
|
||||
try {
|
||||
loading = true
|
||||
runnables = []
|
||||
|
||||
const [scripts, flows] = await Promise.all([
|
||||
ScriptService.listScripts({
|
||||
workspace: workspaceId,
|
||||
dedicatedWorker: true
|
||||
}),
|
||||
FlowService.listFlows({
|
||||
workspace: workspaceId,
|
||||
dedicatedWorker: true
|
||||
})
|
||||
])
|
||||
|
||||
const newRunnables: Runnable[] = []
|
||||
|
||||
// Add scripts with supported languages
|
||||
for (const script of scripts) {
|
||||
if (DEDICATED_WORKER_LANGUAGES.includes(script.language ?? '')) {
|
||||
const tag = `${workspaceId}:${script.path}`
|
||||
newRunnables.push({
|
||||
tag,
|
||||
displayName: script.path,
|
||||
language: script.language ?? 'unknown',
|
||||
type: 'script',
|
||||
path: script.path,
|
||||
selected: selectedTags.includes(tag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add flows
|
||||
for (const flow of flows) {
|
||||
const tag = `${workspaceId}:flow/${flow.path}`
|
||||
newRunnables.push({
|
||||
tag,
|
||||
displayName: flow.path,
|
||||
language: 'flow',
|
||||
type: 'flow',
|
||||
path: flow.path,
|
||||
selected: selectedTags.includes(tag),
|
||||
runners: undefined,
|
||||
loadingRunners: false,
|
||||
expanded: false
|
||||
})
|
||||
}
|
||||
|
||||
runnables = newRunnables
|
||||
|
||||
// Load runners for all flows in parallel
|
||||
await Promise.all(runnables.filter((r) => r.type === 'flow').map((r) => loadFlowRunners(r)))
|
||||
} catch (e) {
|
||||
console.error('Failed to load runnables', e)
|
||||
sendUserToast('Failed to load scripts/flows', true)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRunnable(runnable: Runnable) {
|
||||
runnable.selected = !runnable.selected
|
||||
updateSelectedTags()
|
||||
}
|
||||
|
||||
function toggleExpanded(runnable: Runnable) {
|
||||
runnable.expanded = !runnable.expanded
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
for (const runnable of runnables) {
|
||||
runnable.selected = true
|
||||
}
|
||||
updateSelectedTags()
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
for (const runnable of runnables) {
|
||||
runnable.selected = false
|
||||
}
|
||||
updateSelectedTags()
|
||||
}
|
||||
|
||||
function updateSelectedTags() {
|
||||
selectedTags = runnables.filter((r) => r.selected).map((r) => r.tag)
|
||||
onchange?.(selectedTags)
|
||||
}
|
||||
|
||||
function removeTag(tag: string) {
|
||||
selectedTags = selectedTags.filter((t) => t !== tag)
|
||||
// Also update runnable state if visible
|
||||
const runnable = runnables.find((r) => r.tag === tag)
|
||||
if (runnable) {
|
||||
runnable.selected = false
|
||||
}
|
||||
onchange?.(selectedTags)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (selectedWorkspace) {
|
||||
loadRunnables(selectedWorkspace)
|
||||
}
|
||||
})
|
||||
|
||||
let selectedCount = $derived(runnables.filter((r) => r.selected).length)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<!-- Selected tags summary -->
|
||||
{#if selectedTags.length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="border rounded-md divide-y bg-surface max-h-48 overflow-y-auto">
|
||||
{#each selectedTags as tag (tag)}
|
||||
{@const info = selectedTagsInfo.get(tag)}
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
{#if info?.type === 'flow' && info.runners && info.runners.length > 0}
|
||||
<button
|
||||
class="p-2 hover:bg-surface-hover transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleSelectedTagExpanded(tag)
|
||||
}}
|
||||
>
|
||||
{#if info.expanded}
|
||||
<ChevronDown class="h-3 w-3 text-tertiary" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3 w-3 text-tertiary" />
|
||||
{/if}
|
||||
</button>
|
||||
{: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" />
|
||||
{:else}
|
||||
<CodeXml size={14} class="flex-shrink-0 text-secondary" />
|
||||
{/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>
|
||||
{#if !disabled}
|
||||
<button
|
||||
class="p-2 hover:text-red-500 transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeTag(tag)
|
||||
}}
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#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>
|
||||
{#if runner.stepSummary}
|
||||
<span class="text-secondary truncate flex-1">{runner.stepSummary}</span>
|
||||
{/if}
|
||||
<Badge color="gray" small>
|
||||
{runner.isInline ? runner.language : runner.scriptPath}
|
||||
</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Collapsible selector section -->
|
||||
<div class="border rounded-md">
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-surface-hover transition-colors"
|
||||
onclick={() => (selectorExpanded = !selectorExpanded)}
|
||||
{disabled}
|
||||
>
|
||||
{#if selectorExpanded}
|
||||
<ChevronDown class="h-4 w-4 text-secondary" />
|
||||
{:else}
|
||||
<ChevronRight class="h-4 w-4 text-secondary" />
|
||||
{/if}
|
||||
<span class="text-sm">
|
||||
{selectedTags.length > 0 ? 'Add more scripts/flows' : 'Select scripts/flows'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{#if selectorExpanded}
|
||||
<div class="border-t px-3 py-3 flex flex-col gap-3">
|
||||
<!-- Workspace selector -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-secondary">Workspace</span>
|
||||
<Select
|
||||
bind:value={selectedWorkspace}
|
||||
items={workspaces.map((w) => ({ value: w.id, label: `${w.name} (${w.id})` }))}
|
||||
placeholder="Select workspace..."
|
||||
disabled={disabled || workspacesLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Scripts/flows list -->
|
||||
{#if selectedWorkspace}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-secondary">Scripts/flows with dedicated worker enabled</span
|
||||
>
|
||||
{#if !loading && runnables.length > 0}
|
||||
<div class="flex gap-1">
|
||||
<Button size="xs2" color="light" on:click={selectAll} {disabled}>All</Button>
|
||||
<Button size="xs2" color="light" on:click={deselectAll} {disabled}>None</Button>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
iconOnly
|
||||
startIcon={{ icon: RefreshCcw }}
|
||||
on:click={() => selectedWorkspace && loadRunnables(selectedWorkspace)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<RefreshCcw class="animate-spin h-4 w-4 text-secondary" />
|
||||
<span class="ml-2 text-xs text-secondary">Loading...</span>
|
||||
</div>
|
||||
{:else if runnables.length === 0}
|
||||
<div class="text-xs text-tertiary py-3 text-center">
|
||||
No scripts or flows with dedicated worker enabled found.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="border rounded-md divide-y max-h-64 overflow-y-auto bg-surface">
|
||||
{#each runnables as runnable (runnable.tag)}
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
{#if runnable.type === 'flow' && runnable.runners && runnable.runners.length > 0}
|
||||
<button
|
||||
class="p-2 hover:bg-surface-hover transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleExpanded(runnable)
|
||||
}}
|
||||
{disabled}
|
||||
>
|
||||
{#if runnable.expanded}
|
||||
<ChevronDown class="h-3 w-3 text-tertiary" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3 w-3 text-tertiary" />
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<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"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!disabled) toggleRunnable(runnable)
|
||||
}}
|
||||
{disabled}
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 border rounded flex items-center justify-center flex-shrink-0"
|
||||
class:bg-blue-500={runnable.selected}
|
||||
class:border-blue-500={runnable.selected}
|
||||
>
|
||||
{#if runnable.selected}
|
||||
<Check class="h-3 w-3 text-white" />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="flex-1 text-xs truncate">{runnable.displayName}</span>
|
||||
{#if runnable.type === 'flow' && runnable.runners}
|
||||
<span class="text-xs text-tertiary">
|
||||
{runnable.runners.length}
|
||||
</span>
|
||||
{/if}
|
||||
<Badge color={runnable.type === 'flow' ? 'indigo' : 'blue'} small>
|
||||
{runnable.type === 'flow' ? 'flow' : runnable.language}
|
||||
</Badge>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if runnable.type === 'flow' && runnable.expanded && runnable.runners}
|
||||
<div class="bg-surface-secondary border-t">
|
||||
{#if runnable.runners.length === 0}
|
||||
<div class="px-9 py-1.5 text-xs text-tertiary italic">
|
||||
No eligible steps (python3/bun/deno)
|
||||
</div>
|
||||
{: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"
|
||||
>
|
||||
<span class="font-mono text-tertiary">{runner.stepId}</span>
|
||||
{#if runner.stepSummary}
|
||||
<span class="text-secondary truncate flex-1">
|
||||
{runner.stepSummary}
|
||||
</span>
|
||||
{/if}
|
||||
<Badge color="gray" small>
|
||||
{runner.isInline ? runner.language : runner.scriptPath}
|
||||
</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text-xs text-tertiary">
|
||||
{selectedCount} selected
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,6 +42,7 @@
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import Dropdown from './DropdownV2.svelte'
|
||||
import TagList from './TagList.svelte'
|
||||
import DedicatedWorkersSelector from './DedicatedWorkersSelector.svelte'
|
||||
|
||||
function computeVCpuAndMemory(workers: [string, WorkerPing[]][]) {
|
||||
let vcpus = 0
|
||||
@@ -73,6 +74,7 @@
|
||||
|
||||
let nconfig: {
|
||||
dedicated_worker?: string
|
||||
dedicated_workers?: string[]
|
||||
worker_tags?: string[]
|
||||
priority_tags?: Record<string, number>
|
||||
cache_clear?: number
|
||||
@@ -89,7 +91,9 @@
|
||||
|
||||
function loadNConfig() {
|
||||
nconfig = config
|
||||
? config.worker_tags != undefined || config.dedicated_worker != undefined
|
||||
? config.worker_tags != undefined ||
|
||||
config.dedicated_worker != undefined ||
|
||||
config.dedicated_workers != undefined
|
||||
? config
|
||||
: {
|
||||
worker_tags: []
|
||||
@@ -101,6 +105,12 @@
|
||||
nconfig.priority_tags = {}
|
||||
}
|
||||
|
||||
// Convert legacy dedicated_worker to dedicated_workers array
|
||||
if (nconfig.dedicated_worker && !nconfig.dedicated_workers?.length) {
|
||||
nconfig.dedicated_workers = [nconfig.dedicated_worker]
|
||||
nconfig.dedicated_worker = undefined
|
||||
}
|
||||
|
||||
customEnvVars = []
|
||||
if (nconfig.env_vars_allowlist === undefined) {
|
||||
nconfig.env_vars_allowlist = []
|
||||
@@ -170,6 +180,7 @@
|
||||
| undefined
|
||||
| {
|
||||
dedicated_worker?: string
|
||||
dedicated_workers?: string[]
|
||||
worker_tags?: string[]
|
||||
priority_tags?: Record<string, number>
|
||||
cache_clear?: number
|
||||
@@ -243,7 +254,11 @@
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
let vcpus_memory = $derived(computeVCpuAndMemory(workers))
|
||||
let selected = $derived(nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal')
|
||||
let selected = $derived(
|
||||
nconfig?.dedicated_worker != undefined || (nconfig?.dedicated_workers?.length ?? 0) > 0
|
||||
? 'dedicated'
|
||||
: 'normal'
|
||||
)
|
||||
$effect(() => {
|
||||
;($superadmin || $devopsRole) && listWorkspaces()
|
||||
})
|
||||
@@ -330,17 +345,19 @@
|
||||
nconfig = {}
|
||||
}
|
||||
if (e.detail == 'dedicated') {
|
||||
nconfig.dedicated_worker = ''
|
||||
nconfig.dedicated_workers = nconfig.dedicated_workers ?? []
|
||||
nconfig.dedicated_worker = undefined
|
||||
nconfig.worker_tags = undefined
|
||||
} else {
|
||||
nconfig.dedicated_worker = undefined
|
||||
nconfig.dedicated_workers = undefined
|
||||
nconfig.worker_tags = []
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="normal" label="Any jobs within worker tags" {item} />
|
||||
<ToggleButton value="dedicated" label="Dedicated to a script/flow" {item} />
|
||||
<ToggleButton value="dedicated" label="Dedicated to scripts/flows" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
@@ -493,30 +510,33 @@
|
||||
</Label>
|
||||
{/if}
|
||||
{:else if selected == 'dedicated'}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if $superadmin || $devopsRole}
|
||||
<div class="py-2">
|
||||
<Alert
|
||||
size="xs"
|
||||
type="info"
|
||||
title="Script's runtime setting 'dedicated worker' must be toggled on as well"
|
||||
title="The 'dedicated worker' runtime setting of the runnables must be enabled to be selected here"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if nconfig?.dedicated_worker != undefined}
|
||||
<div
|
||||
><p class="text-xs text-secondary mb-2"
|
||||
>Workers will get killed upon detecting changes. It is assumed they are in an
|
||||
environment where the supervisor will restart them.</p
|
||||
>
|
||||
<input
|
||||
disabled={!canEditConfig}
|
||||
placeholder="<workspace>:<script path>"
|
||||
type="text"
|
||||
onchange={() => {}}
|
||||
bind:value={nconfig.dedicated_worker}
|
||||
/></div
|
||||
>
|
||||
|
||||
<p class="text-xs text-secondary"
|
||||
>Workers will get killed upon detecting changes. It is assumed they are in an environment
|
||||
where the supervisor will restart them.</p
|
||||
>
|
||||
|
||||
{#if nconfig !== undefined}
|
||||
<DedicatedWorkersSelector
|
||||
selectedTags={nconfig.dedicated_workers ?? []}
|
||||
disabled={!canEditConfig}
|
||||
onchange={(tags) => {
|
||||
if (nconfig) {
|
||||
nconfig.dedicated_workers = tags
|
||||
nconfig.dedicated_worker = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user