Compare commits
2 Commits
wmill-scri
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741526b7b8 | ||
|
|
73deef44ed |
587
backend/Cargo.lock
generated
587
backend/Cargo.lock
generated
@@ -125,7 +125,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy 0.8.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1302,6 +1302,34 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.6.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core 0.3.4",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"http-body 0.4.6",
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"itoa",
|
||||||
|
"matchit 0.7.3",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"sync_wrapper 0.1.2",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@@ -1330,7 +1358,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
@@ -1364,7 +1392,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
@@ -1372,6 +1400,23 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"http-body 0.4.6",
|
||||||
|
"mime",
|
||||||
|
"rustversion",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -1387,7 +1432,7 @@ dependencies = [
|
|||||||
"mime",
|
"mime",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -1406,7 +1451,7 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -1541,6 +1586,29 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.66.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"lazycell",
|
||||||
|
"log",
|
||||||
|
"peeking_take_while",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash 1.1.0",
|
||||||
|
"shlex",
|
||||||
|
"syn 2.0.114",
|
||||||
|
"which 4.4.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.69.5"
|
version = "0.69.5"
|
||||||
@@ -1827,7 +1895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
|
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
@@ -3889,7 +3957,7 @@ dependencies = [
|
|||||||
"tokio-socks",
|
"tokio-socks",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-http",
|
"tower-http 0.6.8",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4031,7 +4099,7 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"log",
|
"log",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
@@ -4654,7 +4722,7 @@ dependencies = [
|
|||||||
"deno_error",
|
"deno_error",
|
||||||
"futures",
|
"futures",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -4674,7 +4742,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
"log",
|
"log",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -6340,7 +6408,7 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-retry2",
|
"tokio-retry2",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -6351,9 +6419,9 @@ version = "0.16.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "886aa8ec755382a1fdf4651f6e6ec01f2f3bf49f2cb0f068b9a74cafd574a715"
|
checksum = "886aa8ec755382a1fdf4651f6e6ec01f2f3bf49f2cb0f068b9a74cafd574a715"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6516,7 +6584,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"rand_distr 0.5.1",
|
"rand_distr 0.5.1",
|
||||||
"zerocopy",
|
"zerocopy 0.8.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6576,6 +6644,15 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -6858,6 +6935,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -7006,6 +7089,24 @@ dependencies = [
|
|||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.24.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"log",
|
||||||
|
"rustls 0.22.4",
|
||||||
|
"rustls-native-certs 0.7.3",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.25.0",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@@ -7044,6 +7145,18 @@ dependencies = [
|
|||||||
"webpki-roots 1.0.5",
|
"webpki-roots 1.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-timeout"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
|
||||||
|
dependencies = [
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-io-timeout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-timeout"
|
name = "hyper-timeout"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -7777,7 +7890,7 @@ dependencies = [
|
|||||||
"hyper 1.8.1",
|
"hyper 1.8.1",
|
||||||
"hyper-http-proxy",
|
"hyper-http-proxy",
|
||||||
"hyper-rustls 0.27.7",
|
"hyper-rustls 0.27.7",
|
||||||
"hyper-timeout",
|
"hyper-timeout 0.5.2",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"jsonpath-rust",
|
"jsonpath-rust",
|
||||||
"k8s-openapi",
|
"k8s-openapi",
|
||||||
@@ -7792,7 +7905,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-http",
|
"tower-http 0.6.8",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8036,6 +8149,142 @@ dependencies = [
|
|||||||
"redox_syscall 0.7.0",
|
"redox_syscall 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql"
|
||||||
|
version = "0.9.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2329faffc510cc3c6b4f00169a39177cc7099d3ed7647fc92f7cf26e53a8d976"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-stream",
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bincode",
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"crc32fast",
|
||||||
|
"fallible-iterator 0.3.0",
|
||||||
|
"futures",
|
||||||
|
"http 0.2.12",
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"hyper-rustls 0.25.0",
|
||||||
|
"libsql-hrana",
|
||||||
|
"libsql-sqlite3-parser",
|
||||||
|
"libsql-sys",
|
||||||
|
"libsql_replication",
|
||||||
|
"parking_lot",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"tonic 0.11.0",
|
||||||
|
"tonic-web",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-http 0.4.4",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"zerocopy 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql-ffi"
|
||||||
|
version = "0.9.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cd1c1662822495393327856774f6803be25d85bfdcd5b9d4af35458f5daaf75"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen 0.66.1",
|
||||||
|
"cc",
|
||||||
|
"cmake",
|
||||||
|
"glob",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql-hrana"
|
||||||
|
version = "0.9.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "646d0aa75e412769018422f0da798f72e93bd51964f0b2ddad4317aa779ae444"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"prost 0.12.6",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql-rusqlite"
|
||||||
|
version = "0.9.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a4ce3a78c6e3c2b23b02ab6272df8340e1c53380497979d456882254f348d5f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"fallible-iterator 0.2.0",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink 0.8.4",
|
||||||
|
"libsql-ffi",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql-sqlite3-parser"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15a90128c708356af8f7d767c9ac2946692c9112b4f74f07b99a01a60680e413"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"cc",
|
||||||
|
"fallible-iterator 0.3.0",
|
||||||
|
"indexmap 2.11.1",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"phf 0.11.3",
|
||||||
|
"phf_codegen",
|
||||||
|
"phf_shared 0.11.3",
|
||||||
|
"uncased",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql-sys"
|
||||||
|
version = "0.9.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a3c326fcfc36fe7578238d5ee6b58c529f8c76372acd61ec50267529cdaff95"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libsql-ffi",
|
||||||
|
"libsql-rusqlite",
|
||||||
|
"once_cell",
|
||||||
|
"tracing",
|
||||||
|
"zerocopy 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsql_replication"
|
||||||
|
version = "0.9.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d9a2e469ac8400659bd31f81a745908bcc5cb6b40be2f2ff8de90b15bec5501"
|
||||||
|
dependencies = [
|
||||||
|
"aes 0.8.3",
|
||||||
|
"async-stream",
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"cbc",
|
||||||
|
"libsql-rusqlite",
|
||||||
|
"libsql-sys",
|
||||||
|
"parking_lot",
|
||||||
|
"prost 0.12.6",
|
||||||
|
"serde",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
|
"tonic 0.11.0",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"zerocopy 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
@@ -8654,7 +8903,7 @@ dependencies = [
|
|||||||
"darling 0.20.11",
|
"darling 0.20.11",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -9223,7 +9472,7 @@ version = "0.7.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
|
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
@@ -9554,11 +9803,11 @@ dependencies = [
|
|||||||
"opentelemetry-http",
|
"opentelemetry-http",
|
||||||
"opentelemetry-proto 0.27.0",
|
"opentelemetry-proto 0.27.0",
|
||||||
"opentelemetry_sdk 0.27.1",
|
"opentelemetry_sdk 0.27.1",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -9571,9 +9820,9 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"opentelemetry 0.27.1",
|
"opentelemetry 0.27.1",
|
||||||
"opentelemetry_sdk 0.27.1",
|
"opentelemetry_sdk 0.27.1",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9586,9 +9835,9 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"opentelemetry 0.29.1",
|
"opentelemetry 0.29.1",
|
||||||
"opentelemetry_sdk 0.29.0",
|
"opentelemetry_sdk 0.29.0",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -9874,6 +10123,12 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "peeking_take_while"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -10041,6 +10296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher 1.0.1",
|
"siphasher 1.0.1",
|
||||||
|
"uncased",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10277,7 +10533,7 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy",
|
"zerocopy 0.8.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10305,6 +10561,16 @@ dependencies = [
|
|||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"toml_edit 0.19.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@@ -10451,6 +10717,16 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive 0.12.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
@@ -10458,7 +10734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"prost-derive 0.13.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10474,13 +10750,26 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"regex",
|
"regex",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools 0.12.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-derive"
|
name = "prost-derive"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
@@ -10500,7 +10789,7 @@ version = "0.13.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10725,7 +11014,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
"zerocopy",
|
"zerocopy 0.8.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11020,6 +11309,12 @@ version = "0.8.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "relative-path"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rend"
|
name = "rend"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -11061,13 +11356,13 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls 0.26.4",
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-http",
|
"tower-http 0.6.8",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -11108,12 +11403,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.4",
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-http",
|
"tower-http 0.6.8",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -11310,6 +11605,54 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rquickjs"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d16661bff09e9ed8e01094a188b463de45ec0693ade55b92ed54027d7ba7c40c"
|
||||||
|
dependencies = [
|
||||||
|
"rquickjs-core",
|
||||||
|
"rquickjs-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rquickjs-core"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c8db6379e204ef84c0811e90e7cc3e3e4d7688701db68a00d14a6db6849087b"
|
||||||
|
dependencies = [
|
||||||
|
"async-lock",
|
||||||
|
"relative-path",
|
||||||
|
"rquickjs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rquickjs-macro"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6041104330c019fcd936026ae05e2446f5e8a2abef329d924f25424b7052a2f3"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case 0.6.0",
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"indexmap 2.11.1",
|
||||||
|
"proc-macro-crate 1.3.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rquickjs-core",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rquickjs-sys"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4bc352c6b663604c3c186c000cfcc6c271f4b50bc135a285dd6d4f2a42f9790a"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen 0.69.5",
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@@ -13426,6 +13769,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync_wrapper"
|
name = "sync_wrapper"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -14054,6 +14403,16 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-io-timeout"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -14202,6 +14561,17 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-tungstenite"
|
name = "tokio-tungstenite"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@@ -14336,6 +14706,33 @@ dependencies = [
|
|||||||
"winnow 0.7.14",
|
"winnow 0.7.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream",
|
||||||
|
"async-trait",
|
||||||
|
"axum 0.6.20",
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"h2 0.3.27",
|
||||||
|
"http 0.2.12",
|
||||||
|
"http-body 0.4.6",
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"hyper-timeout 0.4.1",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project",
|
||||||
|
"prost 0.12.6",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -14353,11 +14750,11 @@ dependencies = [
|
|||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.8.1",
|
"hyper 1.8.1",
|
||||||
"hyper-timeout",
|
"hyper-timeout 0.5.2",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"rustls-native-certs 0.8.3",
|
"rustls-native-certs 0.8.3",
|
||||||
"rustls-pemfile 2.2.0",
|
"rustls-pemfile 2.2.0",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
@@ -14371,6 +14768,26 @@ dependencies = [
|
|||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tonic-web"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc3b0e1cedbf19fdfb78ef3d672cb9928e0a91a9cb4629cc0c916e8cff8aaaa1"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bytes",
|
||||||
|
"http 0.2.12",
|
||||||
|
"http-body 0.4.6",
|
||||||
|
"hyper 0.14.32",
|
||||||
|
"pin-project",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic 0.11.0",
|
||||||
|
"tower-http 0.4.4",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -14400,7 +14817,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"sync_wrapper",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
@@ -14425,6 +14842,43 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"http-body 0.4.6",
|
||||||
|
"http-range-header",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.4",
|
||||||
|
"bytes",
|
||||||
|
"http 1.4.0",
|
||||||
|
"http-body 1.0.1",
|
||||||
|
"http-body-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@@ -14790,6 +15244,15 @@ dependencies = [
|
|||||||
"web-time",
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uncased"
|
||||||
|
version = "0.9.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unic-char-property"
|
name = "unic-char-property"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -15718,10 +16181,10 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tungstenite 0.24.0",
|
"tokio-tungstenite 0.24.0",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
"tower 0.5.3",
|
"tower 0.5.3",
|
||||||
"tower-cookies",
|
"tower-cookies",
|
||||||
"tower-http",
|
"tower-http 0.6.8",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ulid",
|
"ulid",
|
||||||
@@ -15866,7 +16329,7 @@ dependencies = [
|
|||||||
"tokio-postgres 0.7.13",
|
"tokio-postgres 0.7.13",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tonic",
|
"tonic 0.12.3",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
@@ -15920,6 +16383,29 @@ dependencies = [
|
|||||||
"windmill-common",
|
"windmill-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windmill-local"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-recursion",
|
||||||
|
"axum 0.7.9",
|
||||||
|
"chrono",
|
||||||
|
"libsql",
|
||||||
|
"rquickjs",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
|
"tower 0.4.13",
|
||||||
|
"tower-http 0.5.2",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
|
"windmill-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windmill-macros"
|
name = "windmill-macros"
|
||||||
version = "1.613.4"
|
version = "1.613.4"
|
||||||
@@ -17128,13 +17614,34 @@ dependencies = [
|
|||||||
"synstructure 0.13.2",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"zerocopy-derive 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.33"
|
version = "0.8.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive 0.8.33",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ members = [
|
|||||||
"./windmill-indexer",
|
"./windmill-indexer",
|
||||||
"./windmill-macros",
|
"./windmill-macros",
|
||||||
"./windmill-oauth",
|
"./windmill-oauth",
|
||||||
|
"./windmill-local",
|
||||||
"./parsers/windmill-parser",
|
"./parsers/windmill-parser",
|
||||||
"./parsers/windmill-parser-ts",
|
"./parsers/windmill-parser-ts",
|
||||||
"./parsers/windmill-parser-go",
|
"./parsers/windmill-parser-go",
|
||||||
|
|||||||
52
backend/windmill-local/Cargo.toml
Normal file
52
backend/windmill-local/Cargo.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[package]
|
||||||
|
name = "windmill-local"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Windmill local mode with libSQL/Turso support for preview execution"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# libSQL - Turso's SQLite fork (used for local and remote Turso connections)
|
||||||
|
# Note: libsql IS the Turso database driver - Turso is built on libSQL
|
||||||
|
libsql = "0.9"
|
||||||
|
|
||||||
|
# Async runtime
|
||||||
|
tokio = { version = "1", features = ["full", "sync", "macros"] }
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
anyhow = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
# UUID for job IDs
|
||||||
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
# Tracing
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
# HTTP server
|
||||||
|
axum = "0.7"
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||||
|
|
||||||
|
# Windmill types (flows, scripts, etc.)
|
||||||
|
windmill-common = { path = "../windmill-common", default-features = false }
|
||||||
|
|
||||||
|
# For input transforms (JavaScript evaluation)
|
||||||
|
rquickjs = { version = "0.8", features = ["bindgen", "classes", "loader", "array-buffer", "futures"] }
|
||||||
|
|
||||||
|
# For recursive async functions
|
||||||
|
async-recursion = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-test = "0.4"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "local_server"
|
||||||
|
path = "examples/local_server.rs"
|
||||||
65
backend/windmill-local/examples/local_server.rs
Normal file
65
backend/windmill-local/examples/local_server.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
//! Example: Run the Windmill local server
|
||||||
|
//!
|
||||||
|
//! This demonstrates running a local Windmill server with libSQL/SQLite backend.
|
||||||
|
//!
|
||||||
|
//! Run with:
|
||||||
|
//! cargo run -p windmill-local --example local_server
|
||||||
|
//!
|
||||||
|
//! Test with:
|
||||||
|
//! # Health check
|
||||||
|
//! curl http://localhost:8000/health
|
||||||
|
//!
|
||||||
|
//! # Run a bash preview (async)
|
||||||
|
//! curl -X POST http://localhost:8000/api/w/local/jobs/run/preview \
|
||||||
|
//! -H "Content-Type: application/json" \
|
||||||
|
//! -d '{"content": "echo Hello World", "language": "bash"}'
|
||||||
|
//!
|
||||||
|
//! # Run a bash preview and wait for result
|
||||||
|
//! curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview \
|
||||||
|
//! -H "Content-Type: application/json" \
|
||||||
|
//! -d '{"content": "echo 42", "language": "bash"}'
|
||||||
|
//!
|
||||||
|
//! # Run a Python preview
|
||||||
|
//! curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview \
|
||||||
|
//! -H "Content-Type: application/json" \
|
||||||
|
//! -d '{"content": "def main(x=1): return x * 2", "language": "python3", "args": {"x": 21}}'
|
||||||
|
//!
|
||||||
|
//! # Run a flow preview (linear flow with two steps)
|
||||||
|
//! curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview_flow \
|
||||||
|
//! -H "Content-Type: application/json" \
|
||||||
|
//! -d '{
|
||||||
|
//! "value": {
|
||||||
|
//! "modules": [
|
||||||
|
//! {"id": "step1", "value": {"type": "rawscript", "language": "bash", "content": "echo 10"}},
|
||||||
|
//! {"id": "step2", "value": {"type": "identity"}}
|
||||||
|
//! ]
|
||||||
|
//! },
|
||||||
|
//! "args": {}
|
||||||
|
//! }'
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use windmill_local::LocalServer;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Initialize tracing
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::from_default_env()
|
||||||
|
.add_directive("windmill_local=info".parse().unwrap()),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let addr: SocketAddr = "0.0.0.0:8000".parse().unwrap();
|
||||||
|
println!("Starting Windmill Local Server on {}", addr);
|
||||||
|
println!();
|
||||||
|
println!("Test endpoints:");
|
||||||
|
println!(" Health: curl http://localhost:8000/health");
|
||||||
|
println!(" Preview: curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview \\");
|
||||||
|
println!(" -H 'Content-Type: application/json' \\");
|
||||||
|
println!(" -d '{{\"content\": \"echo Hello\", \"language\": \"bash\"}}'");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let server = LocalServer::new(addr).await.expect("Failed to create server");
|
||||||
|
server.run().await.expect("Server error");
|
||||||
|
}
|
||||||
161
backend/windmill-local/src/db.rs
Normal file
161
backend/windmill-local/src/db.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
//! Database connection and initialization for local mode
|
||||||
|
|
||||||
|
use libsql::{Builder, Connection, Database};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::schema;
|
||||||
|
|
||||||
|
/// Local database wrapper
|
||||||
|
///
|
||||||
|
/// For local mode, we use a single connection with in-process coordination.
|
||||||
|
/// This simplifies the implementation since we don't need `FOR UPDATE SKIP LOCKED`.
|
||||||
|
pub struct LocalDb {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
db: Database,
|
||||||
|
/// Single connection for all operations (simplifies transaction handling)
|
||||||
|
conn: Arc<Mutex<Connection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalDb {
|
||||||
|
/// Create an in-memory database (for testing/ephemeral use)
|
||||||
|
pub async fn in_memory() -> Result<Self> {
|
||||||
|
let db = Builder::new_local(":memory:").build().await?;
|
||||||
|
let conn = db.connect()?;
|
||||||
|
let local_db = Self {
|
||||||
|
db,
|
||||||
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
|
};
|
||||||
|
local_db.init_schema().await?;
|
||||||
|
Ok(local_db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a file-based database
|
||||||
|
pub async fn file(path: &str) -> Result<Self> {
|
||||||
|
let db = Builder::new_local(path).build().await?;
|
||||||
|
let conn = db.connect()?;
|
||||||
|
let local_db = Self {
|
||||||
|
db,
|
||||||
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
|
};
|
||||||
|
local_db.init_schema().await?;
|
||||||
|
Ok(local_db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a Turso remote database connection
|
||||||
|
/// This would be used for the multi-writer scenario
|
||||||
|
pub async fn turso_remote(url: &str, auth_token: &str) -> Result<Self> {
|
||||||
|
let db = Builder::new_remote(url.to_string(), auth_token.to_string())
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
let conn = db.connect()?;
|
||||||
|
let local_db = Self {
|
||||||
|
db,
|
||||||
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
|
};
|
||||||
|
local_db.init_schema().await?;
|
||||||
|
Ok(local_db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the schema
|
||||||
|
async fn init_schema(&self) -> Result<()> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
// Execute schema as multiple statements
|
||||||
|
conn.execute_batch(schema::SCHEMA).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the database (drop and recreate all tables)
|
||||||
|
pub async fn reset(&self) -> Result<()> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
conn.execute_batch(schema::DROP_SCHEMA).await?;
|
||||||
|
conn.execute_batch(schema::SCHEMA).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the connection (locked)
|
||||||
|
pub async fn conn(&self) -> tokio::sync::MutexGuard<'_, Connection> {
|
||||||
|
self.conn.lock().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a simple query that returns no rows
|
||||||
|
pub async fn execute(&self, sql: &str, params: impl libsql::params::IntoParams) -> Result<u64> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
let rows_affected = conn.execute(sql, params).await?;
|
||||||
|
Ok(rows_affected)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a query and return all rows
|
||||||
|
pub async fn query(
|
||||||
|
&self,
|
||||||
|
sql: &str,
|
||||||
|
params: impl libsql::params::IntoParams,
|
||||||
|
) -> Result<libsql::Rows> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
let rows = conn.query(sql, params).await?;
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a batch of statements (for transactions)
|
||||||
|
pub async fn execute_batch(&self, sql: &str) -> Result<()> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
conn.execute_batch(sql).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_in_memory_db() {
|
||||||
|
let db = LocalDb::in_memory().await.unwrap();
|
||||||
|
|
||||||
|
// Verify tables exist
|
||||||
|
let rows = db
|
||||||
|
.query(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut tables = Vec::new();
|
||||||
|
let mut rows = rows;
|
||||||
|
while let Some(row) = rows.next().await.unwrap() {
|
||||||
|
let name: String = row.get(0).unwrap();
|
||||||
|
tables.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(tables.contains(&"v2_job".to_string()));
|
||||||
|
assert!(tables.contains(&"v2_job_queue".to_string()));
|
||||||
|
assert!(tables.contains(&"v2_job_completed".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reset_db() {
|
||||||
|
let db = LocalDb::in_memory().await.unwrap();
|
||||||
|
|
||||||
|
// Insert a job
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO v2_job (id, kind) VALUES ('test-uuid', 'preview')",
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
db.reset().await.unwrap();
|
||||||
|
|
||||||
|
// Verify job is gone
|
||||||
|
let mut rows = db
|
||||||
|
.query("SELECT COUNT(*) FROM v2_job", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let row = rows.next().await.unwrap().unwrap();
|
||||||
|
let count: i64 = row.get(0).unwrap();
|
||||||
|
assert_eq!(count, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
backend/windmill-local/src/error.rs
Normal file
29
backend/windmill-local/src/error.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//! Error types for local mode
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum LocalError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] libsql::Error),
|
||||||
|
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serialization(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Job not found: {0}")]
|
||||||
|
JobNotFound(uuid::Uuid),
|
||||||
|
|
||||||
|
#[error("Invalid job state: {0}")]
|
||||||
|
InvalidJobState(String),
|
||||||
|
|
||||||
|
#[error("Queue is empty")]
|
||||||
|
QueueEmpty,
|
||||||
|
|
||||||
|
#[error("Execution error: {0}")]
|
||||||
|
Execution(String),
|
||||||
|
|
||||||
|
#[error("Timeout")]
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, LocalError>;
|
||||||
330
backend/windmill-local/src/executor.rs
Normal file
330
backend/windmill-local/src/executor.rs
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
//! Simple script executor for local mode
|
||||||
|
//!
|
||||||
|
//! This is a minimal executor that supports a few languages for demonstration.
|
||||||
|
//! A full implementation would integrate with windmill-worker's execution logic.
|
||||||
|
|
||||||
|
use std::process::Stdio;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
use crate::error::{LocalError, Result};
|
||||||
|
use crate::jobs::ScriptLang;
|
||||||
|
|
||||||
|
/// Result of script execution
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExecutionResult {
|
||||||
|
pub success: bool,
|
||||||
|
pub result: serde_json::Value,
|
||||||
|
pub logs: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a script with the given language and arguments
|
||||||
|
pub async fn execute_script(
|
||||||
|
language: ScriptLang,
|
||||||
|
code: &str,
|
||||||
|
args: &serde_json::Value,
|
||||||
|
) -> Result<ExecutionResult> {
|
||||||
|
match language {
|
||||||
|
ScriptLang::Bash => execute_bash(code, args).await,
|
||||||
|
ScriptLang::Python3 => execute_python(code, args).await,
|
||||||
|
ScriptLang::Deno => execute_deno(code, args).await,
|
||||||
|
ScriptLang::Bun => execute_bun(code, args).await,
|
||||||
|
_ => Ok(ExecutionResult {
|
||||||
|
success: false,
|
||||||
|
result: serde_json::json!({
|
||||||
|
"error": format!("Language {:?} not supported in local mode yet", language)
|
||||||
|
}),
|
||||||
|
logs: format!("Language {:?} not supported in local mode", language),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a bash script
|
||||||
|
async fn execute_bash(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
|
||||||
|
// Create environment variables from args
|
||||||
|
let mut env_vars = Vec::new();
|
||||||
|
if let serde_json::Value::Object(map) = args {
|
||||||
|
for (key, value) in map {
|
||||||
|
let val_str = match value {
|
||||||
|
serde_json::Value::String(s) => s.clone(),
|
||||||
|
_ => value.to_string(),
|
||||||
|
};
|
||||||
|
env_vars.push((key.to_uppercase(), val_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = Command::new("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(code)
|
||||||
|
.envs(env_vars)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to spawn bash: {}", e)))?;
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to wait for bash: {}", e)))?;
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
|
||||||
|
if let Some(mut out) = child.stdout.take() {
|
||||||
|
out.read_to_string(&mut stdout).await.ok();
|
||||||
|
}
|
||||||
|
if let Some(mut err) = child.stderr.take() {
|
||||||
|
err.read_to_string(&mut stderr).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let logs = format!("{}{}", stdout, stderr);
|
||||||
|
let success = status.success();
|
||||||
|
|
||||||
|
// Try to parse stdout as JSON, otherwise use as string
|
||||||
|
let result = if success {
|
||||||
|
let trimmed = stdout.trim();
|
||||||
|
serde_json::from_str(trimmed).unwrap_or_else(|_| serde_json::json!(trimmed))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "error": stderr.trim() })
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExecutionResult {
|
||||||
|
success,
|
||||||
|
result,
|
||||||
|
logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a Python script
|
||||||
|
async fn execute_python(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
|
||||||
|
// Wrap the code to handle args and return JSON result
|
||||||
|
let wrapped_code = format!(
|
||||||
|
r#"
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Args passed as JSON
|
||||||
|
args = json.loads('''{}''')
|
||||||
|
|
||||||
|
# User code
|
||||||
|
{}
|
||||||
|
|
||||||
|
# Call main if it exists
|
||||||
|
if 'main' in dir():
|
||||||
|
result = main(**args)
|
||||||
|
print(json.dumps(result))
|
||||||
|
"#,
|
||||||
|
serde_json::to_string(args)?,
|
||||||
|
code
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = Command::new("python3")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(&wrapped_code)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to spawn python3: {}", e)))?;
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to wait for python3: {}", e)))?;
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
|
||||||
|
if let Some(mut out) = child.stdout.take() {
|
||||||
|
out.read_to_string(&mut stdout).await.ok();
|
||||||
|
}
|
||||||
|
if let Some(mut err) = child.stderr.take() {
|
||||||
|
err.read_to_string(&mut stderr).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let logs = format!("{}{}", stdout, stderr);
|
||||||
|
let success = status.success();
|
||||||
|
|
||||||
|
let result = if success {
|
||||||
|
let trimmed = stdout.trim();
|
||||||
|
// Get the last line as result (in case there's debug output)
|
||||||
|
let last_line = trimmed.lines().last().unwrap_or("");
|
||||||
|
serde_json::from_str(last_line).unwrap_or_else(|_| serde_json::json!(trimmed))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "error": stderr.trim() })
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExecutionResult {
|
||||||
|
success,
|
||||||
|
result,
|
||||||
|
logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a Deno/TypeScript script
|
||||||
|
async fn execute_deno(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
|
||||||
|
// Wrap the code to handle args and return JSON result
|
||||||
|
let wrapped_code = format!(
|
||||||
|
r#"
|
||||||
|
const args = {};
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
// Call main if it exists
|
||||||
|
if (typeof main === 'function') {{
|
||||||
|
const result = await main(args);
|
||||||
|
console.log(JSON.stringify(result));
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
serde_json::to_string(args)?,
|
||||||
|
code
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = Command::new("deno")
|
||||||
|
.arg("eval")
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg(&wrapped_code)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to spawn deno: {}", e)))?;
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to wait for deno: {}", e)))?;
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
|
||||||
|
if let Some(mut out) = child.stdout.take() {
|
||||||
|
out.read_to_string(&mut stdout).await.ok();
|
||||||
|
}
|
||||||
|
if let Some(mut err) = child.stderr.take() {
|
||||||
|
err.read_to_string(&mut stderr).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let logs = format!("{}{}", stdout, stderr);
|
||||||
|
let success = status.success();
|
||||||
|
|
||||||
|
let result = if success {
|
||||||
|
let trimmed = stdout.trim();
|
||||||
|
let last_line = trimmed.lines().last().unwrap_or("");
|
||||||
|
serde_json::from_str(last_line).unwrap_or_else(|_| serde_json::json!(trimmed))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "error": stderr.trim() })
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExecutionResult {
|
||||||
|
success,
|
||||||
|
result,
|
||||||
|
logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a Bun/TypeScript script
|
||||||
|
async fn execute_bun(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
|
||||||
|
// Similar to Deno but using Bun
|
||||||
|
let wrapped_code = format!(
|
||||||
|
r#"
|
||||||
|
const args = {};
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
// Call main if it exists
|
||||||
|
if (typeof main === 'function') {{
|
||||||
|
const result = await main(args);
|
||||||
|
console.log(JSON.stringify(result));
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
serde_json::to_string(args)?,
|
||||||
|
code
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child = Command::new("bun")
|
||||||
|
.arg("eval")
|
||||||
|
.arg(&wrapped_code)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to spawn bun: {}", e)))?;
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to wait for bun: {}", e)))?;
|
||||||
|
|
||||||
|
let mut stdout = String::new();
|
||||||
|
let mut stderr = String::new();
|
||||||
|
|
||||||
|
if let Some(mut out) = child.stdout.take() {
|
||||||
|
out.read_to_string(&mut stdout).await.ok();
|
||||||
|
}
|
||||||
|
if let Some(mut err) = child.stderr.take() {
|
||||||
|
err.read_to_string(&mut stderr).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let logs = format!("{}{}", stdout, stderr);
|
||||||
|
let success = status.success();
|
||||||
|
|
||||||
|
let result = if success {
|
||||||
|
let trimmed = stdout.trim();
|
||||||
|
let last_line = trimmed.lines().last().unwrap_or("");
|
||||||
|
serde_json::from_str(last_line).unwrap_or_else(|_| serde_json::json!(trimmed))
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "error": stderr.trim() })
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExecutionResult {
|
||||||
|
success,
|
||||||
|
result,
|
||||||
|
logs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bash_execution() {
|
||||||
|
let result = execute_script(
|
||||||
|
ScriptLang::Bash,
|
||||||
|
"echo 42",
|
||||||
|
&serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
// Output "42" is parsed as JSON number
|
||||||
|
assert_eq!(result.result, serde_json::json!(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bash_with_args() {
|
||||||
|
let result = execute_script(
|
||||||
|
ScriptLang::Bash,
|
||||||
|
"echo $NAME",
|
||||||
|
&serde_json::json!({"name": "world"}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
assert_eq!(result.result, serde_json::json!("world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bash_json_output() {
|
||||||
|
let result = execute_script(
|
||||||
|
ScriptLang::Bash,
|
||||||
|
r#"echo '{"key": "value"}'"#,
|
||||||
|
&serde_json::json!({}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.success);
|
||||||
|
assert_eq!(result.result, serde_json::json!({"key": "value"}));
|
||||||
|
}
|
||||||
|
}
|
||||||
750
backend/windmill-local/src/flow_executor.rs
Normal file
750
backend/windmill-local/src/flow_executor.rs
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
//! Flow executor for local mode
|
||||||
|
//!
|
||||||
|
//! This module implements flow execution using the real Windmill flow types
|
||||||
|
//! from windmill-common, but with libSQL as the backend.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::value::RawValue;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use windmill_common::flows::{
|
||||||
|
Branch, FlowModule, FlowModuleValue, FlowValue, InputTransform,
|
||||||
|
};
|
||||||
|
use windmill_common::scripts::ScriptLang as WmScriptLang;
|
||||||
|
|
||||||
|
use crate::db::LocalDb;
|
||||||
|
use crate::error::{LocalError, Result};
|
||||||
|
use crate::executor::{execute_script, ExecutionResult};
|
||||||
|
use crate::jobs::ScriptLang;
|
||||||
|
|
||||||
|
/// Flow execution state
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FlowStatus {
|
||||||
|
pub step: usize,
|
||||||
|
pub modules: Vec<ModuleStatus>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub failure_module: Option<FailureModule>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub retry: Option<RetryStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ModuleStatus {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub status_type: ModuleStatusType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub result: Option<serde_json::Value>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub iterator: Option<IteratorStatus>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub branch_chosen: Option<BranchChosen>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub branchall: Option<BranchAllStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub enum ModuleStatusType {
|
||||||
|
WaitingForPriorSteps,
|
||||||
|
WaitingForEvents,
|
||||||
|
WaitingForExecutor,
|
||||||
|
InProgress,
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
Skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IteratorStatus {
|
||||||
|
pub index: usize,
|
||||||
|
pub itered: Vec<serde_json::Value>,
|
||||||
|
pub args: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BranchChosen {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub branch_type: String,
|
||||||
|
pub branch: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BranchAllStatus {
|
||||||
|
pub branch: usize,
|
||||||
|
pub len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FailureModule {
|
||||||
|
pub id: String,
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RetryStatus {
|
||||||
|
pub fail_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context passed through flow execution
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FlowContext {
|
||||||
|
pub flow_input: serde_json::Value,
|
||||||
|
pub previous_result: serde_json::Value,
|
||||||
|
pub results_by_id: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlowContext {
|
||||||
|
pub fn new(flow_input: serde_json::Value) -> Self {
|
||||||
|
Self {
|
||||||
|
flow_input: flow_input.clone(),
|
||||||
|
previous_result: flow_input,
|
||||||
|
results_by_id: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the evaluation context for input transforms
|
||||||
|
pub fn to_eval_context(&self) -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"flow_input": self.flow_input,
|
||||||
|
"previous_result": self.previous_result,
|
||||||
|
"results": self.results_by_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a flow and return the result
|
||||||
|
pub async fn execute_flow(
|
||||||
|
db: &LocalDb,
|
||||||
|
flow_value: &FlowValue,
|
||||||
|
flow_input: serde_json::Value,
|
||||||
|
) -> Result<(serde_json::Value, FlowStatus)> {
|
||||||
|
let mut ctx = FlowContext::new(flow_input);
|
||||||
|
let mut status = FlowStatus {
|
||||||
|
step: 0,
|
||||||
|
modules: Vec::new(),
|
||||||
|
failure_module: None,
|
||||||
|
retry: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute modules sequentially
|
||||||
|
for (idx, module) in flow_value.modules.iter().enumerate() {
|
||||||
|
status.step = idx;
|
||||||
|
|
||||||
|
let module_status = ModuleStatus {
|
||||||
|
id: module.id.clone(),
|
||||||
|
status_type: ModuleStatusType::InProgress,
|
||||||
|
result: None,
|
||||||
|
iterator: None,
|
||||||
|
branch_chosen: None,
|
||||||
|
branchall: None,
|
||||||
|
};
|
||||||
|
status.modules.push(module_status);
|
||||||
|
|
||||||
|
tracing::info!("Executing flow module {}: {}", idx, module.id);
|
||||||
|
|
||||||
|
match execute_module(db, module, &mut ctx).await {
|
||||||
|
Ok(result) => {
|
||||||
|
// Update context with result
|
||||||
|
ctx.results_by_id.insert(module.id.clone(), result.clone());
|
||||||
|
ctx.previous_result = result.clone();
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
if let Some(ms) = status.modules.last_mut() {
|
||||||
|
ms.status_type = ModuleStatusType::Success;
|
||||||
|
ms.result = Some(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Module failed
|
||||||
|
tracing::error!("Flow module {} failed: {}", module.id, e);
|
||||||
|
|
||||||
|
if let Some(ms) = status.modules.last_mut() {
|
||||||
|
ms.status_type = ModuleStatusType::Failure;
|
||||||
|
ms.result = Some(serde_json::json!({"error": e.to_string()}));
|
||||||
|
}
|
||||||
|
|
||||||
|
status.failure_module = Some(FailureModule {
|
||||||
|
id: module.id.clone(),
|
||||||
|
error: e.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if continue_on_error is set
|
||||||
|
if !module.continue_on_error.unwrap_or(false) {
|
||||||
|
return Ok((serde_json::json!({"error": e.to_string()}), status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((ctx.previous_result, status))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a single flow module
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
async fn execute_module(
|
||||||
|
db: &LocalDb,
|
||||||
|
module: &FlowModule,
|
||||||
|
ctx: &mut FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
// Check skip_if condition
|
||||||
|
if let Some(skip_if) = &module.skip_if {
|
||||||
|
let should_skip = evaluate_expr(&skip_if.expr, &ctx.to_eval_context())?;
|
||||||
|
if should_skip.as_bool().unwrap_or(false) {
|
||||||
|
tracing::info!("Skipping module {} due to skip_if condition", module.id);
|
||||||
|
return Ok(ctx.previous_result.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the module value
|
||||||
|
let module_value: FlowModuleValue = serde_json::from_str(module.value.get())
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Failed to parse module value: {}", e)))?;
|
||||||
|
|
||||||
|
match module_value {
|
||||||
|
FlowModuleValue::Identity => {
|
||||||
|
Ok(ctx.previous_result.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::RawScript { content, language, input_transforms, .. } => {
|
||||||
|
let args = resolve_input_transforms(&input_transforms, ctx)?;
|
||||||
|
let lang = convert_script_lang(&language);
|
||||||
|
let result = execute_script(lang, &content, &args).await?;
|
||||||
|
if result.success {
|
||||||
|
Ok(result.result)
|
||||||
|
} else {
|
||||||
|
Err(LocalError::Execution(result.result.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::Script { path, input_transforms, .. } => {
|
||||||
|
// In local mode, we don't have access to saved scripts
|
||||||
|
Err(LocalError::Execution(format!(
|
||||||
|
"Script references (path: {}) are not supported in local mode. Use rawscript instead.",
|
||||||
|
path
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::Flow { path, .. } => {
|
||||||
|
// In local mode, we don't have access to saved flows
|
||||||
|
Err(LocalError::Execution(format!(
|
||||||
|
"Flow references (path: {}) are not supported in local mode. Use inline modules instead.",
|
||||||
|
path
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::ForloopFlow { iterator, modules, skip_failures, parallel, .. } => {
|
||||||
|
execute_forloop(db, &iterator, &modules, skip_failures, parallel, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::WhileloopFlow { modules, skip_failures, .. } => {
|
||||||
|
execute_whileloop(db, &modules, skip_failures, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::BranchOne { branches, default, .. } => {
|
||||||
|
execute_branch_one(db, &branches, &default, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::BranchAll { branches, parallel } => {
|
||||||
|
execute_branch_all(db, &branches, parallel, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::FlowScript { .. } => {
|
||||||
|
Err(LocalError::Execution(
|
||||||
|
"FlowScript (internal reference) is not supported in local mode".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowModuleValue::AIAgent { .. } => {
|
||||||
|
Err(LocalError::Execution(
|
||||||
|
"AIAgent is not supported in local mode".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a for loop
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
async fn execute_forloop(
|
||||||
|
db: &LocalDb,
|
||||||
|
iterator: &InputTransform,
|
||||||
|
modules: &[FlowModule],
|
||||||
|
skip_failures: bool,
|
||||||
|
_parallel: bool, // TODO: implement parallel execution
|
||||||
|
ctx: &mut FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
// Evaluate the iterator expression
|
||||||
|
let iter_value = evaluate_input_transform(iterator, ctx)?;
|
||||||
|
|
||||||
|
let items = match iter_value.as_array() {
|
||||||
|
Some(arr) => arr.clone(),
|
||||||
|
None => {
|
||||||
|
return Err(LocalError::Execution(
|
||||||
|
"For loop iterator must evaluate to an array".to_string()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
for (idx, item) in items.iter().enumerate() {
|
||||||
|
tracing::debug!("For loop iteration {} of {}", idx + 1, items.len());
|
||||||
|
|
||||||
|
// Create iteration context
|
||||||
|
let mut iter_ctx = FlowContext {
|
||||||
|
flow_input: ctx.flow_input.clone(),
|
||||||
|
previous_result: item.clone(),
|
||||||
|
results_by_id: ctx.results_by_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add iter context
|
||||||
|
iter_ctx.results_by_id.insert("iter".to_string(), serde_json::json!({
|
||||||
|
"index": idx,
|
||||||
|
"value": item,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Execute modules in sequence
|
||||||
|
let mut iter_result = item.clone();
|
||||||
|
let mut had_error = false;
|
||||||
|
|
||||||
|
for module in modules {
|
||||||
|
match execute_module(db, module, &mut iter_ctx).await {
|
||||||
|
Ok(result) => {
|
||||||
|
iter_ctx.results_by_id.insert(module.id.clone(), result.clone());
|
||||||
|
iter_ctx.previous_result = result.clone();
|
||||||
|
iter_result = result;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
had_error = true;
|
||||||
|
if skip_failures {
|
||||||
|
tracing::warn!("For loop iteration {} failed (skipping): {}", idx, e);
|
||||||
|
iter_result = serde_json::json!({"error": e.to_string()});
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !had_error || skip_failures {
|
||||||
|
results.push(iter_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::Value::Array(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a while loop
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
async fn execute_whileloop(
|
||||||
|
db: &LocalDb,
|
||||||
|
modules: &[FlowModule],
|
||||||
|
skip_failures: bool,
|
||||||
|
ctx: &mut FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
const MAX_ITERATIONS: usize = 1000;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut iteration = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if iteration >= MAX_ITERATIONS {
|
||||||
|
return Err(LocalError::Execution(format!(
|
||||||
|
"While loop exceeded maximum iterations ({})", MAX_ITERATIONS
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute modules
|
||||||
|
let mut iter_result = ctx.previous_result.clone();
|
||||||
|
let mut should_continue = true;
|
||||||
|
|
||||||
|
for module in modules {
|
||||||
|
match execute_module(db, module, ctx).await {
|
||||||
|
Ok(result) => {
|
||||||
|
ctx.results_by_id.insert(module.id.clone(), result.clone());
|
||||||
|
ctx.previous_result = result.clone();
|
||||||
|
iter_result = result;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if skip_failures {
|
||||||
|
tracing::warn!("While loop iteration {} failed (skipping): {}", iteration, e);
|
||||||
|
iter_result = serde_json::json!({"error": e.to_string()});
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
should_continue = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(iter_result);
|
||||||
|
iteration += 1;
|
||||||
|
|
||||||
|
// Check stop condition (result should be truthy to continue)
|
||||||
|
if !should_continue {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the result indicates we should stop
|
||||||
|
let continue_loop = match &ctx.previous_result {
|
||||||
|
serde_json::Value::Bool(b) => *b,
|
||||||
|
serde_json::Value::Null => false,
|
||||||
|
serde_json::Value::Object(obj) => {
|
||||||
|
// Check for a "continue" field
|
||||||
|
obj.get("continue")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !continue_loop {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::Value::Array(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute branch-one (if/else)
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
async fn execute_branch_one(
|
||||||
|
db: &LocalDb,
|
||||||
|
branches: &[Branch],
|
||||||
|
default: &[FlowModule],
|
||||||
|
ctx: &mut FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
// Find the first matching branch
|
||||||
|
for (idx, branch) in branches.iter().enumerate() {
|
||||||
|
let condition = evaluate_expr(&branch.expr, &ctx.to_eval_context())?;
|
||||||
|
if condition.as_bool().unwrap_or(false) {
|
||||||
|
tracing::debug!("Branch {} matched", idx);
|
||||||
|
return execute_branch_modules(db, &branch.modules, ctx).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No branch matched, execute default
|
||||||
|
tracing::debug!("No branch matched, executing default");
|
||||||
|
execute_branch_modules(db, default, ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute branch-all (parallel branches)
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
async fn execute_branch_all(
|
||||||
|
db: &LocalDb,
|
||||||
|
branches: &[Branch],
|
||||||
|
_parallel: bool, // TODO: implement true parallel execution
|
||||||
|
ctx: &mut FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
// Execute all branches (sequentially for now)
|
||||||
|
for (idx, branch) in branches.iter().enumerate() {
|
||||||
|
tracing::debug!("Executing branch {}", idx);
|
||||||
|
|
||||||
|
let mut branch_ctx = ctx.clone();
|
||||||
|
match execute_branch_modules(db, &branch.modules, &mut branch_ctx).await {
|
||||||
|
Ok(result) => {
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if branch.skip_failure {
|
||||||
|
tracing::warn!("Branch {} failed (skipping): {}", idx, e);
|
||||||
|
results.push(serde_json::json!({"error": e.to_string()}));
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::Value::Array(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a sequence of modules in a branch
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
async fn execute_branch_modules(
|
||||||
|
db: &LocalDb,
|
||||||
|
modules: &[FlowModule],
|
||||||
|
ctx: &mut FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let mut result = ctx.previous_result.clone();
|
||||||
|
|
||||||
|
for module in modules {
|
||||||
|
result = execute_module(db, module, ctx).await?;
|
||||||
|
ctx.results_by_id.insert(module.id.clone(), result.clone());
|
||||||
|
ctx.previous_result = result.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve input transforms to concrete arguments
|
||||||
|
fn resolve_input_transforms(
|
||||||
|
transforms: &HashMap<String, InputTransform>,
|
||||||
|
ctx: &FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let mut args = serde_json::Map::new();
|
||||||
|
|
||||||
|
for (key, transform) in transforms {
|
||||||
|
let value = evaluate_input_transform(transform, ctx)?;
|
||||||
|
args.insert(key.clone(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::Value::Object(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate an input transform
|
||||||
|
fn evaluate_input_transform(
|
||||||
|
transform: &InputTransform,
|
||||||
|
ctx: &FlowContext,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
match transform {
|
||||||
|
InputTransform::Static { value } => {
|
||||||
|
serde_json::from_str(value.get())
|
||||||
|
.map_err(|e| LocalError::Execution(format!("Invalid static value: {}", e)))
|
||||||
|
}
|
||||||
|
InputTransform::Javascript { expr } => {
|
||||||
|
evaluate_expr(expr, &ctx.to_eval_context())
|
||||||
|
}
|
||||||
|
InputTransform::Ai => {
|
||||||
|
Err(LocalError::Execution("AI input transforms are not supported in local mode".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a JavaScript expression
|
||||||
|
fn evaluate_expr(expr: &str, context: &serde_json::Value) -> Result<serde_json::Value> {
|
||||||
|
let expr = expr.trim();
|
||||||
|
|
||||||
|
// Handle comparison operators
|
||||||
|
if let Some(result) = try_evaluate_comparison(expr, context) {
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle simple variable references
|
||||||
|
if let Some(val) = resolve_path(expr, context) {
|
||||||
|
return Ok(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle boolean literals
|
||||||
|
if expr == "true" {
|
||||||
|
return Ok(serde_json::Value::Bool(true));
|
||||||
|
}
|
||||||
|
if expr == "false" {
|
||||||
|
return Ok(serde_json::Value::Bool(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle numeric literals
|
||||||
|
if let Ok(n) = expr.parse::<i64>() {
|
||||||
|
return Ok(serde_json::json!(n));
|
||||||
|
}
|
||||||
|
if let Ok(n) = expr.parse::<f64>() {
|
||||||
|
return Ok(serde_json::json!(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle string literals
|
||||||
|
if (expr.starts_with('"') && expr.ends_with('"')) ||
|
||||||
|
(expr.starts_with('\'') && expr.ends_with('\'')) {
|
||||||
|
return Ok(serde_json::json!(&expr[1..expr.len()-1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For complex expressions, we'd need a full JS runtime
|
||||||
|
tracing::warn!("Complex expression not evaluated: {}", expr);
|
||||||
|
Ok(serde_json::json!(expr))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to resolve a path like "flow_input.x" or "results.a.b"
|
||||||
|
fn resolve_path(path: &str, context: &serde_json::Value) -> Option<serde_json::Value> {
|
||||||
|
let parts: Vec<&str> = path.split('.').collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current = context.get(parts[0])?;
|
||||||
|
|
||||||
|
for part in &parts[1..] {
|
||||||
|
current = current.get(*part)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(current.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to evaluate a comparison expression
|
||||||
|
fn try_evaluate_comparison(expr: &str, context: &serde_json::Value) -> Option<serde_json::Value> {
|
||||||
|
// Supported operators: >, <, >=, <=, ==, !=, ===, !==
|
||||||
|
let operators = ["===", "!==", ">=", "<=", "==", "!=", ">", "<"];
|
||||||
|
|
||||||
|
for op in operators {
|
||||||
|
if let Some(pos) = expr.find(op) {
|
||||||
|
let left = expr[..pos].trim();
|
||||||
|
let right = expr[pos + op.len()..].trim();
|
||||||
|
|
||||||
|
let left_val = evaluate_expr(left, context).ok()?;
|
||||||
|
let right_val = evaluate_expr(right, context).ok()?;
|
||||||
|
|
||||||
|
let result = match op {
|
||||||
|
">" => compare_values(&left_val, &right_val, |a, b| a > b),
|
||||||
|
"<" => compare_values(&left_val, &right_val, |a, b| a < b),
|
||||||
|
">=" => compare_values(&left_val, &right_val, |a, b| a >= b),
|
||||||
|
"<=" => compare_values(&left_val, &right_val, |a, b| a <= b),
|
||||||
|
"==" | "===" => Some(left_val == right_val),
|
||||||
|
"!=" | "!==" => Some(left_val != right_val),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result.map(serde_json::Value::Bool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try logical operators
|
||||||
|
if let Some(pos) = expr.find("&&") {
|
||||||
|
let left = expr[..pos].trim();
|
||||||
|
let right = expr[pos + 2..].trim();
|
||||||
|
let left_val = evaluate_expr(left, context).ok()?;
|
||||||
|
let right_val = evaluate_expr(right, context).ok()?;
|
||||||
|
return Some(serde_json::Value::Bool(
|
||||||
|
is_truthy(&left_val) && is_truthy(&right_val)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pos) = expr.find("||") {
|
||||||
|
let left = expr[..pos].trim();
|
||||||
|
let right = expr[pos + 2..].trim();
|
||||||
|
let left_val = evaluate_expr(left, context).ok()?;
|
||||||
|
let right_val = evaluate_expr(right, context).ok()?;
|
||||||
|
return Some(serde_json::Value::Bool(
|
||||||
|
is_truthy(&left_val) || is_truthy(&right_val)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare two JSON values numerically
|
||||||
|
fn compare_values<F>(left: &serde_json::Value, right: &serde_json::Value, cmp: F) -> Option<bool>
|
||||||
|
where
|
||||||
|
F: Fn(f64, f64) -> bool,
|
||||||
|
{
|
||||||
|
let left_num = value_to_number(left)?;
|
||||||
|
let right_num = value_to_number(right)?;
|
||||||
|
Some(cmp(left_num, right_num))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a JSON value to a number
|
||||||
|
fn value_to_number(val: &serde_json::Value) -> Option<f64> {
|
||||||
|
match val {
|
||||||
|
serde_json::Value::Number(n) => n.as_f64(),
|
||||||
|
serde_json::Value::String(s) => s.parse().ok(),
|
||||||
|
serde_json::Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a value is truthy (JavaScript semantics)
|
||||||
|
fn is_truthy(val: &serde_json::Value) -> bool {
|
||||||
|
match val {
|
||||||
|
serde_json::Value::Null => false,
|
||||||
|
serde_json::Value::Bool(b) => *b,
|
||||||
|
serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
|
||||||
|
serde_json::Value::String(s) => !s.is_empty(),
|
||||||
|
serde_json::Value::Array(a) => !a.is_empty(),
|
||||||
|
serde_json::Value::Object(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert windmill-common ScriptLang to local ScriptLang
|
||||||
|
fn convert_script_lang(lang: &WmScriptLang) -> ScriptLang {
|
||||||
|
match lang {
|
||||||
|
WmScriptLang::Deno => ScriptLang::Deno,
|
||||||
|
WmScriptLang::Python3 => ScriptLang::Python3,
|
||||||
|
WmScriptLang::Bash => ScriptLang::Bash,
|
||||||
|
WmScriptLang::Go => ScriptLang::Go,
|
||||||
|
WmScriptLang::Bun => ScriptLang::Bun,
|
||||||
|
WmScriptLang::Nativets => ScriptLang::Deno, // Fallback to Deno
|
||||||
|
_ => ScriptLang::Bash, // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_evaluate_simple_expr() {
|
||||||
|
let ctx = serde_json::json!({
|
||||||
|
"flow_input": {"x": 10},
|
||||||
|
"previous_result": 42,
|
||||||
|
"results": {"a": "hello"},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input", &ctx).unwrap(),
|
||||||
|
serde_json::json!({"x": 10})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("previous_result", &ctx).unwrap(),
|
||||||
|
serde_json::json!(42)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("results.a", &ctx).unwrap(),
|
||||||
|
serde_json::json!("hello")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.x", &ctx).unwrap(),
|
||||||
|
serde_json::json!(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_evaluate_comparison_expr() {
|
||||||
|
let ctx = serde_json::json!({
|
||||||
|
"flow_input": {"x": 10, "y": 5},
|
||||||
|
"previous_result": 42,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.x > 5", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.x < 5", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.x >= 10", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.y == 5", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("previous_result > 40", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_evaluate_logical_expr() {
|
||||||
|
let ctx = serde_json::json!({
|
||||||
|
"flow_input": {"a": true, "b": false},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simple boolean logic (complex expressions with mixed operators need parentheses support)
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.a && flow_input.a", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.a && flow_input.b", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate_expr("flow_input.b || flow_input.a", &ctx).unwrap(),
|
||||||
|
serde_json::Value::Bool(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
495
backend/windmill-local/src/jobs.rs
Normal file
495
backend/windmill-local/src/jobs.rs
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
//! Job types and operations for local mode
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db::LocalDb;
|
||||||
|
use crate::error::{LocalError, Result};
|
||||||
|
|
||||||
|
/// Job kind (mirrors windmill-common JobKind)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum JobKind {
|
||||||
|
Script,
|
||||||
|
Preview,
|
||||||
|
Flow,
|
||||||
|
FlowPreview,
|
||||||
|
Dependencies,
|
||||||
|
FlowDependencies,
|
||||||
|
ScriptHub,
|
||||||
|
Identity,
|
||||||
|
Http,
|
||||||
|
Graphql,
|
||||||
|
Postgresql,
|
||||||
|
Noop,
|
||||||
|
AppDependencies,
|
||||||
|
DeploymentCallback,
|
||||||
|
SingleScriptFlow,
|
||||||
|
FlowScript,
|
||||||
|
FlowNode,
|
||||||
|
AppScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobKind {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
JobKind::Script => "script",
|
||||||
|
JobKind::Preview => "preview",
|
||||||
|
JobKind::Flow => "flow",
|
||||||
|
JobKind::FlowPreview => "flowpreview",
|
||||||
|
JobKind::Dependencies => "dependencies",
|
||||||
|
JobKind::FlowDependencies => "flowdependencies",
|
||||||
|
JobKind::ScriptHub => "script_hub",
|
||||||
|
JobKind::Identity => "identity",
|
||||||
|
JobKind::Http => "http",
|
||||||
|
JobKind::Graphql => "graphql",
|
||||||
|
JobKind::Postgresql => "postgresql",
|
||||||
|
JobKind::Noop => "noop",
|
||||||
|
JobKind::AppDependencies => "appdependencies",
|
||||||
|
JobKind::DeploymentCallback => "deploymentcallback",
|
||||||
|
JobKind::SingleScriptFlow => "singlescriptflow",
|
||||||
|
JobKind::FlowScript => "flowscript",
|
||||||
|
JobKind::FlowNode => "flownode",
|
||||||
|
JobKind::AppScript => "appscript",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Script language
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ScriptLang {
|
||||||
|
Python3,
|
||||||
|
Deno,
|
||||||
|
Go,
|
||||||
|
Bash,
|
||||||
|
Postgresql,
|
||||||
|
Nativets,
|
||||||
|
Bun,
|
||||||
|
Mysql,
|
||||||
|
Bigquery,
|
||||||
|
Snowflake,
|
||||||
|
Graphql,
|
||||||
|
Powershell,
|
||||||
|
Mssql,
|
||||||
|
Php,
|
||||||
|
Bunnative,
|
||||||
|
Rust,
|
||||||
|
Ansible,
|
||||||
|
Csharp,
|
||||||
|
Oracledb,
|
||||||
|
Nu,
|
||||||
|
Java,
|
||||||
|
Duckdb,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptLang {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ScriptLang::Python3 => "python3",
|
||||||
|
ScriptLang::Deno => "deno",
|
||||||
|
ScriptLang::Go => "go",
|
||||||
|
ScriptLang::Bash => "bash",
|
||||||
|
ScriptLang::Postgresql => "postgresql",
|
||||||
|
ScriptLang::Nativets => "nativets",
|
||||||
|
ScriptLang::Bun => "bun",
|
||||||
|
ScriptLang::Mysql => "mysql",
|
||||||
|
ScriptLang::Bigquery => "bigquery",
|
||||||
|
ScriptLang::Snowflake => "snowflake",
|
||||||
|
ScriptLang::Graphql => "graphql",
|
||||||
|
ScriptLang::Powershell => "powershell",
|
||||||
|
ScriptLang::Mssql => "mssql",
|
||||||
|
ScriptLang::Php => "php",
|
||||||
|
ScriptLang::Bunnative => "bunnative",
|
||||||
|
ScriptLang::Rust => "rust",
|
||||||
|
ScriptLang::Ansible => "ansible",
|
||||||
|
ScriptLang::Csharp => "csharp",
|
||||||
|
ScriptLang::Oracledb => "oracledb",
|
||||||
|
ScriptLang::Nu => "nu",
|
||||||
|
ScriptLang::Java => "java",
|
||||||
|
ScriptLang::Duckdb => "duckdb",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"python3" => Some(ScriptLang::Python3),
|
||||||
|
"deno" => Some(ScriptLang::Deno),
|
||||||
|
"go" => Some(ScriptLang::Go),
|
||||||
|
"bash" => Some(ScriptLang::Bash),
|
||||||
|
"postgresql" => Some(ScriptLang::Postgresql),
|
||||||
|
"nativets" => Some(ScriptLang::Nativets),
|
||||||
|
"bun" => Some(ScriptLang::Bun),
|
||||||
|
"mysql" => Some(ScriptLang::Mysql),
|
||||||
|
"bigquery" => Some(ScriptLang::Bigquery),
|
||||||
|
"snowflake" => Some(ScriptLang::Snowflake),
|
||||||
|
"graphql" => Some(ScriptLang::Graphql),
|
||||||
|
"powershell" => Some(ScriptLang::Powershell),
|
||||||
|
"mssql" => Some(ScriptLang::Mssql),
|
||||||
|
"php" => Some(ScriptLang::Php),
|
||||||
|
"bunnative" => Some(ScriptLang::Bunnative),
|
||||||
|
"rust" => Some(ScriptLang::Rust),
|
||||||
|
"ansible" => Some(ScriptLang::Ansible),
|
||||||
|
"csharp" => Some(ScriptLang::Csharp),
|
||||||
|
"oracledb" => Some(ScriptLang::Oracledb),
|
||||||
|
"nu" => Some(ScriptLang::Nu),
|
||||||
|
"java" => Some(ScriptLang::Java),
|
||||||
|
"duckdb" => Some(ScriptLang::Duckdb),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Job status for completed jobs
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum JobStatus {
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
Canceled,
|
||||||
|
Skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobStatus {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
JobStatus::Success => "success",
|
||||||
|
JobStatus::Failure => "failure",
|
||||||
|
JobStatus::Canceled => "canceled",
|
||||||
|
JobStatus::Skipped => "skipped",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"success" => Some(JobStatus::Success),
|
||||||
|
"failure" => Some(JobStatus::Failure),
|
||||||
|
"canceled" => Some(JobStatus::Canceled),
|
||||||
|
"skipped" => Some(JobStatus::Skipped),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preview job request (simplified from windmill-api Preview struct)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PreviewRequest {
|
||||||
|
pub content: String,
|
||||||
|
pub language: ScriptLang,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: serde_json::Value,
|
||||||
|
pub lock: Option<String>,
|
||||||
|
pub tag: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flow preview request
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FlowPreviewRequest {
|
||||||
|
pub value: serde_json::Value, // FlowValue as JSON
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: serde_json::Value,
|
||||||
|
pub tag: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A queued job (combines v2_job and v2_job_queue)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QueuedJob {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub kind: JobKind,
|
||||||
|
pub script_lang: Option<ScriptLang>,
|
||||||
|
pub raw_code: Option<String>,
|
||||||
|
pub raw_lock: Option<String>,
|
||||||
|
pub raw_flow: Option<serde_json::Value>,
|
||||||
|
pub args: serde_json::Value,
|
||||||
|
pub tag: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub scheduled_for: DateTime<Utc>,
|
||||||
|
pub running: bool,
|
||||||
|
pub parent_job: Option<Uuid>,
|
||||||
|
pub root_job: Option<Uuid>,
|
||||||
|
pub flow_step_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A completed job
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CompletedJob {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub status: JobStatus,
|
||||||
|
pub result: serde_json::Value,
|
||||||
|
pub started_at: Option<DateTime<Utc>>,
|
||||||
|
pub completed_at: DateTime<Utc>,
|
||||||
|
pub duration_ms: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a preview job to the queue
|
||||||
|
pub async fn push_preview(db: &LocalDb, req: PreviewRequest) -> Result<Uuid> {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
let args_json = serde_json::to_string(&req.args)?;
|
||||||
|
let tag = req.tag.as_deref().unwrap_or("deno");
|
||||||
|
|
||||||
|
// Insert into v2_job
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job (id, kind, script_lang, raw_code, raw_lock, args, tag, created_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||||
|
"#,
|
||||||
|
libsql::params![
|
||||||
|
id.to_string(),
|
||||||
|
JobKind::Preview.as_str(),
|
||||||
|
req.language.as_str(),
|
||||||
|
req.content,
|
||||||
|
req.lock,
|
||||||
|
args_json,
|
||||||
|
tag,
|
||||||
|
now.clone(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into v2_job_queue
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job_queue (id, tag, created_at, scheduled_for, running)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, 0)
|
||||||
|
"#,
|
||||||
|
libsql::params![id.to_string(), tag, now.clone(), now],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into v2_job_runtime (for heartbeat tracking)
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO v2_job_runtime (id) VALUES (?1)",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into job_perms (simplified)
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO job_perms (job_id) VALUES (?1)",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!("Pushed preview job: {}", id);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a flow preview job to the queue
|
||||||
|
pub async fn push_flow_preview(db: &LocalDb, req: FlowPreviewRequest) -> Result<Uuid> {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
let args_json = serde_json::to_string(&req.args)?;
|
||||||
|
let flow_json = serde_json::to_string(&req.value)?;
|
||||||
|
let tag = req.tag.as_deref().unwrap_or("flow");
|
||||||
|
|
||||||
|
// Insert into v2_job
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job (id, kind, raw_flow, args, tag, created_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||||
|
"#,
|
||||||
|
libsql::params![
|
||||||
|
id.to_string(),
|
||||||
|
JobKind::FlowPreview.as_str(),
|
||||||
|
flow_json,
|
||||||
|
args_json,
|
||||||
|
tag,
|
||||||
|
now.clone(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into v2_job_queue
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job_queue (id, tag, created_at, scheduled_for, running)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, 0)
|
||||||
|
"#,
|
||||||
|
libsql::params![id.to_string(), tag, now.clone(), now],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into v2_job_runtime
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO v2_job_runtime (id) VALUES (?1)",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into job_perms
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO job_perms (job_id) VALUES (?1)",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert initial flow status
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO v2_job_status (id, flow_status) VALUES (?1, '{}')",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!("Pushed flow preview job: {}", id);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a completed job result (for polling)
|
||||||
|
pub async fn get_completed_job(db: &LocalDb, id: Uuid) -> Result<Option<CompletedJob>> {
|
||||||
|
let mut rows = db
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT id, workspace_id, status, result, started_at, completed_at, duration_ms
|
||||||
|
FROM v2_job_completed
|
||||||
|
WHERE id = ?1
|
||||||
|
"#,
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(row) = rows.next().await? {
|
||||||
|
let status_str: String = row.get(2)?;
|
||||||
|
let status = JobStatus::from_str(&status_str)
|
||||||
|
.ok_or_else(|| LocalError::InvalidJobState(status_str))?;
|
||||||
|
|
||||||
|
let result_str: Option<String> = row.get(3)?;
|
||||||
|
let result: serde_json::Value = result_str
|
||||||
|
.map(|s| serde_json::from_str(&s))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(serde_json::Value::Null);
|
||||||
|
|
||||||
|
let started_at_str: Option<String> = row.get(4)?;
|
||||||
|
let started_at = started_at_str
|
||||||
|
.map(|s| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&Utc)))
|
||||||
|
.transpose()
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let completed_at_str: String = row.get(5)?;
|
||||||
|
let completed_at = DateTime::parse_from_rfc3339(&completed_at_str)
|
||||||
|
.map(|dt| dt.with_timezone(&Utc))
|
||||||
|
.unwrap_or_else(|_| Utc::now());
|
||||||
|
|
||||||
|
let duration_ms: Option<i64> = row.get(6)?;
|
||||||
|
|
||||||
|
Ok(Some(CompletedJob {
|
||||||
|
id,
|
||||||
|
workspace_id: row.get(1)?,
|
||||||
|
status,
|
||||||
|
result,
|
||||||
|
started_at,
|
||||||
|
completed_at,
|
||||||
|
duration_ms,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a job as completed with result
|
||||||
|
pub async fn complete_job(
|
||||||
|
db: &LocalDb,
|
||||||
|
id: Uuid,
|
||||||
|
status: JobStatus,
|
||||||
|
result: serde_json::Value,
|
||||||
|
started_at: DateTime<Utc>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let duration_ms = (now - started_at).num_milliseconds();
|
||||||
|
let result_json = serde_json::to_string(&result)?;
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job_completed (id, workspace_id, status, result, started_at, completed_at, duration_ms)
|
||||||
|
SELECT ?1, workspace_id, ?2, ?3, ?4, ?5, ?6
|
||||||
|
FROM v2_job WHERE id = ?1
|
||||||
|
"#,
|
||||||
|
libsql::params![
|
||||||
|
id.to_string(),
|
||||||
|
status.as_str(),
|
||||||
|
result_json,
|
||||||
|
started_at.to_rfc3339(),
|
||||||
|
now.to_rfc3339(),
|
||||||
|
duration_ms,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Remove from queue
|
||||||
|
db.execute(
|
||||||
|
"DELETE FROM v2_job_queue WHERE id = ?1",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::info!("Completed job {}: {:?}", id, status);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_push_preview() {
|
||||||
|
let db = LocalDb::in_memory().await.unwrap();
|
||||||
|
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: "export function main() { return 42; }".to_string(),
|
||||||
|
language: ScriptLang::Deno,
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
lock: None,
|
||||||
|
tag: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = push_preview(&db, req).await.unwrap();
|
||||||
|
|
||||||
|
// Verify job exists in queue
|
||||||
|
let mut rows = db
|
||||||
|
.query(
|
||||||
|
"SELECT running FROM v2_job_queue WHERE id = ?1",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let row = rows.next().await.unwrap().unwrap();
|
||||||
|
let running: i64 = row.get(0).unwrap();
|
||||||
|
assert_eq!(running, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_complete_job() {
|
||||||
|
let db = LocalDb::in_memory().await.unwrap();
|
||||||
|
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: "export function main() { return 42; }".to_string(),
|
||||||
|
language: ScriptLang::Deno,
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
lock: None,
|
||||||
|
tag: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = push_preview(&db, req).await.unwrap();
|
||||||
|
let started_at = Utc::now();
|
||||||
|
|
||||||
|
complete_job(
|
||||||
|
&db,
|
||||||
|
id,
|
||||||
|
JobStatus::Success,
|
||||||
|
serde_json::json!(42),
|
||||||
|
started_at,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify job is in completed
|
||||||
|
let completed = get_completed_job(&db, id).await.unwrap().unwrap();
|
||||||
|
assert_eq!(completed.status, JobStatus::Success);
|
||||||
|
assert_eq!(completed.result, serde_json::json!(42));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
backend/windmill-local/src/lib.rs
Normal file
31
backend/windmill-local/src/lib.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//! Windmill Local Mode
|
||||||
|
//!
|
||||||
|
//! This crate provides a minimal local mode for Windmill using libSQL (SQLite/Turso)
|
||||||
|
//! instead of PostgreSQL. The goal is to support preview execution end-to-end
|
||||||
|
//! with a lightweight, embedded database.
|
||||||
|
//!
|
||||||
|
//! ## Scope
|
||||||
|
//! - Script preview execution
|
||||||
|
//! - Flow preview execution
|
||||||
|
//! - In-memory or file-based SQLite storage
|
||||||
|
//! - Remote Turso database support for multi-writer scenarios
|
||||||
|
//!
|
||||||
|
//! ## Non-goals (for this experiment)
|
||||||
|
//! - Full feature parity with PostgreSQL mode
|
||||||
|
//! - Multi-worker support (single embedded worker)
|
||||||
|
//! - Persistence of scripts/flows (only jobs)
|
||||||
|
|
||||||
|
pub mod db;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod jobs;
|
||||||
|
pub mod queue;
|
||||||
|
pub mod executor;
|
||||||
|
pub mod flow_executor;
|
||||||
|
pub mod worker;
|
||||||
|
pub mod server;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use db::LocalDb;
|
||||||
|
pub use error::LocalError;
|
||||||
|
pub use worker::Worker;
|
||||||
|
pub use server::LocalServer;
|
||||||
296
backend/windmill-local/src/queue.rs
Normal file
296
backend/windmill-local/src/queue.rs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
//! Queue operations for local mode
|
||||||
|
//!
|
||||||
|
//! Since local mode uses a single worker, we don't need the complex
|
||||||
|
//! `FOR UPDATE SKIP LOCKED` mechanism. Instead, we use simple atomic
|
||||||
|
//! operations with the database lock.
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db::LocalDb;
|
||||||
|
use crate::error::{LocalError, Result};
|
||||||
|
use crate::jobs::{JobKind, QueuedJob, ScriptLang};
|
||||||
|
|
||||||
|
/// Pull the next job from the queue
|
||||||
|
///
|
||||||
|
/// This is simplified from the PostgreSQL version since we have a single worker
|
||||||
|
/// and use the connection mutex for coordination.
|
||||||
|
pub async fn pull_job(db: &LocalDb) -> Result<Option<QueuedJob>> {
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
// Get the next job (ordered by priority, then scheduled_for)
|
||||||
|
// We use a transaction-like approach: SELECT then UPDATE
|
||||||
|
let mut rows = db
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT q.id, j.workspace_id, j.kind, j.script_lang, j.raw_code, j.raw_lock,
|
||||||
|
j.raw_flow, j.args, q.tag, j.created_at, q.scheduled_for,
|
||||||
|
j.parent_job, j.root_job, j.flow_step_id
|
||||||
|
FROM v2_job_queue q
|
||||||
|
JOIN v2_job j ON q.id = j.id
|
||||||
|
WHERE q.running = 0 AND q.scheduled_for <= ?1
|
||||||
|
ORDER BY q.priority DESC, q.scheduled_for ASC
|
||||||
|
LIMIT 1
|
||||||
|
"#,
|
||||||
|
libsql::params![now],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let Some(row) = rows.next().await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let id_str: String = row.get(0)?;
|
||||||
|
let id = Uuid::parse_str(&id_str).map_err(|e| LocalError::InvalidJobState(e.to_string()))?;
|
||||||
|
|
||||||
|
// Mark as running
|
||||||
|
let started_at = Utc::now().to_rfc3339();
|
||||||
|
db.execute(
|
||||||
|
"UPDATE v2_job_queue SET running = 1, started_at = ?2 WHERE id = ?1",
|
||||||
|
libsql::params![id_str.clone(), started_at],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Parse the job fields
|
||||||
|
let kind_str: String = row.get(2)?;
|
||||||
|
let kind = match kind_str.as_str() {
|
||||||
|
"preview" => JobKind::Preview,
|
||||||
|
"flowpreview" => JobKind::FlowPreview,
|
||||||
|
"script" => JobKind::Script,
|
||||||
|
"flow" => JobKind::Flow,
|
||||||
|
"flowscript" => JobKind::FlowScript,
|
||||||
|
"flownode" => JobKind::FlowNode,
|
||||||
|
_ => JobKind::Preview, // Default
|
||||||
|
};
|
||||||
|
|
||||||
|
let lang_str: Option<String> = row.get(3)?;
|
||||||
|
let script_lang = lang_str.and_then(|s| ScriptLang::from_str(&s));
|
||||||
|
|
||||||
|
let raw_code: Option<String> = row.get(4)?;
|
||||||
|
let raw_lock: Option<String> = row.get(5)?;
|
||||||
|
|
||||||
|
let raw_flow_str: Option<String> = row.get(6)?;
|
||||||
|
let raw_flow = raw_flow_str
|
||||||
|
.map(|s| serde_json::from_str(&s))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let args_str: Option<String> = row.get(7)?;
|
||||||
|
let args: serde_json::Value = args_str
|
||||||
|
.map(|s| serde_json::from_str(&s))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
||||||
|
|
||||||
|
let tag: String = row.get(8)?;
|
||||||
|
|
||||||
|
let created_at_str: String = row.get(9)?;
|
||||||
|
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
|
||||||
|
.map(|dt| dt.with_timezone(&Utc))
|
||||||
|
.unwrap_or_else(|_| Utc::now());
|
||||||
|
|
||||||
|
let scheduled_for_str: String = row.get(10)?;
|
||||||
|
let scheduled_for = DateTime::parse_from_rfc3339(&scheduled_for_str)
|
||||||
|
.map(|dt| dt.with_timezone(&Utc))
|
||||||
|
.unwrap_or_else(|_| Utc::now());
|
||||||
|
|
||||||
|
let parent_job_str: Option<String> = row.get(11)?;
|
||||||
|
let parent_job = parent_job_str.and_then(|s| Uuid::parse_str(&s).ok());
|
||||||
|
|
||||||
|
let root_job_str: Option<String> = row.get(12)?;
|
||||||
|
let root_job = root_job_str.and_then(|s| Uuid::parse_str(&s).ok());
|
||||||
|
|
||||||
|
let flow_step_id: Option<String> = row.get(13)?;
|
||||||
|
|
||||||
|
Ok(Some(QueuedJob {
|
||||||
|
id,
|
||||||
|
workspace_id: row.get(1)?,
|
||||||
|
kind,
|
||||||
|
script_lang,
|
||||||
|
raw_code,
|
||||||
|
raw_lock,
|
||||||
|
raw_flow,
|
||||||
|
args,
|
||||||
|
tag,
|
||||||
|
created_at,
|
||||||
|
scheduled_for,
|
||||||
|
running: true,
|
||||||
|
parent_job,
|
||||||
|
root_job,
|
||||||
|
flow_step_id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get queue statistics
|
||||||
|
pub async fn queue_stats(db: &LocalDb) -> Result<QueueStats> {
|
||||||
|
let mut rows = db
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN running = 1 THEN 1 ELSE 0 END) as running,
|
||||||
|
SUM(CASE WHEN running = 0 THEN 1 ELSE 0 END) as pending
|
||||||
|
FROM v2_job_queue
|
||||||
|
"#,
|
||||||
|
(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let row = rows.next().await?.ok_or(LocalError::QueueEmpty)?;
|
||||||
|
|
||||||
|
Ok(QueueStats {
|
||||||
|
total: row.get::<i64>(0)? as u64,
|
||||||
|
running: row.get::<i64>(1).unwrap_or(0) as u64,
|
||||||
|
pending: row.get::<i64>(2).unwrap_or(0) as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct QueueStats {
|
||||||
|
pub total: u64,
|
||||||
|
pub running: u64,
|
||||||
|
pub pending: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update job heartbeat (ping)
|
||||||
|
pub async fn ping_job(db: &LocalDb, id: Uuid) -> Result<()> {
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
db.execute(
|
||||||
|
"UPDATE v2_job_runtime SET ping = ?2 WHERE id = ?1",
|
||||||
|
libsql::params![id.to_string(), now],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update flow status for a running flow job
|
||||||
|
pub async fn update_flow_status(
|
||||||
|
db: &LocalDb,
|
||||||
|
id: Uuid,
|
||||||
|
flow_status: &serde_json::Value,
|
||||||
|
) -> Result<()> {
|
||||||
|
let status_json = serde_json::to_string(flow_status)?;
|
||||||
|
db.execute(
|
||||||
|
"UPDATE v2_job_status SET flow_status = ?2 WHERE id = ?1",
|
||||||
|
libsql::params![id.to_string(), status_json],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a child job for flow execution
|
||||||
|
pub async fn push_flow_child_job(
|
||||||
|
db: &LocalDb,
|
||||||
|
parent_id: Uuid,
|
||||||
|
root_id: Uuid,
|
||||||
|
step_id: &str,
|
||||||
|
kind: JobKind,
|
||||||
|
script_lang: Option<ScriptLang>,
|
||||||
|
raw_code: Option<&str>,
|
||||||
|
args: &serde_json::Value,
|
||||||
|
) -> Result<Uuid> {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
let args_json = serde_json::to_string(args)?;
|
||||||
|
|
||||||
|
// Insert into v2_job
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job (id, kind, script_lang, raw_code, args, parent_job, root_job, flow_step_id, created_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||||
|
"#,
|
||||||
|
libsql::params![
|
||||||
|
id.to_string(),
|
||||||
|
kind.as_str(),
|
||||||
|
script_lang.map(|l| l.as_str()),
|
||||||
|
raw_code,
|
||||||
|
args_json,
|
||||||
|
parent_id.to_string(),
|
||||||
|
root_id.to_string(),
|
||||||
|
step_id,
|
||||||
|
now.clone(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into v2_job_queue
|
||||||
|
db.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO v2_job_queue (id, tag, created_at, scheduled_for, running)
|
||||||
|
VALUES (?1, 'flow', ?2, ?3, 0)
|
||||||
|
"#,
|
||||||
|
libsql::params![id.to_string(), now.clone(), now],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Insert into v2_job_runtime
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO v2_job_runtime (id) VALUES (?1)",
|
||||||
|
libsql::params![id.to_string()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::jobs::{push_preview, PreviewRequest};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_pull_job() {
|
||||||
|
let db = LocalDb::in_memory().await.unwrap();
|
||||||
|
|
||||||
|
// Push a job
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: "export function main() { return 42; }".to_string(),
|
||||||
|
language: ScriptLang::Deno,
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
lock: None,
|
||||||
|
tag: None,
|
||||||
|
};
|
||||||
|
let pushed_id = push_preview(&db, req).await.unwrap();
|
||||||
|
|
||||||
|
// Pull it
|
||||||
|
let job = pull_job(&db).await.unwrap().unwrap();
|
||||||
|
assert_eq!(job.id, pushed_id);
|
||||||
|
assert!(job.running);
|
||||||
|
assert_eq!(job.kind, JobKind::Preview);
|
||||||
|
|
||||||
|
// Queue should now be empty (job is running)
|
||||||
|
let job2 = pull_job(&db).await.unwrap();
|
||||||
|
assert!(job2.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_queue_stats() {
|
||||||
|
let db = LocalDb::in_memory().await.unwrap();
|
||||||
|
|
||||||
|
// Initially empty
|
||||||
|
let stats = queue_stats(&db).await.unwrap();
|
||||||
|
assert_eq!(stats.total, 0);
|
||||||
|
|
||||||
|
// Push two jobs
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: "test".to_string(),
|
||||||
|
language: ScriptLang::Deno,
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
lock: None,
|
||||||
|
tag: None,
|
||||||
|
};
|
||||||
|
push_preview(&db, req.clone()).await.unwrap();
|
||||||
|
push_preview(&db, req).await.unwrap();
|
||||||
|
|
||||||
|
let stats = queue_stats(&db).await.unwrap();
|
||||||
|
assert_eq!(stats.total, 2);
|
||||||
|
assert_eq!(stats.pending, 2);
|
||||||
|
assert_eq!(stats.running, 0);
|
||||||
|
|
||||||
|
// Pull one
|
||||||
|
pull_job(&db).await.unwrap();
|
||||||
|
|
||||||
|
let stats = queue_stats(&db).await.unwrap();
|
||||||
|
assert_eq!(stats.total, 2);
|
||||||
|
assert_eq!(stats.pending, 1);
|
||||||
|
assert_eq!(stats.running, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
218
backend/windmill-local/src/schema.rs
Normal file
218
backend/windmill-local/src/schema.rs
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
//! SQLite schema for local mode
|
||||||
|
//!
|
||||||
|
//! This is a minimal schema supporting preview job execution.
|
||||||
|
//! Key differences from PostgreSQL:
|
||||||
|
//! - ENUMs are TEXT with CHECK constraints
|
||||||
|
//! - JSONB is JSON (stored as TEXT in SQLite)
|
||||||
|
//! - Arrays are JSON arrays
|
||||||
|
//! - No FOR UPDATE SKIP LOCKED (single worker, in-process coordination)
|
||||||
|
|
||||||
|
/// SQL to create the minimal schema for local mode preview execution
|
||||||
|
pub const SCHEMA: &str = r#"
|
||||||
|
-- Job kinds (equivalent to PostgreSQL ENUM)
|
||||||
|
-- Values: script, preview, flow, flowpreview, dependencies, flowdependencies,
|
||||||
|
-- script_hub, identity, http, graphql, postgresql, noop, appdependencies,
|
||||||
|
-- deploymentcallback, singlescriptflow, flowscript, flownode, appscript
|
||||||
|
|
||||||
|
-- Job status (equivalent to PostgreSQL ENUM)
|
||||||
|
-- Values: success, failure, canceled, skipped
|
||||||
|
|
||||||
|
-- Script languages (equivalent to PostgreSQL ENUM)
|
||||||
|
-- Values: python3, deno, go, bash, postgresql, nativets, bun, mysql, bigquery,
|
||||||
|
-- snowflake, graphql, powershell, mssql, php, bunnative, rust, ansible,
|
||||||
|
-- csharp, oracledb, nu, java, duckdb
|
||||||
|
|
||||||
|
-- Main job table (minimal for preview)
|
||||||
|
CREATE TABLE IF NOT EXISTS v2_job (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID as TEXT
|
||||||
|
workspace_id TEXT NOT NULL DEFAULT 'local',
|
||||||
|
|
||||||
|
-- Raw code for preview jobs
|
||||||
|
raw_code TEXT,
|
||||||
|
raw_lock TEXT,
|
||||||
|
raw_flow TEXT, -- JSON for flow definitions
|
||||||
|
|
||||||
|
-- Job metadata
|
||||||
|
tag TEXT DEFAULT 'deno',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
created_by TEXT NOT NULL DEFAULT 'local_user',
|
||||||
|
|
||||||
|
-- Permission context (simplified for local mode)
|
||||||
|
permissioned_as TEXT NOT NULL DEFAULT 'u/local_user',
|
||||||
|
permissioned_as_email TEXT DEFAULT 'local@windmill.local',
|
||||||
|
|
||||||
|
-- Job type info
|
||||||
|
kind TEXT NOT NULL DEFAULT 'preview' CHECK (kind IN (
|
||||||
|
'script', 'preview', 'flow', 'flowpreview', 'dependencies',
|
||||||
|
'flowdependencies', 'script_hub', 'identity', 'http', 'graphql',
|
||||||
|
'postgresql', 'noop', 'appdependencies', 'deploymentcallback',
|
||||||
|
'singlescriptflow', 'flowscript', 'flownode', 'appscript'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Script execution details
|
||||||
|
script_lang TEXT CHECK (script_lang IN (
|
||||||
|
'python3', 'deno', 'go', 'bash', 'postgresql', 'nativets', 'bun',
|
||||||
|
'mysql', 'bigquery', 'snowflake', 'graphql', 'powershell', 'mssql',
|
||||||
|
'php', 'bunnative', 'rust', 'ansible', 'csharp', 'oracledb', 'nu',
|
||||||
|
'java', 'duckdb'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Flow execution details
|
||||||
|
parent_job TEXT, -- UUID reference
|
||||||
|
root_job TEXT, -- UUID reference
|
||||||
|
flow_step INTEGER,
|
||||||
|
flow_step_id TEXT,
|
||||||
|
flow_innermost_root_job TEXT,
|
||||||
|
|
||||||
|
-- Execution settings
|
||||||
|
timeout INTEGER,
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
same_worker INTEGER DEFAULT 0, -- BOOLEAN as INTEGER
|
||||||
|
visible_to_owner INTEGER DEFAULT 1,
|
||||||
|
|
||||||
|
-- Arguments (JSON)
|
||||||
|
args TEXT, -- JSON object
|
||||||
|
|
||||||
|
-- Pre-run error if validation failed
|
||||||
|
pre_run_error TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Job queue table
|
||||||
|
CREATE TABLE IF NOT EXISTS v2_job_queue (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
|
||||||
|
workspace_id TEXT NOT NULL DEFAULT 'local',
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
started_at TEXT,
|
||||||
|
scheduled_for TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
|
||||||
|
-- Queue state
|
||||||
|
running INTEGER NOT NULL DEFAULT 0, -- BOOLEAN
|
||||||
|
canceled_by TEXT,
|
||||||
|
canceled_reason TEXT,
|
||||||
|
|
||||||
|
-- Suspend state (for approval flows)
|
||||||
|
suspend INTEGER DEFAULT 0,
|
||||||
|
suspend_until TEXT,
|
||||||
|
|
||||||
|
-- Execution settings
|
||||||
|
tag TEXT DEFAULT 'deno',
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
worker TEXT,
|
||||||
|
|
||||||
|
FOREIGN KEY (id) REFERENCES v2_job(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for queue ordering (simulates queue_sort_v2)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_queue_sort ON v2_job_queue (
|
||||||
|
priority DESC, scheduled_for ASC, tag
|
||||||
|
) WHERE running = 0;
|
||||||
|
|
||||||
|
-- Job runtime tracking (heartbeat/ping)
|
||||||
|
CREATE TABLE IF NOT EXISTS v2_job_runtime (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
|
||||||
|
ping TEXT, -- Timestamp
|
||||||
|
memory_peak INTEGER,
|
||||||
|
|
||||||
|
FOREIGN KEY (id) REFERENCES v2_job(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Completed jobs with results
|
||||||
|
CREATE TABLE IF NOT EXISTS v2_job_completed (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
|
||||||
|
workspace_id TEXT NOT NULL DEFAULT 'local',
|
||||||
|
|
||||||
|
-- Timing
|
||||||
|
started_at TEXT,
|
||||||
|
completed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
duration_ms INTEGER,
|
||||||
|
|
||||||
|
-- Result
|
||||||
|
result TEXT, -- JSON
|
||||||
|
result_columns TEXT, -- JSON array of column names
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
status TEXT NOT NULL DEFAULT 'success' CHECK (status IN (
|
||||||
|
'success', 'failure', 'canceled', 'skipped'
|
||||||
|
)),
|
||||||
|
|
||||||
|
-- Cancellation details
|
||||||
|
canceled_by TEXT,
|
||||||
|
canceled_reason TEXT,
|
||||||
|
|
||||||
|
-- Flow status (for flow jobs)
|
||||||
|
flow_status TEXT, -- JSON
|
||||||
|
|
||||||
|
-- Execution details
|
||||||
|
memory_peak INTEGER,
|
||||||
|
worker TEXT,
|
||||||
|
deleted INTEGER DEFAULT 0, -- BOOLEAN
|
||||||
|
|
||||||
|
FOREIGN KEY (id) REFERENCES v2_job(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for completed job lookup by workspace and time
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_completed_workspace_time ON v2_job_completed (
|
||||||
|
workspace_id, completed_at DESC
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Flow status tracking (separate from completed to allow updates during execution)
|
||||||
|
CREATE TABLE IF NOT EXISTS v2_job_status (
|
||||||
|
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
|
||||||
|
flow_status TEXT, -- JSON object tracking flow module execution
|
||||||
|
flow_leaf_jobs TEXT, -- JSON object
|
||||||
|
workflow_as_code_status TEXT, -- JSON object
|
||||||
|
|
||||||
|
FOREIGN KEY (id) REFERENCES v2_job(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Simplified job permissions (for local mode, mostly unused)
|
||||||
|
CREATE TABLE IF NOT EXISTS job_perms (
|
||||||
|
job_id TEXT PRIMARY KEY,
|
||||||
|
email TEXT DEFAULT 'local@windmill.local',
|
||||||
|
username TEXT DEFAULT 'local_user',
|
||||||
|
is_admin INTEGER DEFAULT 1, -- BOOLEAN
|
||||||
|
is_operator INTEGER DEFAULT 0, -- BOOLEAN
|
||||||
|
workspace_id TEXT DEFAULT 'local',
|
||||||
|
groups TEXT, -- JSON array
|
||||||
|
folders TEXT, -- JSON array of objects
|
||||||
|
|
||||||
|
FOREIGN KEY (job_id) REFERENCES v2_job(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Simple audit log (optional for local mode)
|
||||||
|
CREATE TABLE IF NOT EXISTS audit (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
workspace_id TEXT DEFAULT 'local',
|
||||||
|
timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
username TEXT DEFAULT 'local_user',
|
||||||
|
operation TEXT NOT NULL,
|
||||||
|
action_kind TEXT CHECK (action_kind IN ('create', 'update', 'delete', 'execute')),
|
||||||
|
resource TEXT,
|
||||||
|
parameters TEXT -- JSON
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Job logs storage
|
||||||
|
CREATE TABLE IF NOT EXISTS job_logs (
|
||||||
|
job_id TEXT PRIMARY KEY,
|
||||||
|
workspace_id TEXT DEFAULT 'local',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
logs TEXT,
|
||||||
|
log_offset INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
FOREIGN KEY (job_id) REFERENCES v2_job(id)
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
/// SQL to drop all tables (for testing/reset)
|
||||||
|
pub const DROP_SCHEMA: &str = r#"
|
||||||
|
DROP TABLE IF EXISTS job_logs;
|
||||||
|
DROP TABLE IF EXISTS audit;
|
||||||
|
DROP TABLE IF EXISTS job_perms;
|
||||||
|
DROP TABLE IF EXISTS v2_job_status;
|
||||||
|
DROP TABLE IF EXISTS v2_job_completed;
|
||||||
|
DROP TABLE IF EXISTS v2_job_runtime;
|
||||||
|
DROP TABLE IF EXISTS v2_job_queue;
|
||||||
|
DROP TABLE IF EXISTS v2_job;
|
||||||
|
"#;
|
||||||
419
backend/windmill-local/src/server.rs
Normal file
419
backend/windmill-local/src/server.rs
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
//! HTTP server for local mode
|
||||||
|
//!
|
||||||
|
//! Provides a minimal API compatible with Windmill's preview endpoints.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::watch;
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db::LocalDb;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::jobs::{
|
||||||
|
get_completed_job, push_flow_preview, push_preview, FlowPreviewRequest,
|
||||||
|
JobStatus, PreviewRequest, ScriptLang,
|
||||||
|
};
|
||||||
|
use crate::worker::Worker;
|
||||||
|
|
||||||
|
/// Application state shared across handlers
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: Arc<LocalDb>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local server that runs the API and embedded worker
|
||||||
|
pub struct LocalServer {
|
||||||
|
db: Arc<LocalDb>,
|
||||||
|
addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalServer {
|
||||||
|
/// Create a new local server
|
||||||
|
pub async fn new(addr: SocketAddr) -> Result<Self> {
|
||||||
|
let db = Arc::new(LocalDb::in_memory().await?);
|
||||||
|
Ok(Self { db, addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a local server with a file-based database
|
||||||
|
pub async fn with_file(addr: SocketAddr, db_path: &str) -> Result<Self> {
|
||||||
|
let db = Arc::new(LocalDb::file(db_path).await?);
|
||||||
|
Ok(Self { db, addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a local server connected to a remote Turso database
|
||||||
|
pub async fn with_turso(addr: SocketAddr, url: &str, auth_token: &str) -> Result<Self> {
|
||||||
|
let db = Arc::new(LocalDb::turso_remote(url, auth_token).await?);
|
||||||
|
Ok(Self { db, addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the server
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
||||||
|
// Start the embedded worker
|
||||||
|
let worker_db = self.db.clone();
|
||||||
|
let worker_handle = tokio::spawn(async move {
|
||||||
|
let mut worker = Worker::new(worker_db, shutdown_rx);
|
||||||
|
if let Err(e) = worker.run().await {
|
||||||
|
tracing::error!("Worker error: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build the router
|
||||||
|
let state = Arc::new(AppState { db: self.db });
|
||||||
|
let app = create_router(state);
|
||||||
|
|
||||||
|
// Run the server
|
||||||
|
tracing::info!("Local server listening on {}", self.addr);
|
||||||
|
let listener = tokio::net::TcpListener::bind(self.addr).await.unwrap();
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
let shutdown_signal = async move {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("Failed to install CTRL+C signal handler");
|
||||||
|
tracing::info!("Shutdown signal received");
|
||||||
|
shutdown_tx.send(true).ok();
|
||||||
|
};
|
||||||
|
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(shutdown_signal)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Wait for worker to finish
|
||||||
|
worker_handle.await.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the API router
|
||||||
|
fn create_router(state: Arc<AppState>) -> Router {
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_origin(Any)
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any);
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
// Health check
|
||||||
|
.route("/health", get(health_check))
|
||||||
|
// Preview endpoints (mimics windmill-api)
|
||||||
|
.route("/api/w/:workspace/jobs/run/preview", post(run_preview))
|
||||||
|
.route(
|
||||||
|
"/api/w/:workspace/jobs/run_wait_result/preview",
|
||||||
|
post(run_wait_result_preview),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/:workspace/jobs/run/preview_flow",
|
||||||
|
post(run_preview_flow),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/:workspace/jobs/run_wait_result/preview_flow",
|
||||||
|
post(run_wait_result_preview_flow),
|
||||||
|
)
|
||||||
|
// Get job result
|
||||||
|
.route(
|
||||||
|
"/api/w/:workspace/jobs_u/completed/get_result/:job_id",
|
||||||
|
get(get_job_result),
|
||||||
|
)
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(cors)
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Request/Response Types ===
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct PreviewPayload {
|
||||||
|
content: String,
|
||||||
|
language: String,
|
||||||
|
#[serde(default)]
|
||||||
|
args: serde_json::Value,
|
||||||
|
lock: Option<String>,
|
||||||
|
tag: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct FlowPreviewPayload {
|
||||||
|
value: serde_json::Value,
|
||||||
|
#[serde(default)]
|
||||||
|
args: serde_json::Value,
|
||||||
|
tag: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct JobCreatedResponse {
|
||||||
|
job_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Handlers ===
|
||||||
|
|
||||||
|
async fn health_check() -> &'static str {
|
||||||
|
"OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a preview job (async - returns job ID)
|
||||||
|
async fn run_preview(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(_workspace): Path<String>,
|
||||||
|
Json(payload): Json<PreviewPayload>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let lang = match ScriptLang::from_str(&payload.language) {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: format!("Unknown language: {}", payload.language),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: payload.content,
|
||||||
|
language: lang,
|
||||||
|
args: payload.args,
|
||||||
|
lock: payload.lock,
|
||||||
|
tag: payload.tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
match push_preview(&state.db, req).await {
|
||||||
|
Ok(job_id) => (
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(JobCreatedResponse {
|
||||||
|
job_id: job_id.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: e.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a preview job and wait for result
|
||||||
|
async fn run_wait_result_preview(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(_workspace): Path<String>,
|
||||||
|
Json(payload): Json<PreviewPayload>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let lang = match ScriptLang::from_str(&payload.language) {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({"error": format!("Unknown language: {}", payload.language)})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: payload.content,
|
||||||
|
language: lang,
|
||||||
|
args: payload.args,
|
||||||
|
lock: payload.lock,
|
||||||
|
tag: payload.tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
let job_id = match push_preview(&state.db, req).await {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({"error": e.to_string()})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll for result with timeout
|
||||||
|
wait_for_result(&state.db, job_id, Duration::from_secs(60)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a flow preview job (async - returns job ID)
|
||||||
|
async fn run_preview_flow(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(_workspace): Path<String>,
|
||||||
|
Json(payload): Json<FlowPreviewPayload>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let req = FlowPreviewRequest {
|
||||||
|
value: payload.value,
|
||||||
|
args: payload.args,
|
||||||
|
tag: payload.tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
match push_flow_preview(&state.db, req).await {
|
||||||
|
Ok(job_id) => (
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(JobCreatedResponse {
|
||||||
|
job_id: job_id.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: e.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a flow preview job and wait for result
|
||||||
|
async fn run_wait_result_preview_flow(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path(_workspace): Path<String>,
|
||||||
|
Json(payload): Json<FlowPreviewPayload>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let req = FlowPreviewRequest {
|
||||||
|
value: payload.value,
|
||||||
|
args: payload.args,
|
||||||
|
tag: payload.tag,
|
||||||
|
};
|
||||||
|
|
||||||
|
let job_id = match push_flow_preview(&state.db, req).await {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({"error": e.to_string()})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll for result with timeout
|
||||||
|
wait_for_result(&state.db, job_id, Duration::from_secs(120)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the result of a completed job
|
||||||
|
async fn get_job_result(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Path((_workspace, job_id)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let job_id = match Uuid::parse_str(&job_id) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({"error": "Invalid job ID"})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match get_completed_job(&state.db, job_id).await {
|
||||||
|
Ok(Some(job)) => {
|
||||||
|
if job.status == JobStatus::Success {
|
||||||
|
(StatusCode::OK, Json(job.result)).into_response()
|
||||||
|
} else {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(job.result)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Json(serde_json::json!({"error": "Job not found or not completed"})),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({"error": e.to_string()})),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll for job completion with timeout
|
||||||
|
async fn wait_for_result(
|
||||||
|
db: &LocalDb,
|
||||||
|
job_id: Uuid,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> axum::response::Response {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let fast_poll_duration = Duration::from_secs(2);
|
||||||
|
let fast_poll_interval = Duration::from_millis(50);
|
||||||
|
let slow_poll_interval = Duration::from_millis(200);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if start.elapsed() > timeout {
|
||||||
|
return (
|
||||||
|
StatusCode::REQUEST_TIMEOUT,
|
||||||
|
Json(serde_json::json!({"error": "Timeout waiting for job result"})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
match get_completed_job(db, job_id).await {
|
||||||
|
Ok(Some(job)) => {
|
||||||
|
if job.status == JobStatus::Success {
|
||||||
|
return (StatusCode::OK, Json(job.result)).into_response();
|
||||||
|
} else {
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(job.result)).into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Job not completed yet, keep polling
|
||||||
|
let interval = if start.elapsed() < fast_poll_duration {
|
||||||
|
fast_poll_interval
|
||||||
|
} else {
|
||||||
|
slow_poll_interval
|
||||||
|
};
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({"error": e.to_string()})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::http::Request;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check() {
|
||||||
|
let db = Arc::new(LocalDb::in_memory().await.unwrap());
|
||||||
|
let state = Arc::new(AppState { db });
|
||||||
|
let app = create_router(state);
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
204
backend/windmill-local/src/worker.rs
Normal file
204
backend/windmill-local/src/worker.rs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
//! Worker for local mode
|
||||||
|
//!
|
||||||
|
//! A single embedded worker that pulls jobs from the queue and executes them.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::watch;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use crate::db::LocalDb;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::executor::{execute_script, ExecutionResult};
|
||||||
|
use crate::flow_executor;
|
||||||
|
use crate::jobs::{complete_job, JobKind, JobStatus, QueuedJob};
|
||||||
|
use crate::queue::pull_job;
|
||||||
|
use windmill_common::flows::FlowValue;
|
||||||
|
|
||||||
|
/// Worker that processes jobs from the queue
|
||||||
|
pub struct Worker {
|
||||||
|
db: Arc<LocalDb>,
|
||||||
|
/// Channel to signal shutdown
|
||||||
|
shutdown_rx: watch::Receiver<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
/// Create a new worker
|
||||||
|
pub fn new(db: Arc<LocalDb>, shutdown_rx: watch::Receiver<bool>) -> Self {
|
||||||
|
Self { db, shutdown_rx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the worker loop
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
tracing::info!("Worker started");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Check for shutdown signal
|
||||||
|
if *self.shutdown_rx.borrow() {
|
||||||
|
tracing::info!("Worker received shutdown signal");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to pull a job
|
||||||
|
match pull_job(&self.db).await {
|
||||||
|
Ok(Some(job)) => {
|
||||||
|
tracing::info!("Processing job: {} (kind: {:?})", job.id, job.kind);
|
||||||
|
if let Err(e) = self.process_job(job).await {
|
||||||
|
tracing::error!("Error processing job: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// No jobs available, wait a bit before polling again
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::time::sleep(Duration::from_millis(100)) => {}
|
||||||
|
_ = self.shutdown_rx.changed() => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error pulling job: {}", e);
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Worker stopped");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a single job
|
||||||
|
async fn process_job(&self, job: QueuedJob) -> Result<()> {
|
||||||
|
let started_at = Utc::now();
|
||||||
|
|
||||||
|
match job.kind {
|
||||||
|
JobKind::Preview => {
|
||||||
|
self.process_preview_job(job, started_at).await
|
||||||
|
}
|
||||||
|
JobKind::FlowPreview => {
|
||||||
|
self.process_flow_preview_job(job, started_at).await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Unsupported job kind
|
||||||
|
let error_result = serde_json::json!({
|
||||||
|
"error": format!("Unsupported job kind in local mode: {:?}", job.kind)
|
||||||
|
});
|
||||||
|
complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a script preview job
|
||||||
|
async fn process_preview_job(&self, job: QueuedJob, started_at: chrono::DateTime<Utc>) -> Result<()> {
|
||||||
|
let Some(code) = &job.raw_code else {
|
||||||
|
let error_result = serde_json::json!({"error": "No code provided for preview"});
|
||||||
|
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(lang) = job.script_lang else {
|
||||||
|
let error_result = serde_json::json!({"error": "No language specified for preview"});
|
||||||
|
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the script
|
||||||
|
let exec_result = execute_script(lang, code, &job.args).await;
|
||||||
|
|
||||||
|
match exec_result {
|
||||||
|
Ok(ExecutionResult { success, result, logs }) => {
|
||||||
|
tracing::debug!("Job {} logs:\n{}", job.id, logs);
|
||||||
|
let status = if success { JobStatus::Success } else { JobStatus::Failure };
|
||||||
|
complete_job(&self.db, job.id, status, result, started_at).await
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_result = serde_json::json!({"error": e.to_string()});
|
||||||
|
complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a flow preview job using the full flow executor
|
||||||
|
async fn process_flow_preview_job(&self, job: QueuedJob, started_at: chrono::DateTime<Utc>) -> Result<()> {
|
||||||
|
let Some(flow_json) = &job.raw_flow else {
|
||||||
|
let error_result = serde_json::json!({"error": "No flow definition provided"});
|
||||||
|
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the flow value using windmill-common types
|
||||||
|
let flow_value: FlowValue = match serde_json::from_value(flow_json.clone()) {
|
||||||
|
Ok(fv) => fv,
|
||||||
|
Err(e) => {
|
||||||
|
let error_result = serde_json::json!({"error": format!("Failed to parse flow: {}", e)});
|
||||||
|
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if flow_value.modules.is_empty() {
|
||||||
|
let error_result = serde_json::json!({"error": "Flow has no modules"});
|
||||||
|
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Executing flow with {} modules", flow_value.modules.len());
|
||||||
|
|
||||||
|
// Execute the flow using the full flow executor
|
||||||
|
match flow_executor::execute_flow(&self.db, &flow_value, job.args.clone()).await {
|
||||||
|
Ok((result, status)) => {
|
||||||
|
let is_failure = status.failure_module.is_some();
|
||||||
|
let final_result = serde_json::json!({
|
||||||
|
"result": result,
|
||||||
|
"flow_status": status
|
||||||
|
});
|
||||||
|
|
||||||
|
let job_status = if is_failure {
|
||||||
|
JobStatus::Failure
|
||||||
|
} else {
|
||||||
|
JobStatus::Success
|
||||||
|
};
|
||||||
|
|
||||||
|
complete_job(&self.db, job.id, job_status, final_result, started_at).await
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_result = serde_json::json!({"error": e.to_string()});
|
||||||
|
complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::jobs::{get_completed_job, push_preview, PreviewRequest, ScriptLang};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_worker_processes_bash_preview() {
|
||||||
|
let db = Arc::new(LocalDb::in_memory().await.unwrap());
|
||||||
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
||||||
|
// Push a bash preview job
|
||||||
|
let req = PreviewRequest {
|
||||||
|
content: "echo 42".to_string(),
|
||||||
|
language: ScriptLang::Bash,
|
||||||
|
args: serde_json::json!({}),
|
||||||
|
lock: None,
|
||||||
|
tag: None,
|
||||||
|
};
|
||||||
|
let job_id = push_preview(&db, req).await.unwrap();
|
||||||
|
|
||||||
|
// Create and run worker for one iteration
|
||||||
|
let mut worker = Worker::new(db.clone(), shutdown_rx);
|
||||||
|
|
||||||
|
// Process one job then shutdown
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
shutdown_tx.send(true).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.run().await.unwrap();
|
||||||
|
|
||||||
|
// Check the job completed
|
||||||
|
let completed = get_completed_job(&db, job_id).await.unwrap();
|
||||||
|
assert!(completed.is_some());
|
||||||
|
let completed = completed.unwrap();
|
||||||
|
assert_eq!(completed.status, JobStatus::Success);
|
||||||
|
// Output "42" is parsed as JSON number
|
||||||
|
assert_eq!(completed.result, serde_json::json!(42));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user