feat: refactor entirely json processing in favor or rawjson to handle larger payloads (#2446)

* raw json

* raw json

* raw json

* raw json

* the big rawjson rewrite

* resolve merge conflicts

* all

* progress

* progress

* all compile

* remove mut

* remove mut

* remove mut

* arc queuedjob

* finish

* finish

* finish

* fixes

* progress

* fix result

* small fixes

* small fixes

* fix tests

* fix tests

* fix tests

* optimize eval timeout

* optimize eval timeout

* optimize eval timeout

* handle big args and result in get jobs

* improve args

* fix

* fix

* fix tests

* fix tests

* done

* add bigquery to native picker

* add bigquery

---------

Co-authored-by: gbouv <guillaume@windmill.dev>
This commit is contained in:
Ruben Fiszel
2023-10-16 10:46:55 +02:00
committed by GitHub
parent a970108108
commit 2ee57f3f84
67 changed files with 2423 additions and 1919 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
with:
ref: benchmarks
- name: benchmark
timeout-minutes: 10
timeout-minutes: 20
run: deno run --unstable -A -r
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
-c https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT mem_peak FROM queue WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "mem_peak",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "0715955b7e98cc669a88eca6556cd46b7f9c07fcf32a24f85b69720b54e6e95f"
}
@@ -37,6 +37,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT result, id\n FROM completed_job\n WHERE id = ANY($1)\n AND workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"UuidArray",
"Text"
]
},
"nullable": [
true,
false
]
},
"hash": "1f040850c2a82bc09789226b167c43fd4935cfbb4951760a4d527665b70a5ac7"
}
@@ -67,6 +67,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -28,6 +28,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, approver, resume_id FROM resume_job WHERE job = $1 ORDER BY created_at ASC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "approver",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "resume_id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "541ebd3bac65431237cf3b882dfdcd61ca97c253d9754d05bba59fda89841067"
}
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -46,6 +46,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled",
"query": "UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
false
]
},
"hash": "913c634de21d46b8841f8a7c25c408da7c572f9e685db6351848cbf6e9253efc"
"hash": "8b221f0d08f3304364e56f5c4894fea42975b03d21904655d724a35163413d29"
}
@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT raw_flow->'modules'->$1::int->'stop_after_if'->>'expr' as stop_early_expr,\n (raw_flow->'modules'->$1::int->'stop_after_if'->>'skip_if_stopped')::bool as skip_if_stopped,\n args \n FROM queue\n WHERE id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stop_early_expr",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "skip_if_stopped",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "args",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int4",
"Uuid"
]
},
"nullable": [
null,
null,
true
]
},
"hash": "8cea673a5b17bc7cf671b539c2c1eb34731c77aaf42dc12e45472eeb134887d0"
}
@@ -40,6 +40,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -60,6 +60,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result FROM completed_job WHERE id = ANY($1) AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"UuidArray",
"Text"
]
},
"nullable": [
true
]
},
"hash": "a227548b6604c56bfc15eb780bd8ee72a89dc6701a50f5048e928bd87baa7b9a"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource\n (workspace_id, path, value, resource_type)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Varchar"
]
},
"nullable": []
},
"hash": "ad9ccde8d831461f1f312d530867714577a825cd40d6655ed61b0b343b5d4482"
}
@@ -42,6 +42,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result FROM completed_job WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "c2849e67b9fea0dc46e6d7000f5a0c9dab89ae80a183d9255f8fdb356b4bc61c"
}
@@ -1,15 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
"query": "SELECT running, substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "running",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 1,
"ordinal": 2,
"name": "mem_peak",
"type_info": "Int4"
}
@@ -22,9 +27,10 @@
]
},
"nullable": [
false,
null,
true
]
},
"hash": "b69f747eae8b13a0a8d6914a3a7ad322554fcfe62cd28c5f0a475f18dd770d61"
"hash": "dda02bee4e15e0f3a2b0cb17ca86ae7ee3a4c0413d3484d3bc960796b39cd650"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "error_handler",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "f8b34e09453d51d3df5be20652938a890ee353fec64ee73c312a3352da7f7515"
}
+2 -1
View File
@@ -7383,6 +7383,7 @@ version = "1.183.0"
dependencies = [
"anyhow",
"async-recursion",
"axum",
"bigdecimal 0.4.1",
"chrono",
"chrono-tz",
@@ -7441,6 +7442,7 @@ dependencies = [
"prometheus",
"rand 0.8.5",
"regex",
"reqwest",
"rsmq_async",
"rust_decimal",
"serde",
@@ -7452,7 +7454,6 @@ dependencies = [
"tracing",
"urlencoding",
"uuid 1.4.1",
"windmill-api-client",
"windmill-audit",
"windmill-common",
"windmill-parser",
+3 -3
View File
@@ -11,7 +11,6 @@ members = [
"./windmill-worker",
"./windmill-common",
"./windmill-audit",
"./windmill-api-client",
"./parsers/windmill-parser",
"./parsers/windmill-parser-ts",
"./parsers/windmill-parser-wasm",
@@ -45,7 +44,6 @@ tokio.workspace = true
dotenv.workspace = true
windmill-common = { workspace = true, features = ["tracing_init"] }
windmill-api.workspace = true
windmill-api-client.workspace = true
windmill-worker.workspace = true
futures.workspace = true
tracing.workspace = true
@@ -72,10 +70,11 @@ reqwest.workspace = true
windmill-queue.workspace = true
axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
[workspace.dependencies]
windmill-api = { path = "./windmill-api" }
windmill-api-client = { path = "./windmill-api-client" }
windmill-queue = { path = "./windmill-queue" }
windmill-worker = { path = "./windmill-worker" }
windmill-common = { path = "./windmill-common" }
@@ -88,6 +87,7 @@ windmill-parser-go = { path = "./parsers/windmill-parser-go" }
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
windmill-api-client = { path = "./windmill-api-client" }
axum = { version = "^0", features = ["headers"] }
headers = "^0"
+8 -9
View File
@@ -5,15 +5,14 @@ contains files used to build the "root" binary.
## Components
| name | description |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| [windmill-api](./windmill-api/) | The API server, exposing functionality to other components and the frontend |
| [windmill-api-client](./windmill-api-client/) | An autogenerated Rust API client, used by other components to talk to the API |
| [windmill-audit](./windmill-audit/) | Contains audit functionality, allowing different components to record important actions |
| [windmill-common](./windmill-common/) | Common code shared by all crates |
| [windmill-queue](./windmill-queue/) | Contains job & flow queuing functionality, commonly written to by the API server and read from by workers |
| [windmill-worker](./windmill-worker/) | The worker. Used to process and execute flows & jobs. |
| [parsers](./parsers/) | Contains code to parse signatures in different langauges. |
| name | description |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| [windmill-api](./windmill-api/) | The API server, exposing functionality to other components and the frontend |
| [windmill-audit](./windmill-audit/) | Contains audit functionality, allowing different components to record important actions |
| [windmill-common](./windmill-common/) | Common code shared by all crates |
| [windmill-queue](./windmill-queue/) | Contains job & flow queuing functionality, commonly written to by the API server and read from by workers |
| [windmill-worker](./windmill-worker/) | The worker. Used to process and execute flows & jobs. |
| [parsers](./parsers/) | Contains code to parse signatures in different langauges. |
### Compile sqlx for offline ci
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,3 @@
-- Add up migration script here
UPDATE config set config = '{"worker_tags": ["nativets", "postgresq", "mysql", "graphql", "snowflake", "bigquery"]}'::jsonb where name = 'worker__native' and config = '{"worker_tags": ["nativets", "postgresql", "mysql", "graphql"
, "snowflake"]}'::jsonb ;
View File
+1 -2
View File
@@ -1,6 +1,5 @@
use std::{collections::HashMap, fmt::Display, ops::Mul, str::FromStr, sync::Arc, time::Duration};
use once_cell::sync::OnceCell;
use serde::de::DeserializeOwned;
use sqlx::{Pool, Postgres};
use tokio::{
@@ -639,7 +638,6 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
base_internal_url: base_internal_url.to_string(),
token,
workspace: job.workspace_id.to_string(),
client: OnceCell::new(),
};
let last_ping = job.last_ping.clone();
@@ -647,6 +645,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
db,
&client,
&job,
0,
error::Error::ExecutionErr(format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {})",
last_ping
+6 -5
View File
@@ -1,4 +1,6 @@
use std::sync::Arc;
use windmill_api_client::types::{NewScript, NewScriptLanguage};
use std::str::FromStr;
#[cfg(feature = "enterprise")]
use chrono::Timelike;
@@ -7,6 +9,7 @@ use futures::StreamExt;
use futures::{stream, Stream};
use serde::Deserialize;
use serde_json::json;
use sqlx::types::Json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres};
use tokio::sync::RwLock;
@@ -22,7 +25,6 @@ use sqlx::query;
#[cfg(feature = "enterprise")]
use windmill_api_client::types::{EditSchedule, NewSchedule, ScriptArgs};
use windmill_api_client::types::{NewScript, NewScriptLanguage};
use windmill_common::worker::WORKER_CONFIG;
use windmill_common::{
@@ -34,7 +36,6 @@ use windmill_common::{
use windmill_queue::PushIsolationLevel;
use serde::Serialize;
use std::str::FromStr;
#[derive(Debug, sqlx::FromRow, Serialize)]
pub struct CompletedJob {
@@ -872,12 +873,12 @@ impl RunJob {
async fn push(self, db: &Pool<Postgres>) -> Uuid {
let RunJob { payload, args } = self;
let tx = PushIsolationLevel::IsolatedRoot(db.clone(), None);
let (uuid, tx) = windmill_queue::push::<rsmq_async::MultiplexedRsmq>(
let (uuid, tx) = windmill_queue::push::<_, rsmq_async::MultiplexedRsmq>(
&db,
tx,
"test-workspace",
payload,
args,
Json(args),
/* user */ "test-user",
/* email */ "test@windmill.dev",
/* permissioned_as */ "u/test-user".to_string(),
@@ -1874,7 +1875,7 @@ async fn test_invalid_first_step(db: Pool<Postgres>) {
assert_eq!(
job.json_result().unwrap(),
serde_json::json!( {"error": {"name": "InternalErr", "message": "Expected an array value, found: {}"}})
serde_json::json!( {"error": {"name": "InternalErr", "message": "Expected an array value, found: invalid type: map, expected a sequence at line 1 column 0"}})
);
}
+24 -35
View File
@@ -24,7 +24,7 @@ use axum::{
use hyper::StatusCode;
use magic_crypt::MagicCryptTrait;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use serde_json::{json, value::RawValue};
use sha2::{Digest, Sha256};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::{types::Uuid, FromRow};
@@ -40,7 +40,7 @@ use windmill_common::{
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, Pagination, StripPath,
},
};
use windmill_queue::{push, PushIsolationLevel, QueueTransaction};
use windmill_queue::{push, PushArgs, PushIsolationLevel, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -124,7 +124,7 @@ pub struct AppWithLastVersionAndDraft {
pub draft_only: Option<bool>,
}
pub type StaticFields = Map<String, Value>;
pub type StaticFields = HashMap<String, Box<RawValue>>;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
@@ -518,7 +518,7 @@ async fn create_app(
tx,
&w_id,
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
serde_json::Map::new(),
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
@@ -761,7 +761,7 @@ async fn update_app(
tx,
&w_id,
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
serde_json::Map::new(),
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
@@ -798,7 +798,7 @@ async fn update_app(
#[derive(Debug, Deserialize, Clone)]
pub struct ExecuteApp {
pub args: Map<String, serde_json::Value>,
pub args: Box<RawValue>,
// - script: script/<path>
// - flow: flow/<path>
pub path: Option<String>,
@@ -900,17 +900,17 @@ async fn execute_component(
}
};
let (job_payload, args, tag) = match &payload {
let (job_payload, args, tag) = match payload {
ExecuteApp { args, component, raw_code: Some(raw_code), path: None, .. } => {
let content = &raw_code.content;
let payload = JobPayload::Code(raw_code.clone());
let path = digest(content);
let args = build_args(policy, component, path, args)?;
let args = build_args(policy, &component, path, args)?;
(payload, args, None)
}
ExecuteApp { args, component, raw_code: None, path: Some(path), .. } => {
let (payload, tag) = get_payload_tag_from_prefixed_path(path, &db, &w_id).await?;
let args = build_args(policy, component, path.to_string(), args)?;
let (payload, tag) = get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?;
let args = build_args(policy, &component, path.to_string(), args)?;
(payload, args, tag)
}
_ => unreachable!(),
@@ -1002,11 +1002,17 @@ fn build_args(
policy: Policy,
component: &str,
path: String,
args: &Map<String, Value>,
) -> Result<Map<String, Value>> {
args: Box<RawValue>,
) -> Result<PushArgs<Box<RawValue>>> {
// disallow var and res access in args coming from the user for security reasons
args.into_iter()
.try_for_each(|x| disallow_var_res_access(x.1))?;
{
let args_str = args.to_string();
if args_str.contains("$var:") || args_str.contains("$res:") {
return Err(Error::BadRequest(format!(
"For security reasons, variable or resource access is not allowed as dynamic argument"
)));
}
}
let key = format!("{}:{}", component, &path);
let static_args = policy
.triggerables
@@ -1015,7 +1021,7 @@ fn build_args(
.map(|x| x.clone())
.or_else(|| {
if matches!(policy.execution_mode, ExecutionMode::Viewer) {
Some(Map::new())
Some(HashMap::new())
} else {
None
}
@@ -1023,26 +1029,9 @@ fn build_args(
.ok_or_else(|| {
Error::BadRequest(format!("path {} is not allowed in the app policy", path))
})?;
let mut args = args.clone();
let mut extra = HashMap::new();
for (k, v) in static_args {
args.insert(k.to_string(), v.to_owned());
}
Ok(args)
}
fn disallow_var_res_access(args: &serde_json::Value) -> Result<()> {
match args {
Value::Object(v) => v.into_iter().try_for_each(|x| disallow_var_res_access(x.1)),
Value::Array(arr) => arr.into_iter().try_for_each(|v| disallow_var_res_access(v)),
Value::String(s) => {
if s.starts_with("$var:") || s.starts_with("$res:") {
Err(Error::BadRequest(format!(
"For security reasons, variable or resource access is not allowed as dynamic argument"
)))
} else {
Ok(())
}
}
_ => Ok(()),
extra.insert(k.to_string(), v.to_owned());
}
Ok(PushArgs { extra, args: sqlx::types::Json(args) })
}
+10 -18
View File
@@ -6,23 +6,24 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use axum::{
extract::{Extension, Path, Query},
extract::{Extension, Path},
routing::{get, post, put},
Router,
};
use hyper::{HeaderMap, StatusCode};
use serde::Deserialize;
use hyper::StatusCode;
use serde_json::value::RawValue;
use sqlx::types::Json;
use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
utils::{not_found_if_none, StripPath},
};
use windmill_queue::PushArgs;
use crate::{
db::{ApiAuthed, DB},
jobs::{add_include_headers, add_raw_string, JsonOrForm},
};
use crate::db::{ApiAuthed, DB};
const KEEP_LAST: i64 = 8;
@@ -85,21 +86,12 @@ pub async fn new_payload(
Ok(StatusCode::CREATED)
}
#[derive(Deserialize, Clone)]
pub struct IncludeHeaderQuery {
include_header: Option<String>,
}
pub async fn update_payload(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(run_query): Query<IncludeHeaderQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<RawValue>>>,
) -> Result<StatusCode> {
let mut tx = db.begin().await?;
let args = add_include_headers(&run_query.include_header, headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
sqlx::query!(
"
@@ -110,7 +102,7 @@ pub async fn update_payload(
",
&w_id,
&path.to_path(),
serde_json::json!(args),
Json(args) as Json<PushArgs<HashMap<String, Box<RawValue>>>>,
)
.execute(&mut *tx)
.await?;
+3 -2
View File
@@ -38,6 +38,7 @@ use windmill_common::{
scripts::Schema,
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_queue::PushArgs;
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
pub fn workspaced_service() -> Router {
@@ -290,7 +291,7 @@ async fn create_flow(
tx,
&w_id,
JobPayload::FlowDependencies { path: nf.path.clone() },
serde_json::Map::new(),
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
@@ -482,7 +483,7 @@ async fn update_flow(
tx,
&w_id,
JobPayload::FlowDependencies { path: nf.path.clone() },
serde_json::Map::new(),
PushArgs::empty(),
&authed.username,
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
+76 -272
View File
@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::{
@@ -19,13 +21,12 @@ use axum::{
extract::{FromRequest, Json, Path, Query},
response::{IntoResponse, Response},
routing::{get, post},
Extension, Form, RequestExt, Router,
Extension, Router,
};
use base64::Engine;
use bytes::Bytes;
use chrono::Utc;
use hmac::Mac;
use hyper::{header::CONTENT_TYPE, http, HeaderMap, Request, StatusCode};
use hyper::{http, Request, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sql_builder::{prelude::*, quote, SqlBuilder};
use sqlx::types::JsonRawValue;
@@ -46,7 +47,7 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
};
use windmill_queue::{job_is_complete, push, PushIsolationLevel};
use windmill_queue::{empty_args, job_is_complete, push, PushArgs, PushIsolationLevel};
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
@@ -87,18 +88,6 @@ pub fn workspaced_service() -> Router {
.head(|| async { "" })
.layer(cors.clone()),
)
.route(
"/openai_sync/p/*script_path",
post(openai_sync_script_by_path)
.head(|| async { "" })
.layer(cors.clone()),
)
.route(
"/openai_sync/f/*script_path",
post(openai_sync_flow_by_path)
.head(|| async { "" })
.layer(cors.clone()),
)
.route(
"/run/h/:hash",
post(run_job_by_hash)
@@ -190,11 +179,16 @@ pub fn global_root_service() -> Router {
Router::new().route("/db_clock", get(get_db_clock))
}
#[derive(Deserialize)]
struct JsonPath {
pub json_path: Option<String>,
}
async fn get_result_by_id(
Extension(db): Extension<DB>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id).await?;
Query(JsonPath { json_path }): Query<JsonPath>,
) -> windmill_common::error::JsonResult<Box<JsonRawValue>> {
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id, json_path).await?;
Ok(Json(res))
}
@@ -350,7 +344,7 @@ async fn get_job(
) -> error::Result<Response> {
let cjob_option = sqlx::query("SELECT
id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
args, CASE WHEN pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
CASE WHEN pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, CASE WHEN pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
raw_lock, email, visible_to_owner, mem_peak, tag
FROM completed_job WHERE id = $1 AND workspace_id = $2")
@@ -363,7 +357,11 @@ async fn get_job(
Ok(Json(job).into_response())
} else {
let job_o = sqlx::query_as::<_, QueuedJob>(
"SELECT *
"SELECT id, workspace_id, parent_job, created_by, created_at, started_at, scheduled_for, running,
script_hash, script_path, CASE WHEN pg_column_size(args) < 2000000 THEN args ELSE '{\"reason\": \"WINDMILL_TOO_BIG\"}'::jsonb END as args, logs, raw_code, canceled, canceled_by, canceled_reason, last_ping,
job_kind, env_id, schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language,
suspend, suspend_until, same_worker, raw_lock, pre_run_error, email, visible_to_owner, mem_peak,
root_job, leaf_jobs, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl
FROM queue WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
@@ -407,7 +405,7 @@ pub struct CompletedJob<'rows> {
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
pub args: Option<&'rows JsonRawValue>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub result: Option<&'rows JsonRawValue>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -502,21 +500,12 @@ pub struct RunJobQuery {
scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
scheduled_in_secs: Option<i64>,
parent_job: Option<Uuid>,
include_header: Option<String>,
invisible_to_owner: Option<bool>,
queue_limit: Option<i64>,
payload: Option<String>,
job_id: Option<Uuid>,
}
lazy_static::lazy_static! {
static ref INCLUDE_HEADERS: Vec<String> = std::env::var("INCLUDE_HEADERS")
.ok().map(|x| x
.split(',')
.map(|s| s.to_string())
.collect()).unwrap_or_default();
}
impl RunJobQuery {
async fn get_scheduled_for<'c>(
&self,
@@ -531,38 +520,6 @@ impl RunJobQuery {
Ok(None)
}
}
fn add_include_headers(
&self,
headers: HeaderMap,
args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
return add_include_headers(&self.include_header, headers, args);
}
}
pub fn add_include_headers(
include_header: &Option<String>,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let whitelist = include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
}
#[derive(Deserialize)]
@@ -1485,7 +1442,7 @@ struct Preview {
content: Option<String>,
kind: Option<PreviewKind>,
path: Option<String>,
args: Option<serde_json::Map<String, serde_json::Value>>,
args: Option<Box<JsonRawValue>>,
language: Option<ScriptLang>,
tag: Option<String>,
}
@@ -1494,78 +1451,10 @@ struct Preview {
struct PreviewFlow {
value: FlowValue,
path: Option<String>,
args: Option<serde_json::Map<String, serde_json::Value>>,
args: Option<Box<JsonRawValue>>,
tag: Option<String>,
}
pub struct JsonOrForm(
pub Option<serde_json::Map<String, serde_json::Value>>,
pub Option<String>,
);
#[axum::async_trait]
impl<S> FromRequest<S, axum::body::Body> for JsonOrForm
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<Self, Self::Rejection> {
let content_type_header = req.headers().get(CONTENT_TYPE);
let content_type = content_type_header.and_then(|value| value.to_str().ok());
if content_type.is_none() || content_type.unwrap().starts_with("application/json") {
if req
.uri()
.query()
.map(|x| x.contains("raw=true"))
.unwrap_or(false)
{
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
let str = String::from_utf8(bytes.to_vec()).map_err(|e| {
Error::BadRequest(format!("invalid utf8: {}", e)).into_response()
})?;
let payload =
serde_json::from_str::<Option<serde_json::Value>>(&str).map_err(|e| {
Error::BadRequest(format!("invalid json: {}", e)).into_response()
})?;
return match payload {
Some(serde_json::Value::Object(map)) => Ok(Self(Some(map), Some(str))),
None => Ok(Self(None, Some(str))),
Some(x) => {
let mut map = serde_json::Map::new();
map.insert("body".to_string(), x);
Ok(Self(Some(map), Some(str)))
}
};
} else {
let Json(payload): Json<Option<serde_json::Value>> =
req.extract().await.map_err(IntoResponse::into_response)?;
return match payload {
Some(serde_json::Value::Object(map)) => Ok(Self(Some(map), None)),
None => Ok(Self(None, None)),
Some(x) => {
let mut map = serde_json::Map::new();
map.insert("body".to_string(), x);
Ok(Self(Some(map), None))
}
};
}
} else if content_type
.unwrap()
.starts_with("application/x-www-form-urlencoded")
{
let Form(payload) = req.extract().await.map_err(IntoResponse::into_response)?;
return Ok(Self(Some(payload), None));
} else {
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
}
}
}
pub struct QueryOrBody<D>(pub Option<D>);
#[axum::async_trait]
@@ -1673,8 +1562,7 @@ pub async fn run_flow_by_path(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -1691,8 +1579,6 @@ pub async fn run_flow_by_path(
.flatten();
check_tag_available_for_workspace(&w_id, &tag).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -1728,8 +1614,7 @@ pub async fn run_job_by_path(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -1740,8 +1625,6 @@ pub async fn run_job_by_path(
let (job_payload, tag) = script_path_to_payload(script_path, &db, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -1934,12 +1817,14 @@ pub async fn run_wait_result_job_by_path_get(
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let args = if let Some(payload) = payload_r {
let payload_args = if let Some(payload) = payload_r {
payload?
} else {
serde_json::Map::new()
HashMap::new()
};
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(empty_args()) };
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
check_scopes(&authed, || format!("run:script/{script_path}"))?;
@@ -1983,7 +1868,7 @@ pub async fn run_wait_result_flow_by_path_get(
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
headers: HeaderMap,
Query(run_query): Query<RunJobQuery>,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
@@ -1998,22 +1883,16 @@ pub async fn run_wait_result_flow_by_path_get(
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let args = if let Some(payload) = payload_r {
let payload_args = if let Some(payload) = payload_r {
payload?
} else {
serde_json::Map::new()
HashMap::new()
};
let args = PushArgs { extra: payload_args, args: sqlx::types::Json(HashMap::new()) };
run_wait_result_flow_by_path_internal(
db,
run_query,
flow_path,
authed,
rsmq,
user_db,
headers,
Some(args),
None,
w_id,
db, run_query, flow_path, authed, rsmq, user_db, args, w_id,
)
.await
}
@@ -2025,8 +1904,7 @@ pub async fn run_wait_result_script_by_path(
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -2039,58 +1917,7 @@ pub async fn run_wait_result_script_by_path(
rsmq,
user_db,
w_id,
headers,
args,
raw_string,
)
.await
}
fn convert_from_openai_json(
json: Option<serde_json::Map<String, serde_json::Value>>,
) -> error::Result<Option<serde_json::Map<String, serde_json::Value>>> {
if let Some(m) = json {
let mut new_json = serde_json::Map::new();
let input_keys = m
.get("inputKeys")
.and_then(|x| x.as_array())
.map(|x| x.to_owned())
.unwrap_or_default();
let input_values = m
.get("inputValues")
.and_then(|x| x.as_array())
.map(|x| x.to_owned())
.unwrap_or_default();
for (k, v) in input_keys.into_iter().zip(input_values.into_iter()) {
new_json.insert(k.as_str().unwrap_or_else(|| "invalid_key").to_string(), v);
}
Ok(Some(new_json))
} else {
Ok(None)
}
}
pub async fn openai_sync_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::JsonResult<serde_json::Value> {
run_wait_result_script_by_path_internal(
db,
run_query,
script_path,
authed,
rsmq,
user_db,
w_id,
headers,
convert_from_openai_json(args)?,
raw_string,
)
.await
}
@@ -2103,9 +1930,7 @@ async fn run_wait_result_script_by_path_internal(
rsmq: Option<rsmq_async::MultiplexedRsmq>,
user_db: UserDB,
w_id: String,
headers: HeaderMap,
args: Option<serde_json::Map<String, serde_json::Value>>,
raw_string: Option<String>,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> Result<Json<serde_json::Value>, Error> {
check_queue_too_long(&db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
@@ -2113,8 +1938,6 @@ async fn run_wait_result_script_by_path_internal(
let (job_payload, tag) = script_path_to_payload(script_path, &db, &w_id).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2153,8 +1976,7 @@ pub async fn run_wait_result_script_by_hash(
Extension(db): Extension<DB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -2173,8 +1995,6 @@ pub async fn run_wait_result_script_by_hash(
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2214,31 +2034,6 @@ pub async fn run_wait_result_script_by_hash(
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_hash))).await
}
pub async fn openai_sync_flow_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::JsonResult<serde_json::Value> {
run_wait_result_flow_by_path_internal(
db,
run_query,
flow_path,
authed,
rsmq,
user_db,
headers,
convert_from_openai_json(args)?,
raw_string,
w_id,
)
.await
}
pub async fn run_wait_result_flow_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -2246,14 +2041,13 @@ pub async fn run_wait_result_flow_by_path(
Extension(db): Extension<DB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, rsmq, user_db, headers, args, raw_string, w_id,
db, run_query, flow_path, authed, rsmq, user_db, args, w_id,
)
.await
}
@@ -2265,9 +2059,7 @@ async fn run_wait_result_flow_by_path_internal(
authed: ApiAuthed,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
user_db: UserDB,
headers: HeaderMap,
args: Option<serde_json::Map<String, serde_json::Value>>,
raw_string: Option<String>,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
w_id: String,
) -> Result<Json<serde_json::Value>, Error> {
check_queue_too_long(&db, run_query.queue_limit).await?;
@@ -2276,8 +2068,7 @@ async fn run_wait_result_flow_by_path_internal(
check_scopes(&authed, || format!("run:flow/{flow_path}"))?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
let tag = sqlx::query_scalar!(
"SELECT tag from flow WHERE path = $1 and workspace_id = $2",
flow_path,
@@ -2324,7 +2115,6 @@ async fn run_preview_job(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(preview): Json<Preview>,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
@@ -2337,7 +2127,6 @@ async fn run_preview_job(
));
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
check_tag_available_for_workspace(&w_id, &preview.tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2358,7 +2147,7 @@ async fn run_preview_job(
cache_ttl: None,
}),
},
args,
preview.args.unwrap_or_default(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
@@ -2436,7 +2225,7 @@ async fn add_batch_jobs(
value: batch_info.flow_value.clone().unwrap(),
path: None,
},
serde_json::Map::new(),
PushArgs::empty(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
@@ -2519,7 +2308,6 @@ async fn run_preview_flow_job(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(raw_flow): Json<PreviewFlow>,
) -> error::Result<(StatusCode, String)> {
check_scopes(&authed, || format!("runflow"))?;
@@ -2529,7 +2317,6 @@ async fn run_preview_flow_job(
));
}
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
check_tag_available_for_workspace(&w_id, &raw_flow.tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2538,7 +2325,7 @@ async fn run_preview_flow_job(
tx,
&w_id,
JobPayload::RawFlow { value: raw_flow.value, path: raw_flow.path },
args,
raw_flow.args.unwrap_or_default(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
@@ -2569,8 +2356,7 @@ pub async fn run_job_by_hash(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
args: PushArgs<HashMap<String, Box<JsonRawValue>>>,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -2588,8 +2374,7 @@ pub async fn run_job_by_hash(
check_scopes(&authed, || format!("run:script/{path}"))?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let args = add_raw_string(raw_string, args);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into(), rsmq);
@@ -2648,21 +2433,22 @@ async fn get_job_update(
Path((w_id, id)): Path<(String, Uuid)>,
Query(JobUpdateQuery { running, log_offset }): Query<JobUpdateQuery>,
) -> error::JsonResult<JobUpdate> {
let mut tx = db.begin().await?;
let record = sqlx::query!(
"SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
"SELECT running, substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
log_offset,
&w_id,
&id
)
.fetch_optional(&mut *tx)
.fetch_optional(&db)
.await?;
if let Some(record) = record {
tx.commit().await?;
Ok(Json(JobUpdate {
running: if !running { Some(true) } else { None },
running: if !running && record.running {
Some(true)
} else {
None
},
completed: None,
new_logs: record.logs,
mem_peak: record.mem_peak,
@@ -2675,10 +2461,9 @@ async fn get_job_update(
&w_id,
&id
)
.fetch_optional(&mut *tx)
.fetch_optional(&db)
.await?;
let logs = not_found_if_none(logs, "Job Update", id.to_string())?;
tx.commit().await?;
Ok(Json(JobUpdate {
running: Some(false),
completed: Some(true),
@@ -2851,7 +2636,10 @@ async fn get_completed_job<'a>(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::Result<Response> {
let job_o = sqlx::query("SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2")
let job_o = sqlx::query("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
CASE WHEN pg_column_size(args) < 2000000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN pg_column_size(result) < 2000000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind, env_id,
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
raw_lock, email, visible_to_owner, mem_peak, tag FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&db)
@@ -2875,13 +2663,29 @@ impl<'a> IntoResponse for RawResult<'a> {
async fn get_completed_job_result(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(JsonPath { json_path }): Query<JsonPath>,
) -> error::Result<Response> {
let result_o =
let result_o = if let Some(json_path) = json_path {
sqlx::query(
"SELECT result #> $3 as result FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
.bind(w_id)
.bind(
json_path
.split(".")
.map(|x| x.to_string())
.collect::<Vec<_>>(),
)
.fetch_optional(&db)
.await?
} else {
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.fetch_optional(&db)
.await?;
.await?
};
let result = not_found_if_none(result_o, "Completed Job", id.to_string())?;
Ok(RawResult::from_row(&result)?.into_response())
+1 -1
View File
@@ -201,7 +201,6 @@ pub async fn run_server(
users::global_service().layer(Extension(argon2.clone())),
)
.nest("/settings", settings::global_service())
.nest("/jobs", jobs::global_root_service())
.nest("/workers", workers::global_service())
.nest("/configs", configs::global_service())
.nest("/scripts", scripts::global_service())
@@ -212,6 +211,7 @@ pub async fn run_server(
.nest("/schedules", schedule::global_service())
.route_layer(from_extractor::<ApiAuthed>())
.route_layer(from_extractor::<users::Tokened>())
.nest("/jobs", jobs::global_root_service())
.nest(
"/saml",
saml::global_service().layer(Extension(Arc::new(sp_extension.0))),
+1 -1
View File
@@ -903,7 +903,7 @@ async fn slack_command(
tx,
&settings.workspace_id,
payload,
map,
sqlx::types::Json(map),
&form.user_name,
&settings.slack_email,
"g/slack".to_string(),
+7 -4
View File
@@ -21,7 +21,7 @@ use axum::{
};
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::{value::RawValue, Value};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::{FromRow, Postgres, Transaction};
use uuid::Uuid;
@@ -117,7 +117,7 @@ pub struct ListableResource {
#[derive(Deserialize)]
pub struct CreateResource {
pub path: String,
pub value: Option<serde_json::Value>,
pub value: Option<Box<RawValue>>,
pub description: Option<String>,
pub resource_type: String,
}
@@ -125,7 +125,7 @@ pub struct CreateResource {
struct EditResource {
path: Option<String>,
description: Option<String>,
value: Option<serde_json::Value>,
value: Option<Box<RawValue>>,
}
#[derive(Deserialize)]
@@ -496,6 +496,9 @@ async fn create_resource(
check_path_conflict(&mut tx, &w_id, &resource.path).await?;
}
let res_value = resource.value.unwrap_or_default();
let raw_json = sqlx::types::Json(res_value.as_ref());
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type)
@@ -503,7 +506,7 @@ async fn create_resource(
DO UPDATE SET value = $3, description = $4, resource_type = $5",
w_id,
resource.path,
resource.value,
raw_json as sqlx::types::Json<&RawValue>,
resource.description,
resource.resource_type,
)
+4 -2
View File
@@ -46,7 +46,9 @@ use windmill_common::{
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
},
};
use windmill_queue::{self, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
use windmill_queue::{
self, schedule::push_scheduled_job, PushArgs, PushIsolationLevel, QueueTransaction,
};
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
@@ -592,7 +594,7 @@ async fn create_script(
tx,
&w_id,
JobPayload::Dependencies { hash, dependencies, language: ns.language, path: ns.path },
serde_json::Map::new(),
PushArgs::empty(),
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
+1 -1
View File
@@ -549,7 +549,7 @@ async fn run_slack_message_test_job(
false,
w_id.as_str(),
&format!("script/{}", req.hub_script_path.as_str()),
&json!(fake_result),
sqlx::types::Json(&fake_result),
0,
Utc::now(),
Some(json!(extra_args)),
+8 -6
View File
@@ -11,8 +11,8 @@ use std::time::Duration;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::flows::FlowValue;
use crate::more_serde::default_false;
use crate::{flows::FlowValue, more_serde::is_default};
const MINUTES: Duration = Duration::from_secs(60);
const HOURS: Duration = MINUTES.saturating_mul(60);
@@ -20,21 +20,24 @@ const HOURS: Duration = MINUTES.saturating_mul(60);
pub const MAX_RETRY_ATTEMPTS: u16 = 1000;
pub const MAX_RETRY_INTERVAL: Duration = HOURS.saturating_mul(6);
pub fn is_retry_default(v: &RetryStatus) -> bool {
v.fail_count == 0 && v.failed_jobs.is_empty()
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FlowStatus {
pub step: i32,
pub modules: Vec<FlowStatusModule>,
pub failure_module: FlowStatusModuleWParent,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
#[serde(skip_serializing_if = "is_retry_default")]
pub retry: RetryStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct RetryStatus {
pub fail_count: u16,
pub previous_result: Option<serde_json::Value>,
pub failed_jobs: Vec<Uuid>,
}
@@ -47,7 +50,6 @@ pub struct Iterator {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BranchAllStatus {
pub branch: usize,
pub previous_result: serde_json::Value,
pub len: usize,
}
@@ -189,7 +191,7 @@ impl FlowStatus {
.unwrap_or_else(|| "failure".to_string()),
},
},
retry: RetryStatus { fail_count: 0, previous_result: None, failed_jobs: vec![] },
retry: RetryStatus { fail_count: 0, failed_jobs: vec![] },
}
}
+3 -2
View File
@@ -12,6 +12,7 @@ use std::{
};
use serde::{self, Deserialize, Serialize, Serializer};
use serde_json::value::RawValue;
use crate::{
more_serde::{
@@ -171,7 +172,7 @@ pub struct Suspend {
pub struct Mock {
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_value: Option<serde_json::Value>,
pub return_value: Option<Box<RawValue>>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
@@ -203,7 +204,7 @@ impl FlowModule {
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(
tag = "type",
rename_all(serialize = "lowercase", deserialize = "lowercase")
+14 -5
View File
@@ -1,5 +1,8 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, Transaction};
use serde_json::value::RawValue;
use sqlx::{types::Json, Pool, Postgres, Transaction};
use uuid::Uuid;
use crate::{
@@ -27,7 +30,7 @@ pub enum JobKind {
Noop,
}
#[derive(Debug, sqlx::FromRow, Serialize, Clone)]
#[derive(sqlx::FromRow, Debug, Serialize, Clone)]
pub struct QueuedJob {
pub workspace_id: String,
pub id: Uuid,
@@ -43,7 +46,7 @@ pub struct QueuedJob {
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
pub args: Option<Json<HashMap<String, Box<RawValue>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -95,6 +98,14 @@ pub struct QueuedJob {
}
impl QueuedJob {
pub fn get_args(&self) -> HashMap<String, Box<RawValue>> {
if let Some(args) = self.args.as_ref() {
args.0.clone()
} else {
HashMap::new()
}
}
pub fn script_path(&self) -> &str {
self.script_path
.as_ref()
@@ -112,9 +123,7 @@ impl QueuedJob {
self.script_path()
)
}
}
impl QueuedJob {
pub fn parse_raw_flow(&self) -> Option<FlowValue> {
self.raw_flow
.as_ref()
+11
View File
@@ -3,6 +3,7 @@ use std::{collections::HashMap, sync::Arc};
use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use tokio::sync::RwLock;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::ServerConfig, DB};
@@ -260,3 +261,13 @@ pub struct WorkerConfig {
pub additional_python_paths: Option<Vec<String>>,
pub pip_local_dependencies: Option<Vec<String>>,
}
pub fn to_raw_value<T: Serialize>(result: &T) -> Box<RawValue> {
serde_json::value::to_raw_value(result)
.unwrap_or_else(|_| RawValue::from_string("{}".to_string()).unwrap())
}
pub fn to_raw_value_owned(result: serde_json::Value) -> Box<RawValue> {
serde_json::value::to_raw_value(&result)
.unwrap_or_else(|_| RawValue::from_string("{}".to_string()).unwrap())
}
+2 -1
View File
@@ -37,4 +37,5 @@ tokio.workspace = true
futures-core.workspace = true
itertools.workspace = true
async-recursion.workspace = true
bigdecimal.workspace = true
bigdecimal.workspace = true
axum.workspace = true
+332 -89
View File
@@ -10,12 +10,23 @@ use std::{collections::HashMap, vec};
use anyhow::Context;
use async_recursion::async_recursion;
use axum::{
body::Bytes,
extract::FromRequest,
http::Request,
response::{IntoResponse, Response},
Form, RequestExt,
};
use bigdecimal::ToPrimitive;
use chrono::{DateTime, Duration, Utc};
use reqwest::Client;
use reqwest::{
header::{HeaderMap, CONTENT_TYPE},
Client, StatusCode,
};
use rsmq_async::RsmqConnection;
use serde_json::json;
use sqlx::{Pool, Postgres, Transaction};
use serde::Serialize;
use serde_json::{json, value::RawValue};
use sqlx::{types::Json, FromRow, Pool, Postgres, Transaction};
#[cfg(feature = "benchmark")]
use std::time::Instant;
use tracing::{instrument, Instrument};
@@ -36,7 +47,7 @@ use windmill_common::{
schedule::{schedule_to_user, Schedule},
scripts::{ScriptHash, ScriptLang},
users::{username_to_permissioned_as, SUPERADMIN_SECRET_EMAIL},
worker::WORKER_CONFIG,
worker::{to_raw_value, WORKER_CONFIG},
DB, METRICS_ENABLED,
};
@@ -120,6 +131,7 @@ pub async fn cancel_job<'c: 'async_recursion>(
&db,
&job_running,
format!("canceled by {username}: (force cancel: {force_cancel})"),
job_running.mem_peak.unwrap_or(0),
&e,
None,
rsmq.clone(),
@@ -166,20 +178,39 @@ pub async fn cancel_job<'c: 'async_recursion>(
Ok((tx, Some(id)))
}
#[derive(Serialize)]
pub struct WrappedError<T: Serialize> {
pub error: T,
}
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(
pub async fn add_completed_job_error<
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send,
>(
db: &Pool<Postgres>,
queued_job: &QueuedJob,
logs: String,
e: &serde_json::Value,
mem_peak: i32,
e: T,
metrics: Option<Metrics>,
rsmq: Option<R>,
) -> Result<serde_json::Value, Error> {
) -> Result<WrappedError<T>, Error> {
if *METRICS_ENABLED {
metrics.map(|m| m.worker_execution_failed.inc());
}
let result = serde_json::json!({ "error": e });
let _ = add_completed_job(db, &queued_job, false, false, &result, logs, rsmq).await?;
let result = WrappedError { error: e };
let _ = add_completed_job(
db,
&queued_job,
false,
false,
Json(&result),
logs,
mem_peak,
rsmq,
)
.await?;
Ok(result)
}
@@ -206,13 +237,17 @@ lazy_static::lazy_static! {
}
#[instrument(level = "trace", skip_all, name = "add_completed_job")]
pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
pub async fn add_completed_job<
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send,
>(
db: &Pool<Postgres>,
queued_job: &QueuedJob,
success: bool,
skipped: bool,
result: &serde_json::Value,
result: Json<&T>,
logs: String,
mem_peak: i32,
rsmq: Option<R>,
) -> Result<Uuid, Error> {
// tracing::error!("Start");
@@ -245,14 +280,11 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
None
};
let mem_peak = sqlx::query_scalar!("SELECT mem_peak FROM queue WHERE id = $1", &queued_job.id)
.fetch_optional(db)
.await
.ok()
.flatten()
.flatten();
let mut tx: QueueTransaction<'_, R> = (rsmq.clone(), db.begin().await?).into();
let job_id = queued_job.id.clone();
let job_id = queued_job.id;
// tracing::error!("1 {:?}", start.elapsed());
let mem_peak = mem_peak.max(queued_job.mem_peak.unwrap_or(0));
let _duration: i64 = sqlx::query_scalar!(
"INSERT INTO completed_job AS cj
( workspace_id
@@ -298,8 +330,8 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
success,
queued_job.script_hash.map(|x| x.0),
queued_job.script_path,
queued_job.args,
result,
&queued_job.args as &Option<Json<HashMap<String, Box<RawValue>>>>,
result as Json<&T>,
logs,
queued_job.raw_code,
queued_job.raw_lock,
@@ -317,15 +349,18 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
duration as Option<i64>,
queued_job.email,
queued_job.visible_to_owner,
mem_peak,
if mem_peak > 0 { Some(mem_peak) } else { None },
queued_job.tag,
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
// tracing::error!("2 {:?}", start.elapsed());
// tracing::error!("Added completed job {:#?}", queued_job);
tx = delete_job(tx, &queued_job.workspace_id, job_id).await?;
// tracing::error!("3 {:?}", start.elapsed());
if !queued_job.is_flow_step
&& queued_job.schedule_path.is_some()
&& queued_job.script_path.is_some()
@@ -337,7 +372,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
queued_job.script_path.as_ref().unwrap(),
&queued_job.workspace_id,
success,
&result,
result,
job_id,
queued_job.started_at.unwrap_or(chrono::Utc::now()),
)
@@ -399,7 +434,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
&& queued_job.parent_job.is_none()
&& !success
{
if let Err(e) = send_error_to_global_handler(rsmq.clone(), &queued_job, db, &result).await {
if let Err(e) = send_error_to_global_handler(rsmq.clone(), &queued_job, db, result).await {
tracing::error!(
"Could not run global error handler for job {}: {}",
&queued_job.id,
@@ -407,8 +442,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
);
}
if let Err(e) =
send_error_to_workspace_handler(rsmq.clone(), &queued_job, db, &result).await
if let Err(e) = send_error_to_workspace_handler(rsmq.clone(), &queued_job, db, result).await
{
tracing::error!(
"Could not run workspace error handler for job {}: {}",
@@ -419,16 +453,20 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
}
tracing::debug!("Added completed job {}", queued_job.id);
// tracing::error!("{:?}", start.elapsed());
// tracing::error!("4 {:?}", start.elapsed());
Ok(queued_job.id)
}
pub async fn run_error_handler<R: rsmq_async::RsmqConnection + Clone + Send>(
pub async fn run_error_handler<
'a,
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send,
>(
rsmq: Option<R>,
queued_job: &QueuedJob,
db: &Pool<Postgres>,
result: &serde_json::Value,
result: Json<&'a T>,
error_handler_path: &str,
error_handler_extra_args: Option<serde_json::Value>,
is_global: bool,
@@ -437,16 +475,22 @@ pub async fn run_error_handler<R: rsmq_async::RsmqConnection + Clone + Send>(
let script_w_id = if is_global { "admins" } else { w_id }; // script workspace id
let job_id = queued_job.id;
let (job_payload, tag) = script_path_to_payload(&error_handler_path, db, script_w_id).await?;
let mut args = result.as_object().unwrap().clone();
args.insert("workspace_id".to_string(), json!(w_id));
args.insert("job_id".to_string(), json!(job_id));
args.insert("path".to_string(), json!(queued_job.script_path));
args.insert("is_flow".to_string(), json!(queued_job.raw_flow.is_some()));
args.insert("email".to_string(), json!(queued_job.email));
let mut extra = HashMap::new();
extra.insert("workspace_id".to_string(), to_raw_value(&w_id));
extra.insert("job_id".to_string(), to_raw_value(&job_id));
extra.insert("path".to_string(), to_raw_value(&queued_job.script_path));
extra.insert(
"is_flow".to_string(),
to_raw_value(&queued_job.raw_flow.is_some()),
);
extra.insert("email".to_string(), to_raw_value(&queued_job.email));
if let Some(extra_args) = error_handler_extra_args {
if let serde_json::Value::Object(args_m) = extra_args {
args.extend(args_m);
for (k, v) in args_m {
extra.insert(k, to_raw_value(&v));
}
} else {
return Err(error::Error::ExecutionErr(
"args of scripts needs to be dict".to_string(),
@@ -461,7 +505,7 @@ pub async fn run_error_handler<R: rsmq_async::RsmqConnection + Clone + Send>(
tx,
script_w_id,
job_payload,
args,
PushArgs { extra, args: result.to_owned() },
if is_global { "global" } else { "error_handler" },
if is_global {
SUPERADMIN_SECRET_EMAIL
@@ -497,11 +541,15 @@ pub async fn run_error_handler<R: rsmq_async::RsmqConnection + Clone + Send>(
Ok(())
}
pub async fn send_error_to_global_handler<R: rsmq_async::RsmqConnection + Clone + Send>(
pub async fn send_error_to_global_handler<
'a,
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send,
>(
rsmq: Option<R>,
queued_job: &QueuedJob,
db: &Pool<Postgres>,
result: &serde_json::Value,
result: Json<&'a T>,
) -> Result<(), Error> {
if let Some(ref global_error_handler) = *GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE {
run_error_handler(
@@ -519,11 +567,16 @@ pub async fn send_error_to_global_handler<R: rsmq_async::RsmqConnection + Clone
Ok(())
}
pub async fn send_error_to_workspace_handler<R: rsmq_async::RsmqConnection + Clone + Send>(
pub async fn send_error_to_workspace_handler<
'a,
'c,
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send,
>(
rsmq: Option<R>,
queued_job: &QueuedJob,
db: &Pool<Postgres>,
result: &serde_json::Value,
result: Json<&'a T>,
) -> Result<(), Error> {
let w_id = &queued_job.workspace_id;
let mut tx = db.begin().await?;
@@ -620,20 +673,25 @@ pub async fn handle_maybe_scheduled_job<'c, R: rsmq_async::RsmqConnection + Clon
}
}
#[derive(Clone)]
#[derive(Clone, Serialize)]
struct CompletedJobSubset {
success: bool,
result: Option<serde_json::Value>,
started_at: chrono::DateTime<chrono::Utc>,
}
async fn apply_schedule_handlers<'c, R: rsmq_async::RsmqConnection + Clone + Send + 'c>(
async fn apply_schedule_handlers<
'a,
'c,
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send + 'c,
>(
mut tx: QueueTransaction<'c, R>,
db: &Pool<Postgres>,
schedule_path: &str,
script_path: &str,
w_id: &str,
success: bool,
result: &serde_json::Value,
result: Json<&'a T>,
job_id: Uuid,
started_at: DateTime<Utc>,
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
@@ -790,7 +848,12 @@ async fn apply_schedule_handlers<'c, R: rsmq_async::RsmqConnection + Clone + Sen
Ok(tx)
}
pub async fn handle_on_failure<'c, R: rsmq_async::RsmqConnection + Clone + Send + 'c>(
pub async fn handle_on_failure<
'a,
'c,
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send + 'c,
>(
db: &Pool<Postgres>,
tx: QueueTransaction<'c, R>,
schedule_path: &str,
@@ -798,7 +861,7 @@ pub async fn handle_on_failure<'c, R: rsmq_async::RsmqConnection + Clone + Send
is_flow: bool,
w_id: &str,
on_failure_path: &str,
result: &serde_json::Value,
result: Json<&'a T>,
failed_times: i32,
started_at: DateTime<Utc>,
extra_args: Option<serde_json::Value>,
@@ -808,16 +871,18 @@ pub async fn handle_on_failure<'c, R: rsmq_async::RsmqConnection + Clone + Send
) -> windmill_common::error::Result<(Uuid, QueueTransaction<'c, R>)> {
let (payload, tag) = get_payload_tag_from_prefixed_path(on_failure_path, db, w_id).await?;
let mut args = result.clone().as_object().unwrap().clone();
args.insert("schedule_path".to_string(), json!(schedule_path));
args.insert("path".to_string(), json!(script_path));
args.insert("is_flow".to_string(), json!(is_flow));
args.insert("started_at".to_string(), json!(started_at));
args.insert("failed_times".to_string(), json!(failed_times));
let mut extra = HashMap::new();
extra.insert("schedule_path".to_string(), to_raw_value(&schedule_path));
extra.insert("path".to_string(), to_raw_value(&script_path));
extra.insert("is_flow".to_string(), to_raw_value(&is_flow));
extra.insert("started_at".to_string(), to_raw_value(&started_at));
extra.insert("failed_times".to_string(), to_raw_value(&failed_times));
if let Some(args_v) = extra_args {
if let serde_json::Value::Object(args_m) = args_v {
args.extend(args_m);
for (k, v) in args_m {
extra.insert(k, to_raw_value(&v));
}
} else {
return Err(error::Error::ExecutionErr(
"args of scripts needs to be dict".to_string(),
@@ -831,7 +896,7 @@ pub async fn handle_on_failure<'c, R: rsmq_async::RsmqConnection + Clone + Send
tx,
w_id,
payload,
args,
PushArgs { extra, args: result.to_owned() },
username,
email,
permissioned_as,
@@ -857,7 +922,20 @@ pub async fn handle_on_failure<'c, R: rsmq_async::RsmqConnection + Clone + Send
return Ok((uuid, tx));
}
async fn handle_on_recovery<'c, R: rsmq_async::RsmqConnection + Clone + Send + 'c>(
// #[derive(Serialize)]
// pub struct RecoveryValue<T> {
// error_started_at: chrono::DateTime<Utc>,
// schedule_path: String,
// path: String,
// is_flow: boolean,
// extra_args: serde_json::Value
// }
async fn handle_on_recovery<
'a,
'c,
T: Serialize + Send + Sync,
R: rsmq_async::RsmqConnection + Clone + Send + 'c,
>(
db: &Pool<Postgres>,
tx: QueueTransaction<'c, R>,
schedule_path: &str,
@@ -866,7 +944,7 @@ async fn handle_on_recovery<'c, R: rsmq_async::RsmqConnection + Clone + Send + '
w_id: &str,
on_recovery_path: &str,
error_job: CompletedJobSubset,
successful_job_result: &serde_json::Value,
successful_job_result: Json<&'a T>,
successful_times: i32,
successful_job_started_at: DateTime<Utc>,
extra_args: Option<serde_json::Value>,
@@ -886,7 +964,11 @@ async fn handle_on_recovery<'c, R: rsmq_async::RsmqConnection + Clone + Send + '
args.insert("schedule_path".to_string(), json!(schedule_path));
args.insert("path".to_string(), json!(script_path));
args.insert("is_flow".to_string(), json!(is_flow));
args.insert("success_result".to_string(), successful_job_result.clone());
args.insert(
"success_result".to_string(),
serde_json::from_str(&serde_json::to_string(&successful_job_result).unwrap())
.unwrap_or_else(|_| json!("{}")),
);
args.insert("success_times".to_string(), json!(successful_times));
args.insert(
"success_started_at".to_string(),
@@ -1074,14 +1156,14 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
// if using redis, only one message at a time can be poped from the queue. Process only this message and move to the next elligible job
// In this case, the job might be a job from the same script path, but we can't optimise this further
// if using posgtres, then we're able to re-queue the entire batch of scheduled job for this script_path, so we do it
let _requeued_job = sqlx::query_as::<_, QueuedJob>(&format!(
let requeued_job_tag = sqlx::query_scalar::<_, String>(&format!(
"UPDATE queue
SET running = false
, started_at = null
, scheduled_for = '{estimated_next_schedule_timestamp}'
, logs = CASE WHEN logs IS NULL OR logs = '' THEN '{job_log_event}'::text WHEN logs LIKE '%{job_log_event}' THEN logs ELSE concat(logs, '{job_log_line_break}{job_log_event}'::text) END
WHERE id = '{job_uuid}'
RETURNING *"
RETURNING tag"
))
.fetch_one(&mut tx)
.await
@@ -1091,20 +1173,19 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
rsmq.send_message(
job_uuid.to_bytes_le().to_vec(),
Option::Some(estimated_next_schedule_timestamp),
_requeued_job.tag,
requeued_job_tag,
);
}
tx.commit().await?;
} else {
// if using posgtres, then we're able to re-queue the entire batch of scheduled job for this script_path, so we do it
let _requeued_jobs = sqlx::query_as::<_, QueuedJob>(&format!(
sqlx::query(&format!(
"UPDATE queue
SET running = false
, started_at = null
, scheduled_for = '{estimated_next_schedule_timestamp}'
, logs = CASE WHEN logs IS NULL OR logs = '' THEN '{job_log_event}'::text WHEN logs LIKE '%{job_log_event}' THEN logs ELSE concat(logs, '{job_log_line_break}{job_log_event}'::text) END
WHERE (id = '{job_uuid}') OR (script_path = '{job_script_path}' AND running = false AND scheduled_for <= now())
RETURNING *"
WHERE (id = '{job_uuid}') OR (script_path = '{job_script_path}' AND running = false AND scheduled_for <= now())"
))
.fetch_all(&mut tx)
.await
@@ -1154,7 +1235,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
.map_err(|_| anyhow::anyhow!("Failed to parsed Redis message"))?,
);
let m2 = sqlx::query_as::<_, QueuedJob>(
let m2r = sqlx::query(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
@@ -1166,6 +1247,11 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
.bind(uuid)
.fetch_optional(db)
.await?;
let m2 = if let Some(row) = m2r {
Some(QueuedJob::from_row(&row)?)
} else {
None
};
rsmq.delete_message(&tag.unwrap(), &msg.id)
.await
@@ -1190,7 +1276,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
let tags = config.worker_tags.clone();
drop(config);
let r = if suspend_first {
sqlx::query_as::<_, QueuedJob>("UPDATE queue
sqlx::query("UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
@@ -1210,14 +1296,18 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
} else {
None
};
let r = if let Some(row) = r {
Some(QueuedJob::from_row(&row)?)
} else {
None
};
if r.is_none() {
// #[cfg(feature = "benchmark")]
// let instant = Instant::now();
let tags = WORKER_CONFIG.read().await.worker_tags.clone();
let r = sqlx::query_as::<_, QueuedJob>(
let r = sqlx::query(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
@@ -1239,7 +1329,11 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
// #[cfg(feature = "benchmark")]
// println!("pull query: {:?}", instant.elapsed());
r
if let Some(row) = r {
Some(QueuedJob::from_row(&row)?)
} else {
None
}
} else {
r
}
@@ -1247,13 +1341,19 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
Ok(job)
}
#[derive(FromRow)]
struct ResultR {
result: Option<Json<Box<RawValue>>>,
}
#[async_recursion]
pub async fn get_result_by_id(
db: Pool<Postgres>,
w_id: String,
flow_id: Uuid,
node_id: String,
) -> error::Result<serde_json::Value> {
json_path: Option<String>,
) -> error::Result<Box<RawValue>> {
let flow_job_result = sqlx::query!(
"SELECT leaf_jobs->$1::text as leaf_jobs, parent_job FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
node_id,
@@ -1265,7 +1365,7 @@ pub async fn get_result_by_id(
let flow_job_result = windmill_common::utils::not_found_if_none(
flow_job_result,
"Flow result by id",
"Flow result by id in leaf jobs",
format!("{}, {}", flow_id, node_id),
)?;
@@ -1281,7 +1381,7 @@ pub async fn get_result_by_id(
.await?
.flatten()
.unwrap_or(parent_job);
return get_result_by_id(db, w_id, root_job, node_id).await;
return get_result_by_id(db, w_id, root_job, node_id, json_path).await;
}
let result_id = windmill_common::utils::not_found_if_none(
@@ -1292,27 +1392,37 @@ pub async fn get_result_by_id(
let value = match result_id {
JobResult::ListJob(x) => {
let rows = sqlx::query_scalar!(
let rows = sqlx::query(
"SELECT result FROM completed_job WHERE id = ANY($1) AND workspace_id = $2",
x.as_slice(),
w_id,
)
.bind(x.as_slice())
.bind(w_id)
.fetch_all(&db)
.await?
.into_iter()
.filter_map(|x| x)
.collect::<Vec<serde_json::Value>>();
serde_json::json!(rows)
.filter_map(|x| ResultR::from_row(&x).ok().and_then(|x| x.result))
.collect::<Vec<Json<Box<RawValue>>>>();
to_raw_value(&rows)
}
JobResult::SingleJob(x) => sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
x,
w_id,
JobResult::SingleJob(x) => sqlx::query(
"SELECT result #> $3 as result FROM completed_job WHERE id = $1 AND workspace_id = $2",
)
.bind(x)
.bind(w_id)
.bind(
json_path
.map(|x| x.split(".").map(|x| x.to_string()).collect::<Vec<_>>())
.unwrap_or_default(),
)
.fetch_optional(&db)
.await?
.map(|r| {
ResultR::from_row(&r)
.ok()
.and_then(|x| x.result.map(|x| x.0))
})
.flatten()
.unwrap_or(serde_json::Value::Null),
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null)),
};
Ok(value)
@@ -1366,7 +1476,7 @@ pub async fn get_queued_job<'c>(
w_id: &str,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<Option<QueuedJob>> {
let r = sqlx::query_as::<_, QueuedJob>(
let r = sqlx::query(
"SELECT *
FROM queue WHERE id = $1 AND workspace_id = $2",
)
@@ -1374,7 +1484,11 @@ pub async fn get_queued_job<'c>(
.bind(w_id)
.fetch_optional(&mut **tx)
.await?;
Ok(r)
if let Some(row) = r {
Ok(Some(QueuedJob::from_row(&row)?.to_owned()))
} else {
Ok(None)
}
}
pub enum PushIsolationLevel<'c, R: rsmq_async::RsmqConnection + Send + 'c> {
@@ -1407,13 +1521,144 @@ macro_rules! fetch_scalar_isolated {
};
}
use sqlx::types::JsonRawValue;
#[derive(Serialize)]
pub struct PushArgs<T> {
#[serde(flatten)]
pub extra: HashMap<String, Box<RawValue>>,
#[serde(flatten)]
pub args: Json<T>,
}
#[axum::async_trait]
impl<S> FromRequest<S, axum::body::Body> for PushArgs<HashMap<String, Box<RawValue>>>
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(
req: Request<axum::body::Body>,
_state: &S,
) -> Result<Self, Self::Rejection> {
let (content_type, mut extra, use_raw) = {
let headers_map = req.headers();
let content_type_header = headers_map.get(CONTENT_TYPE);
let content_type = content_type_header.and_then(|value| value.to_str().ok());
(
content_type,
build_extra(&headers_map),
req.uri().query().is_some_and(|x| x.contains("raw=true")),
)
};
if content_type.is_none() || content_type.unwrap().starts_with("application/json") {
let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;
let str = String::from_utf8(bytes.to_vec())
.map_err(|e| Error::BadRequest(format!("invalid utf8: {}", e)).into_response())?;
if use_raw {
extra.insert("raw_string".to_string(), to_raw_value(&str));
}
let wrap_body = str.len() > 0 && str.chars().next().unwrap() != '{';
if wrap_body {
let args = serde_json::from_str::<Option<Box<RawValue>>>(&str)
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)).into_response())?
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
let mut hm = HashMap::new();
hm.insert("body".to_string(), args);
Ok(PushArgs { extra, args: Json(hm) })
} else {
let hm = serde_json::from_str::<Option<HashMap<String, Box<JsonRawValue>>>>(&str)
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)).into_response())?
.unwrap_or_else(HashMap::new);
Ok(PushArgs { extra, args: Json(hm) })
}
} else if content_type
.unwrap()
.starts_with("application/x-www-form-urlencoded")
{
let Form(payload): Form<Option<HashMap<String, Box<RawValue>>>> =
req.extract().await.map_err(IntoResponse::into_response)?;
return Ok(PushArgs {
extra: HashMap::new(),
args: Json(payload.unwrap_or_else(HashMap::new)),
});
} else {
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
}
}
}
lazy_static::lazy_static! {
static ref INCLUDE_HEADERS: Vec<String> = std::env::var("INCLUDE_HEADERS")
.ok().map(|x| x
.split(',')
.map(|s| s.to_string())
.collect()).unwrap_or_default();
}
pub fn build_extra(headers: &HeaderMap) -> HashMap<String, Box<RawValue>> {
let mut args = HashMap::new();
let whitelist = headers
.get("include_header")
.map(|s| {
s.to_str()
.unwrap_or_default()
.split(",")
.map(|s| s.to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default();
whitelist
.iter()
.chain(INCLUDE_HEADERS.iter())
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
to_raw_value(&v.to_str().unwrap().to_string()),
);
}
});
args
}
impl PushArgs<HashMap<String, Box<RawValue>>> {
pub fn empty() -> Self {
PushArgs { extra: HashMap::new(), args: Json(HashMap::new()) }
}
}
pub fn empty_args() -> Box<RawValue> {
return JsonRawValue::from_string("{}".to_string()).unwrap();
}
impl From<HashMap<String, Box<JsonRawValue>>> for PushArgs<HashMap<String, Box<JsonRawValue>>> {
fn from(value: HashMap<String, Box<JsonRawValue>>) -> Self {
PushArgs { extra: HashMap::new(), args: Json(value) }
}
}
// impl<T> From<PushArgsInner<T>> for PushArgs<T> {
// fn from(value: PushArgsInner<T>) -> Self {
// PushArgs::Unwrapped(value)
// }
// }
// #[instrument(level = "trace", skip_all)]
pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection + Send + 'c>(
_db: &Pool<Postgres>,
mut tx: PushIsolationLevel<'c, R>,
workspace_id: &str,
job_payload: JobPayload,
args: serde_json::Map<String, serde_json::Value>,
args: T,
user: &str,
email: &str,
permissioned_as: String,
@@ -1430,8 +1675,6 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
custom_timeout: Option<i32>,
flow_step_id: Option<String>,
) -> Result<(Uuid, QueueTransaction<'c, R>), Error> {
let args_json = serde_json::Value::Object(args);
#[cfg(feature = "enterprise")]
{
let premium_workspace = *CLOUD_HOSTED
@@ -1835,7 +2078,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
script_path.clone(),
raw_code,
raw_lock,
args_json,
Json(args) as Json<T>,
job_kind.clone() as JobKind,
schedule_path,
raw_flow.map(|f| serde_json::json!(f)),
@@ -1852,7 +2095,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
concurrency_time_window_s,
custom_timeout,
flow_step_id,
cache_ttl
cache_ttl,
)
.fetch_one(&mut tx)
.await
+1 -1
View File
@@ -23,7 +23,6 @@ windmill-common = { workspace = true, features = [
"prometheus",
"tracing_init",
] }
windmill-api-client.workspace = true
windmill-parser.workspace = true
windmill-parser-ts.workspace = true
windmill-parser-go.workspace = true
@@ -73,6 +72,7 @@ pem = { workspace = true, optional = true }
urlencoding.workspace = true
nix.workspace = true
bytes.workspace = true
reqwest.workspace = true
[build-dependencies]
deno_fetch.workspace = true
+37 -46
View File
@@ -2,17 +2,18 @@ use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use regex::Regex;
use serde_json::{json, Value};
use serde_json::{json, value::RawValue};
use sqlx::types::Json;
use tokio::process::Command;
use windmill_common::{error::Error, jobs::QueuedJob};
use windmill_common::{error::Error, jobs::QueuedJob, worker::to_raw_value};
const BIN_BASH: &str = "/bin/bash";
const NSJAIL_CONFIG_RUN_BASH_CONTENT: &str = include_str!("../nsjail/run.bash.config.proto");
use crate::{
common::{
get_reserved_variables, handle_child, read_file, read_file_content, set_logs,
start_child_process, transform_json_value, write_file,
build_args_map, get_reserved_variables, handle_child, read_file, read_file_content,
set_logs, start_child_process, write_file,
},
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
TZ_ENV,
@@ -26,6 +27,7 @@ lazy_static::lazy_static! {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_bash_job(
logs: &mut String,
mem_peak: &mut i32,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
@@ -35,7 +37,7 @@ pub async fn handle_bash_job(
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
) -> Result<serde_json::Value, Error> {
) -> Result<Box<RawValue>, Error> {
logs.push_str("\n\n--- BASH CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
write_file(
@@ -48,29 +50,19 @@ pub async fn handle_bash_job(
let mut reserved_variables = get_reserved_variables(job, &token, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let client = client.get_authed().await;
let hm = match transform_json_value(
"args",
&client,
&job.workspace_id,
job.args.clone().unwrap_or_else(|| json!({})),
job,
db,
)
.await?
{
Value::Object(ref hm) => hm.clone(),
_ => serde_json::Map::new(),
let args = build_args_map(job, client, db).await?.map(Json);
let job_args = if args.is_some() {
args.as_ref()
} else {
job.args.as_ref()
};
let args_owned = windmill_parser_bash::parse_bash_sig(&content)?
.args
.iter()
.map(|arg| {
hm.get(&arg.name)
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => serde_json::to_string(v).ok(),
})
job_args
.and_then(|x| x.get(&arg.name).map(|x| raw_to_string(x.get())))
.unwrap_or_else(String::new)
})
.collect::<Vec<String>>();
@@ -122,6 +114,7 @@ pub async fn handle_bash_job(
&job.id,
db,
logs,
mem_peak,
child,
!*DISABLE_NSJAIL,
worker_name,
@@ -143,7 +136,7 @@ pub async fn handle_bash_job(
if let Ok(metadata) = tokio::fs::metadata(&result_out_path).await {
if metadata.len() > 0 {
let result = read_file_content(&result_out_path).await?;
return Ok(json!(result));
return Ok(to_raw_value(&json!(result)));
}
}
@@ -153,12 +146,20 @@ pub async fn handle_bash_job(
.last()
.map(|x| ANSI_ESCAPE_RE.replace_all(x, "").to_string())
.unwrap_or_else(String::new));
Ok(last_line)
Ok(to_raw_value(&last_line))
}
fn raw_to_string(x: &str) -> String {
match serde_json::from_str::<serde_json::Value>(x) {
Ok(serde_json::Value::String(x)) => x,
Ok(x) => serde_json::to_string(&x).unwrap_or_else(|_| String::new()),
_ => String::new(),
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_powershell_job(
logs: &mut String,
mem_peak: &mut i32,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
@@ -168,36 +169,25 @@ pub async fn handle_powershell_job(
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
) -> Result<serde_json::Value, Error> {
) -> Result<Box<RawValue>, Error> {
logs.push_str("\n\n--- POWERSHELL CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
let pwsh_args = {
let client = client.get_authed().await;
let hm = match transform_json_value(
"args",
&client,
&job.workspace_id,
job.args.clone().unwrap_or_else(|| json!({})),
job,
db,
)
.await?
{
Value::Object(ref hm) => hm.clone(),
_ => serde_json::Map::new(),
let args = build_args_map(job, client, db).await?.map(Json);
let job_args = if args.is_some() {
args.as_ref()
} else {
job.args.as_ref()
};
let args_owned = windmill_parser_bash::parse_powershell_sig(&content)?
let args_owned = windmill_parser_bash::parse_bash_sig(&content)?
.args
.iter()
.map(|arg| {
(
arg.name.clone(),
hm.get(&arg.name)
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => serde_json::to_string(v).ok(),
})
job_args
.and_then(|x| x.get(&arg.name).map(|x| raw_to_string(x.get())))
.unwrap_or_else(String::new),
)
})
@@ -262,6 +252,7 @@ pub async fn handle_powershell_job(
&job.id,
db,
logs,
mem_peak,
child,
!*DISABLE_NSJAIL,
worker_name,
@@ -277,5 +268,5 @@ pub async fn handle_powershell_job(
.last()
.map(|x| ANSI_ESCAPE_RE.replace_all(x, "").to_string())
.unwrap_or_else(String::new));
Ok(last_line)
Ok(to_raw_value(&last_line))
}
@@ -1,12 +1,12 @@
use serde_json::{json, Value};
use windmill_common::error::Error;
use serde_json::{json, value::RawValue, Value};
use windmill_common::jobs::QueuedJob;
use windmill_common::{error::Error, worker::to_raw_value};
use windmill_parser_sql::parse_bigquery_sig;
use windmill_queue::HTTP_CLIENT;
use serde::Deserialize;
use crate::{common::transform_json_value, AuthedClient};
use crate::{common::build_args_values, AuthedClientBackgroundTask};
use gcp_auth::{AuthenticationManager, CustomServiceAccount};
@@ -52,19 +52,12 @@ struct BigqueryError {
}
pub async fn do_bigquery(
job: QueuedJob,
client: &AuthedClient,
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), &job, db).await?)
} else {
None
};
let bigquery_args: Value = serde_json::from_value(args.unwrap_or_else(|| json!({})))
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
) -> windmill_common::error::Result<Box<RawValue>> {
let bigquery_args = build_args_values(job, client, db).await?;
let database = bigquery_args
.get("database")
@@ -85,14 +78,6 @@ pub async fn do_bigquery(
.await
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let args = &job
.args
.clone()
.unwrap_or_else(|| json!({}))
.as_object()
.map(|x| x.to_owned())
.unwrap_or_else(|| json!({}).as_object().unwrap().to_owned());
let mut statement_values: Vec<Value> = vec![];
let sig = parse_bigquery_sig(&query)
@@ -102,7 +87,7 @@ pub async fn do_bigquery(
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
let arg_n = arg.clone().name;
let arg_v = args.get(&arg.name).cloned().unwrap_or(json!(""));
let arg_v = bigquery_args.get(&arg.name).cloned().unwrap_or(json!(""));
let bigquery_v = if arg_t.ends_with("[]") {
let base_type = arg_t.strip_suffix("[]").unwrap_or(&arg_t);
json!({
@@ -114,7 +99,7 @@ pub async fn do_bigquery(
}
},
"parameterValue": {
"arrayValues": args
"arrayValues": bigquery_args
.get(&arg.name)
.unwrap_or(&json!([]))
.as_array()
@@ -177,7 +162,7 @@ pub async fn do_bigquery(
}
if result.rows.is_none() || result.rows.as_ref().unwrap().len() == 0 {
return Ok(Value::Array(vec![]));
return Ok(serde_json::from_str("[]").unwrap());
}
if result.schema.is_none() {
@@ -217,9 +202,9 @@ pub async fn do_bigquery(
});
Value::from(row_map)
})
.collect();
.collect::<Vec<_>>();
return Ok(rows);
return Ok(to_raw_value(&rows));
}
Err(e) => match response.json::<BigqueryErrorResponse>().await {
Ok(bq_err) => return Err(Error::ExecutionErr(bq_err.error.message)),
+22 -6
View File
@@ -9,6 +9,7 @@ use anyhow::Context;
use base64::Engine;
use itertools::Itertools;
use regex::Regex;
use serde_json::value::RawValue;
use uuid::Uuid;
#[cfg(feature = "enterprise")]
@@ -63,6 +64,7 @@ lazy_static::lazy_static! {
pub async fn gen_lockfile(
logs: &mut String,
mem_peak: &mut i32,
job_id: &Uuid,
w_id: &str,
db: &sqlx::Pool<sqlx::Postgres>,
@@ -109,6 +111,7 @@ pub async fn gen_lockfile(
job_id,
db,
logs,
mem_peak,
child_process,
false,
worker_name,
@@ -152,6 +155,7 @@ pub async fn gen_lockfile(
install_lockfile(
logs,
mem_peak,
job_id,
w_id,
db,
@@ -187,6 +191,7 @@ pub async fn gen_lockfile(
pub async fn install_lockfile(
logs: &mut String,
mem_peak: &mut i32,
job_id: &Uuid,
w_id: &str,
db: &sqlx::Pool<sqlx::Postgres>,
@@ -208,6 +213,7 @@ pub async fn install_lockfile(
job_id,
db,
logs,
mem_peak,
child_process,
false,
worker_name,
@@ -241,6 +247,7 @@ pub fn get_trusted_deps(code: &str) -> Vec<String> {
pub async fn handle_bun_job(
requirements_o: Option<String>,
logs: &mut String,
mem_peak: &mut i32,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
@@ -250,7 +257,7 @@ pub async fn handle_bun_job(
worker_name: &str,
envs: HashMap<String, String>,
shared_mount: &str,
) -> error::Result<serde_json::Value> {
) -> error::Result<Box<RawValue>> {
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
@@ -283,6 +290,7 @@ pub async fn handle_bun_job(
install_lockfile(
logs,
mem_peak,
&job.id,
&job.workspace_id,
db,
@@ -305,6 +313,7 @@ pub async fn handle_bun_job(
set_logs(&logs, &job.id, &db).await;
let _ = gen_lockfile(
logs,
mem_peak,
&job.id,
&job.workspace_id,
db,
@@ -385,12 +394,12 @@ run().catch(async (e) => {{
};
let reserved_variables_args_out_f = async {
let client = client.get_authed().await;
let args_and_out_f = async {
create_args_and_out_file(&client, job, job_dir, db).await?;
Ok(()) as Result<()>
};
let reserved_variables_f = async {
let client = client.get_authed().await;
let vars = get_reserved_variables(job, &client.token, db).await?;
Ok(vars) as Result<HashMap<String, String>>
};
@@ -491,6 +500,7 @@ plugin(p)
&job.id,
db,
logs,
mem_peak,
child,
false,
worker_name,
@@ -528,6 +538,9 @@ pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String
return bun_envs;
}
#[cfg(feature = "enterprise")]
use std::sync::Arc;
#[cfg(feature = "enterprise")]
pub async fn start_worker(
requirements_o: Option<String>,
@@ -541,7 +554,7 @@ pub async fn start_worker(
script_path: &str,
token: &str,
job_completed_tx: Sender<JobCompleted>,
mut jobs_rx: Receiver<QueuedJob>,
mut jobs_rx: Receiver<Arc<QueuedJob>>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> Result<()> {
use std::task::Poll;
@@ -549,6 +562,7 @@ pub async fn start_worker(
use futures::{future, Future};
let mut logs = "".to_string();
let mut mem_peak: i32 = 0;
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url).await;
@@ -591,6 +605,7 @@ pub async fn start_worker(
}
install_lockfile(
&mut logs,
&mut mem_peak,
&Uuid::nil(),
&w_id,
db,
@@ -604,6 +619,7 @@ pub async fn start_worker(
logs.push_str("\n\n--- BUN INSTALL ---\n");
let _ = gen_lockfile(
&mut logs,
&mut mem_peak,
&Uuid::nil(),
&w_id,
db,
@@ -807,8 +823,8 @@ plugin(p)
tracing::debug!("processed job");
let result = serde_json::from_str(&line).expect("json is ok");
let job: QueuedJob = jobs.pop_front().expect("pop");
job_completed_tx.send(JobCompleted { job , result, logs: "".to_string(), success: true, cached_res_path: None, token: token.to_string() }).await.unwrap();
let job: Arc<QueuedJob> = jobs.pop_front().expect("pop");
job_completed_tx.send(JobCompleted { job , result, logs: "".to_string(), mem_peak: 0, success: true, cached_res_path: None, token: token.to_string() }).await.unwrap();
} else {
tracing::info!("dedicated worker process exited");
break;
@@ -820,7 +836,7 @@ plugin(p)
tracing::debug!("received job");
jobs.push_back(job.clone());
// write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({"x": job.id}))).expect("serialize")).await?;
write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({}))).expect("serialize")).await?;
write_stdin(&mut stdin, &serde_json::to_string(&job.args).expect("serialize")).await?;
stdin.flush().await.context("stdin flush")?;
} else {
tracing::debug!("job channel closed");
+202 -62
View File
@@ -1,11 +1,14 @@
use async_recursion::async_recursion;
use itertools::Itertools;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
use regex::Regex;
use serde::Serialize;
use serde_json::value::RawValue;
use serde_json::{json, Value};
use sqlx::{Pool, Postgres};
use tokio::process::Command;
use tokio::{fs::File, io::AsyncReadExt};
use windmill_api_client::{types::CreateResource, Client};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::{
error::{self, Error},
@@ -41,24 +44,60 @@ use futures::{
};
use crate::{
AuthedClient, MAX_RESULT_SIZE, MAX_WAIT_FOR_SIGTERM, ROOT_CACHE_DIR, TIMEOUT_DURATION,
WHITELIST_ENVS,
AuthedClient, AuthedClientBackgroundTask, MAX_RESULT_SIZE, MAX_WAIT_FOR_SIGTERM,
ROOT_CACHE_DIR, TIMEOUT_DURATION, WHITELIST_ENVS,
};
pub async fn build_args_map<'a>(
job: &'a QueuedJob,
client: &AuthedClientBackgroundTask,
db: &Pool<Postgres>,
) -> error::Result<Option<HashMap<String, Box<RawValue>>>> {
if let Some(args) = &job.args {
return transform_json(client, &job.workspace_id, &args.0, &job, db).await;
}
return Ok(None);
}
pub async fn build_args_values(
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
db: &Pool<Postgres>,
) -> error::Result<HashMap<String, serde_json::Value>> {
if let Some(args) = &job.args {
transform_json_as_values(client, &job.workspace_id, &args.0, &job, db).await
} else {
Ok(HashMap::new())
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn create_args_and_out_file(
client: &AuthedClient,
client: &AuthedClientBackgroundTask,
job: &QueuedJob,
job_dir: &str,
db: &Pool<Postgres>,
) -> Result<(), Error> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), job, db).await?)
if let Some(args) = &job.args {
if let Some(x) = transform_json(client, &job.workspace_id, &args.0, job, db).await? {
write_file(
job_dir,
"args.json",
&serde_json::to_string(&x).unwrap_or_else(|_| "{}".to_string()),
)
.await?;
} else {
write_file(
job_dir,
"args.json",
&serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string()),
)
.await?;
}
} else {
None
write_file(job_dir, "args.json", "{}").await?;
};
let ser_args = serde_json::to_string(&args).map_err(|e| Error::ExecutionErr(e.to_string()))?;
write_file(job_dir, "args.json", &ser_args).await?;
write_file(job_dir, "result.json", "").await?;
Ok(())
}
@@ -81,6 +120,80 @@ pub async fn write_file_binary(dir: &str, path: &str, content: &[u8]) -> error::
Ok(file)
}
lazy_static::lazy_static! {
static ref RE_RES_VAR: Regex = Regex::new(r#"\$(?:var|res)\:"#).unwrap();
}
pub async fn transform_json<'a>(
client: &AuthedClientBackgroundTask,
workspace: &str,
vs: &'a HashMap<String, Box<RawValue>>,
job: &QueuedJob,
db: &Pool<Postgres>,
) -> error::Result<Option<HashMap<String, Box<RawValue>>>> {
let mut has_match = false;
for (_, v) in vs {
let inner_vs = v.get();
if (*RE_RES_VAR).is_match(inner_vs) {
has_match = true;
break;
}
}
if !has_match {
return Ok(None);
}
let mut r = HashMap::new();
for (k, v) in vs {
let inner_vs = v.get();
if (*RE_RES_VAR).is_match(inner_vs) {
let value = serde_json::from_str(inner_vs).map_err(|e| {
error::Error::InternalErr(format!("Error while parsing inner arg: {e}"))
})?;
let transformed =
transform_json_value(&k, &client.get_authed().await, workspace, value, job, db)
.await?;
let as_raw = serde_json::from_value(transformed).map_err(|e| {
error::Error::InternalErr(format!("Error while parsing inner arg: {e}"))
})?;
r.insert(k.to_string(), as_raw);
} else {
r.insert(k.to_string(), v.to_owned());
}
}
Ok(Some(r))
}
pub async fn transform_json_as_values<'a>(
client: &AuthedClientBackgroundTask,
workspace: &str,
vs: &'a HashMap<String, Box<RawValue>>,
job: &QueuedJob,
db: &Pool<Postgres>,
) -> error::Result<HashMap<String, serde_json::Value>> {
let mut r: HashMap<String, serde_json::Value> = HashMap::new();
for (k, v) in vs {
let inner_vs = v.get();
if (*RE_RES_VAR).is_match(inner_vs) {
let value = serde_json::from_str(inner_vs).map_err(|e| {
error::Error::InternalErr(format!("Error while parsing inner arg: {e}"))
})?;
let transformed =
transform_json_value(&k, &client.get_authed().await, workspace, value, job, db)
.await?;
let as_raw = serde_json::from_value(transformed).map_err(|e| {
error::Error::InternalErr(format!("Error while parsing inner arg: {e}"))
})?;
r.insert(k.to_string(), as_raw);
} else {
r.insert(
k.to_string(),
serde_json::from_str(v.get()).unwrap_or_else(|_| serde_json::Value::Null),
);
}
}
Ok(r)
}
#[async_recursion]
pub async fn transform_json_value(
name: &str,
@@ -94,11 +207,10 @@ pub async fn transform_json_value(
Value::String(y) if y.starts_with("$var:") => {
let path = y.strip_prefix("$var:").unwrap();
client
.get_client()
.get_variable_value(workspace, path)
.get_variable_value(path)
.await
.map(|x| json!(x))
.map_err(|_| Error::NotFound(format!("Variable {path} not found for `{name}`")))
.map(|v| json!(v.into_inner()))
}
Value::String(y) if y.starts_with("$res:") => {
let path = y.strip_prefix("$res:").unwrap();
@@ -107,12 +219,13 @@ pub async fn transform_json_value(
"Argument `{name}` is an invalid resource path: {path}",
)));
}
Ok(client
.get_client()
.get_resource_value_interpolated(workspace, path, Some(&job.id))
client
.get_resource_value_interpolated::<serde_json::Value>(
path,
Some(job.id.to_string()),
)
.await
.map_err(|_| Error::NotFound(format!("Resource {path} not found for `{name}`")))?
.into_inner())
.map_err(|_| Error::NotFound(format!("Resource {path} not found for `{name}`")))
}
Value::String(y) if y.starts_with("$") => {
let flow_path = if let Some(uuid) = job.parent_job {
@@ -167,30 +280,31 @@ pub async fn read_file_content(path: &str) -> error::Result<String> {
file.read_to_string(&mut content).await?;
Ok(content)
}
pub async fn read_file(path: &str) -> error::Result<serde_json::Value> {
// tracing::error!("START1");
// let start = Instant::now();
let r = if *CLOUD_HOSTED {
let content = read_file_content(path).await?;
if content.len() > MAX_RESULT_SIZE {
return Err(error::Error::ExecutionErr("Result is too large for the cloud app (limit 2MB).
If using this script as part of the flow, use the shared folder to pass heavy data between steps.".to_owned()));
};
serde_json::from_str(&content)
.map_err(|e| error::Error::ExecutionErr(format!("Error parsing result: {e}")))
} else {
let file = std::fs::File::open(path)
.map_err(|e| error::Error::ExecutionErr(format!("Error opening file {path}: {e}")))?;
let reader = std::io::BufReader::new(file);
serde_json::from_reader(reader)
.map_err(|e| error::Error::ExecutionErr(format!("Error parsing result: {e}")))
};
// tracing::error!("{:?}", start.elapsed());
return r;
pub async fn read_file_bytes(path: &str) -> error::Result<Vec<u8>> {
let mut file = File::open(path).await?;
let mut content = Vec::new();
file.read_to_end(&mut content).await?;
Ok(content)
}
pub async fn read_result(job_dir: &str) -> error::Result<serde_json::Value> {
//this skips more steps than from_str at the cost of being unsafe. The source must ALWAUS gemerate valid json or this can cause UB in the worst case
pub fn unsafe_raw(json: String) -> Box<RawValue> {
unsafe { std::mem::transmute::<Box<str>, Box<RawValue>>(json.into()) }
}
pub async fn read_file(path: &str) -> error::Result<Box<RawValue>> {
let content = read_file_content(path).await?;
if *CLOUD_HOSTED && content.len() > MAX_RESULT_SIZE {
return Err(error::Error::ExecutionErr("Result is too large for the cloud app (limit 2MB).
If using this script as part of the flow, use the shared folder to pass heavy data between steps.".to_owned()));
};
let r = unsafe_raw(content);
return Ok(r);
}
pub async fn read_result(job_dir: &str) -> error::Result<Box<RawValue>> {
return read_file(&format!("{job_dir}/result.json")).await;
}
@@ -302,6 +416,7 @@ pub async fn handle_child(
job_id: &Uuid,
db: &Pool<Postgres>,
logs: &mut String,
mem_peak: &mut i32,
mut child: Child,
nsjail: bool,
worker_name: &str,
@@ -343,6 +458,7 @@ pub async fn handle_child(
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut i = 0;
loop {
tokio::select!(
_ = rx.recv() => break,
@@ -358,10 +474,12 @@ pub async fn handle_child(
.await
.expect("update worker ping");
}
let mem_peak = get_mem_peak(pid, nsjail).await;
tracing::info!("{job_id} in {} still running. mem peak: {}kB", _w_id, mem_peak);
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
if sqlx::query_scalar!("UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled", mem_peak, job_id)
let current_mem = get_mem_peak(pid, nsjail).await;
if current_mem > *mem_peak {
*mem_peak = current_mem
}
tracing::info!("{job_id} in {_w_id} still running. mem: {current_mem}kB, peak mem: {mem_peak}kB");
if sqlx::query_scalar!("UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled", *mem_peak, job_id)
.fetch_optional(&db)
.await
.map(|v| Some(true) == v)
@@ -570,7 +688,7 @@ pub async fn handle_child(
let (wait_result, _) = tokio::join!(wait_on_child, lines);
tracing::info!(%job_id, "child process '{child_name}' for {job_id} took {}ms", start.elapsed().as_millis());
tracing::info!(%job_id, "child process '{child_name}' for {job_id} took {}ms, mem_peak: {:?}", start.elapsed().as_millis(), mem_peak);
match wait_result {
_ if *too_many_logs.borrow() => Err(Error::ExecutionErr(format!(
"logs or result reached limit. (current max size: {MAX_RESULT_SIZE} characters)"
@@ -657,27 +775,49 @@ fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize) {
}
}
pub fn hash_args(v: &serde_json::Value) -> i64 {
let mut dh = DefaultHasher::new();
serde_json::to_string(v).unwrap().hash(&mut dh);
dh.finish() as i64
pub fn hash_args(v: &Option<sqlx::types::Json<HashMap<String, Box<RawValue>>>>) -> i64 {
if let Some(vs) = v {
let mut dh = DefaultHasher::new();
let hm = &vs.0;
for k in hm.keys().sorted() {
k.hash(&mut dh);
hm.get(k).unwrap().get().hash(&mut dh);
}
dh.finish() as i64
} else {
0
}
}
pub async fn save_in_cache(client: &AuthedClient, job: &QueuedJob, cached_path: String, r: &Value) {
let client: &Client = client.get_client();
#[derive(Serialize)]
struct StoreCachedResource<'a> {
expire: i64,
value: &'a RawValue,
}
pub async fn save_in_cache<'a>(
db: &Pool<Postgres>,
job: &QueuedJob,
cached_path: String,
r: &'a RawValue,
) {
let expire = chrono::Utc::now().timestamp() + job.cache_ttl.unwrap() as i64;
let cr = &CreateResource {
path: cached_path,
description: None,
resource_type: "cache".to_string(),
value: serde_json::json!({
"value": r,
"expire": expire
}),
};
if let Err(e) = client
.create_resource(&job.workspace_id, Some(true), cr)
.await
let store_cache_resource = StoreCachedResource { expire, value: r };
let raw_json = sqlx::types::Json(store_cache_resource);
if let Err(e) = sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, resource_type)
VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path)
DO UPDATE SET value = $3",
job.workspace_id,
cached_path,
raw_json as sqlx::types::Json<StoreCachedResource>,
"cache"
)
.execute(db)
.await
{
tracing::error!("Error creating cache resource {e}")
}
+9 -4
View File
@@ -1,6 +1,7 @@
use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use serde_json::value::RawValue;
use uuid::Uuid;
use crate::{
@@ -73,6 +74,7 @@ pub async fn generate_deno_lock(
job_id: &Uuid,
code: &str,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
w_id: &str,
@@ -117,6 +119,7 @@ pub async fn generate_deno_lock(
job_id,
db,
logs,
mem_peak,
child_process,
false,
worker_name,
@@ -138,6 +141,7 @@ pub async fn generate_deno_lock(
pub async fn handle_deno_job(
requirements_o: Option<String>,
logs: &mut String,
mem_peak: &mut i32,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
@@ -146,7 +150,7 @@ pub async fn handle_deno_job(
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
) -> error::Result<serde_json::Value> {
) -> error::Result<Box<RawValue>> {
// let mut start = Instant::now();
logs.push_str("\n\n--- DENO CODE EXECUTION ---\n");
@@ -247,18 +251,18 @@ run().catch(async (e) => {{
};
let reserved_variables_args_out_f = async {
let client = client.get_authed().await;
let args_and_out_f = async {
create_args_and_out_file(&client, job, job_dir, db).await?;
Ok(()) as Result<()>
};
let reserved_variables_f = async {
let client = client.get_authed().await;
let mut vars = get_reserved_variables(job, &client.token, db).await?;
vars.insert("RUST_LOG".to_string(), "info".to_string());
Ok(vars) as Result<HashMap<String, String>>
Ok((vars, client.token)) as Result<(HashMap<String, String>, String)>
};
let (_, reserved_variables) = tokio::try_join!(args_and_out_f, reserved_variables_f)?;
Ok((reserved_variables, client.token)) as error::Result<(HashMap<String, String>, String)>
Ok(reserved_variables) as error::Result<(HashMap<String, String>, String)>
};
let (_, (reserved_variables, token), _, _, _) = tokio::try_join!(
@@ -325,6 +329,7 @@ run().catch(async (e) => {{
&job.id,
db,
logs,
mem_peak,
child,
false,
worker_name,
+11 -3
View File
@@ -1,6 +1,7 @@
use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use serde_json::value::RawValue;
use tokio::{
fs::{DirBuilder, File},
io::AsyncReadExt,
@@ -33,6 +34,7 @@ lazy_static::lazy_static! {
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_go_job(
logs: &mut String,
mem_peak: &mut i32,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
@@ -43,7 +45,7 @@ pub async fn handle_go_job(
base_internal_url: &str,
worker_name: &str,
envs: HashMap<String, String>,
) -> Result<serde_json::Value, Error> {
) -> Result<Box<RawValue>, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
let bin_path = if let Some(requirements) = requirements_o.clone() {
@@ -68,8 +70,6 @@ pub async fn handle_go_job(
(false, false)
};
let client = &client.get_authed().await;
if !bin_exists {
logs.push_str("\n\n--- GO DEPENDENCIES SETUP ---\n");
set_logs(logs, &job.id, db).await;
@@ -78,6 +78,7 @@ pub async fn handle_go_job(
&job.id,
inner_content,
logs,
mem_peak,
job_dir,
db,
true,
@@ -193,6 +194,7 @@ func Run(req Req) (interface{{}}, error){{
&job.id,
db,
logs,
mem_peak,
build_go_process,
false,
worker_name,
@@ -216,6 +218,8 @@ func Run(req Req) (interface{{}}, error){{
create_args_and_out_file(client, job, job_dir, db).await?;
}
let client = &client.get_authed().await;
let reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let child = if !*DISABLE_NSJAIL {
@@ -270,6 +274,7 @@ func Run(req Req) (interface{{}}, error){{
&job.id,
db,
logs,
mem_peak,
child,
!*DISABLE_NSJAIL,
worker_name,
@@ -307,6 +312,7 @@ pub async fn install_go_dependencies(
job_id: &Uuid,
code: &str,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
non_dep_job: bool,
@@ -329,6 +335,7 @@ pub async fn install_go_dependencies(
job_id,
db,
logs,
mem_peak,
child_process,
false,
worker_name,
@@ -392,6 +399,7 @@ pub async fn install_go_dependencies(
job_id,
db,
logs,
mem_peak,
child_process,
false,
worker_name,
+21 -24
View File
@@ -1,13 +1,14 @@
use std::collections::HashMap;
use serde_json::{json, Value};
use serde_json::{json, value::RawValue};
use sqlx::types::Json;
use windmill_common::error::Error;
use windmill_common::jobs::QueuedJob;
use windmill_queue::HTTP_CLIENT;
use serde::Deserialize;
use crate::{common::transform_json_value, AuthedClient};
use crate::{common::build_args_map, AuthedClientBackgroundTask};
#[derive(Deserialize)]
struct GraphqlApi {
@@ -18,7 +19,7 @@ struct GraphqlApi {
#[derive(Deserialize)]
struct GraphqlResponse {
data: Option<Value>,
data: Option<Box<RawValue>>,
errors: Option<Vec<GraphqlError>>,
}
@@ -28,34 +29,28 @@ struct GraphqlError {
}
pub async fn do_graphql(
job: QueuedJob,
client: &AuthedClient,
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), &job, db).await?)
) -> windmill_common::error::Result<Box<RawValue>> {
let args = build_args_map(job, client, db).await?.map(Json);
let job_args = if args.is_some() {
args.as_ref()
} else {
None
job.args.as_ref()
};
let graphql_args: serde_json::Value = serde_json::from_value(args.unwrap_or_else(|| json!({})))
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let api =
serde_json::from_value::<GraphqlApi>(graphql_args.get("api").unwrap_or(&json!({})).clone())
.map_err(|e: serde_json::Error| Error::ExecutionErr(e.to_string()))?;
let args = &job
.args
.clone()
.unwrap_or_else(|| json!({}))
.as_object()
.map(|x| x.to_owned())
.unwrap_or_else(|| json!({}).as_object().unwrap().to_owned());
let api = if let Some(db) = job_args.as_ref().and_then(|x| x.get("api")) {
serde_json::from_str::<GraphqlApi>(db.get())
.map_err(|e| Error::ExecutionErr(e.to_string()))?
} else {
return Err(Error::BadRequest("Missing api argument".to_string()));
};
let mut request = HTTP_CLIENT.post(api.base_url).json(&json!({
"query": query,
"variables": args
"variables": job_args
}));
if let Some(token) = &api.bearer_token {
@@ -89,5 +84,7 @@ pub async fn do_graphql(
}
// And then check that we got back the same string we sent over.
return Ok(result.data.unwrap_or(json!({})));
return Ok(result
.data
.unwrap_or_else(|| serde_json::from_str("{}").unwrap()));
}
+216 -73
View File
@@ -20,7 +20,8 @@ use deno_web::{BlobStore, TimersPermission};
use itertools::Itertools;
use lazy_static::lazy_static;
use regex::Regex;
use serde_json::Value;
use serde_json::value::RawValue;
use sqlx::types::Json;
use tokio::{
sync::{mpsc, oneshot},
time::timeout,
@@ -28,7 +29,7 @@ use tokio::{
use uuid::Uuid;
use windmill_common::{error::Error, flow_status::JobResult};
use crate::AuthedClient;
use crate::{common::unsafe_raw, AuthedClient};
#[derive(Debug, Clone)]
pub struct IdContext {
@@ -68,12 +69,56 @@ impl TimersPermission for PermissionsContainer {
}
pub struct OptAuthedClient(Option<AuthedClient>);
pub async fn eval_timeout(
expr: String,
env: Vec<(String, serde_json::Value)>,
transform_context: HashMap<String, Arc<Box<RawValue>>>,
flow_input: Option<Arc<HashMap<String, Box<RawValue>>>>,
authed_client: Option<&AuthedClient>,
by_id: Option<IdContext>,
) -> anyhow::Result<serde_json::Value> {
) -> anyhow::Result<Box<RawValue>> {
let expr = expr.trim().to_string();
for (k,v) in transform_context.iter() {
if k == &expr {
return Ok(v.as_ref().clone())
}
}
if expr.starts_with("flow_input.") {
if let Some(ref flow_input) = flow_input {
for (k,v) in flow_input.iter() {
if &format!("flow_input.{k}") == &expr {
// tracing::error!("FLOW_INPUT");
return Ok(v.clone())
}
}
}
}
let p_id = by_id.as_ref().map(|x| format!("results.{}", x.previous_id));
if p_id.is_some() && transform_context.contains_key("previous_result") && &expr == p_id.as_ref().unwrap() {
// tracing::error!("PREVIOUS_RESULT");
return Ok(transform_context.get("previous_result").unwrap().as_ref().clone())
}
if by_id.is_some() && authed_client.is_some() {
if let Some(x) = RE_FULL.captures(&expr).and_then(|x| x.get(1).map(|y| y.as_str())) {
// tracing::error!("{:?}", x.split(".").collect::<Vec<_>>());
let arr = x.split(".").collect::<Vec<_>>();
let mut iter = arr.iter();
iter.next();
if let Some(id) = iter.next() {
let path = iter.join(".");
let query = if path.is_empty() { None } else { Some(path) };
return authed_client.unwrap().get_result_by_id(&by_id.as_ref().unwrap().flow_job.to_string(), id, query).await;
}
}
}
let expr2 = expr.clone();
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
let has_client = authed_client.is_some();
@@ -81,7 +126,7 @@ pub async fn eval_timeout(
timeout(
std::time::Duration::from_millis(10000),
tokio::task::spawn_blocking(move || {
let mut ops = vec![];
let mut ops = vec![op_get_context::DECL];
if authed_client.is_some() {
ops.extend([
@@ -107,11 +152,35 @@ pub async fn eval_timeout(
..Default::default()
};
let mut context_keys = transform_context
.keys()
.filter(|x|
expr.contains(&x.to_string())
)
.map(|x| x.clone())
.collect_vec();
if !context_keys.contains(&"previous_result".to_string()) && (p_id.is_some() && expr.contains(p_id.as_ref().unwrap())) || expr.contains("error") {
context_keys.push("previous_result".to_string());
}
let has_flow_input = expr.contains("flow_input");
if has_flow_input {
context_keys.push("flow_input".to_string())
}
let mut js_runtime = JsRuntime::new(options);
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put(OptAuthedClient(authed_client.clone()));
op_state.put(TransformContext {
flow_input: if has_flow_input { flow_input } else { None },
envs: transform_context
.into_iter()
.filter(|(a, _)| context_keys.contains(a))
.collect(),
})
}
sender
@@ -123,15 +192,21 @@ pub async fn eval_timeout(
.build()?;
// pretty frail but this it to make the expr more user friendly and not require the user to write await
let expr = ["variable", "step", "resource", "result_by_id"]
let expr = ["variable", "resource"]
.into_iter()
.fold(expr, replace_with_await);
let expr = replace_with_await_result(expr);
let r = runtime.block_on(eval(&mut js_runtime, &expr, env, by_id, has_client))?;
let r = runtime.block_on(eval(
&mut js_runtime,
&expr,
context_keys,
by_id,
has_client,
))?;
Ok(r) as anyhow::Result<Value>
Ok(r) as anyhow::Result<Box<RawValue>>
}),
)
.await
@@ -155,7 +230,9 @@ fn replace_with_await(expr: String, fn_name: &str) -> String {
s
}
lazy_static! {
static ref RE: Regex = Regex::new("(?m)(?P<r>results.([a-z]|[A-Z]|_|[1-9])+)").unwrap();
static ref RE: Regex = Regex::new(r"(?m)(?P<r>results\.(?:[a-z]|[A-Z]|_|[1-9])+)").unwrap();
static ref RE_FULL: Regex = Regex::new(r"(?m)^results((?:\.(?:(?:[a-z]|[A-Z]|_|[1-9])+))+)$").unwrap();
}
fn replace_with_await_result(expr: String) -> String {
@@ -184,10 +261,10 @@ fn add_closing_bracket(s: &str) -> String {
async fn eval(
context: &mut JsRuntime,
expr: &str,
env: Vec<(String, serde_json::Value)>,
transform_context: Vec<String>,
by_id: Option<IdContext>,
has_client: bool,
) -> anyhow::Result<serde_json::Value> {
) -> anyhow::Result<Box<RawValue>> {
let (api_code, by_id_code) = if has_client {
let by_id_code = if let Some(by_id) = by_id {
format!(
@@ -205,12 +282,12 @@ async function result_by_id(node_id) {{
}}
}} else {{
let flow_job_id = "{}";
return await Deno.core.opAsync("op_get_id", [flow_job_id, node_id]);
return JSON.parse(await Deno.core.opAsync("op_get_id", [flow_job_id, node_id]));
}}
}}
async function get_result(id) {{
return await Deno.core.opAsync("op_get_result", [id]);
return JSON.parse(await Deno.core.opAsync("op_get_result", [id]));
}}
const results = new Proxy({{}}, {{
get: function(target, name, receiver) {{
@@ -261,24 +338,30 @@ async function resource(path) {{
};
let code = format!(
r#"
function get_from_env(name) {{
return JSON.parse(Deno.core.ops.op_get_context([name]));
}}
{api_code}
{}
{}
{by_id_code}
{HAS_CYCLE}
((async () => {{
{f};
}})()).then((r) => hasCycle(r) ? 'cycle detected' : r)
}})()).then((r) => hasCycle(r) ? 'cycle detected' : r).then(JSON.stringify)
"#,
env.into_iter()
.map(|(a, b)| {
format!(
"let {a} = {};\n",
serde_json::to_string(&b)
.unwrap_or_else(|_| "\"error serializing value\"".to_string())
)
})
transform_context
.iter()
.map(|a| { format!("let {a} = get_from_env(\"{a}\");\n",) })
.join(""),
if expr.contains("error") && transform_context.contains(&"previous_result".to_string()) {
"let error = previous_result.error"
} else {
""
},
);
let global = context.execute_script("<anon>", code.into())?;
let global = context.resolve_value(global).await?;
@@ -286,7 +369,8 @@ async function resource(path) {{
let local = v8::Local::new(scope, global);
// Deserialize a `v8` object into a Rust type using `serde_v8`,
// in this case deserialize to a JSON `Value`.
Ok(serde_v8::from_v8::<serde_json::Value>(scope, local)?)
let r = serde_v8::from_v8::<String>(scope, local)?;
Ok(unsafe_raw(r))
}
const HAS_CYCLE: &str = r#"
@@ -341,11 +425,7 @@ async fn op_variable(
let path = &args[0];
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.get_variable_value(&client.workspace, path)
.await?;
Ok(result.into_inner())
Ok(client.get_variable_value(path).await?)
} else {
anyhow::bail!("No client found in op state");
}
@@ -355,16 +435,15 @@ async fn op_variable(
async fn op_get_result(
op_state: Rc<RefCell<OpState>>,
args: Vec<String>,
) -> Result<serde_json::Value, anyhow::Error> {
) -> Result<String, anyhow::Error> {
let id = &args[0];
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.get_completed_job_result(&client.workspace, &id.parse()?)
.get_completed_job_result::<Box<RawValue>>(id, None)
.await?
.clone();
Ok(serde_json::json!(result))
Ok(result.get().to_string())
} else {
anyhow::bail!("No client found in op state");
}
@@ -374,18 +453,16 @@ async fn op_get_result(
async fn op_get_id(
op_state: Rc<RefCell<OpState>>,
args: Vec<String>,
) -> Result<Option<serde_json::Value>, anyhow::Error> {
) -> Result<Option<String>, anyhow::Error> {
let flow_job_id = &args[0];
let node_id = &args[1];
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.result_by_id(&client.workspace, flow_job_id, node_id)
.await
.map_or(None, |e| Some(e.into_inner()));
Ok(result)
.get_result_by_id::<Option<Box<RawValue>>>(flow_job_id, node_id, None)
.await?;
Ok(result.map(|x| x.get().to_string()))
} else {
anyhow::bail!("No client found in op state");
}
@@ -400,16 +477,36 @@ async fn op_resource(
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
if let Some(client) = client {
let result = client
.get_client()
.get_resource_value_interpolated(&client.workspace, path, None)
.await?;
Ok(result.into_inner())
client.get_resource_value_interpolated(path, None).await
} else {
anyhow::bail!("No client found in op state");
}
}
pub struct TransformContext {
pub envs: HashMap<String, Arc<Box<RawValue>>>,
pub flow_input: Option<Arc<HashMap<String, Box<RawValue>>>>,
}
#[op]
fn op_get_context(op_state: Rc<RefCell<OpState>>, args: Vec<String>) -> String {
let id = &args[0];
let ops = op_state.borrow();
let client = ops.borrow::<TransformContext>();
if id == "flow_input" {
return client
.flow_input
.as_ref()
.and_then(|x| serde_json::to_string(&x).ok())
.unwrap_or_else(|| "null".to_string());
}
return client
.envs
.get(id)
.and_then(|x| serde_json::to_string(x).ok())
.unwrap_or_else(String::new);
}
pub fn transpile_ts(expr: String) -> anyhow::Result<String> {
let parsed = deno_ast::parse_module(ParseParams {
specifier: "eval.ts".to_string(),
@@ -425,7 +522,7 @@ pub fn transpile_ts(expr: String) -> anyhow::Result<String> {
static RUNTIME_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/FETCH_SNAPSHOT.bin"));
pub struct MainArgs {
args: Vec<serde_json::Value>,
args: Vec<Option<Box<RawValue>>>,
}
pub struct LogString {
@@ -435,10 +532,14 @@ pub struct LogString {
pub async fn eval_fetch_timeout(
ts_expr: String,
js_expr: String,
args: serde_json::Map<String, Value>,
) -> anyhow::Result<(serde_json::Value, String)> {
args: Option<&Json<HashMap<String, Box<RawValue>>>>,
) -> anyhow::Result<(Box<RawValue>, String)> {
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
let ts_expr2 = ts_expr.clone();
let parsed_args = windmill_parser_ts::parse_deno_signature(&ts_expr, true)?.args;
let spread = parsed_args.into_iter().map(|x| args.as_ref().and_then(|args| args.0.get(&x.name).map(|x| x.clone()))).collect::<Vec<_>>();
timeout(
std::time::Duration::from_secs(100),
tokio::task::spawn_blocking(move || {
@@ -497,9 +598,7 @@ pub async fn eval_fetch_timeout(
return y*2;
});
let parsed_args = windmill_parser_ts::parse_deno_signature(&ts_expr, true)?.args;
let spread = parsed_args.into_iter().map(|x| args.get(&x.name).map(|x| x.clone()).unwrap_or(serde_json::Value::Null)).collect::<Vec<_>>();
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
@@ -527,7 +626,7 @@ pub async fn eval_fetch_timeout(
let r = runtime.block_on(future)?;
// tracing::info!("total: {:?}", instant.elapsed());
(r as anyhow::Result<Value>).map(|x| (x, js_runtime.op_state().borrow().borrow::<LogString>().s.clone()))
(r as anyhow::Result<Box<RawValue>>).map(|x| (x, js_runtime.op_state().borrow().borrow::<LogString>().s.clone()))
}),
)
.await
@@ -541,7 +640,7 @@ pub async fn eval_fetch_timeout(
})??
}
async fn eval_fetch(js_runtime: &mut JsRuntime, expr: &str) -> anyhow::Result<serde_json::Value> {
async fn eval_fetch(js_runtime: &mut JsRuntime, expr: &str) -> anyhow::Result<Box<RawValue>> {
let _ = js_runtime
.load_side_module(
&deno_core::resolve_url("file:///eval.ts")?,
@@ -552,8 +651,8 @@ async fn eval_fetch(js_runtime: &mut JsRuntime, expr: &str) -> anyhow::Result<se
let global = js_runtime.execute_script(
"<anon>",
r#"
let args = Deno.core.ops.op_get_static_args()
import("file:///eval.ts").then((module) => module.main(...args))
let args = Deno.core.ops.op_get_static_args().map(JSON.parse)
import("file:///eval.ts").then((module) => module.main(...args)).then(JSON.stringify)
"#
.to_string()
.into(),
@@ -564,12 +663,13 @@ import("file:///eval.ts").then((module) => module.main(...args))
let local = v8::Local::new(scope, global);
// Deserialize a `v8` object into a Rust type using `serde_v8`,
// in this case deserialize to a JSON `Value`.
Ok(serde_v8::from_v8::<serde_json::Value>(scope, local)?)
let r = serde_v8::from_v8::<String>(scope, local)?;
Ok(unsafe_raw(r))
}
#[op]
fn op_get_static_args(op_state: Rc<RefCell<OpState>>) -> Vec<serde_json::Value> {
return op_state.borrow().borrow::<MainArgs>().args.clone();
fn op_get_static_args(op_state: Rc<RefCell<OpState>>) -> Vec<Option<String>> {
return op_state.borrow().borrow::<MainArgs>().args.iter().map(|x| x.as_ref().map(|y| y.get().to_string())).collect_vec();
}
#[op]
@@ -585,21 +685,52 @@ fn op_log(op_state: Rc<RefCell<OpState>>, args: Vec<String>) {
mod tests {
use serde_json::json;
use windmill_common::worker::to_raw_value;
// Note this useful idiom: importing names from outer (for mod tests) scope.
use super::*;
#[tokio::test]
async fn test_eval() -> anyhow::Result<()> {
let env = vec![
("params".to_string(), json!({"test": 2})),
("value".to_string(), json!({"test": 2})),
];
let mut env = HashMap::new();
env.insert(
"params".to_string(),
Arc::new(to_raw_value(&json!({"test": 2}))),
);
env.insert(
"value".to_string(),
Arc::new(to_raw_value(&json!({"test": 2}))),
);
let code = "value.test + params.test";
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let res = eval(&mut runtime, code, env, None, false).await?;
assert_eq!(res, json!(4));
let ops = vec![op_get_context::DECL];
let ext = Extension { name: "js_eval", ops: ops.into(), ..Default::default() };
let exts = vec![ext];
let options = RuntimeOptions {
extensions: exts,
..Default::default()
};
let mut runtime = JsRuntime::new(options);
{
let op_state = runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put(TransformContext { flow_input: None, envs: env.clone() })
}
let res = eval(
&mut runtime,
code,
vec!["params".to_string(), "value".to_string()],
None,
false,
)
.await?;
assert_eq!(res.get(), "4");
Ok(())
}
@@ -612,20 +743,33 @@ multiline template`";
let mut runtime = JsRuntime::new(RuntimeOptions::default());
let res = eval(&mut runtime, code, env, None, false).await?;
assert_eq!(res, json!("my 5\nmultiline template"));
assert_eq!(res.get(), "\"my 5\\nmultiline template\"");
Ok(())
}
#[tokio::test]
async fn test_eval_timeout() -> anyhow::Result<()> {
let env = vec![
("params".to_string(), json!({"test": 2})),
("value".to_string(), json!({"test": 2})),
];
let mut env = HashMap::new();
env.insert(
"params".to_string(),
Arc::new(to_raw_value(&json!({"test": 2}))),
);
env.insert(
"value".to_string(),
Arc::new(to_raw_value(&json!({"test": 2}))),
);
let code = r#"params.test"#;
let res = eval_timeout(code.to_string(), env, None, None).await?;
assert_eq!(res, json!(2));
let mut js_runtime = JsRuntime::new(RuntimeOptions::default());
{
let op_state = js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put(TransformContext { flow_input: None, envs: env.clone() })
}
let res = eval_timeout(code.to_string(), env, None, None, None).await?;
assert_eq!(res.get(), "2");
Ok(())
}
@@ -633,9 +777,8 @@ multiline template`";
async fn test_eval_fetch_timeout() -> anyhow::Result<()> {
let code = r#"export async function main() { return "" }"#;
let res =
eval_fetch_timeout(code.to_string(), code.to_string(), serde_json::Map::new()).await?;
assert_eq!(res.0, "".to_string());
let res = eval_fetch_timeout(code.to_string(), code.to_string(), None).await?;
assert_eq!(res.0.get(), "\"\"");
Ok(())
}
}
+28 -24
View File
@@ -3,14 +3,15 @@ use mysql_async::{
consts::ColumnType, prelude::*, FromValueError, OptsBuilder, Params, Row, SslOpts,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::{json, value::RawValue, Value};
use sqlx::types::Json;
use windmill_common::{
error::{to_anyhow, Error},
jobs::QueuedJob,
};
use windmill_parser_sql::parse_mysql_sig;
use crate::{common::transform_json_value, AuthedClient};
use crate::{common::build_args_map, AuthedClientBackgroundTask};
#[derive(Deserialize)]
struct MysqlDatabase {
@@ -23,23 +24,24 @@ struct MysqlDatabase {
}
pub async fn do_mysql(
job: QueuedJob,
client: &AuthedClient,
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), &job, db).await?)
) -> windmill_common::error::Result<Box<RawValue>> {
let args = build_args_map(job, client, db).await?.map(Json);
let job_args = if args.is_some() {
args.as_ref()
} else {
None
job.args.as_ref()
};
let mysql_args: serde_json::Value = serde_json::from_value(args.unwrap_or_else(|| json!({})))
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let database = serde_json::from_value::<MysqlDatabase>(
mysql_args.get("database").unwrap_or(&json!({})).clone(),
)
.map_err(|e: serde_json::Error| Error::ExecutionErr(e.to_string()))?;
let database = if let Some(db) = job_args.and_then(|x| x.get("database")) {
serde_json::from_str::<MysqlDatabase>(db.get())
.map_err(|e| Error::ExecutionErr(e.to_string()))?
} else {
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let opts = OptsBuilder::default()
.db_name(Some(database.database))
@@ -61,13 +63,6 @@ pub async fn do_mysql(
let pool = mysql_async::Pool::new(opts);
let mut conn = pool.get_conn().await.map_err(to_anyhow)?;
let args = &job
.args
.clone()
.unwrap_or_else(|| json!({}))
.as_object()
.map(|x| x.to_owned())
.unwrap_or_else(|| json!({}).as_object().unwrap().to_owned());
let mut statement_values: Vec<mysql_async::Value> = vec![];
let sig = parse_mysql_sig(&query)
@@ -76,9 +71,18 @@ pub async fn do_mysql(
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "text".to_string());
let mysql_v = match args.get(arg.name.as_str()).unwrap_or_else(|| &json!(null)) {
let mysql_v = match job
.args
.as_ref()
.and_then(|x| {
x.get(arg.name.as_str())
.map(|x| serde_json::from_str::<serde_json::Value>(x.get()).ok())
})
.flatten()
.unwrap_or_else(|| json!(null))
{
Value::Null => mysql_async::Value::NULL,
Value::Bool(b) => mysql_async::Value::Int(if *b { 1 } else { 0 }),
Value::Bool(b) => mysql_async::Value::Int(if b { 1 } else { 0 }),
Value::String(s) => mysql_async::Value::Bytes(s.as_bytes().to_vec()),
Value::Number(n)
if n.is_i64() && (arg_t == "int" || arg_t == "integer" || arg_t == "smallint") =>
@@ -115,7 +119,7 @@ pub async fn do_mysql(
pool.disconnect().await.map_err(to_anyhow)?;
// And then check that we got back the same string we sent over.
return Ok(json!(rows));
return Ok(windmill_common::worker::to_raw_value(&json!(rows)));
}
fn convert_row_to_value(row: Row) -> serde_json::Value {
+21 -24
View File
@@ -5,6 +5,7 @@ use native_tls::{Certificate, TlsConnector};
use postgres_native_tls::MakeTlsConnector;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use serde::Deserialize;
use serde_json::value::RawValue;
use serde_json::Map;
use serde_json::{json, Value};
use tokio_postgres::types::IsNull;
@@ -18,11 +19,12 @@ use tokio_postgres::{
};
use uuid::Uuid;
use windmill_common::error::{self, Error};
use windmill_common::worker::to_raw_value;
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser_sql::parse_pgsql_sig;
use crate::common::transform_json_value;
use crate::AuthedClient;
use crate::common::build_args_values;
use crate::AuthedClientBackgroundTask;
use bytes::BytesMut;
use urlencoding::encode;
@@ -38,22 +40,19 @@ struct PgDatabase {
}
pub async fn do_postgresql(
job: QueuedJob,
client: &AuthedClient,
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
) -> error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), &job, db).await?)
} else {
None
};
) -> error::Result<Box<RawValue>> {
let pg_args = build_args_values(job, client, db).await?;
let pg_args: serde_json::Value = serde_json::from_value(args.unwrap_or_else(|| json!({})))
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let database =
serde_json::from_value::<PgDatabase>(pg_args.get("database").unwrap_or(&json!({})).clone())
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let database = if let Some(db) = pg_args.get("database") {
serde_json::from_value::<PgDatabase>(db.clone())
.map_err(|e| Error::ExecutionErr(e.to_string()))?
} else {
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let sslmode = database.sslmode.unwrap_or("prefer".to_string());
let database_string = format!(
"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode={sslmode}",
@@ -107,13 +106,6 @@ pub async fn do_postgresql(
(client, handle)
};
let args = &job
.args
.clone()
.unwrap_or_else(|| json!({}))
.as_object()
.map(|x| x.to_owned())
.unwrap_or_else(|| json!({}).as_object().unwrap().to_owned());
let mut statement_values: Vec<serde_json::Value> = vec![];
let sig = parse_pgsql_sig(&query)
@@ -121,7 +113,12 @@ pub async fn do_postgresql(
.args;
for arg in &sig {
statement_values.push(args.get(&arg.name).unwrap_or(&json!(null)).clone());
statement_values.push(
pg_args
.get(&arg.name)
.map(|x| x.to_owned())
.unwrap_or_else(|| serde_json::Value::Null),
);
}
let query_params = statement_values
@@ -152,7 +149,7 @@ pub async fn do_postgresql(
handle.abort();
// And then check that we got back the same string we sent over.
return Ok(result);
return Ok(to_raw_value(&result));
}
#[derive(Debug)]
+11 -2
View File
@@ -2,6 +2,7 @@ use std::{collections::HashMap, process::Stdio};
use itertools::Itertools;
use regex::Regex;
use serde_json::value::RawValue;
use sqlx::{Pool, Postgres};
use tokio::{
fs::{metadata, DirBuilder, File},
@@ -64,6 +65,7 @@ pub async fn pip_compile(
job_id: &Uuid,
requirements: &str,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &Pool<Postgres>,
worker_name: &str,
@@ -128,6 +130,7 @@ pub async fn pip_compile(
job_id,
db,
logs,
mem_peak,
child_process,
false,
worker_name,
@@ -164,13 +167,14 @@ pub async fn handle_python_job(
worker_name: &str,
job: &QueuedJob,
logs: &mut String,
mem_peak: &mut i32,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
inner_content: &String,
shared_mount: &str,
base_internal_url: &str,
envs: HashMap<String, String>,
) -> windmill_common::error::Result<serde_json::Value> {
) -> windmill_common::error::Result<Box<RawValue>> {
create_dependencies_dir(job_dir).await;
let mut additional_python_paths: Vec<String> = WORKER_CONFIG
@@ -199,6 +203,7 @@ pub async fn handle_python_job(
&job.id,
&requirements,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -221,6 +226,7 @@ pub async fn handle_python_job(
&job.id,
&job.workspace_id,
logs,
mem_peak,
db,
worker_name,
job_dir,
@@ -284,7 +290,6 @@ pub async fn handle_python_job(
})
.collect::<Vec<String>>()
.join("");
let client = client.get_authed().await;
create_args_and_out_file(&client, job, job_dir, db).await?;
let import_loader = if relative_imports {
@@ -385,6 +390,7 @@ except Exception as e:
);
write_file(job_dir, "wrapper.py", &wrapper_content).await?;
let client = client.get_authed().await;
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let additional_python_paths_folders = additional_python_paths.iter().join(":");
if !*DISABLE_NSJAIL {
@@ -471,6 +477,7 @@ mount {{
&job.id,
db,
logs,
mem_peak,
child,
!*DISABLE_NSJAIL,
worker_name,
@@ -488,6 +495,7 @@ pub async fn handle_python_reqs(
job_id: &Uuid,
w_id: &str,
logs: &mut String,
mem_peak: &mut i32,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
job_dir: &str,
@@ -637,6 +645,7 @@ pub async fn handle_python_reqs(
&job_id,
db,
logs,
mem_peak,
child,
false,
worker_name,
@@ -2,17 +2,17 @@ use base64::{engine, Engine as _};
use core::fmt::Write;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use pem;
use serde_json::{json, Value};
use serde_json::{json, value::RawValue, Value};
use sha2::{Digest, Sha256};
use windmill_common::error::Error;
use windmill_common::jobs::QueuedJob;
use windmill_common::{error::Error, worker::to_raw_value};
use windmill_parser_sql::parse_snowflake_sig;
use windmill_queue::HTTP_CLIENT;
use serde::{Deserialize, Serialize};
use crate::{common::transform_json_value, AuthedClient};
use crate::{common::build_args_values, AuthedClientBackgroundTask};
#[derive(Serialize)]
struct Claims {
@@ -61,19 +61,12 @@ struct SnowflakeError {
}
pub async fn do_snowflake(
job: QueuedJob,
client: &AuthedClient,
job: &QueuedJob,
client: &AuthedClientBackgroundTask,
query: &str,
db: &sqlx::Pool<sqlx::Postgres>,
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), &job, db).await?)
} else {
None
};
let snowflake_args: Value = serde_json::from_value(args.unwrap_or_else(|| json!({})))
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
) -> windmill_common::error::Result<Box<RawValue>> {
let snowflake_args = build_args_values(job, client, db).await?;
let database = serde_json::from_value::<SnowflakeDatabase>(
snowflake_args.get("database").unwrap_or(&json!({})).clone(),
@@ -107,14 +100,6 @@ pub async fn do_snowflake(
let token = encode(&Header::new(Algorithm::RS256), &claims, &private_key)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let args = &job
.args
.clone()
.unwrap_or_else(|| json!({}))
.as_object()
.map(|x| x.to_owned())
.unwrap_or_else(|| json!({}).as_object().unwrap().to_owned());
let mut bindings = serde_json::Map::new();
let sig = parse_snowflake_sig(&query)
.map_err(|x| Error::ExecutionErr(x.to_string()))?
@@ -123,7 +108,7 @@ pub async fn do_snowflake(
let mut i = 1;
for arg in &sig {
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
let arg_v = args.get(&arg.name).cloned().unwrap_or(json!(""));
let arg_v = snowflake_args.get(&arg.name).cloned().unwrap_or(json!(""));
let snowflake_v = convert_typ_val(arg_t, arg_v);
bindings.insert(i.to_string(), snowflake_v);
@@ -187,20 +172,24 @@ pub async fn do_snowflake(
));
}
let rows = result
.data
.iter()
.map(|row| {
let mut row_map = serde_json::Map::new();
row.iter()
.zip(result.resultSetMetaData.rowType.iter())
.for_each(|(val, row_type)| {
row_map
.insert(row_type.name.clone(), parse_val(&val, &row_type.r#type));
});
Value::from(row_map)
})
.collect();
let rows = to_raw_value(
&result
.data
.iter()
.map(|row| {
let mut row_map = serde_json::Map::new();
row.iter()
.zip(result.resultSetMetaData.rowType.iter())
.for_each(|(val, row_type)| {
row_map.insert(
row_type.name.clone(),
parse_val(&val, &row_type.r#type),
);
});
Value::from(row_map)
})
.collect::<Vec<_>>(),
);
Ok(rows)
}
+283 -145
View File
@@ -9,11 +9,10 @@
use anyhow::Result;
use const_format::concatcp;
use itertools::Itertools;
use once_cell::sync::OnceCell;
use prometheus::core::{AtomicU64, GenericCounter};
#[cfg(feature = "benchmark")]
use serde::Serialize;
use sqlx::{Pool, Postgres};
use reqwest::Response;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sqlx::{types::Json, Pool, Postgres};
use std::{
collections::HashMap,
sync::{
@@ -22,7 +21,6 @@ use std::{
},
time::Duration,
};
use windmill_api_client::Client;
use uuid::Uuid;
use windmill_common::{
@@ -32,14 +30,15 @@ use windmill_common::{
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::SUPERADMIN_SECRET_EMAIL,
utils::{rd_string, StripPath},
worker::{update_ping, CLOUD_HOSTED, WORKER_CONFIG},
worker::{to_raw_value, to_raw_value_owned, update_ping, CLOUD_HOSTED, WORKER_CONFIG},
DB, IS_READY, METRICS_ENABLED,
};
use windmill_queue::{
canceled_job_to_result, get_queued_job, pull, push, PushIsolationLevel, HTTP_CLIENT,
canceled_job_to_result, empty_args, get_queued_job, pull, push, PushArgs, PushIsolationLevel,
WrappedError, HTTP_CLIENT,
};
use serde_json::{json, Value};
use serde_json::{json, value::RawValue, Value};
use tokio::{
fs::{symlink, DirBuilder},
@@ -71,7 +70,7 @@ use windmill_queue::{add_completed_job, add_completed_job_error};
use crate::{
bash_executor::{handle_bash_job, handle_powershell_job, ANSI_ESCAPE_RE},
bun_executor::{gen_lockfile, get_trusted_deps, handle_bun_job},
common::{hash_args, read_result, save_in_cache, transform_json_value, write_file},
common::{build_args_map, hash_args, read_result, save_in_cache, write_file},
deno_executor::{generate_deno_lock, handle_deno_job},
go_executor::{handle_go_job, install_go_dependencies},
graphql_executor::do_graphql,
@@ -98,16 +97,17 @@ pub async fn create_token_for_owner_in_bg(
if job.workspace_id != "" {
let mut locked = rw_lock.clone().write_owned().await;
let db = db.clone();
let job = job.clone();
let w_id = job.workspace_id.clone();
let owner = job.permissioned_as.clone();
let email = job.email.clone();
tokio::spawn(async move {
let job = job.clone();
let token = create_token_for_owner(
&db.clone(),
&job.workspace_id,
&job.permissioned_as,
&w_id,
&owner,
"ephemeral-script",
*SCRIPT_TOKEN_EXPIRY,
&job.email,
&email,
)
.await
.expect("could not create job token");
@@ -293,7 +293,6 @@ pub struct AuthedClientBackgroundTask {
pub base_internal_url: String,
pub workspace: String,
pub token: Arc<RwLock<String>>,
pub client: OnceCell<Client>,
}
impl AuthedClientBackgroundTask {
@@ -302,7 +301,6 @@ impl AuthedClientBackgroundTask {
base_internal_url: self.base_internal_url.clone(),
workspace: self.workspace.clone(),
token: self.get_token().await,
client: self.client.clone(),
};
}
pub async fn get_token(&self) -> String {
@@ -314,14 +312,110 @@ pub struct AuthedClient {
pub base_internal_url: String,
pub workspace: String,
pub token: String,
pub client: OnceCell<Client>,
}
impl AuthedClient {
pub fn get_client(&self) -> &Client {
return self.client.get_or_init(|| {
windmill_api_client::create_client(&self.base_internal_url, self.token.clone())
});
pub async fn get(&self, url: &str, query: Vec<(&str, String)>) -> anyhow::Result<Response> {
Ok(HTTP_CLIENT
.get(url)
.query(&query)
.header(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
)
.header(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.token))?,
)
.send()
.await?)
}
pub async fn get_resource_value<T: DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/resources/get_value/{}",
self.base_internal_url, self.workspace, path
);
let response = self.get(&url, vec![]).await?;
match response.status().as_u16() {
200u16 => Ok(response.json::<T>().await?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_variable_value(&self, path: &str) -> anyhow::Result<String> {
let url = format!(
"{}/api/w/{}/variables/get_value/{}",
self.base_internal_url, self.workspace, path
);
let response = self.get(&url, vec![]).await?;
match response.status().as_u16() {
200u16 => Ok(response.json::<String>().await?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_resource_value_interpolated<T: DeserializeOwned>(
&self,
path: &str,
job_id: Option<String>,
) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/resources/get_value_interpolated/{}",
self.base_internal_url, self.workspace, path
);
let mut query = Vec::with_capacity(1usize);
if let Some(v) = &job_id {
query.push(("job_id", v.to_string()));
}
let response = self.get(&url, query).await?;
match response.status().as_u16() {
200u16 => Ok(response.json::<T>().await?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_completed_job_result<T: DeserializeOwned>(
&self,
path: &str,
json_path: Option<String>,
) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/jobs_u/completed/get_result/{}",
self.base_internal_url, self.workspace, path
);
let query = if let Some(json_path) = json_path {
vec![("json_path", json_path)]
} else {
vec![]
};
let response = self.get(&url, query).await?;
match response.status().as_u16() {
200u16 => Ok(response.json::<T>().await?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
pub async fn get_result_by_id<T: DeserializeOwned>(
&self,
flow_job_id: &str,
node_id: &str,
json_path: Option<String>,
) -> anyhow::Result<T> {
let url = format!(
"{}/api/w/{}/jobs/result_by_id/{}/{}",
self.base_internal_url, self.workspace, flow_job_id, node_id
);
let query = if let Some(json_path) = json_path {
vec![("json_path", json_path)]
} else {
vec![]
};
let response = self.get(&url, query).await?;
match response.status().as_u16() {
200u16 => Ok(response.json::<T>().await?),
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
}
}
}
@@ -357,14 +451,12 @@ async fn handle_receive_completed_job<
let metrics = build_language_metrics(&worker_execution_failed.clone(), &jc.job.language);
let token = jc.token.clone();
let workspace = jc.job.workspace_id.clone();
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
workspace,
token,
client: OnceCell::new(),
};
let client =
AuthedClient { base_internal_url: base_internal_url.to_string(), workspace, token };
let job = jc.job.clone();
let mem_peak = jc.mem_peak.clone();
if let Err(err) = process_completed_job(
&jc,
jc,
&client,
&db,
&worker_dir,
@@ -377,7 +469,8 @@ async fn handle_receive_completed_job<
handle_job_error(
&db,
&client,
&jc.job,
job.as_ref(),
mem_peak,
err,
metrics,
false,
@@ -795,7 +888,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
#[cfg(feature = "enterprise")]
{
let (dedicated_worker_tx, dedicated_worker_rx) =
mpsc::channel::<QueuedJob>(MAX_BUFFERED_DEDICATED_JOBS);
mpsc::channel::<Arc<QueuedJob>>(MAX_BUFFERED_DEDICATED_JOBS);
let mut killpill_rx = killpill_rx.resubscribe();
let db = db.clone();
let worker_dir = worker_dir.clone();
@@ -891,7 +984,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
(Some(dedicated_worker_tx), Some(handle))
}
} else {
(None, None) as (Option<Sender<QueuedJob>>, Option<JoinHandle<()>>)
(None, None) as (Option<Sender<Arc<QueuedJob>>>, Option<JoinHandle<()>>)
};
#[cfg(feature = "benchmark")]
@@ -1080,7 +1173,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
#[cfg(feature = "benchmark")]
let send_start = Instant::now();
if let Err(e) = dedicated_worker_tx.send(job.clone()).await {
if let Err(e) = dedicated_worker_tx.send(Arc::new(job)).await {
tracing::info!("failed to send jobs to dedicated workers. Likely dedicated worker has been shut down. This is normal: {e:?}");
}
@@ -1097,10 +1190,11 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
job_completed_tx
.send(JobCompleted {
job,
job: Arc::new(job),
success: true,
result: json!({}),
result: empty_args(),
logs: String::new(),
mem_peak: 0,
cached_res_path: None,
token: "".to_string(),
})
@@ -1173,11 +1267,11 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
base_internal_url: base_internal_url.to_string(),
token,
workspace: job.workspace_id.to_string(),
client: OnceCell::new(),
};
let arc_job = Arc::new(job);
if let Some(err) = handle_queued_job(
job.clone(),
arc_job.clone(),
db,
&authed_client,
&worker_name,
@@ -1196,7 +1290,8 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
handle_job_error(
db,
&authed_client.get_authed().await,
&job,
arc_job.as_ref(),
0,
err,
metrics,
false,
@@ -1213,7 +1308,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
.expect("no timer found")
.inc_by(duration);
if !*KEEP_JOB_DIR && !(job.is_flow() && same_worker) {
if !*KEEP_JOB_DIR && !(arc_job.is_flow() && same_worker) {
let _ = tokio::fs::remove_dir_all(job_dir).await;
}
}
@@ -1303,7 +1398,7 @@ async fn queue_init_bash_maybe<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
concurrency_time_window_s: None,
cache_ttl: None,
}),
serde_json::Map::new(),
PushArgs::empty(),
worker_name,
"worker@windmill.dev",
SUPERADMIN_SECRET_EMAIL.to_string(),
@@ -1345,7 +1440,7 @@ async fn queue_init_bash_maybe<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
// ) -> error::Result<()> {
pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
JobCompleted { job, result, logs, success, cached_res_path, .. }: &JobCompleted,
JobCompleted { job, result, logs, mem_peak, success, cached_res_path, .. }: JobCompleted,
client: &AuthedClient,
db: &DB,
worker_dir: &str,
@@ -1353,18 +1448,19 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
same_worker_tx: Sender<Uuid>,
rsmq: Option<R>,
) -> windmill_common::error::Result<()> {
if *success {
if success {
// println!("bef completed job{:?}", SystemTime::now());
if let Some(cached_path) = cached_res_path {
save_in_cache(&client, &job, cached_path.to_string(), &result).await;
save_in_cache(db, &job, cached_path.to_string(), &result).await;
}
add_completed_job(
db,
&job,
true,
false,
result,
logs.to_string(),
Json(&result),
logs,
mem_peak.to_owned(),
rsmq.clone(),
)
.await?;
@@ -1377,7 +1473,7 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
&job.id,
&job.workspace_id,
true,
result.clone(),
&result,
metrics.clone(),
false,
same_worker_tx.clone(),
@@ -1393,7 +1489,8 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
db,
&job,
logs.to_string(),
&result,
mem_peak.to_owned(),
result,
metrics.clone(),
rsmq.clone(),
)
@@ -1407,7 +1504,7 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
&job.id,
&job.workspace_id,
false,
result,
&serde_json::value::to_raw_value(&result).unwrap(),
metrics,
false,
same_worker_tx,
@@ -1461,6 +1558,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
db: &Pool<Postgres>,
client: &AuthedClient,
job: &QueuedJob,
mem_peak: i32,
err: Error,
metrics: Option<Metrics>,
unrecoverable: bool,
@@ -1479,6 +1577,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
db,
job,
format!("Unexpected error during job execution:\n{err}"),
mem_peak,
&err,
metrics.clone(),
rsmq_2,
@@ -1497,6 +1596,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
(job.id, Uuid::nil(), Some(update_job_future))
};
let wrapped_error = WrappedError { error: json!(err) };
let updated_flow = update_flow_status_after_job_completion(
db,
client,
@@ -1504,7 +1604,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
&job_status_to_update,
&job.workspace_id,
false,
json!({ "error": err }),
&serde_json::value::to_raw_value(&wrapped_error).unwrap(),
metrics.clone(),
unrecoverable,
same_worker_tx,
@@ -1525,6 +1625,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
db,
&parent_job,
format!("Unexpected error during flow job error handling:\n{err}"),
mem_peak,
&e,
metrics.clone(),
rsmq,
@@ -1545,15 +1646,18 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
tracing::error!(job_id = %job.id, "error handling job: {err:?} {} {} {}", job.id, job.workspace_id, job.created_by);
}
fn extract_error_value(log_lines: &str, i: i32) -> serde_json::Value {
return json!({"message": format!("ExitCode: {i}, last log lines:\n{}", ANSI_ESCAPE_RE.replace_all(log_lines.trim(), "").to_string()), "name": "ExecutionErr"});
fn extract_error_value(log_lines: &str, i: i32) -> Box<RawValue> {
return to_raw_value(
&json!({"message": format!("ExitCode: {i}, last log lines:\n{}", ANSI_ESCAPE_RE.replace_all(log_lines.trim(), "").to_string()), "name": "ExecutionErr"}),
);
}
#[derive(Debug, Clone)]
pub struct JobCompleted {
pub job: QueuedJob,
pub result: serde_json::Value,
pub job: Arc<QueuedJob>,
pub result: Box<RawValue>,
pub logs: String,
pub mem_peak: i32,
pub success: bool,
pub cached_res_path: Option<String>,
pub token: String,
@@ -1582,32 +1686,38 @@ pub async fn get_content(job: &QueuedJob, db: &Pool<Postgres>) -> Result<String,
}
async fn do_nativets(
job: QueuedJob,
job: &QueuedJob,
logs: String,
client: &AuthedClient,
client: &AuthedClientBackgroundTask,
code: String,
db: &Pool<Postgres>,
) -> windmill_common::error::Result<(serde_json::Value, String)> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone(), &job, db).await?)
) -> windmill_common::error::Result<(Box<RawValue>, String)> {
let args = build_args_map(job, client, db).await?.map(Json);
let job_args = if args.is_some() {
args.as_ref()
} else {
None
job.args.as_ref()
};
let args = args
.as_ref()
.map(|x| x.clone())
.unwrap_or_else(|| json!({}))
.as_object()
.unwrap()
.clone();
let result = eval_fetch_timeout(code.clone(), transpile_ts(code)?, args).await?;
let result = eval_fetch_timeout(code.clone(), transpile_ts(code)?, job_args).await?;
Ok((result.0, [logs, result.1].join("\n\n")))
}
#[derive(Deserialize)]
struct CachedResource {
expire: i64,
value: Box<RawValue>,
}
#[derive(Deserialize, Serialize, Default)]
pub struct PreviousResult<'a> {
#[serde(borrow)]
pub previous_result: Option<&'a RawValue>,
}
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
job: QueuedJob,
job: Arc<QueuedJob>,
db: &DB,
client: &AuthedClientBackgroundTask,
worker_name: &str,
@@ -1619,10 +1729,10 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
job_completed_tx: Sender<JobCompleted>,
) -> windmill_common::error::Result<()> {
if job.canceled {
return Err(Error::JsonErr(canceled_job_to_result(&job)))?;
return Err(Error::JsonErr(canceled_job_to_result(&job)));
}
if let Some(e) = job.pre_run_error {
return Err(Error::ExecutionErr(e));
if let Some(e) = &job.pre_run_error {
return Err(Error::ExecutionErr(e.to_string()));
}
let step = if job.is_flow_step {
@@ -1641,7 +1751,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
};
let cached_res_path = if job.cache_ttl.is_some() {
let args_hash = hash_args(&job.args.clone().unwrap_or_else(|| json!({})));
let args_hash = hash_args(&job.args);
if job.is_flow_step {
let flow_path = sqlx::query_scalar!(
"SELECT script_path FROM queue WHERE id = $1",
@@ -1665,54 +1775,42 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
if let Some(cached_res_path) = cached_res_path.clone() {
let authed_client = client.get_authed().await;
let client: &Client = authed_client.get_client();
let resource = client
.get_resource_value(&job.workspace_id, &cached_res_path)
let resource = authed_client
.get_resource_value::<CachedResource>(&cached_res_path)
.await;
if let Ok(resource) = resource {
let v = resource.into_inner();
if let Some(o) = v.as_object() {
let expire = o.get("expire");
if expire.is_some()
&& expire
.unwrap()
.as_i64()
.map(|x| x > chrono::Utc::now().timestamp())
.unwrap_or(false)
{
let result = v
.get("value")
.map(|x| x.to_owned())
.unwrap_or_else(|| json!({}));
let logs = "Job skipped because args & path found in cache and not expired"
.to_string();
let expire = resource.expire;
if expire > chrono::Utc::now().timestamp() {
let result = resource.value;
let logs =
"Job skipped because args & path found in cache and not expired".to_string();
job_completed_tx
.send(JobCompleted {
job,
result,
logs,
success: true,
cached_res_path: None,
token: authed_client.token,
})
.await
.expect("send job completed");
job_completed_tx
.send(JobCompleted {
job: job,
result,
logs,
mem_peak: 0,
success: true,
cached_res_path: None,
token: authed_client.token,
})
.await
.expect("send job completed");
return Ok(());
}
return Ok(());
}
}
};
match job.job_kind {
JobKind::FlowPreview | JobKind::Flow => {
let args = job.args.clone().unwrap_or(Value::Null);
let args = job.get_args();
handle_flow(
&job,
db,
&client.get_authed().await,
args,
to_raw_value(&args),
same_worker_tx,
worker_dir,
rsmq,
@@ -1721,6 +1819,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
}
_ => {
let mut logs = "".to_string();
let mut mem_peak: i32 = 0;
// println!("handle queue {:?}", SystemTime::now());
if let Some(log_str) = &job.logs {
logs.push_str(&log_str);
@@ -1745,6 +1844,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
handle_dependency_job(
&job,
&mut logs,
&mut mem_peak,
job_dir,
db,
worker_name,
@@ -1757,6 +1857,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
JobKind::FlowDependencies => handle_flow_dependency_job(
&job,
&mut logs,
&mut mem_peak,
job_dir,
db,
worker_name,
@@ -1765,10 +1866,11 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
&client.get_token().await,
)
.await
.map(|()| Value::Null),
.map(|()| serde_json::from_str("{}").unwrap()),
JobKind::AppDependencies => handle_app_dependency_job(
&job,
&mut logs,
&mut mem_peak,
job_dir,
db,
worker_name,
@@ -1777,23 +1879,23 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
&client.get_token().await,
)
.await
.map(|()| Value::Null),
JobKind::Identity => match job.args.clone() {
Some(Value::Object(args))
if args.len() == 1 && args.contains_key("previous_result") =>
{
Ok(args.get("previous_result").unwrap().clone())
}
args @ _ => Ok(args.unwrap_or_else(|| Value::Null)),
},
.map(|()| serde_json::from_str("{}").unwrap()),
JobKind::Identity => Ok(job
.args
.as_ref()
.map(|x| x.get("previous_result"))
.flatten()
.map(|x| x.to_owned())
.unwrap_or_else(|| serde_json::from_str("{}").unwrap())),
_ => {
handle_code_execution_job(
&job,
job.as_ref(),
db,
client,
job_dir,
worker_dir,
&mut logs,
&mut mem_peak,
base_internal_url,
worker_name,
)
@@ -1802,7 +1904,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
};
//it's a test job, no need to update the db
if job.workspace_id == "" {
if job.as_ref().workspace_id == "" {
return Ok(());
}
process_result(
@@ -1811,6 +1913,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
job_dir,
job_completed_tx,
logs,
mem_peak,
cached_res_path,
client.get_token().await,
)
@@ -1821,11 +1924,12 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
}
async fn process_result(
job: QueuedJob,
result: error::Result<serde_json::Value>,
job: Arc<QueuedJob>,
result: error::Result<Box<RawValue>>,
job_dir: &str,
job_completed_tx: Sender<JobCompleted>,
logs: String,
mem_peak: i32,
cached_res_path: Option<String>,
token: String,
) -> error::Result<()> {
@@ -1833,9 +1937,10 @@ async fn process_result(
Ok(r) => {
job_completed_tx
.send(JobCompleted {
job,
job: job,
result: r,
logs,
mem_peak,
success: true,
cached_res_path,
token: token,
@@ -1848,7 +1953,7 @@ async fn process_result(
Error::ExitStatus(i) => {
let res = read_result(job_dir).await.ok();
if res.is_some() && res.clone().unwrap().is_object() {
if res.is_some() {
res.unwrap()
} else {
let last_10_log_lines = logs
@@ -1866,17 +1971,18 @@ async fn process_result(
extract_error_value(log_lines, i)
}
}
err @ _ => {
json!({"message": format!("error during execution of the script:\n{}", err), "name": "ExecutionErr"})
}
err @ _ => to_raw_value(
&json!({"message": format!("error during execution of the script:\n{}", err), "name": "ExecutionErr"}),
),
};
// in the happy path and if job not a flow step, we can delegate updating the completed job in the background
job_completed_tx
.send(JobCompleted {
job,
result: error_value,
job: job,
result: to_raw_value(&error_value),
logs: logs,
mem_peak,
success: false,
cached_res_path,
token: token,
@@ -1927,9 +2033,10 @@ async fn handle_code_execution_job(
job_dir: &str,
worker_dir: &str,
logs: &mut String,
mem_peak: &mut i32,
base_internal_url: &str,
worker_name: &str,
) -> error::Result<serde_json::Value> {
) -> error::Result<Box<RawValue>> {
let (inner_content, requirements_o, language, envs) = match job.job_kind {
JobKind::Preview => (
job.raw_code
@@ -1975,9 +2082,9 @@ async fn handle_code_execution_job(
};
if language == Some(ScriptLang::Postgresql) {
return do_postgresql(job.clone(), &client.get_authed().await, &inner_content, db).await;
return do_postgresql(job, &client, &inner_content, db).await;
} else if language == Some(ScriptLang::Mysql) {
return do_mysql(job.clone(), &client.get_authed().await, &inner_content, db).await;
return do_mysql(job, &client, &inner_content, db).await;
} else if language == Some(ScriptLang::Bigquery) {
#[cfg(not(feature = "enterprise"))]
{
@@ -1988,7 +2095,7 @@ async fn handle_code_execution_job(
#[cfg(feature = "enterprise")]
{
return do_bigquery(job.clone(), &client.get_authed().await, &inner_content, db).await;
return do_bigquery(job, &client, &inner_content, db).await;
}
} else if language == Some(ScriptLang::Snowflake) {
#[cfg(not(feature = "enterprise"))]
@@ -2000,10 +2107,10 @@ async fn handle_code_execution_job(
#[cfg(feature = "enterprise")]
{
return do_snowflake(job.clone(), &client.get_authed().await, &inner_content, db).await;
return do_snowflake(job, &client, &inner_content, db).await;
}
} else if language == Some(ScriptLang::Graphql) {
return do_graphql(job.clone(), &client.get_authed().await, &inner_content, db).await;
return do_graphql(job, &client, &inner_content, db).await;
} else if language == Some(ScriptLang::Nativets) {
logs.push_str("\n--- FETCH TS EXECUTION ---\n");
let code = format!(
@@ -2011,14 +2118,7 @@ async fn handle_code_execution_job(
&client.get_token().await,
inner_content
);
let (result, ts_logs) = do_nativets(
job.clone(),
logs.clone(),
&client.get_authed().await,
code,
db,
)
.await?;
let (result, ts_logs) = do_nativets(job, logs.clone(), &client, code, db).await?;
*logs = ts_logs;
return Ok(result);
}
@@ -2057,7 +2157,7 @@ mount {{
let envs = build_envs(envs)?;
let result: error::Result<serde_json::Value> = match language {
let result: error::Result<Box<RawValue>> = match language {
None => {
return Err(Error::ExecutionErr(
"Require language to be not null".to_string(),
@@ -2071,6 +2171,7 @@ mount {{
worker_name,
job,
logs,
mem_peak,
db,
client,
&inner_content,
@@ -2084,6 +2185,7 @@ mount {{
handle_deno_job(
requirements_o,
logs,
mem_peak,
job,
db,
client,
@@ -2099,6 +2201,7 @@ mount {{
handle_bun_job(
requirements_o,
logs,
mem_peak,
job,
db,
client,
@@ -2114,6 +2217,7 @@ mount {{
Some(ScriptLang::Go) => {
handle_go_job(
logs,
mem_peak,
job,
db,
client,
@@ -2130,6 +2234,7 @@ mount {{
Some(ScriptLang::Bash) => {
handle_bash_job(
logs,
mem_peak,
job,
db,
client,
@@ -2145,6 +2250,7 @@ mount {{
Some(ScriptLang::Powershell) => {
handle_powershell_job(
logs,
mem_peak,
job,
db,
client,
@@ -2177,13 +2283,14 @@ mount {{
async fn handle_dependency_job(
job: &QueuedJob,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
worker_dir: &str,
base_internal_url: &str,
token: &str,
) -> error::Result<serde_json::Value> {
) -> error::Result<Box<RawValue>> {
let content = capture_dependency_job(
&job.id,
job.language.as_ref().map(|v| Ok(v)).unwrap_or_else(|| {
@@ -2196,6 +2303,7 @@ async fn handle_dependency_job(
.map(|a| a.as_str())
.unwrap_or_else(|| "no raw code"),
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2216,7 +2324,9 @@ async fn handle_dependency_job(
)
.execute(db)
.await?;
Ok(json!({ "success": "Successful lock file generation", "lock": content }))
Ok(to_raw_value_owned(
json!({ "success": "Successful lock file generation", "lock": content }),
))
}
Err(error) => {
sqlx::query!(
@@ -2235,6 +2345,7 @@ async fn handle_dependency_job(
async fn handle_flow_dependency_job(
job: &QueuedJob,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
@@ -2258,6 +2369,7 @@ async fn handle_flow_dependency_job(
flow.modules,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2298,6 +2410,7 @@ async fn lock_modules(
modules: Vec<FlowModule>,
job: &QueuedJob,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
@@ -2333,6 +2446,7 @@ async fn lock_modules(
modules,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2354,6 +2468,7 @@ async fn lock_modules(
b.modules,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2374,6 +2489,7 @@ async fn lock_modules(
b.modules,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2389,6 +2505,7 @@ async fn lock_modules(
default,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2422,6 +2539,7 @@ async fn lock_modules(
&language,
&dependencies,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2479,6 +2597,7 @@ async fn lock_modules_app(
value: Value,
job: &QueuedJob,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
@@ -2521,6 +2640,7 @@ async fn lock_modules_app(
&language,
&dependencies,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2560,6 +2680,7 @@ async fn lock_modules_app(
b,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2581,6 +2702,7 @@ async fn lock_modules_app(
b,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2601,6 +2723,7 @@ async fn lock_modules_app(
async fn handle_app_dependency_job(
job: &QueuedJob,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
@@ -2628,6 +2751,7 @@ async fn handle_app_dependency_job(
value,
job,
logs,
mem_peak,
job_dir,
db,
worker_name,
@@ -2665,6 +2789,7 @@ async fn capture_dependency_job(
job_language: &ScriptLang,
job_raw_code: &str,
logs: &mut String,
mem_peak: &mut i32,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
@@ -2677,8 +2802,17 @@ async fn capture_dependency_job(
match job_language {
ScriptLang::Python3 => {
create_dependencies_dir(job_dir).await;
let req: std::result::Result<String, Error> =
pip_compile(job_id, job_raw_code, logs, job_dir, db, worker_name, w_id).await;
let req: std::result::Result<String, Error> = pip_compile(
job_id,
job_raw_code,
logs,
mem_peak,
job_dir,
db,
worker_name,
w_id,
)
.await;
// install the dependencies to pre-fill the cache
if let Ok(req) = req.as_ref() {
let r = handle_python_reqs(
@@ -2686,6 +2820,7 @@ async fn capture_dependency_job(
job_id,
w_id,
logs,
mem_peak,
db,
worker_name,
job_dir,
@@ -2708,6 +2843,7 @@ async fn capture_dependency_job(
job_id,
job_raw_code,
logs,
mem_peak,
job_dir,
db,
false,
@@ -2723,6 +2859,7 @@ async fn capture_dependency_job(
job_id,
job_raw_code,
logs,
mem_peak,
job_dir,
db,
w_id,
@@ -2737,6 +2874,7 @@ async fn capture_dependency_job(
let trusted_deps = get_trusted_deps(job_raw_code);
let req = gen_lockfile(
logs,
mem_peak,
job_id,
w_id,
db,
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -478,7 +478,7 @@
on:click={() => editor?.reloadWebsocket()}
startIcon={{
icon: faRotate,
classes: !websocketAlive[lang] ? 'animate-spin' : ''
classes: websocketAlive[lang] == false ? 'animate-spin' : ''
}}
title="Reload assistants"
>
@@ -403,9 +403,12 @@
<TabContent value={'oauth'}>
<div>
<h4 class="pb-4">SSO</h4>
<Alert type="warning" title="Limited to 50 SSO users">
Without EE, the number of SSO users is limited to 50. SCIM/SAML is available on EE
</Alert>
{#if !$enterpriseLicense}
<Alert type="warning" title="Limited to 50 SSO users">
Without EE, the number of SSO users is limited to 50. SCIM/SAML is available on EE
</Alert>
{/if}
<div class="py-1" />
<Alert type="info" title="Test on a separate tab">
The recommended workflow is to to save your oauth setting and test them directly on the
+15 -443
View File
@@ -1,451 +1,23 @@
<script lang="ts">
import { Alert, Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import CronInput from '$lib/components/CronInput.svelte'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { FlowService, ScheduleService, Script, ScriptService, type Flow } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { faList, faSave } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Section from '$lib/components/Section.svelte'
import { tick } from 'svelte'
import ScheduleEditorInner from './ScheduleEditorInner.svelte'
const slackErrorHandler = 'hub/2431/slack/schedule-error-handler-slack'
const slackRecoveryHandler = 'hub/2430/slack/schedule-recovery-handler-slack'
let initialPath = ''
let edit = true
let schedule: string = '0 0 12 * *'
let timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
let itemKind: 'flow' | 'script' = 'script'
let errorHandleritemKind: 'flow' | 'script' = 'script'
let errorHandlerPath: string | undefined = undefined
let errorHandlerCustomInitialPath: string | undefined = undefined
let errorHandlerSelected: 'custom' | 'slack' = 'slack'
let errorHandlerExtraArgs: Record<string, any> = {}
let recoveryHandlerPath: string | undefined = undefined
let recoveryHandlerCustomInitialPath: string | undefined = undefined
let recoveryHandlerSelected: 'custom' | 'slack' = 'slack'
let recoveryHandlerItemKind: 'flow' | 'script' = 'script'
let recoveryHandlerExtraArgs: Record<string, any> = {}
let failedTimes = 1
let failedExact = false
let recoveredTimes = 1
let script_path = ''
let initialScriptPath = ''
export function openEdit(ePath: string, isFlow: boolean) {
is_flow = isFlow
initialPath = ePath
itemKind = is_flow ? 'flow' : 'script'
if (path == ePath) {
loadSchedule()
} else {
path = ePath
}
edit = true
drawer?.openDrawer()
let open = false
export async function openEdit(ePath: string, isFlow: boolean) {
open = true
await tick()
drawer?.openEdit(ePath, isFlow)
}
export function openNew(is_flow: boolean, initial_script_path?: string) {
edit = false
itemKind = is_flow ? 'flow' : 'script'
initialScriptPath = initial_script_path ?? ''
path = initialScriptPath
initialPath = initialScriptPath
script_path = initialScriptPath
errorHandlerSelected = $enterpriseLicense ? 'slack' : 'custom'
errorHandleritemKind = 'script'
errorHandlerPath = undefined
errorHandlerCustomInitialPath = undefined
errorHandlerExtraArgs = {}
recoveryHandlerSelected = $enterpriseLicense ? 'slack' : 'custom'
recoveryHandlerPath = undefined
recoveryHandlerCustomInitialPath = undefined
recoveryHandlerItemKind = 'script'
recoveryHandlerExtraArgs = {}
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
drawer?.openDrawer()
export async function openNew(is_flow: boolean, initial_script_path?: string) {
open = true
await tick()
drawer?.openNew(is_flow, initial_script_path)
}
$: is_flow = itemKind == 'flow'
let runnable: Script | Flow | undefined
let args: Record<string, any> = {}
let isValid = true
let path: string = ''
let enabled: boolean = false
let pathError = ''
let validCRON = true
$: allowSchedule = isValid && validCRON && script_path != ''
$: script_path != '' && loadScript(script_path)
// set isValid to true when a script/flow without any properties is selected
$: runnable?.schema &&
runnable.schema.properties &&
Object.keys(runnable.schema.properties).length === 0 &&
(isValid = true)
const dispatch = createEventDispatcher()
async function loadScript(p: string | undefined): Promise<void> {
if (p) {
if (is_flow) {
runnable = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: p })
} else {
runnable = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: p })
}
} else {
runnable = undefined
}
}
let can_write = true
async function loadSchedule(): Promise<void> {
try {
const s = await ScheduleService.getSchedule({
workspace: $workspaceStore!,
path: initialPath
})
enabled = s.enabled
schedule = s.schedule
timezone = s.timezone
script_path = s.script_path ?? ''
is_flow = s.is_flow
if (s.on_failure) {
let splitted = s.on_failure.split('/')
errorHandleritemKind = splitted[0] as 'flow' | 'script'
errorHandlerPath = splitted.slice(1)?.join('/')
errorHandlerCustomInitialPath = errorHandlerPath
failedTimes = s.on_failure_times ?? 1
failedExact = s.on_failure_exact ?? false
errorHandlerExtraArgs = s.on_failure_extra_args ?? {}
if (errorHandlerPath !== slackErrorHandler) {
errorHandlerSelected = 'custom'
}
} else {
errorHandlerPath = undefined
errorHandleritemKind = 'script'
}
if (s.on_recovery) {
let splitted = s.on_recovery.split('/')
recoveryHandlerItemKind = splitted[0] as 'flow' | 'script'
recoveryHandlerPath = splitted.slice(1)?.join('/')
recoveryHandlerCustomInitialPath = recoveryHandlerPath
recoveredTimes = s.on_recovery_times ?? 1
recoveryHandlerExtraArgs = s.on_recovery_extra_args ?? {}
if (recoveryHandlerPath !== slackRecoveryHandler) {
recoveryHandlerSelected = 'custom'
}
} else {
recoveryHandlerPath = undefined
recoveryHandlerItemKind = 'script'
}
args = s.args ?? {}
can_write = canWrite(s.path, s.extra_perms, $userStore)
} catch (err) {
sendUserToast(`Could not load schedule: ${err}`, true)
}
}
async function scheduleScript(): Promise<void> {
if (errorHandlerSelected === 'slack' && !emptyString(errorHandlerPath)) {
errorHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token'
}
if (recoveryHandlerSelected === 'slack' && !emptyString(recoveryHandlerPath)) {
recoveryHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token'
}
if (edit) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
schedule: formatCron(schedule),
timezone,
args,
on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined,
on_failure_times: failedTimes,
on_failure_exact: failedExact,
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined,
on_recovery: recoveryHandlerPath
? `${recoveryHandlerItemKind}/${recoveryHandlerPath}`
: undefined,
on_recovery_times: recoveredTimes,
on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {},
}
})
sendUserToast(`Schedule ${path} updated`)
} else {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path,
schedule: formatCron(schedule),
timezone,
script_path,
is_flow,
args,
enabled: true,
on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined,
on_failure_times: failedTimes,
on_failure_exact: failedExact,
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined,
on_recovery: recoveryHandlerPath
? `${recoveryHandlerItemKind}/${recoveryHandlerPath}`
: undefined,
on_recovery_times: recoveredTimes,
on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {},
}
})
sendUserToast(`Schedule ${path} created`)
}
dispatch('update')
drawer.closeDrawer()
}
$: {
if ($workspaceStore) {
if (edit && path != '') {
loadSchedule()
}
}
}
let drawer: Drawer
let drawer: ScheduleEditorInner
</script>
<Drawer size="900px" bind:this={drawer}>
<DrawerContent
title={edit ? `Edit schedule ${initialPath}` : 'New schedule'}
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
{#if edit}
<div class="mr-8">
<Button
size="sm"
variant="border"
startIcon={{ icon: faList }}
disabled={!allowSchedule || pathError != '' || emptyString(script_path)}
href={`/runs/${script_path}`}
>
View Runs
</Button>
</div>
<div class="mr-8 center-center -mt-2">
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'enable', left: 'disable' }}
on:change={async (e) => {
await ScheduleService.setScheduleEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
}}
/>
</div>
{/if}
<Button
startIcon={{ icon: faSave }}
disabled={!allowSchedule || pathError != '' || emptyString(script_path) || (errorHandlerSelected == 'slack' && !emptyString(errorHandlerPath) && emptyString(errorHandlerExtraArgs['channel']))}
on:click={scheduleScript}
>
{edit ? 'Save' : 'Schedule'}
</Button>
</svelte:fragment>
<div class="flex flex-col gap-8">
{#if !edit}
<Section label="Metadata">
<Path
checkInitialPathExistence
bind:error={pathError}
bind:path
{initialPath}
namePlaceholder="schedule"
kind="schedule"
/>
</Section>
{/if}
<Section label="Schedule">
<svelte:fragment slot="header">
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
</svelte:fragment>
<CronInput disabled={!can_write} bind:schedule bind:timezone bind:validCRON />
</Section>
<Section label="Runnable">
{#if !edit}
<p class="text-xs mb-1 text-tertiary">
Pick a script or flow to be triggered by the schedule<Required required={true} />
</p>
<ScriptPicker
disabled={initialScriptPath != '' || !can_write}
initialPath={initialScriptPath}
kinds={[Script.kind.SCRIPT]}
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
/>
{:else}
<Alert type="info" title="Runnable path cannot be edited">
Once a schedule is created, the runnable path cannot be changed. However, when renaming
a script or a flow, the runnable path will automatically update itself.
</Alert>
<div class="my-2" />
<ScriptPicker
disabled
initialPath={script_path}
scriptPath={script_path}
allowFlow={true}
{itemKind}
/>
{/if}
<div class="mt-6">
{#if runnable}
{#if runnable?.schema && runnable.schema.properties && Object.keys(runnable.schema.properties).length > 0}
<SchemaForm disabled={!can_write} schema={runnable.schema} bind:isValid bind:args />
{:else}
<div class="text-xs texg-gray-700">
This {is_flow ? 'flow' : 'script'} takes no argument
</div>
{/if}
{:else}
<div class="text-xs texg-gray-700 my-2">
Pick a {is_flow ? 'flow' : 'script'} and fill its argument here
</div>
{/if}
</div>
</Section>
<Section label="Error handler">
<ErrorOrRecoveryHandler
isEditable={can_write}
handlersOnlyForEe={['slack']}
showScriptHelpText={true}
bind:handlerSelected={errorHandlerSelected}
bind:handlerPath={errorHandlerPath}
customInitialScriptPath={errorHandlerCustomInitialPath}
slackHandlerScriptPath={slackErrorHandler}
slackToggleText="Alert channel on error"
customScriptTemplate="/scripts/add?hub=hub%2F2420%2Fwindmill%2Fschedule_error_handler_template"
bind:customHandlerKind={errorHandleritemKind}
bind:handlerExtraArgs={errorHandlerExtraArgs}
>
<svelte:fragment slot="custom-tab-tooltip">
<Tooltip>
<div class="flex gap-20 items-start mt-3">
<div class="text-sm"
>The following args will be passed to the error handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that failed.</li>
<li><b>is_flow</b>: Whether the runnable is a flow.</li>
<li><b>schedule_path</b>: The path of the schedule.</li>
<li><b>error</b>: The error details.</li>
<li
><b>failed_times</b>: Minimum number of times the schedule failed before calling
the error handler.</li
>
<li><b>started_at</b>: The start datetime of the latest job that failed.</li>
</ul>
</div>
</div>
</Tooltip>
</svelte:fragment>
</ErrorOrRecoveryHandler>
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center mt-4 font-semibold text-sm gap-2">
<p class="{emptyString(errorHandlerPath) ? 'text-tertiary' : ''}"
>{#if !$enterpriseLicense}<span class="text-normal text-2xs">(ee only)</span>{/if} Triggered
when schedule failed</p
>
<select class="!w-14" bind:value={failedExact} disabled={!$enterpriseLicense || emptyString(errorHandlerPath)}>
<option value={false}>&gt;=</option>
<option value={true}>==</option>
</select>
<input
type="number"
class="!w-14 text-center {emptyString(errorHandlerPath) ? 'text-tertiary' : ''}"
bind:value={failedTimes}
disabled={!$enterpriseLicense}
min="1"
/>
<p class="{emptyString(errorHandlerPath) ? 'text-tertiary' : ''}">time{failedTimes > 1 ? 's in a row' : ''}</p>
</div>
</div>
</Section>
<Section label="Recovery handler">
<svelte:fragment slot="header">
<div class="flex flex-row gap-2">
{#if !$enterpriseLicense}<span class="text-normal text-2xs">(ee only)</span>{/if}
</div>
</svelte:fragment>
<ErrorOrRecoveryHandler
isEditable={can_write && !emptyString($enterpriseLicense)}
handlersOnlyForEe={[]}
bind:handlerSelected={recoveryHandlerSelected}
bind:handlerPath={recoveryHandlerPath}
customInitialScriptPath={recoveryHandlerCustomInitialPath}
slackHandlerScriptPath={slackRecoveryHandler}
slackToggleText="Alert channel when error recovered"
customScriptTemplate="/scripts/add?hub=hub%2F2421%2Fwindmill%2Fschedule_recovery_handler_template"
bind:customHandlerKind={recoveryHandlerItemKind}
bind:handlerExtraArgs={recoveryHandlerExtraArgs}
>
<svelte:fragment slot="custom-tab-tooltip">
<Tooltip>
<div class="flex gap-20 items-start mt-3">
<div class=" text-sm"
>The following args will be passed to the recovery handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that recovered.</li>
<li><b>is_flow</b>: Whether the runnable is a flow.</li>
<li><b>schedule_path</b>: The path of the schedule.</li>
<li><b>error</b>: The error of the last job that errored</li>
<li><b>error_started_at</b>: The start datetime of the last job that errored</li>
<li
><b>success_times</b>: The number of times the schedule succeeded before calling
the recovery handler.</li
>
<li><b>success_result</b>: The result of the latest successful job</li>
<li><b>success_started_at</b>: The start datetime of the latest successful job</li
>
</ul>
</div>
</div>
</Tooltip>
</svelte:fragment>
</ErrorOrRecoveryHandler>
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center mt-5 font-semibold text-sm {emptyString(recoveryHandlerPath) ? 'text-tertiary' : ''}">
<p>Triggered when schedule recovered</p>
<input
type="number"
class="!w-14 mx-2 text-center"
bind:value={recoveredTimes}
min="1"
/>
<p>time{recoveredTimes > 1 ? 's in a row' : ''}</p>
</div>
</div>
</Section>
</div>
</DrawerContent>
</Drawer>
{#if open}
<ScheduleEditorInner on:update bind:this={drawer} />
{/if}
@@ -0,0 +1,470 @@
<script lang="ts">
import { Alert, Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import CronInput from '$lib/components/CronInput.svelte'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { FlowService, ScheduleService, Script, ScriptService, type Flow } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { faList, faSave } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Section from '$lib/components/Section.svelte'
const slackErrorHandler = 'hub/2431/slack/schedule-error-handler-slack'
const slackRecoveryHandler = 'hub/2430/slack/schedule-recovery-handler-slack'
let initialPath = ''
let edit = true
let schedule: string = '0 0 12 * *'
let timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
let itemKind: 'flow' | 'script' = 'script'
let errorHandleritemKind: 'flow' | 'script' = 'script'
let errorHandlerPath: string | undefined = undefined
let errorHandlerCustomInitialPath: string | undefined = undefined
let errorHandlerSelected: 'custom' | 'slack' = 'slack'
let errorHandlerExtraArgs: Record<string, any> = {}
let recoveryHandlerPath: string | undefined = undefined
let recoveryHandlerCustomInitialPath: string | undefined = undefined
let recoveryHandlerSelected: 'custom' | 'slack' = 'slack'
let recoveryHandlerItemKind: 'flow' | 'script' = 'script'
let recoveryHandlerExtraArgs: Record<string, any> = {}
let failedTimes = 1
let failedExact = false
let recoveredTimes = 1
let script_path = ''
let initialScriptPath = ''
export function openEdit(ePath: string, isFlow: boolean) {
is_flow = isFlow
initialPath = ePath
itemKind = is_flow ? 'flow' : 'script'
if (path == ePath) {
loadSchedule()
} else {
path = ePath
}
edit = true
drawer?.openDrawer()
}
export function openNew(is_flow: boolean, initial_script_path?: string) {
edit = false
itemKind = is_flow ? 'flow' : 'script'
initialScriptPath = initial_script_path ?? ''
path = initialScriptPath
initialPath = initialScriptPath
script_path = initialScriptPath
errorHandlerSelected = $enterpriseLicense ? 'slack' : 'custom'
errorHandleritemKind = 'script'
errorHandlerPath = undefined
errorHandlerCustomInitialPath = undefined
errorHandlerExtraArgs = {}
recoveryHandlerSelected = $enterpriseLicense ? 'slack' : 'custom'
recoveryHandlerPath = undefined
recoveryHandlerCustomInitialPath = undefined
recoveryHandlerItemKind = 'script'
recoveryHandlerExtraArgs = {}
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
drawer?.openDrawer()
}
$: is_flow = itemKind == 'flow'
let runnable: Script | Flow | undefined
let args: Record<string, any> = {}
let isValid = true
let path: string = ''
let enabled: boolean = false
let pathError = ''
let validCRON = true
$: allowSchedule = isValid && validCRON && script_path != ''
$: script_path != '' && loadScript(script_path)
// set isValid to true when a script/flow without any properties is selected
$: runnable?.schema &&
runnable.schema.properties &&
Object.keys(runnable.schema.properties).length === 0 &&
(isValid = true)
const dispatch = createEventDispatcher()
async function loadScript(p: string | undefined): Promise<void> {
if (p) {
if (is_flow) {
runnable = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path: p })
} else {
runnable = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path: p })
}
} else {
runnable = undefined
}
}
let can_write = true
async function loadSchedule(): Promise<void> {
try {
const s = await ScheduleService.getSchedule({
workspace: $workspaceStore!,
path: initialPath
})
enabled = s.enabled
schedule = s.schedule
timezone = s.timezone
script_path = s.script_path ?? ''
is_flow = s.is_flow
if (s.on_failure) {
let splitted = s.on_failure.split('/')
errorHandleritemKind = splitted[0] as 'flow' | 'script'
errorHandlerPath = splitted.slice(1)?.join('/')
errorHandlerCustomInitialPath = errorHandlerPath
failedTimes = s.on_failure_times ?? 1
failedExact = s.on_failure_exact ?? false
errorHandlerExtraArgs = s.on_failure_extra_args ?? {}
if (errorHandlerPath !== slackErrorHandler) {
errorHandlerSelected = 'custom'
}
} else {
errorHandlerPath = undefined
errorHandleritemKind = 'script'
}
if (s.on_recovery) {
let splitted = s.on_recovery.split('/')
recoveryHandlerItemKind = splitted[0] as 'flow' | 'script'
recoveryHandlerPath = splitted.slice(1)?.join('/')
recoveryHandlerCustomInitialPath = recoveryHandlerPath
recoveredTimes = s.on_recovery_times ?? 1
recoveryHandlerExtraArgs = s.on_recovery_extra_args ?? {}
if (recoveryHandlerPath !== slackRecoveryHandler) {
recoveryHandlerSelected = 'custom'
}
} else {
recoveryHandlerPath = undefined
recoveryHandlerItemKind = 'script'
}
args = s.args ?? {}
can_write = canWrite(s.path, s.extra_perms, $userStore)
} catch (err) {
sendUserToast(`Could not load schedule: ${err}`, true)
}
}
async function scheduleScript(): Promise<void> {
if (errorHandlerSelected === 'slack' && !emptyString(errorHandlerPath)) {
errorHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token'
}
if (recoveryHandlerSelected === 'slack' && !emptyString(recoveryHandlerPath)) {
recoveryHandlerExtraArgs['slack'] = '$res:f/slack_bot/bot_token'
}
if (edit) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
schedule: formatCron(schedule),
timezone,
args,
on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined,
on_failure_times: failedTimes,
on_failure_exact: failedExact,
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined,
on_recovery: recoveryHandlerPath
? `${recoveryHandlerItemKind}/${recoveryHandlerPath}`
: undefined,
on_recovery_times: recoveredTimes,
on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {}
}
})
sendUserToast(`Schedule ${path} updated`)
} else {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path,
schedule: formatCron(schedule),
timezone,
script_path,
is_flow,
args,
enabled: true,
on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined,
on_failure_times: failedTimes,
on_failure_exact: failedExact,
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined,
on_recovery: recoveryHandlerPath
? `${recoveryHandlerItemKind}/${recoveryHandlerPath}`
: undefined,
on_recovery_times: recoveredTimes,
on_recovery_extra_args: recoveryHandlerPath ? recoveryHandlerExtraArgs : {}
}
})
sendUserToast(`Schedule ${path} created`)
}
dispatch('update')
drawer.closeDrawer()
}
$: {
if ($workspaceStore) {
if (edit && path != '') {
loadSchedule()
}
}
}
let drawer: Drawer
</script>
<Drawer size="900px" bind:this={drawer}>
<DrawerContent
title={edit ? `Edit schedule ${initialPath}` : 'New schedule'}
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
{#if edit}
<div class="mr-8">
<Button
size="sm"
variant="border"
startIcon={{ icon: faList }}
disabled={!allowSchedule || pathError != '' || emptyString(script_path)}
href={`/runs/${script_path}`}
>
View Runs
</Button>
</div>
<div class="mr-8 center-center -mt-2">
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'enable', left: 'disable' }}
on:change={async (e) => {
await ScheduleService.setScheduleEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
}}
/>
</div>
{/if}
<Button
startIcon={{ icon: faSave }}
disabled={!allowSchedule ||
pathError != '' ||
emptyString(script_path) ||
(errorHandlerSelected == 'slack' &&
!emptyString(errorHandlerPath) &&
emptyString(errorHandlerExtraArgs['channel']))}
on:click={scheduleScript}
>
{edit ? 'Save' : 'Schedule'}
</Button>
</svelte:fragment>
<div class="flex flex-col gap-8">
{#if !edit}
<Section label="Metadata">
<Path
checkInitialPathExistence
bind:error={pathError}
bind:path
{initialPath}
namePlaceholder="schedule"
kind="schedule"
/>
</Section>
{/if}
<Section label="Schedule">
<svelte:fragment slot="header">
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
</svelte:fragment>
<CronInput disabled={!can_write} bind:schedule bind:timezone bind:validCRON />
</Section>
<Section label="Runnable">
{#if !edit}
<p class="text-xs mb-1 text-tertiary">
Pick a script or flow to be triggered by the schedule<Required required={true} />
</p>
<ScriptPicker
disabled={initialScriptPath != '' || !can_write}
initialPath={initialScriptPath}
kinds={[Script.kind.SCRIPT]}
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
/>
{:else}
<Alert type="info" title="Runnable path cannot be edited">
Once a schedule is created, the runnable path cannot be changed. However, when renaming
a script or a flow, the runnable path will automatically update itself.
</Alert>
<div class="my-2" />
<ScriptPicker
disabled
initialPath={script_path}
scriptPath={script_path}
allowFlow={true}
{itemKind}
/>
{/if}
<div class="mt-6">
{#if runnable}
{#if runnable?.schema && runnable.schema.properties && Object.keys(runnable.schema.properties).length > 0}
<SchemaForm disabled={!can_write} schema={runnable.schema} bind:isValid bind:args />
{:else}
<div class="text-xs texg-gray-700">
This {is_flow ? 'flow' : 'script'} takes no argument
</div>
{/if}
{:else}
<div class="text-xs texg-gray-700 my-2">
Pick a {is_flow ? 'flow' : 'script'} and fill its argument here
</div>
{/if}
</div>
</Section>
<Section label="Error handler">
<ErrorOrRecoveryHandler
isEditable={can_write}
handlersOnlyForEe={['slack']}
showScriptHelpText={true}
bind:handlerSelected={errorHandlerSelected}
bind:handlerPath={errorHandlerPath}
customInitialScriptPath={errorHandlerCustomInitialPath}
slackHandlerScriptPath={slackErrorHandler}
slackToggleText="Alert channel on error"
customScriptTemplate="/scripts/add?hub=hub%2F2420%2Fwindmill%2Fschedule_error_handler_template"
bind:customHandlerKind={errorHandleritemKind}
bind:handlerExtraArgs={errorHandlerExtraArgs}
>
<svelte:fragment slot="custom-tab-tooltip">
<Tooltip>
<div class="flex gap-20 items-start mt-3">
<div class="text-sm"
>The following args will be passed to the error handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that failed.</li>
<li><b>is_flow</b>: Whether the runnable is a flow.</li>
<li><b>schedule_path</b>: The path of the schedule.</li>
<li><b>error</b>: The error details.</li>
<li
><b>failed_times</b>: Minimum number of times the schedule failed before
calling the error handler.</li
>
<li><b>started_at</b>: The start datetime of the latest job that failed.</li>
</ul>
</div>
</div>
</Tooltip>
</svelte:fragment>
</ErrorOrRecoveryHandler>
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center mt-4 font-semibold text-sm gap-2">
<p class={emptyString(errorHandlerPath) ? 'text-tertiary' : ''}
>{#if !$enterpriseLicense}<span class="text-normal text-2xs">(ee only)</span>{/if} Triggered
when schedule failed</p
>
<select
class="!w-14"
bind:value={failedExact}
disabled={!$enterpriseLicense || emptyString(errorHandlerPath)}
>
<option value={false}>&gt;=</option>
<option value={true}>==</option>
</select>
<input
type="number"
class="!w-14 text-center {emptyString(errorHandlerPath) ? 'text-tertiary' : ''}"
bind:value={failedTimes}
disabled={!$enterpriseLicense}
min="1"
/>
<p class={emptyString(errorHandlerPath) ? 'text-tertiary' : ''}
>time{failedTimes > 1 ? 's in a row' : ''}</p
>
</div>
</div>
</Section>
<Section label="Recovery handler">
<svelte:fragment slot="header">
<div class="flex flex-row gap-2">
{#if !$enterpriseLicense}<span class="text-normal text-2xs">(ee only)</span>{/if}
</div>
</svelte:fragment>
<ErrorOrRecoveryHandler
isEditable={can_write && !emptyString($enterpriseLicense)}
handlersOnlyForEe={[]}
bind:handlerSelected={recoveryHandlerSelected}
bind:handlerPath={recoveryHandlerPath}
customInitialScriptPath={recoveryHandlerCustomInitialPath}
slackHandlerScriptPath={slackRecoveryHandler}
slackToggleText="Alert channel when error recovered"
customScriptTemplate="/scripts/add?hub=hub%2F2421%2Fwindmill%2Fschedule_recovery_handler_template"
bind:customHandlerKind={recoveryHandlerItemKind}
bind:handlerExtraArgs={recoveryHandlerExtraArgs}
>
<svelte:fragment slot="custom-tab-tooltip">
<Tooltip>
<div class="flex gap-20 items-start mt-3">
<div class=" text-sm"
>The following args will be passed to the recovery handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that recovered.</li>
<li><b>is_flow</b>: Whether the runnable is a flow.</li>
<li><b>schedule_path</b>: The path of the schedule.</li>
<li><b>error</b>: The error of the last job that errored</li>
<li><b>error_started_at</b>: The start datetime of the last job that errored</li
>
<li
><b>success_times</b>: The number of times the schedule succeeded before
calling the recovery handler.</li
>
<li><b>success_result</b>: The result of the latest successful job</li>
<li
><b>success_started_at</b>: The start datetime of the latest successful job</li
>
</ul>
</div>
</div>
</Tooltip>
</svelte:fragment>
</ErrorOrRecoveryHandler>
<div class="flex flex-row items-center justify-between">
<div
class="flex flex-row items-center mt-5 font-semibold text-sm {emptyString(
recoveryHandlerPath
)
? 'text-tertiary'
: ''}"
>
<p>Triggered when schedule recovered</p>
<input
type="number"
class="!w-14 mx-2 text-center"
bind:value={recoveredTimes}
min="1"
/>
<p>time{recoveredTimes > 1 ? 's in a row' : ''}</p>
</div>
</div>
</Section>
</div>
</DrawerContent>
</Drawer>
@@ -157,6 +157,7 @@
job.mem_peak = previewJobUpdates.mem_peak
}
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
console.log({ a: previewJobUpdates.running, b: previewJobUpdates.completed })
job = await JobService.getJob({ workspace: workspace!, id })
}
} else {
@@ -45,7 +45,7 @@
'other',
'bun'
]
const nativeTags = ['nativets', 'postgresql', 'mysql', 'graphql', 'snowflake']
const nativeTags = ['nativets', 'postgresql', 'mysql', 'graphql', 'snowflake', 'bigquery']
let newTag: string = ''
$: selected = nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal'
@@ -9,9 +9,18 @@ export async function getResourceTypes() {
if (rts) {
return rts
} else {
const nrts = await ResourceService.listResourceTypeNames({ workspace: get(workspaceStore)! })
resourceTypesStore.set(nrts)
return nrts
let workspace = get(workspaceStore)
if (workspace){
try {
const nrts = await ResourceService.listResourceTypeNames({ workspace: workspace })
resourceTypesStore.set(nrts)
return nrts
} catch (e) {
return ["error_fetching_names"]
}
} else {
return ['workspace_is_undefined']
}
}
}
+3
View File
@@ -3,6 +3,7 @@ import { derived, type Readable, writable } from 'svelte/store'
import type { UserWorkspaceList } from '$lib/gen/models/UserWorkspaceList.js'
import type { TokenResponse } from './gen'
import type { IntrospectionQuery } from 'graphql'
import { resourceTypesStore } from './components/resourceTypesStore'
export interface UserExt {
email: string
@@ -108,6 +109,7 @@ export const dbSchemas = writable<DBSchemas>({})
export function switchWorkspace(workspace: string | undefined) {
localStorage.removeItem('flow')
localStorage.removeItem('app')
resourceTypesStore.set(undefined)
workspaceStore.set(workspace)
}
@@ -115,6 +117,7 @@ export function clearStores(): void {
localStorage.removeItem('flow')
localStorage.removeItem('app')
localStorage.removeItem('workspace')
resourceTypesStore.set(undefined)
userStore.set(undefined)
workspaceStore.set(undefined)
usersWorkspaceStore.set(undefined)
@@ -10,6 +10,9 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import FlowGraph from '$lib/components/graph/FlowGraph.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { workspaceStore } from '$lib/stores'
$workspaceStore = $page.params.workspace
let job: Job | undefined = undefined
let currentApprovers: { resume_id: number; approver: string }[] = []
@@ -145,7 +148,7 @@
{/if}
</div>
</div>
<h2 class="mt-4">Flow arguments</h2>
<h2 class="mt-4 mb-2">Flow arguments</h2>
<JobArgs args={job?.args} />
<div class="mt-8">