Compare commits

...

116 Commits

Author SHA1 Message Date
Faton Ramadani
b3fbf69b35 fix(frontend): fix chartjs options 2023-09-07 17:57:50 +02:00
HugoCasa
ec7d923cca feat: big query schema explorer (#2247)
* feat: big query schema explorer

* fix: schema caching
2023-09-07 17:49:21 +02:00
HugoCasa
24c5802115 fix: benchmark svg (#2249) 2023-09-07 17:36:19 +02:00
Henri Courdent
a881fab58e Logos dark mode (#2248) 2023-09-07 16:39:12 +02:00
HugoCasa
c496602e9e feat: benchmarks graph (#2244)
* feat: benchmarks graph

* feat: benchmarks graph gh action

* fix: gh action for testing

* fix: gh action

* Update benchmark.yml

* Update benchmark.yml

* Update benchmark.yml

* feat: deployed scripts + more langs

* fix: gh action regex

* fix: increase time

* fix: title + logs
2023-09-07 15:45:08 +02:00
Ruben Fiszel
75a149009a fix: pass TZ from env to runtimes 2023-09-07 12:20:29 +02:00
Ruben Fiszel
611024aa60 chore(main): release 1.168.3 (#2243)
* chore(main): release 1.168.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-07 11:52:32 +02:00
Ruben Fiszel
7c68f5892a adjust group limit to remove native windmill groups 2023-09-07 09:51:09 +02:00
Ruben Fiszel
fb6bc71a79 adjust group limit to remove native windmill groups 2023-09-07 09:49:56 +02:00
Ruben Fiszel
ef4073e0e6 use pointerdown for flow builder menu item 2023-09-07 09:28:10 +02:00
Ruben Fiszel
cdddd8c6a5 nit fix on static editor 2023-09-07 09:14:17 +02:00
Ruben Fiszel
ab1cd1d976 fix sqlx build 2023-09-07 08:53:12 +02:00
Ruben Fiszel
0c91581fcd fix: add stable ids to rows in AppTable 2023-09-07 08:35:34 +02:00
Ruben Fiszel
205e2f604c ux nits 2023-09-07 01:35:03 +02:00
Ruben Fiszel
fbbab5c874 fix: add list resource types names 2023-09-07 01:09:10 +02:00
Ruben Fiszel
057b8e4ac1 remove extra bun build in bun loader 2023-09-06 19:03:11 +02:00
Ruben Fiszel
6bc0e373fc fix: reduce aggregate period to list users in workspace 2023-09-06 18:21:00 +02:00
Ruben Fiszel
e61b6b8f36 pre-warm go 2023-09-06 14:31:34 +02:00
Ruben Fiszel
8afa901272 pre-warm go 2023-09-06 14:27:23 +02:00
Ruben Fiszel
1a9cc6b9a6 chore(main): release 1.168.2 (#2242)
* chore(main): release 1.168.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-06 13:41:47 +02:00
Ruben Fiszel
64e7fb56e4 fix: fix sqlx build 2023-09-06 13:39:46 +02:00
Ruben Fiszel
ffffbaa5c0 chore(main): release 1.168.1 (#2241)
* chore(main): release 1.168.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-06 12:38:30 +02:00
Ruben Fiszel
92c8146a57 fix: fix sqlx build 2023-09-06 12:36:54 +02:00
Ruben Fiszel
101ca902a0 chore(main): release 1.168.0 (#2240)
* chore(main): release 1.168.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-06 10:57:55 +02:00
Ruben Fiszel
c80f155602 feat: dedicated workers for native-throughput performance (EE only) (#2239)
* dedicated worker

* dedicated worker

* merge

* progress

* progress

* progress

* merge

* update

* sqlx

* fix imports
2023-09-06 10:32:53 +02:00
Ruben Fiszel
d39711915a chore(main): release 1.167.0 (#2229)
* chore(main): release 1.167.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-05 23:48:28 +02:00
Faton Ramadani
e4c896b4b9 fix(frontend): add virtual list (#2218)
* fix(frontend): add virtual list

* feat(frontend): add virtual list to runs page

* feat(frontend): fix everything

* feat(frontend): add message when the limit is reached
2023-09-05 16:49:25 +02:00
Henri Courdent
538f9ed27a Higlighted native languages integrations (#2231)
* Higlighted native languages integrations

* Excluded native integrations from the rest of integrations

* Fix bug filter
2023-09-05 10:06:32 +02:00
Ruben Fiszel
0a585eccd2 chore(main): release 1.167.0 (#2227)
* chore(main): release 1.167.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-04 02:39:09 +02:00
Ruben Fiszel
d09788c216 fix(cli): prioritize correctly content file to resolve for ts types 2023-09-04 02:38:39 +02:00
Ruben Fiszel
2906d535a1 fix(cli): prioritize correctly content file to resolve for ts types 2023-09-04 02:34:51 +02:00
Ruben Fiszel
dff1cd9a64 feat: submit result in background thread (unify architecture for dedicated worker) (#2226)
* merge

* all

* all
2023-09-04 01:56:23 +02:00
Ruben Fiszel
76edfd6d9e chore(main): release 1.166.1 (#2225)
* chore(main): release 1.166.1

* Apply automatic changes

* Update CHANGELOG.md

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-03 18:19:42 +02:00
Ruben Fiszel
b3d7661114 fix initialization 2023-09-03 18:12:44 +02:00
Ruben Fiszel
12ef348b7f fix initialization 2023-09-03 18:05:23 +02:00
Ruben Fiszel
b0ed0f9648 fix: fix setting is ready for s3 workers 2023-09-03 16:52:44 +02:00
Ruben Fiszel
a4d1c987d4 fix: fix setting is ready for s3 workers 2023-09-03 16:47:05 +02:00
Ruben Fiszel
8f8806a1b9 fix: fix setting is ready for s3 workers 2023-09-03 16:18:23 +02:00
Ruben Fiszel
37ec5107c4 improve webhook curl 2023-09-03 14:55:52 +02:00
Ruben Fiszel
6417c07aa3 chore(main): release 1.166.0 (#2211)
* chore(main): release 1.166.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-03 14:03:25 +02:00
Ruben Fiszel
f912f1de86 fix: update wmill python generator thus updating windmill-api 2023-09-03 14:01:51 +02:00
Ruben Fiszel
b35ffd435d fix: fix datetime handling for python 2023-09-03 11:19:22 +02:00
Ruben Fiszel
ffcb977c47 update benchmark action 2023-09-03 10:37:29 +02:00
Ruben Fiszel
0a20c683d2 update benchmark action 2023-09-03 10:35:56 +02:00
Ruben Fiszel
65f193e9e7 update benchmark action 2023-09-03 10:31:48 +02:00
Ruben Fiszel
073b8d7319 update benchmark action 2023-09-03 10:29:49 +02:00
Ruben Fiszel
4316214f32 update benchmark action 2023-09-03 10:23:50 +02:00
Ruben Fiszel
135444d228 add regular benchmarks 2023-09-03 09:57:25 +02:00
Ruben Fiszel
069e2d18d5 perf: improve queue performance (#2222)
* benchs

* progress

* prog

* progress

* progress

* progress

* update

* progress

* progress

* progress

* progress

* progress

* all

* all

* improve queries

* improve queries

* rm imv

* all

* all
2023-09-03 09:44:16 +02:00
Ruben Fiszel
e31d843bf9 bump deno to 1.36.3 2023-09-01 14:53:17 +02:00
Ruben Fiszel
719a7b11da fix: see run detail in a new tab 2023-09-01 09:32:05 +02:00
Faton Ramadani
77f8eac21e feat(frontend): App stepper debug (#2202)
* fix(frontend): Fix overflow issue

* feat(frontend): Add Stepper debug + add last action output

* feat(frontend): revert

* feat(frontend): correctly handled debug mode

* fix(frontend): PR comments
2023-09-01 08:03:45 +02:00
Faton Ramadani
9787edb67c fix(frontend): Fix App Modal z-index (#2210)
* fix(frontend): Fix App Modal z-index

* fix(frontend): fix build
2023-09-01 08:01:26 +02:00
Ruben Fiszel
a34b91b386 chore(main): release 1.165.0 (#2207)
* chore(main): release 1.165.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-09-01 00:05:37 +02:00
Ruben Fiszel
ada88a2bf9 feat: improve queue performance when queue grows large 2023-08-31 23:41:04 +02:00
Ruben Fiszel
41107c7cfa feat: support partial go dependency pinning 2023-08-31 22:12:47 +02:00
Ruben Fiszel
16fa99a4ea clarify cache usage for scripts/flows 2023-08-31 15:19:01 +02:00
Ruben Fiszel
d311d76557 fix: uniformize that all job links specify the workspace 2023-08-31 12:55:28 +02:00
Ruben Fiszel
1998c2c6e0 chore(main): release 1.164.0 (#2205)
* chore(main): release 1.164.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-31 12:30:37 +02:00
Ruben Fiszel
276cd6dac3 feat: add workspace variable to worker tag 2023-08-31 11:00:57 +02:00
Bryan
bb749c14f8 fix(frontend): allow using Docker in Flow (#2201) 2023-08-31 08:52:05 +02:00
Faton Ramadani
482e46e61b Fix run page navigation (#2200)
* fix(frontend): Fix overflow issue

* fix(frontend): Properly handle browser navigation

* fix(frontend): revert changes

* fix(frontend): remove debug code
2023-08-31 00:17:52 +02:00
Ruben Fiszel
00df43c68c make frontend bundlable 2023-08-30 19:31:34 +02:00
Ruben Fiszel
e780131bea reworks consts 2023-08-30 18:43:21 +02:00
Ruben Fiszel
d7580fefb9 chore(main): release 1.163.1 (#2199)
* chore(main): release 1.163.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-30 18:31:44 +02:00
Ruben Fiszel
11e1ecbcda fix: avoid perpetual spinning of recompute all component 2023-08-30 18:27:12 +02:00
Ruben Fiszel
200fc0720e clarify cache usage for scripts/flows 2023-08-30 17:44:40 +02:00
Ruben Fiszel
e6f56d4dec chore(main): release 1.163.0 (#2198)
* chore(main): release 1.163.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-30 17:26:31 +02:00
Ruben Fiszel
380267a965 nits 2023-08-30 17:26:30 +02:00
Ruben Fiszel
61df339343 fix: fix cyclical loop in apps 2023-08-30 17:12:55 +02:00
Ruben Fiszel
a002b242fc reduce barrier sync logs 2023-08-30 14:55:03 +02:00
Ruben Fiszel
cf012114c5 improve global cache to s3 panel 2023-08-30 14:09:54 +02:00
Ruben Fiszel
6200f91ecd fix build 2023-08-30 13:43:40 +02:00
Ruben Fiszel
7c5ea569a8 feat: add global cache configuration 2023-08-30 13:35:41 +02:00
Ruben Fiszel
7f7a97f009 fix premium info computation 2023-08-29 21:41:26 +02:00
Ruben Fiszel
39b30a7ca3 fix premium info computation 2023-08-29 21:27:53 +02:00
Ruben Fiszel
ffb61afc3f fix premium info computation 2023-08-29 21:23:15 +02:00
Ruben Fiszel
1b3f78f585 exclude cache and state from tarball export 2023-08-29 19:36:25 +02:00
Ruben Fiszel
1f56b27598 chore(main): release 1.162.2 (#2197)
* chore(main): release 1.162.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-29 16:45:39 +02:00
Ruben Fiszel
4704899a81 fix: fix incorrect bump 2023-08-29 16:44:05 +02:00
Ruben Fiszel
c6ff9e38da chore(main): release 1.162.1 (#2196) 2023-08-29 16:39:46 +02:00
Ruben Fiszel
6c5a8a3613 fix: fix deps incompatibilities 2023-08-29 16:38:02 +02:00
Ruben Fiszel
2e7862d84f chore(main): release 1.162.0 (#2194)
* chore(main): release 1.162.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-29 15:59:24 +02:00
Ruben Fiszel
bf0014c387 feat: add cache to inline scripts 2023-08-29 15:57:01 +02:00
Faton Ramadani
095969f125 feat(frontend): Filter runs by user (#2187)
* feat(frontend): Filter runs by user

* feat(frontend): Improve UI

* feat(frontend): Add filter by folder

* feat(frontend): Clean up filters

* feat(frontend): Clean up filters

* feat(frontend): add shortcut to filter by folder

* fix(frontend): Fix filters

* fix(frontend): remove logs

* fix(frontend): Fix filter by syncing

* fix(frontend): refactor all filters

* fix(frontend): refactor all filters

* fix(frontend): refactor all filters

* fix(frontend): refactor all filters

* fix(frontend): add missing tooltips

* fix(frontend): correcly set args and result filters + handle status filter
2023-08-29 15:42:56 +02:00
Ruben Fiszel
dfa9ed5c00 improve cache paths 2023-08-29 15:29:55 +02:00
Ruben Fiszel
67f868f08e fix: add setState, getState to client 2023-08-29 15:26:23 +02:00
Ruben Fiszel
81575468e7 fix get latest deployed for path 2023-08-29 15:12:53 +02:00
Ruben Fiszel
30ea354cae fix: relative imports in deno 2023-08-29 15:05:00 +02:00
Ruben Fiszel
03e48a4ca5 feat: add caching to flows and scripts (#2193)
* cachettl

* all
2023-08-29 13:00:20 +02:00
Ruben Fiszel
af2a417902 fix list state preservation across re-renders 2023-08-28 20:42:53 +02:00
Ruben Fiszel
d469802186 chore(main): release 1.161.0 (#2190)
* chore(main): release 1.161.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-28 20:20:48 +02:00
Ruben Fiszel
770a3e8835 fix: allow deno to --write lock when using lockfiles 2023-08-28 20:16:32 +02:00
Ruben Fiszel
6354c95bb7 feat: early stop for flows 2023-08-28 17:46:16 +02:00
Ruben Fiszel
d0d041fde3 feat: concurrency limits for flows 2023-08-28 16:02:48 +02:00
Faton Ramadani
272ff63e40 feat(frontend): rework premium plans (#2155)
* feat(frontend): rework premium plans

* feat(frontend): update premium plan page
2023-08-28 14:26:18 +02:00
Ruben Fiszel
840fbbcbb1 fix: fix refresh init in presence of app stepper 2023-08-28 12:36:54 +02:00
Ruben Fiszel
ba61c7bd77 fix build 2023-08-28 09:44:36 +02:00
Ruben Fiszel
2c7c1025b1 fix compilation 2023-08-28 09:00:13 +02:00
Ruben Fiszel
245904195c fix script_path_start 2023-08-28 02:37:06 +02:00
Ruben Fiszel
1db214f9d0 limit number of groups to 5 in ce 2023-08-28 02:23:36 +02:00
Ruben Fiszel
697f94a73d chore(main): release 1.160.0 (#2189)
* chore(main): release 1.160.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-27 23:15:39 +02:00
Ruben Fiszel
6a7e405454 improve job queue counter clarity 2023-08-27 23:11:04 +02:00
Ruben Fiszel
88838f31bb fix sqlx 2023-08-27 23:05:47 +02:00
Ruben Fiszel
34e2a8001a feat: add parallelism control to forloops 2023-08-27 23:03:19 +02:00
Ruben Fiszel
e2a053c022 fix parrallel forloop and branchall failure handling 2023-08-27 16:05:51 +02:00
Ruben Fiszel
3c4b451655 chore(main): release 1.159.0 (#2183)
* chore(main): release 1.159.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-08-27 15:50:19 +02:00
Ruben Fiszel
487d56cb0f fix: operation are redacted instead of username which fix audit logs for non admin users 2023-08-27 15:48:38 +02:00
Ruben Fiszel
135f3e121a fix editor bar colors 2023-08-27 15:34:20 +02:00
Ruben Fiszel
033f675484 fix cancel jobs 2023-08-27 15:09:54 +02:00
Ruben Fiszel
0dfdf8fa1b fix: canceling jobs 2023-08-27 14:40:20 +02:00
Ruben Fiszel
b492fd9884 feat: add support for root certificate in postgresql 2023-08-27 11:58:36 +02:00
Ruben Fiszel
a60db77920 use postgresql backend fo r testing connection 2023-08-27 11:25:34 +02:00
Ruben Fiszel
fe1e419fa8 feat: support to set linked secret variable to any field of a newly created resource 2023-08-27 11:03:49 +02:00
Ruben Fiszel
a478272a9b pin approval to 1.158.2+ 2023-08-26 15:26:20 +02:00
Ruben Fiszel
24ac1e25ff fix: fix app table footer label when -1 2023-08-26 12:13:22 +02:00
236 changed files with 10423 additions and 3145 deletions

2
.github/Dockerfile vendored
View File

@@ -1,7 +1,7 @@
FROM nikolaik/python-nodejs
RUN npm install -g @apidevtools/swagger-cli
RUN pip install openapi-python-client
RUN pip install openapi-python-client==0.15.1
RUN pip install poetry

View File

@@ -4,4 +4,4 @@ RUN python3 -m pip install pipx poetry
RUN python3 -m pipx ensurepath
ENV PATH="/root/.local/bin:${PATH}"
ENV PATH="/usr/local/bin:${PATH}"
RUN pipx install openapi-python-client==0.11.6 --include-deps
RUN pipx install openapi-python-client==0.15.1 --include-deps

54
.github/workflows/benchmark.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Run benchmarks
on:
schedule:
- cron: "0 0 */1 * *"
workflow_dispatch:
jobs:
benchmark:
runs-on: [self-hosted, new]
services:
postgres:
image: postgres
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
windmill:
image: ghcr.io/windmill-labs/windmill:main
env:
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
options: >-
--pull always --health-interval 10s --health-timeout 5s
--health-retries 5 --health-cmd "curl
http://localhost:8000/api/version"
ports:
- 8000:8000
steps:
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- uses: actions/checkout@v4
with:
ref: benchmarks
- name: benchmark
timeout-minutes: 10
run: deno run --unstable -A
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
--host http://localhost:8000
-e admin@windmill.dev
-p changeme
-c https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
--branch ${GITHUB_REF##ref/head/}
- name: Push changes
run: |
pwd
git add .
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Update benchmarks"
git push

View File

@@ -96,6 +96,7 @@ jobs:
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise

View File

@@ -3,7 +3,10 @@ env:
IMAGE_NAME: ${{ github.repository }}
name: Build windmill:mssql
on: workflow_dispatch
on:
workflow_dispatch:
schedule:
- cron: "0 0 */4 * *"
concurrency:
group: ${{ github.ref }}-mssql

View File

@@ -1,6 +1,197 @@
# Changelog
## [1.168.3](https://github.com/windmill-labs/windmill/compare/v1.168.2...v1.168.3) (2023-09-07)
### Bug Fixes
* add list resource types names ([fbbab5c](https://github.com/windmill-labs/windmill/commit/fbbab5c874748547a9ff3e58c1b7b22c90766f4f))
* add stable ids to rows in AppTable ([0c91581](https://github.com/windmill-labs/windmill/commit/0c91581fcdf3a141f36e34610935aa100fcfee52))
* reduce aggregate period to list users in workspace ([6bc0e37](https://github.com/windmill-labs/windmill/commit/6bc0e373fc6088636f09d217e8800a32337291ea))
## [1.168.2](https://github.com/windmill-labs/windmill/compare/v1.168.1...v1.168.2) (2023-09-06)
### Bug Fixes
* fix sqlx build ([64e7fb5](https://github.com/windmill-labs/windmill/commit/64e7fb56e41b45bc2476d0e98fa99dcbc355cfe0))
## [1.168.1](https://github.com/windmill-labs/windmill/compare/v1.168.0...v1.168.1) (2023-09-06)
### Bug Fixes
* fix sqlx build ([92c8146](https://github.com/windmill-labs/windmill/commit/92c8146a5778290b5a76c2ea5685f95b85be2e38))
## [1.168.0](https://github.com/windmill-labs/windmill/compare/v1.167.0...v1.168.0) (2023-09-06)
### Features
* dedicated workers for native-throughput performance (EE only) ([#2239](https://github.com/windmill-labs/windmill/issues/2239)) ([c80f155](https://github.com/windmill-labs/windmill/commit/c80f155602eca972842be7bd560395a06e4e0ae6))
### Bug Fixes
* **frontend:** add virtual list ([#2218](https://github.com/windmill-labs/windmill/issues/2218)) ([e4c896b](https://github.com/windmill-labs/windmill/commit/e4c896b4b9f28b2fa219be249a2794faf3f1b7d0))
## [1.167.1](https://github.com/windmill-labs/windmill/compare/v1.167.0...v1.167.1) (2023-09-05)
### Bug Fixes
* **frontend:** add virtual list ([#2218](https://github.com/windmill-labs/windmill/issues/2218)) ([e4c896b](https://github.com/windmill-labs/windmill/commit/e4c896b4b9f28b2fa219be249a2794faf3f1b7d0))
## [1.167.0](https://github.com/windmill-labs/windmill/compare/v1.166.1...v1.167.0) (2023-09-04)
### Features
* submit result in background thread (unify architecture for dedicated worker) ([#2226](https://github.com/windmill-labs/windmill/issues/2226)) ([dff1cd9](https://github.com/windmill-labs/windmill/commit/dff1cd9a64f755f239eb57599c104c47f4d33b12))
### Bug Fixes
* **cli:** prioritize correctly content file to resolve for ts types ([2906d53](https://github.com/windmill-labs/windmill/commit/2906d535a126f4fe2cfe6dffda46e5fe841056da))
## [1.166.1](https://github.com/windmill-labs/windmill/compare/v1.166.0...v1.166.1) (2023-09-03)
### Bug Fixes
* fix setting is ready for s3 workers ([b0ed0f9](https://github.com/windmill-labs/windmill/commit/b0ed0f964843247d11ecfe586f1565589df95ff6))
## [1.166.0](https://github.com/windmill-labs/windmill/compare/v1.165.0...v1.166.0) (2023-09-03)
### Features
* **frontend:** App stepper debug ([#2202](https://github.com/windmill-labs/windmill/issues/2202)) ([77f8eac](https://github.com/windmill-labs/windmill/commit/77f8eac21e0edfa1eada617d78a498a3a6ae1dce))
### Bug Fixes
* fix datetime handling for python ([b35ffd4](https://github.com/windmill-labs/windmill/commit/b35ffd435de97ed34fcda69490abd734ea3229fa))
* **frontend:** Fix App Modal z-index ([#2210](https://github.com/windmill-labs/windmill/issues/2210)) ([9787edb](https://github.com/windmill-labs/windmill/commit/9787edb67c329265bf179fe304d00cdc1df7042e))
* see run detail in a new tab ([719a7b1](https://github.com/windmill-labs/windmill/commit/719a7b11da81f68452ba9fc22ff456fe1ddde1de))
* update wmill python generator thus updating windmill-api ([f912f1d](https://github.com/windmill-labs/windmill/commit/f912f1de86e91c5cdbc0012e2362467c4965936a))
### Performance Improvements
* improve queue performance ([#2222](https://github.com/windmill-labs/windmill/issues/2222)) ([069e2d1](https://github.com/windmill-labs/windmill/commit/069e2d18d586aa3d407e3b089d1ad94b2b838af0))
## [1.165.0](https://github.com/windmill-labs/windmill/compare/v1.164.0...v1.165.0) (2023-08-31)
### Features
* improve queue performance when queue grows large ([ada88a2](https://github.com/windmill-labs/windmill/commit/ada88a2bf94fec71187bbdb210065de43d4cd3fb))
* support partial go dependency pinning ([41107c7](https://github.com/windmill-labs/windmill/commit/41107c7cfa7b56099a9c8b08cfb16ff3cf840ff2))
### Bug Fixes
* uniformize that all job links specify the workspace ([d311d76](https://github.com/windmill-labs/windmill/commit/d311d76557432a72a5d6d7ab010aeb1fe0e599de))
## [1.164.0](https://github.com/windmill-labs/windmill/compare/v1.163.1...v1.164.0) (2023-08-31)
### Features
* add workspace variable to worker tag ([276cd6d](https://github.com/windmill-labs/windmill/commit/276cd6dac39b7cb181ac46e3edea79a3a3bcff8d))
### Bug Fixes
* **frontend:** allow using Docker in Flow ([#2201](https://github.com/windmill-labs/windmill/issues/2201)) ([bb749c1](https://github.com/windmill-labs/windmill/commit/bb749c14f877f7cb1e8642b881a00aedfeb08f7d))
## [1.163.1](https://github.com/windmill-labs/windmill/compare/v1.163.0...v1.163.1) (2023-08-30)
### Bug Fixes
* avoid perpetual spinning of recompute all component ([11e1ecb](https://github.com/windmill-labs/windmill/commit/11e1ecbcda92f5ab643b776094ef10005d51b579))
## [1.163.0](https://github.com/windmill-labs/windmill/compare/v1.162.2...v1.163.0) (2023-08-30)
### Features
* add global cache configuration ([7c5ea56](https://github.com/windmill-labs/windmill/commit/7c5ea569a8102ef052d42216e2ff8d4c3169a7a5))
### Bug Fixes
* fix cyclical loop in apps ([61df339](https://github.com/windmill-labs/windmill/commit/61df339343767e63cbe7a4e75f1fd4f848dbd7e0))
## [1.162.2](https://github.com/windmill-labs/windmill/compare/v1.162.1...v1.162.2) (2023-08-29)
### Bug Fixes
* fix incorrect bump ([4704899](https://github.com/windmill-labs/windmill/commit/4704899a81cb281b99949c934184e23b199b2ed8))
## [1.162.1](https://github.com/windmill-labs/windmill/compare/v1.162.0...v1.162.1) (2023-08-29)
### Bug Fixes
* fix deps incompatibilities ([6c5a8a3](https://github.com/windmill-labs/windmill/commit/6c5a8a3613b4608e6d2b57e7f40cd4ab2d1af9ae))
## [1.162.0](https://github.com/windmill-labs/windmill/compare/v1.161.0...v1.162.0) (2023-08-29)
### Features
* add cache to inline scripts ([bf0014c](https://github.com/windmill-labs/windmill/commit/bf0014c387361ce358d31c7cbc44a9c4c97606df))
* add caching to flows and scripts ([#2193](https://github.com/windmill-labs/windmill/issues/2193)) ([03e48a4](https://github.com/windmill-labs/windmill/commit/03e48a4ca557cd2c385988d3a935cea38bc6e81e))
* **frontend:** Filter runs by user ([#2187](https://github.com/windmill-labs/windmill/issues/2187)) ([095969f](https://github.com/windmill-labs/windmill/commit/095969f125e9186cb4f02f75e914ef9a70e3abc4))
### Bug Fixes
* add setState, getState to client ([67f868f](https://github.com/windmill-labs/windmill/commit/67f868f08ed10f3f7c185af67bff7080c339e974))
* relative imports in deno ([30ea354](https://github.com/windmill-labs/windmill/commit/30ea354cae91ea040b3112c4138a1e5f0d7ab530))
## [1.161.0](https://github.com/windmill-labs/windmill/compare/v1.160.0...v1.161.0) (2023-08-28)
### Features
* concurrency limits for flows ([d0d041f](https://github.com/windmill-labs/windmill/commit/d0d041fde37ceda5e3a04e5da9c87d6b7e5691b3))
* early stop for flows ([6354c95](https://github.com/windmill-labs/windmill/commit/6354c95bb74c5d1af838234c0146176a0d3e408e))
* **frontend:** rework premium plans ([#2155](https://github.com/windmill-labs/windmill/issues/2155)) ([272ff63](https://github.com/windmill-labs/windmill/commit/272ff63e4072b4c25a46c133b518649f88b7598e))
### Bug Fixes
* allow deno to --write lock when using lockfiles ([770a3e8](https://github.com/windmill-labs/windmill/commit/770a3e8835637af1b1e017ecc1675e526ca40345))
* fix refresh init in presence of app stepper ([840fbbc](https://github.com/windmill-labs/windmill/commit/840fbbcbb1f969ef3b000f9e50d5c5dde8371995))
## [1.160.0](https://github.com/windmill-labs/windmill/compare/v1.159.0...v1.160.0) (2023-08-27)
### Features
* add parallelism control to forloops ([34e2a80](https://github.com/windmill-labs/windmill/commit/34e2a8001afa8bb948bf907383bffbc8aa11901f))
## [1.159.0](https://github.com/windmill-labs/windmill/compare/v1.158.2...v1.159.0) (2023-08-27)
### Features
* add support for root certificate in postgresql ([b492fd9](https://github.com/windmill-labs/windmill/commit/b492fd98846ff4b4e073bb41de91dd84f0bd7031))
* support to set linked secret variable to any field of a newly created resource ([fe1e419](https://github.com/windmill-labs/windmill/commit/fe1e419fa83db6a9db59aac23490e52cd3649f51))
### Bug Fixes
* canceling jobs ([0dfdf8f](https://github.com/windmill-labs/windmill/commit/0dfdf8fa1be88d601f7dbf7b348aaf8a3ae8e2fd))
* fix app table footer label when -1 ([24ac1e2](https://github.com/windmill-labs/windmill/commit/24ac1e25ff87eef591e9f766bd0e7991b3668723))
* operation are redacted instead of username which fix audit logs for non admin users ([487d56c](https://github.com/windmill-labs/windmill/commit/487d56cb0fedde47c77cdb7a4b5424b51c4a2e10))
## [1.158.2](https://github.com/windmill-labs/windmill/compare/v1.158.1...v1.158.2) (2023-08-26)

View File

@@ -96,8 +96,8 @@ SHELL ["/bin/bash", "-c"]
RUN apt update -y
RUN apt install -y unzip curl
RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/LukeChannings/deno-arm64/releases/download/v1.36.2/deno-linux-arm64.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.36.2/deno-x86_64-unknown-linux-gnu.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/LukeChannings/deno-arm64/releases/download/v1.36.3/deno-linux-arm64.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.36.3/deno-x86_64-unknown-linux-gnu.zip -o deno.zip || true
RUN unzip deno.zip && rm deno.zip
@@ -164,6 +164,9 @@ RUN set -eux; \
ENV PATH="${PATH}:/usr/local/go/bin"
ENV GO_PATH=/usr/local/go/bin/go
# go build is slower the first time it is ran, so we prewarm it in the build
RUN mkdir -p /tmp/gobuildwarm && cd /tmp/gobuildwarm && go mod init gobuildwarm && printf "package foo\nimport (\"fmt\")\nfunc main() { fmt.Println(42) }" > warm.go && go build -x && rm -rf /tmp/gobuildwarm
ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools

View File

@@ -85,7 +85,7 @@ https://github.com/windmill-labs/windmill/assets/122811744/0b132cd1-ee67-4505-82
shared on [WindmillHub](https://hub.windmill.dev).
![Step 3](./imgs/windmill-flow.png)
4. Build complex UI on top of your scripts and flows.
4. Build complex UIs on top of your scripts and flows.
![Step 4](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a cron schedule '_/5 _ \* \* \*' or

3
backend/.gitignore vendored
View File

@@ -1,4 +1,5 @@
target/
.env
oauth.json
windmill-api/openapi-deref.yaml
windmill-api/openapi-deref.yaml
tracing.folded

View File

@@ -1,30 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE(j.min_started_at, q.min_started_at) AS min_started_at, COALESCE(completed_count, 0) + COALESCE(running_count, 0) AS total_count\n FROM\n (SELECT script_path, MIN(started_at) as min_started_at, COUNT(*) as completed_count\n FROM completed_job\n WHERE script_path = $1 AND started_at + INTERVAL '1 MILLISECOND' * duration_ms > (now() - INTERVAL '1 second' * $2) AND workspace_id = $3\n GROUP BY script_path) as j\n FULL OUTER JOIN\n (SELECT script_path, MIN(started_at) as min_started_at, COUNT(*) as running_count\n FROM queue\n WHERE script_path = $1 AND running = true AND workspace_id = $3\n GROUP BY script_path) as q\n ON q.script_path = j.script_path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "min_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "total_count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Float8",
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "09eb148c69fec39d01bcc96a0976ea0ead76049350250142d086015228d36338"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "PostgreSQL",
"query": "select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell"
]
}
}
}
},
{
"ordinal": 5,
"name": "dedicated_worker",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
true,
true,
true,
true,
false,
true
]
},
"hash": "123c0608e229c29187009b7961355ddd99c4ad1f46b876dd86e372b84d806ecd"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO token\n (token, label, super_admin, email)\n VALUES ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "15ef5759a2ccd7b7f9fd3f2ce0d54d01fe0a2c7e9692ac4ce29a86eb509e1a1d"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM global_settings WHERE name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "1c1577b9963d907c4245a027fece57285ce64ac41a84681b32deb71e452334c1"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM queue",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "1e232d4b978d133ba542d0abe5afaa0031f630c5336c0b805590b5ea4508ed04"
}

View File

@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE(j.min_started_at, q.min_started_at) AS min_started_at, COALESCE(completed_count, 0) AS completed_count\n FROM\n (SELECT script_path, MIN(started_at) as min_started_at, COUNT(*) as completed_count\n FROM completed_job\n WHERE script_path = $1 AND job_kind != 'dependencies' AND started_at + INTERVAL '1 MILLISECOND' * duration_ms > (now() - INTERVAL '1 second' * $2) AND workspace_id = $3 AND canceled = false\n GROUP BY script_path) as j\n FULL OUTER JOIN\n (SELECT script_path, MIN(started_at) as min_started_at\n FROM queue\n WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $3 AND canceled = false\n GROUP BY script_path) as q\n ON q.script_path = j.script_path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "min_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "completed_count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Float8",
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "330e85c6fe52355971262d3a44c2b75a95b1d042b9e0995186aa5a2d0b7ee552"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE queue\n SET suspend = $1, suspend_until = now() + interval '14 day', running = true\n WHERE id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Uuid"
]
},
"nullable": []
},
"hash": "3c83e6adf21b7a8572529f9175cf6d2d30ea8c921ae2bf751b6825a3e453a012"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)",
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)",
"describe": {
"columns": [],
"parameters": {
@@ -57,10 +57,12 @@
"Bool",
"VarcharArray",
"Int4",
"Int4"
"Int4",
"Int4",
"Bool"
]
},
"nullable": []
},
"hash": "9ee1423945740c17dedc2fc7ea7e7183380c43d0feeb615c651aa486eef3bd53"
"hash": "438b5b5d29b05846c2e074cad2404e797527841cd97dba80c271cbefafae65cc"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM resource WHERE workspace_id = $1",
"query": "SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
"describe": {
"columns": [
{
@@ -48,5 +48,5 @@
false
]
},
"hash": "fa258894bd90ea6586669e5810c5c6bcb42d5e1e68fab27fb185d06962b6454a"
"hash": "45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE concurrency_counter SET counter = counter - 1 WHERE concurrency_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "467247beca4dae321ab3acc3a99f655412761157cda54bbd3e58cada3de3396c"
}

View File

@@ -0,0 +1,80 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell"
]
}
}
}
},
{
"ordinal": 6,
"name": "dedicated_worker",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true,
true,
true,
true,
false,
true
]
},
"hash": "5cd89ab614d3cac80fb81627267ee85b191263989b0c78b2bfce77e796e96825"
}

View File

@@ -1,44 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell"
]
}
}
}
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "637ead7701775609c772c9985450273b942202cdf9017ce6b5ba612e66ce3875"
}

View File

@@ -0,0 +1,80 @@
{
"db_name": "PostgreSQL",
"query": "select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell"
]
}
}
}
},
{
"ordinal": 6,
"name": "dedicated_worker",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false,
true,
true,
true,
true,
false,
true
]
},
"hash": "65835f2e5ad38f7cc6b147dadfef6f580f15bca96d9746c9359e98ca793f8f1f"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE concurrency_counter SET counter = counter - 1 WHERE concurrency_id = $1 RETURNING counter",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "counter",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "71879f3644dde27f2d93e5511e420579dfd2ffab72bb8480a1622b68703d4ea7"
}

View File

@@ -1,41 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false AND lock IS not NULL AND lock_error_logs IS NULL)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "concurrency_time_window_s",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "7435be9a71a6b30980d180047486a47d69e36132d40ef0f53448e96a9866842b"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO concurrency_counter VALUES ($1, 1)\n ON CONFLICT (concurrency_id) \n DO UPDATE SET counter = concurrency_counter.counter + 1\n RETURNING concurrency_counter.counter",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "counter",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "86652246b20f9bd023d3419ec382176780a87f32673b53de6f899564c1494147"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "select tag, concurrent_limit, concurrency_time_window_s from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "concurrency_time_window_s",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "8f0d98661f2a32659f37a123320d14288ce88c9d6e6279bdc36334410023f5e1"
}

View File

@@ -1,41 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND archived = false)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "concurrency_time_window_s",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "9289709b2727425be0e7f1e341d6acd46e1e79d5018f6a18cb41d50ca3f4ba11"
}

View File

@@ -1,8 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 AND workspace_id = $4 ",
"query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 AND workspace_id = $4 RETURNING id",
"describe": {
"columns": [],
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
@@ -11,7 +17,9 @@
"Text"
]
},
"nullable": []
"nullable": [
false
]
},
"hash": "b201a427430e732c45f57e0ebeba7ff9c2cbbd9f6b15266dcea789586b09e1e8"
"hash": "960dce02871e9697d7aca2fcd5a75eefc3db7179e49fbf20c7e62cbcb235683e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1",
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1 AND scheduled_for <= now() AND running = false",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "c4fb68ce79c1cec23cadc515285578ca18398504cf703091d06191ff68d95399"
"hash": "9da1bf41c433c1f781bcc8ea5d86fd796b7ff92434f5ed4682c4445a02c2c720"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id)\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28) RETURNING id",
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl)\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29) RETURNING id",
"describe": {
"columns": [
{
@@ -80,12 +80,13 @@
"Int4",
"Int4",
"Int4",
"Varchar"
"Varchar",
"Int4"
]
},
"nullable": [
false
]
},
"hash": "01d5e4e371a507602fca01190965a4f20ff806315a9ed02b372977e2d9479338"
"hash": "9e8c3ff3d6b31e366e15beda1e96e03e870ccc3b353401439bc0ed8ff219249b"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET suspend = suspend - 1 WHERE parent_job = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "a4ae245dcf7e4b930cd45701db0b7c45f2a5797e8b6724bade5b964d4334c098"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "a59b70164dc87224d09a04d5469ca217eb19a15a250c3b83ca63f606f89b9681"
}

View File

@@ -0,0 +1,80 @@
{
"db_name": "PostgreSQL",
"query": "select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND\n deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell"
]
}
}
}
},
{
"ordinal": 6,
"name": "dedicated_worker",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true,
true,
true,
true,
false,
true
]
},
"hash": "b224cdd1221fc9e7227ef8e8c025eedc09bceb82bca349f3e31c8513ebbf0192"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "cec906e3bcfe5b477f23e8bb44500b97166c789ab1cf6487679d092eebf3a48b"
}

View File

@@ -1,41 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "select path, tag, concurrent_limit, concurrency_time_window_s from script where hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "concurrency_time_window_s",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "d164caec6637ab53d8c2e0faad653a458f31a976bb23c3a43d35b8476b066954"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value->>'name' as name, path from resource WHERE resource_type = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null,
false
]
},
"hash": "ed5764ead5e8f433adfe666976664876b22dcb2b34dcd99f1e44cd03cb9ab4ef"
}

380
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.158.2"
version = "1.168.3"
authors.workspace = true
edition.workspace = true
@@ -22,7 +22,7 @@ members = [
]
[workspace.package]
version = "1.158.2"
version = "1.168.3"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -36,7 +36,8 @@ incremental = true
[features]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
benchmark = ["windmill-api/benchmark"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark"]
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
[dependencies]
anyhow.workspace = true
@@ -188,4 +189,4 @@ samael = { version = "0.0.12", features = ["xmlsec"] }
gcp_auth = "0.9.0"
rust_decimal = {version = "1.31.0", features = ["db-postgres"]}
jsonwebtoken = "8.3.0"
pem = "3.0.1"
pem = "3.0.1"

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN cache_ttl INTEGER;
ALTER TABLE queue ADD COLUMN cache_ttl INTEGER;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,6 @@
-- Add up migration script here
CREATE TABLE global_settings (
name VARCHAR(255) PRIMARY KEY,
value JSONB NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
CREATE INDEX IF NOT EXISTS queue_sort ON queue (scheduled_for, created_at) WHERE running = false;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,13 @@
-- Add up migration script here
DROP INDEX IF EXISTS queue_sort;
CREATE INDEX IF NOT EXISTS queue_sort ON queue (scheduled_for, created_at, tag) WHERE running = false;
CREATE TABLE IF NOT EXISTS concurrency_counter (
concurrency_id VARCHAR(1000) PRIMARY KEY,
counter INTEGER NOT NULL
);
DROP INDEX IF EXISTS root_queue_index_suspended;
DROP INDEX IF EXISTS queue_suspended;
CREATE INDEX IF NOT EXISTS queue_suspended ON queue (created_at, suspend_until, suspend, tag) WHERE suspend_until is not null;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN dedicated_worker boolean;

1
backend/out.json Normal file
View File

@@ -0,0 +1 @@
{"completed":true,"result":{"error":{"name":"Exception","stack":" File \"/tmp/u/ruben/gentle_script.py\", line 35, in main\n print(wmill.get_variable(\"u/admin/adorable_postgresql\"))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n File \"/tmp/windmill/cache/pip/wmill==1.166.0/wmill/client.py\", line 336, in get_variable\n raise Exception(\n","message":"Variable at path u/admin/adorable_postgresql does not exist or you do not have read permissions on it"}}}

View File

@@ -12,4 +12,6 @@ path = "./src/lib.rs"
windmill-parser.workspace = true
itertools.workspace = true
anyhow.workspace = true
gosyn.workspace = true
gosyn.workspace = true
lazy_static.workspace = true
regex.workspace = true

View File

@@ -6,8 +6,13 @@ use gosyn::{
};
use itertools::Itertools;
use regex::Regex;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
lazy_static::lazy_static! {
pub static ref REQUIRE_PARSE: Regex = Regex::new(r"//require (.*)\n").unwrap();
}
pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let filtered_code = filter_non_main(code);
let file = parse_source(&filtered_code).map_err(|x| anyhow::anyhow!(x.to_string()))?;
@@ -46,7 +51,14 @@ pub fn parse_go_imports(code: &str) -> anyhow::Result<Vec<String>> {
})
.collect();
imports.sort();
Ok(imports)
Ok([
imports,
REQUIRE_PARSE
.captures_iter(code)
.map(|x| x[1].to_string())
.collect_vec(),
]
.concat())
}
fn get_name(param: &Field) -> String {

View File

@@ -791,6 +791,7 @@ CREATE TABLE public.script (
envs character varying(1000)[],
concurrent_limit integer,
concurrency_time_window_s integer,
dedicated_worker boolean,
CONSTRAINT proper_id CHECK (((path)::text ~ '^[ufg](\/[\w-]+){2,}$'::text))
);

View File

@@ -20,7 +20,7 @@ use tokio::{
sync::RwLock,
};
use windmill_api::{LICENSE_KEY, OAUTH_CLIENTS, SMTP_CLIENT};
use windmill_common::{utils::rd_string, METRICS_ADDR};
use windmill_common::{global_settings::ENV_SETTINGS, utils::rd_string, METRICS_ADDR};
use windmill_worker::{
BUN_CACHE_DIR, BUN_TMP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
DENO_TMP_CACHE_DIR, DENO_TMP_CACHE_DIR_DEPS, DENO_TMP_CACHE_DIR_NPM, GO_BIN_CACHE_DIR,
@@ -40,15 +40,19 @@ mod monitor;
async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
#[cfg(not(feature = "flamegraph"))]
windmill_common::tracing_init::initialize_tracing();
#[cfg(feature = "flamegraph")]
let _guard = windmill_common::tracing_init::setup_flamegraph();
let num_workers = std::env::var("NUM_WORKERS")
.ok()
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
if num_workers > 1 {
tracing::warn!("We recommend using at most 1 worker per container, use more only if you know what you are doing.");
tracing::warn!("We STRONGLY recommend using at most 1 worker per container, unless this worker is dedicated to native jobs only. ");
}
let metrics_addr: Option<SocketAddr> = *METRICS_ADDR;
@@ -92,6 +96,7 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Database connected");
let rsmq = if let Some(config) = rsmq_config {
tracing::info!("Redis config set: {:?}", config);
Some(rsmq_async::MultiplexedRsmq::new(config).await.unwrap())
} else {
None
@@ -119,68 +124,7 @@ Windmill Community Edition {GIT_VERSION}
##############################"
);
display_config(vec![
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"TIMEOUT",
"ZOMBIE_JOB_TIMEOUT",
"RESTART_ZOMBIE_JOBS",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"SERVER_BIND_ADDR",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"GOPRIVATE",
"GOPROXY",
"NETRC",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PATH",
"HOME",
"DATABASE_CONNECTIONS",
"TIMEOUT_WAIT_RESULT",
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"NPM_CONFIG_REGISTRY",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
"GLOBAL_CACHE_INTERVAL",
"WORKER_TAGS",
"CUSTOM_TAGS",
"JOB_RETENTION_SECS",
"WAIT_RESULT_FAST_POLL_DURATION_SECS",
"WAIT_RESULT_SLOW_POLL_INTERVAL_MS",
"WAIT_RESULT_FAST_POLL_INTERVAL_MS",
"EXIT_AFTER_NO_JOB_FOR_SECS",
"REQUEST_SIZE_LIMIT",
"SMTP_HOST",
"SMTP_USERNAME",
"SMTP_PORT",
"SMTP_TLS_IMPLICIT",
"CREATE_WORKSPACE_REQUIRE_SUPERADMIN",
"GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE",
]);
if std::env::var("WHITELIST_WORKSPACES").is_ok()
|| std::env::var("BLACKLIST_WORKSPACES").is_ok()
{
panic!("WHITELIST_WORKSPACES and BLACKLIST_WORKSPACES have been removed, please use Worker Groups instead");
}
display_config(&ENV_SETTINGS);
tracing::info!("Loading OAuth providers...: {:#?}", *OAUTH_CLIENTS);
if let Some(ref smtp) = *SMTP_CLIENT {
@@ -264,7 +208,7 @@ Windmill Community Edition {GIT_VERSION}
Ok(())
}
fn display_config(envs: Vec<&str>) {
fn display_config(envs: &[&str]) {
tracing::info!(
"config: {}",
envs.iter()

View File

@@ -133,7 +133,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
let _ = handle_job_error(
db,
&client,
job,
&job,
error::Error::ExecutionErr(format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {})",
last_ping
@@ -145,7 +145,6 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
true,
same_worker_tx_never_used,
"",
base_internal_url,
rsmq.clone(),
)
.await;

View File

@@ -11,6 +11,10 @@ INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
INSERT INTO workspace_key(workspace_id, kind, key) VALUES
('test-workspace', 'cloud', 'test-key');
INSERT INTO workspace_settings (workspace_id) VALUES
('test-workspace');
insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true);
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (

View File

@@ -1055,6 +1055,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
iterator: InputTransform::Javascript { expr: "result".to_string() },
skip_failures: false,
parallel: false,
parallelism: None,
modules: vec![FlowModule {
id: "c".to_string(),
value: FlowModuleValue::RawScript {
@@ -1198,6 +1199,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
iterator: InputTransform::Static { value: json!([1, 2, 3]) },
skip_failures: false,
parallel: false,
parallelism: None,
modules: vec![
FlowModule {
id: "d".to_string(),
@@ -1617,6 +1619,7 @@ func main(derp string) (string, error) {
language: ScriptLang::Go,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None
}))
.arg("derp", json!("world"))
.run_until_complete(&db, port)
@@ -1646,6 +1649,7 @@ echo "hello $msg"
language: ScriptLang::Bash,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None
}))
.arg("msg", json!("world"))
.run_until_complete(&db, port)
@@ -1673,6 +1677,7 @@ def main():
lock: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
@@ -1705,6 +1710,7 @@ def main():
lock: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
@@ -1736,6 +1742,7 @@ def main():
lock: None,
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None
});
let result = run_job_in_new_worker_until_complete(&db, job, port)
@@ -2549,14 +2556,14 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
assert!(matches!(
m.value,
windmill_api_client::types::FlowModuleValue::RawScript(RawScript {
language: windmill_api_client::types::RawScriptLanguage::Deno | windmill_api_client::types::RawScriptLanguage::Bash,
language: windmill_api_client::types::RawScriptLanguage::Bash,
lock: Some(ref lock),
..
}) if lock == "")
|| matches!(
m.value,
windmill_api_client::types::FlowModuleValue::RawScript(RawScript{
language: windmill_api_client::types::RawScriptLanguage::Go | windmill_api_client::types::RawScriptLanguage::Python3,
language: windmill_api_client::types::RawScriptLanguage::Go | windmill_api_client::types::RawScriptLanguage::Python3 | windmill_api_client::types::RawScriptLanguage::Deno,
lock: Some(ref lock),
..
}) if lock.len() > 0)

View File

@@ -19,9 +19,11 @@ uuid.workspace = true
serde_json.workspace = true
rand.workspace = true
base64.workspace = true
openapiv3 = "=1.0.2"
[build-dependencies]
prettyplease = "0.1.25"
progenitor = { git = "https://github.com/oxidecomputer/progenitor", rev = "3d96016ae8d422e90513b2d34fb5b63eeab30b01" }
serde_json = "1.0"
syn = "1.0"
syn = "1.0"
openapiv3 = "=1.0.2"

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.158.2
version: 1.168.3
title: Windmill API
contact:
@@ -522,6 +522,59 @@ paths:
schema:
type: boolean
/settings/global/{key}:
get:
summary: get global settings
operationId: getGlobal
tags:
- setting
parameters:
- $ref: "#/components/parameters/Key"
responses:
"200":
description: status
content:
application/json:
schema: {}
post:
summary: post global settings
operationId: setGlobal
tags:
- setting
parameters:
- $ref: "#/components/parameters/Key"
requestBody:
description: value set
required: true
content:
application/json:
schema:
type: object
properties:
value: {}
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/settings/local:
get:
summary: get local settings
operationId: getLocal
tags:
- setting
responses:
"200":
description: status
content:
application/json:
schema: {}
/users/email:
get:
summary: get current user email (if logged in)
@@ -1951,6 +2004,33 @@ paths:
items:
$ref: "#/components/schemas/ListableResource"
/w/{workspace}/resources/list_names/{name}:
get:
summary: list resource names
operationId: listResourceNames
tags:
- resource
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Name"
responses:
"200":
description: resource list names
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
path:
type: string
required:
- name
- path
/w/{workspace}/resources/type/create:
post:
summary: create resource_type
@@ -3597,6 +3677,8 @@ paths:
type: string
path:
type: string
cache_ttl:
type: integer
required:
- content
- language
@@ -5448,6 +5530,12 @@ components:
name: token
parameters:
Key:
name: key
in: path
required: true
schema:
type: string
WorkspaceId:
name: workspace
in: path
@@ -5831,7 +5919,10 @@ components:
type: array
items:
type: integer
cache_ttl:
type: number
dedicated_worker:
type: boolean
required:
- hash
- path
@@ -5905,6 +5996,10 @@ components:
type: array
items:
type: integer
cache_ttl:
type: number
dedicated_worker:
type: boolean
required:
- path
- summary

View File

@@ -210,7 +210,7 @@ async fn create_flow(
w_id,
nf.path,
nf.summary,
nf.description,
nf.description.unwrap_or_else(String::new),
nf.value,
&authed.username,
nf.schema.and_then(|x| serde_json::to_string(&x.0).ok()),
@@ -352,7 +352,7 @@ async fn update_flow(
edited_at = now(), schema = $6::text::json, dependency_job = NULL, draft_only = NULL, tag = $9 WHERE path = $7 AND workspace_id = $8",
nf.path,
nf.summary,
nf.description,
nf.description.unwrap_or_else(String::new),
nf.value,
&authed.username,
schema.and_then(|x| serde_json::to_string(&x).ok()),
@@ -714,6 +714,7 @@ mod tests {
modules: vec![],
skip_failures: true,
parallel: false,
parallelism: None,
},
stop_after_if: Some(StopAfterIf {
expr: "previous.isEmpty()".to_string(),
@@ -748,6 +749,10 @@ mod tests {
timeout: None,
}),
same_worker: false,
concurrent_limit: None,
concurrency_time_window_s: None,
skip_expr: None,
cache_ttl: None,
};
let expect = serde_json::json!({
"modules": [

View File

@@ -200,8 +200,21 @@ pub async fn require_is_owner(
}
}
async fn _check_nb_of_groups(db: &DB) -> Result<()> {
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",)
.fetch_one(db)
.await?;
if nb_groups.unwrap_or(0) >= 5 {
return Err(Error::BadRequest(
"You have reached the maximum number of groups (5 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
.to_string(),
));
}
return Ok(());
}
async fn create_group(
authed: ApiAuthed,
Extension(_db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ng): Json<NewGroup>,
@@ -210,6 +223,9 @@ async fn create_group(
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
#[cfg(not(feature = "enterprise"))]
_check_nb_of_groups(&_db).await?;
sqlx::query!(
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4)",
w_id,
@@ -520,8 +536,7 @@ struct IGroup {
name: String,
emails: Option<Vec<String>>,
}
async fn list_igroups(authed: ApiAuthed, Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
require_super_admin(&db, &authed.email).await?;
async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
let groups = sqlx::query_as!(

View File

@@ -305,13 +305,21 @@ pub async fn get_path_for_hash<'c>(
Ok(path)
}
pub async fn get_path_tag_and_limits_for_hash(
pub async fn get_path_tag_limits_cache_for_hash(
db: &DB,
w_id: &str,
hash: i64,
) -> error::Result<(String, Option<String>, Option<i32>, Option<i32>)> {
) -> error::Result<(
String,
Option<String>,
Option<i32>,
Option<i32>,
Option<i32>,
ScriptLang,
Option<bool>,
)> {
let script = sqlx::query!(
"select path, tag, concurrent_limit, concurrency_time_window_s from script where hash = $1 AND workspace_id = $2",
"select path, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
hash,
w_id
)
@@ -327,6 +335,9 @@ pub async fn get_path_tag_and_limits_for_hash(
script.tag,
script.concurrent_limit,
script.concurrency_time_window_s,
script.cache_ttl,
script.language,
script.dedicated_worker,
))
}
@@ -685,7 +696,7 @@ async fn count_queue_jobs(
Ok(Json(
sqlx::query_as!(
QueueStats,
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1",
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1 AND scheduled_for <= now() AND running = false",
w_id
)
.fetch_one(&db)
@@ -1340,6 +1351,7 @@ impl From<UnifiedJob> for Job {
concurrency_time_window_s: uj.concurrency_time_window_s,
timeout: None,
flow_step_id: None,
cache_ttl: None,
}),
t => panic!("job type {} not valid", t),
}
@@ -2027,8 +2039,15 @@ pub async fn run_wait_result_script_by_hash(
check_queue_too_long(&db, run_query.queue_limit).await?;
let hash = script_hash.0;
let (path, tag, concurrent_limit, concurrency_time_window_s) =
get_path_tag_and_limits_for_hash(&db, &w_id, hash).await?;
let (
path,
tag,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
@@ -2045,6 +2064,9 @@ pub async fn run_wait_result_script_by_hash(
path: path,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
},
args,
&authed.username,
@@ -2218,6 +2240,7 @@ async fn run_preview_job(
lock: None,
concurrent_limit: None, // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
concurrency_time_window_s: None, // TODO(gbouv): same as above
cache_ttl: None,
}),
},
args,
@@ -2243,6 +2266,7 @@ async fn run_preview_job(
Ok((StatusCode::CREATED, uuid.to_string()))
}
#[tracing::instrument(level = "trace", skip_all)]
async fn add_noop_jobs(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -2350,8 +2374,15 @@ pub async fn run_job_by_hash(
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::Result<(StatusCode, String)> {
let hash = script_hash.0;
let (path, tag, concurrent_limit, concurrency_time_window_s) =
get_path_tag_and_limits_for_hash(&db, &w_id, hash).await?;
let (
path,
tag,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
@@ -2369,6 +2400,9 @@ pub async fn run_job_by_hash(
path: path,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
},
args,
&authed.username,
@@ -2472,7 +2506,7 @@ fn list_completed_jobs_query(
}
if let Some(ps) = &lq.script_path_start {
sqlb.and_where_like_left("script_path", "?".bind(ps));
sqlb.and_where_like_left("script_path", ps);
}
if let Some(p) = &lq.script_path_exact {
sqlb.and_where_eq("script_path", "?".bind(p));

View File

@@ -59,6 +59,7 @@ mod saml;
mod schedule;
mod scim;
mod scripts;
mod settings;
mod static_assets;
mod tracing_init;
mod users;
@@ -97,7 +98,7 @@ lazy_static::lazy_static! {
.build().unwrap();
pub static ref OAUTH_CLIENTS: AllClients = build_oauth_clients(&BASE_URL)
.map_err(|e| tracing::error!("Error building oauth clients: {}", e))
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
.unwrap();
pub static ref SMTP_CLIENT: Option<SmtpClientBuilder<String>> = {
@@ -155,7 +156,9 @@ pub async fn run_server(
for tag in ALL_TAGS.clone() {
let r =
rsmq_async::RsmqConnection::create_queue(&mut rsmq, &tag, None, None, None).await;
if r.is_ok() {
if let Err(e) = r {
tracing::info!("Redis queue {tag} could not be created: {e}");
} else {
tracing::info!("Redis queue {tag} created");
}
}
@@ -233,6 +236,7 @@ pub async fn run_server(
"/users",
users::global_service().layer(Extension(argon2.clone())),
)
.nest("/settings", settings::global_service())
.nest("/jobs", jobs::global_root_service())
.nest("/workers", workers::global_service())
.nest("/scripts", scripts::global_service())

View File

@@ -799,19 +799,29 @@ async fn slack_command(
(JobPayload::Flow(path.to_string()), None)
} else {
let path = path.strip_prefix("script/").unwrap_or_else(|| path);
let (script_hash, tag, concurrent_limit, concurrency_time_window_s) =
windmill_common::get_latest_deployed_hash_for_path(
&db,
&settings.workspace_id,
path,
)
.await?;
let (
script_hash,
tag,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
) = windmill_common::get_latest_deployed_hash_for_path(
&db,
&settings.workspace_id,
path,
)
.await?;
(
JobPayload::ScriptHash {
hash: script_hash,
path: path.to_owned(),
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
},
tag,
)

View File

@@ -31,6 +31,7 @@ use windmill_common::{
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_resources))
.route("/list_names/:type", get(list_names))
.route("/get/*path", get(get_resource))
.route("/exists/*path", get(exists_resource))
.route("/get_value/*path", get(get_resource_value))
@@ -117,6 +118,32 @@ pub struct ListResourceQuery {
resource_type: Option<String>,
resource_type_exclude: Option<String>,
}
#[derive(Serialize, FromRow)]
pub struct NamePath {
name: String,
path: String,
}
async fn list_names(
authed: ApiAuthed,
Path((w_id, rt)): Path<(String, String)>,
Extension(user_db): Extension<UserDB>,
) -> JsonResult<Vec<NamePath>> {
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query!(
"SELECT value->>'name' as name, path from resource WHERE resource_type = $1 AND workspace_id = $2",
rt,
&w_id
)
.fetch_all(&mut *tx)
.await?
.into_iter()
.filter_map(|x| x.name.map(|name| NamePath { name, path: x.path }))
.collect::<Vec<_>>();
tx.commit().await?;
Ok(Json(rows))
}
async fn list_resources(
authed: ApiAuthed,
Query(lq): Query<ListResourceQuery>,

View File

@@ -69,6 +69,10 @@ pub struct ScriptWDraft {
pub concurrent_limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_time_window_s: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_ttl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dedicated_worker: Option<bool>,
}
pub fn global_service() -> Router {
@@ -79,10 +83,12 @@ pub fn global_service() -> Router {
}
pub fn global_unauthed_service() -> Router {
Router::new().route(
"/tokened_raw/:workspace/:token/*path",
get(get_tokened_raw_script_by_path),
)
Router::new()
.route(
"/tokened_raw/:workspace/:token/*path",
get(get_tokened_raw_script_by_path),
)
.route("/empty_ts/*path", get(get_empty_ts_script_by_path))
}
pub fn workspaced_service() -> Router {
@@ -370,8 +376,8 @@ async fn create_script(
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \
draft_only, envs, concurrent_limit, concurrency_time_window_s) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)",
draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)",
&w_id,
&hash.0,
ns.path,
@@ -390,7 +396,9 @@ async fn create_script(
ns.draft_only,
envs,
ns.concurrent_limit,
ns.concurrency_time_window_s
ns.concurrency_time_window_s,
ns.cache_ttl,
ns.dedicated_worker
)
.execute(&mut tx)
.await?;
@@ -587,7 +595,7 @@ async fn get_script_by_path_w_draft(
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, draft.value as draft FROM script LEFT JOIN draft ON
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, draft.value as draft, dedicated_worker FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
@@ -633,6 +641,10 @@ async fn get_tokened_raw_script_by_path(
return raw_script_by_path(authed, Extension(user_db), Path((w_id, path))).await;
}
async fn get_empty_ts_script_by_path() -> String {
return String::new();
}
async fn raw_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -0,0 +1,83 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2023
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use crate::{
db::{ApiAuthed, DB},
utils::require_super_admin,
};
use axum::{
extract::{Extension, Path},
routing::{get, post},
Json, Router,
};
use windmill_common::{
error::{self, JsonResult},
global_settings::ENV_SETTINGS,
};
pub fn global_service() -> Router {
Router::new()
.route("/local", get(get_local_settings))
.route(
"/global/:key",
post(set_global_setting).get(get_global_setting),
)
}
pub async fn get_local_settings(
Extension(db): Extension<DB>,
authed: ApiAuthed,
) -> error::JsonResult<serde_json::Value> {
require_super_admin(&db, &authed.email).await?;
let mut settings = serde_json::Map::new();
for key in ENV_SETTINGS.iter() {
if let Some(value) = std::env::var(key).ok() {
settings.insert(key.to_string(), serde_json::Value::String(value));
}
}
Ok(Json(serde_json::Value::Object(settings)))
}
#[derive(serde::Deserialize)]
pub struct Value {
pub value: serde_json::Value,
}
pub async fn set_global_setting(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Path(key): Path<String>,
Json(value): Json<Value>,
) -> error::Result<()> {
require_super_admin(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()",
key,
value.value
)
.execute(&db)
.await?;
tracing::info!("Set global setting {} to {}", key, value.value);
Ok(())
}
pub async fn get_global_setting(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Path(key): Path<String>,
) -> JsonResult<serde_json::Value> {
require_super_admin(&db, &authed.email).await?;
let value = sqlx::query!("SELECT value FROM global_settings WHERE name = $1", key)
.fetch_optional(&db)
.await?
.map(|x| x.value);
Ok(Json(value.unwrap_or_else(|| serde_json::Value::Null)))
}

View File

@@ -10,6 +10,13 @@ use ::tracing::{field, Span};
use hyper::Response;
use tower_http::trace::{MakeSpan, OnFailure, OnResponse};
lazy_static::lazy_static! {
static ref LOG_REQUESTS: bool = std::env::var("LOG_REQUESTS")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
}
#[derive(Clone)]
pub struct MyOnResponse {}
@@ -20,11 +27,13 @@ impl<B> OnResponse<B> for MyOnResponse {
latency: std::time::Duration,
_span: &tracing::Span,
) {
tracing::info!(
latency = latency.as_millis(),
status = response.status().as_u16(),
"response"
)
if *LOG_REQUESTS {
tracing::info!(
latency = latency.as_millis(),
status = response.status().as_u16(),
"response"
)
}
}
}

View File

@@ -717,7 +717,7 @@ async fn list_users(
WHERE workspace_id = $1
AND job_kind NOT IN ('flow', 'flowpreview')
AND email = usr.email
AND now() - '5 week'::interval < created_at
AND now() - '1 week'::interval < created_at
) usage
WHERE workspace_id = $1
",

View File

@@ -132,16 +132,18 @@ async fn get_variable(
let decrypt_secret = q.decrypt_secret.unwrap_or(true);
let r = if variable.is_secret {
audit_log(
&mut *tx,
&authed.username,
"variables.decrypt_secret",
ActionKind::Execute,
&w_id,
Some(&variable.path),
None,
)
.await?;
if decrypt_secret {
audit_log(
&mut *tx,
&authed.username,
"variables.decrypt_secret",
ActionKind::Execute,
&w_id,
Some(&variable.path),
None,
)
.await?;
}
let value = variable.value.unwrap_or_else(|| "".to_string());
ListableVariable {
value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() {

View File

@@ -252,7 +252,7 @@ async fn premium_info(
require_admin(authed.is_admin, &authed.username)?;
let mut tx = db.begin().await?;
let row = sqlx::query_as::<_, PremiumWorkspaceInfo>(
"SELECT premium, usage.usage FROM workspace LEFT JOIN usage ON workspace.id = $1 AND usage.is_workspace IS true WHERE workspace.id = $1",
"SELECT premium, usage.usage FROM workspace LEFT JOIN usage ON usage.id = $1 AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date) AND usage.is_workspace IS true WHERE workspace.id = $1",
)
.bind(w_id)
.fetch_one(&mut *tx)
@@ -1515,7 +1515,7 @@ async fn tarball_workspace(
if !skip_resources.unwrap_or(false) {
let resources = sqlx::query_as!(
Resource,
"SELECT * FROM resource WHERE workspace_id = $1",
"SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
&w_id
)
.fetch_all(&db)

View File

@@ -44,7 +44,7 @@ pub struct AuditLog {
pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
db: E,
username: &str,
operation: &str,
_operation: &str,
action_kind: ActionKind,
w_id: &str,
_resource: Option<&str>,
@@ -60,10 +60,10 @@ pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
let _resource: Option<&str> = Some("EE only");
#[cfg(not(feature = "enterprise"))]
let username: &str = "redacted";
let _operation: &str = "redacted";
tracing::info!(
operation = operation,
operation = _operation,
action_kind = ?action_kind,
resource = _resource,
parameters = %p_json,
@@ -78,7 +78,7 @@ pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
)
.bind(w_id)
.bind(username)
.bind(operation)
.bind(_operation)
.bind(action_kind)
.bind(_resource)
.bind(p_json)

View File

@@ -16,6 +16,7 @@ tracing_init = [
"dep:tracing",
"dep:tracing-subscriber",
]
flamegraph = ["dep:tracing-flame"]
[lib]
@@ -41,4 +42,5 @@ hyper = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, optional = true }
lazy_static.workspace = true
lazy_static.workspace = true
tracing-flame = { version = "^0", optional = true }

View File

@@ -94,7 +94,13 @@ impl IntoResponse for Error {
}
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
};
tracing::error!(error = e.to_string());
if matches!(status, axum::http::StatusCode::NOT_FOUND) {
tracing::warn!(not_found = e.to_string());
} else {
tracing::error!(error = e.to_string());
};
axum::response::Response::builder()
.header("Content-Type", "text/plain")
.status(status)

View File

@@ -60,7 +60,7 @@ pub struct ListableFlow {
pub struct NewFlow {
pub path: String,
pub summary: String,
pub description: String,
pub description: Option<String>,
pub value: serde_json::Value,
pub schema: Option<Schema>,
pub draft_only: Option<bool>,
@@ -76,6 +76,15 @@ pub struct FlowValue {
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub same_worker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrent_limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_time_window_s: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_expr: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_ttl: Option<u32>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
@@ -256,6 +265,8 @@ pub enum FlowModuleValue {
skip_failures: bool,
#[serde(default = "default_false")]
parallel: bool,
#[serde(skip_serializing_if = "Option::is_none")]
parallelism: Option<u16>,
},
BranchOne {
branches: Vec<BranchOneModules>,
@@ -286,6 +297,17 @@ pub enum FlowModuleValue {
Identity,
}
impl FlowModuleValue {
pub fn is_simple(&self) -> bool {
match self {
FlowModuleValue::Script { .. } => true,
FlowModuleValue::Flow { .. } => true,
FlowModuleValue::RawScript { .. } => true,
_ => false,
}
}
}
fn ordered_map<S>(value: &HashMap<String, InputTransform>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,

View File

@@ -0,0 +1,58 @@
pub const WORKER_S3_BUCKET_SYNC: &str = "worker_s3_bucket_sync";
pub const ENV_SETTINGS: [&str; 54] = [
"DISABLE_NSJAIL",
"DISABLE_SERVER",
"NUM_WORKERS",
"METRICS_ADDR",
"JSON_FMT",
"BASE_URL",
"TIMEOUT",
"ZOMBIE_JOB_TIMEOUT",
"RESTART_ZOMBIE_JOBS",
"SLEEP_QUEUE",
"MAX_LOG_SIZE",
"SERVER_BIND_ADDR",
"PORT",
"KEEP_JOB_DIR",
"S3_CACHE_BUCKET",
"TAR_CACHE_RATE",
"COOKIE_DOMAIN",
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"GOPRIVATE",
"GOPROXY",
"NETRC",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
"PATH",
"HOME",
"DATABASE_CONNECTIONS",
"TIMEOUT_WAIT_RESULT",
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"NPM_CONFIG_REGISTRY",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
"GLOBAL_CACHE_INTERVAL",
"WORKER_TAGS",
"CUSTOM_TAGS",
"JOB_RETENTION_SECS",
"WAIT_RESULT_FAST_POLL_DURATION_SECS",
"WAIT_RESULT_SLOW_POLL_INTERVAL_MS",
"WAIT_RESULT_FAST_POLL_INTERVAL_MS",
"EXIT_AFTER_NO_JOB_FOR_SECS",
"REQUEST_SIZE_LIMIT",
"SMTP_HOST",
"SMTP_USERNAME",
"SMTP_PORT",
"SMTP_TLS_IMPLICIT",
"CREATE_WORKSPACE_REQUIRE_SUPERADMIN",
"GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE",
];

View File

@@ -90,6 +90,8 @@ pub struct QueuedJob {
pub timeout: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_step_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_ttl: Option<i32>,
}
impl QueuedJob {
@@ -99,6 +101,17 @@ impl QueuedJob {
.map(String::as_str)
.unwrap_or("tmp/main")
}
pub fn is_flow(&self) -> bool {
matches!(self.job_kind, JobKind::Flow | JobKind::FlowPreview)
}
pub fn full_path(&self) -> String {
format!(
"{}/{}",
if self.is_flow() { "flow" } else { "script" },
self.script_path()
)
}
}
impl QueuedJob {
@@ -156,6 +169,7 @@ impl Default for QueuedJob {
concurrency_time_window_s: None,
timeout: None,
flow_step_id: None,
cache_ttl: None,
}
}
}
@@ -170,6 +184,9 @@ pub enum JobPayload {
path: String,
concurrent_limit: Option<i32>,
concurrency_time_window_s: Option<i32>,
cache_ttl: Option<i32>,
dedicated_worker: Option<bool>,
language: ScriptLang,
},
Code(RawCode),
Dependencies {
@@ -202,6 +219,7 @@ pub struct RawCode {
pub lock: Option<String>,
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
}
type Tag = String;
@@ -216,14 +234,24 @@ pub async fn script_path_to_payload(
let (job_payload, tag) = if script_path.starts_with("hub/") {
(JobPayload::ScriptHub { path: script_path.to_owned() }, None)
} else {
let (script_hash, tag, concurrent_limit, concurrency_time_window_s) =
get_latest_deployed_hash_for_path(db, w_id, script_path).await?;
let (
script_hash,
tag,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
) = get_latest_deployed_hash_for_path(db, w_id, script_path).await?;
(
JobPayload::ScriptHash {
hash: script_hash,
path: script_path.to_owned(),
concurrent_limit,
concurrency_time_window_s,
cache_ttl: cache_ttl,
language,
dedicated_worker,
},
tag,
)
@@ -235,9 +263,16 @@ pub async fn script_hash_to_tag_and_limits<'c>(
script_hash: &ScriptHash,
db: &mut Transaction<'c, Postgres>,
w_id: &String,
) -> error::Result<(Option<Tag>, Option<i32>, Option<i32>)> {
) -> error::Result<(
Option<Tag>,
Option<i32>,
Option<i32>,
Option<i32>,
ScriptLang,
Option<bool>,
)> {
let script = sqlx::query!(
"select tag, concurrent_limit, concurrency_time_window_s from script where hash = $1 AND workspace_id = $2",
"select tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where hash = $1 AND workspace_id = $2",
script_hash.0,
w_id
)
@@ -252,6 +287,9 @@ pub async fn script_hash_to_tag_and_limits<'c>(
script.tag,
script.concurrent_limit,
script.concurrency_time_window_s,
script.cache_ttl,
script.language,
script.dedicated_worker,
))
}

View File

@@ -9,6 +9,7 @@
use std::net::SocketAddr;
use error::Error;
use scripts::ScriptLang;
use sqlx::{Pool, Postgres};
pub mod apps;
@@ -17,6 +18,7 @@ pub mod error;
pub mod external_ip;
pub mod flow_status;
pub mod flows;
pub mod global_settings;
pub mod jobs;
pub mod more_serde;
pub mod oauth2;
@@ -163,24 +165,35 @@ pub async fn get_latest_deployed_hash_for_path(
db: &DB,
w_id: &str,
script_path: &str,
) -> error::Result<(scripts::ScriptHash, Option<Tag>, Option<i32>, Option<i32>)> {
) -> error::Result<(
scripts::ScriptHash,
Option<Tag>,
Option<i32>,
Option<i32>,
Option<i32>,
ScriptLang,
Option<bool>,
)> {
let r_o = sqlx::query!(
"select hash, tag, concurrent_limit, concurrency_time_window_s from script where path = $1 AND workspace_id = $2 AND
"select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND archived = false AND lock IS not NULL AND lock_error_logs IS NULL)",
deleted = false AND lock IS not NULL AND lock_error_logs IS NULL)",
script_path,
w_id
)
.fetch_optional(db)
.await?;
let script = utils::not_found_if_none(r_o, "script", script_path)?;
let script = utils::not_found_if_none(r_o, "deployed script", script_path)?;
Ok((
scripts::ScriptHash(script.hash),
script.tag,
script.concurrent_limit,
script.concurrency_time_window_s,
script.cache_ttl,
script.language,
script.dedicated_worker,
))
}
@@ -188,9 +201,17 @@ pub async fn get_latest_hash_for_path<'c>(
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
w_id: &str,
script_path: &str,
) -> error::Result<(scripts::ScriptHash, Option<Tag>, Option<i32>, Option<i32>)> {
) -> error::Result<(
scripts::ScriptHash,
Option<Tag>,
Option<i32>,
Option<i32>,
Option<i32>,
ScriptLang,
Option<bool>,
)> {
let r_o = sqlx::query!(
"select hash, tag, concurrent_limit, concurrency_time_window_s from script where path = $1 AND workspace_id = $2 AND
"select hash, tag, concurrent_limit, concurrency_time_window_s, cache_ttl, language as \"language: ScriptLang\", dedicated_worker from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND archived = false)",
script_path,
@@ -206,5 +227,8 @@ pub async fn get_latest_hash_for_path<'c>(
script.tag,
script.concurrent_limit,
script.concurrency_time_window_s,
script.cache_ttl,
script.language,
script.dedicated_worker,
))
}

View File

@@ -214,6 +214,8 @@ pub struct NewScript {
pub envs: Option<Vec<String>>,
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
pub dedicated_worker: Option<bool>,
}
#[derive(Deserialize)]

View File

@@ -48,3 +48,19 @@ pub fn initialize_tracing() {
.init(),
}
}
#[cfg(feature = "flamegraph")]
use tracing_flame::FlameLayer;
#[cfg(feature = "flamegraph")]
pub fn setup_flamegraph() -> impl Drop {
// let fmt_layer = Layer::default();
let (flame_layer, _guard) = FlameLayer::with_file("./tracing.folded").unwrap();
tracing_subscriber::registry()
// .with(fmt_layer)
.with(flame_layer)
.init();
_guard
}

View File

@@ -11,6 +11,7 @@ path = "src/lib.rs"
[features]
default = []
enterprise = []
benchmark = []
[dependencies]
windmill-audit.workspace = true

View File

@@ -8,6 +8,9 @@
use std::{collections::HashMap, sync::atomic::AtomicBool, vec};
#[cfg(feature = "benchmark")]
use std::time::Instant;
use anyhow::Context;
use async_recursion::async_recursion;
use chrono::{DateTime, Duration, Utc};
@@ -84,33 +87,36 @@ lazy_static::lazy_static! {
"hub".to_string(),
"other".to_string()];
pub static ref ACCEPTED_TAGS: Vec<String> = std::env::var("WORKER_TAGS")
pub static ref DEDICATED_WORKER: Option<(String, String)> = std::env::var("DEDICATED_WORKER")
.ok()
.map(|x| {
let splitted = x.split(':').to_owned().collect_vec();
if splitted.len() != 2 {
panic!("DEDICATED_WORKER should be in the form of <workspace>:<script_path>")
} else {
let workspace = splitted[0];
let script_path = splitted[1];
(workspace.to_string(), script_path.to_string())
}
});
pub static ref ACCEPTED_TAGS: Vec<String> = {
let worker_tags = std::env::var("WORKER_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect())
.unwrap_or_else(|| DEFAULT_TAGS.clone()) ;
.unwrap_or_else(|| DEFAULT_TAGS.clone());
if let Some(ref dedicated_worker) = DEDICATED_WORKER.as_ref() {
vec![format!("{}:{}", dedicated_worker.0, dedicated_worker.1)]
} else {
worker_tags
}
};
pub static ref IS_WORKER_TAGS_DEFINED: bool = std::env::var("WORKER_TAGS").ok().is_some();
pub static ref PULL_QUERY: String = format!(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE ((running = false AND scheduled_for <= now())
OR (suspend_until IS NOT NULL AND (suspend <= 0 OR suspend_until <= now())))
{}
ORDER BY scheduled_for, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *",
format!(" AND ({})",
ACCEPTED_TAGS.clone().into_iter().map(|x| format!("(tag = '{x}')")).join(" OR "))
);
// When compiled in 'benchmark' mode, this flags is exposed via the /workers/toggle endpoint
// and make it possible to disable to current active workers (such that they don't pull any)
@@ -140,30 +146,33 @@ pub async fn cancel_job<'c: 'async_recursion>(
return Ok((tx, None));
}
let job_running = job_running.unwrap();
if job_running.running
&& job_running.job_kind != JobKind::Flow
&& job_running.job_kind != JobKind::FlowPreview
if ((job_running.running || job_running.root_job.is_some())
|| (job_running.job_kind == JobKind::Flow || job_running.job_kind == JobKind::FlowPreview))
&& !force_cancel
{
sqlx::query!(
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 \
AND workspace_id = $4 ",
let id = sqlx::query_scalar!(
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 AND workspace_id = $4 RETURNING id",
username,
reason,
id,
w_id
)
.execute(&mut *tx)
.fetch_optional(&mut *tx)
.await?;
if let Some(id) = id {
tracing::info!("Soft cancelling job {}", id);
}
} else {
let reason = reason
.clone()
.unwrap_or_else(|| "No reason provided".to_string());
let e = serde_json::json!({"message": format!("Job canceled: {reason} by {username}"), "name": "Canceled", "reason": reason, "canceler": username});
let add_job = add_completed_job_error(
&db,
&job_running,
format!("canceled by {username}: (force cancel: {force_cancel})"),
serde_json::json!({"message": format!("Job canceled: {reason} by {username}"), "name": "Canceled", "reason": reason, "canceler": username}),
&e,
None,
rsmq.clone(),
)
@@ -214,7 +223,7 @@ pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Sen
db: &Pool<Postgres>,
queued_job: &QueuedJob,
logs: String,
e: serde_json::Value,
e: &serde_json::Value,
metrics: Option<Metrics>,
rsmq: Option<R>,
) -> Result<serde_json::Value, Error> {
@@ -222,7 +231,7 @@ pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Sen
metrics.map(|m| m.worker_execution_failed.inc());
}
let result = serde_json::json!({ "error": e });
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs, rsmq).await?;
let _ = add_completed_job(db, &queued_job, false, false, &result, logs, rsmq).await?;
Ok(result)
}
@@ -248,13 +257,13 @@ lazy_static::lazy_static! {
pub static ref GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE: Option<String> = std::env::var("GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE").ok();
}
#[instrument(level = "trace", skip_all)]
#[instrument(level = "trace", skip_all, name = "add_completed_job")]
pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
db: &Pool<Postgres>,
queued_job: &QueuedJob,
success: bool,
skipped: bool,
result: serde_json::Value,
result: &serde_json::Value,
logs: String,
rsmq: Option<R>,
) -> Result<Uuid, Error> {
@@ -362,6 +371,7 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
.await
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
// tracing::error!("Added completed job {:#?}", queued_job);
tx = delete_job(tx, &queued_job.workspace_id, job_id).await?;
if !queued_job.is_flow_step
&& queued_job.schedule_path.is_some()
@@ -396,6 +406,19 @@ pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
)
.await?;
}
if queued_job.concurrent_limit.is_some() {
if let Err(e) = sqlx::query_scalar!(
"UPDATE concurrency_counter SET counter = counter - 1 WHERE concurrency_id = $1",
queued_job.full_path()
)
.execute(&mut tx)
.await
{
tracing::error!("Could not decrement concurrency counter: {}", e);
}
tracing::debug!("decremented concurrency counter");
}
tx.commit().await?;
#[cfg(feature = "enterprise")]
@@ -937,14 +960,15 @@ async fn handle_on_recovery<'c, R: rsmq_async::RsmqConnection + Clone + Send + '
pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
db: &Pool<Postgres>,
rsmq: Option<R>,
suspend_first: bool,
) -> windmill_common::error::Result<Option<QueuedJob>> {
// let rs = rd_string(2);
// let instant = Instant::now();
loop {
let tx: QueueTransaction<'_, _> = (rsmq.clone(), db.clone().begin().await?).into();
let (job, mut tx) =
pull_single_job_and_mark_as_running_no_concurrency_limit(tx, rsmq.clone()).await?;
let job = pull_single_job_and_mark_as_running_no_concurrency_limit(
db,
rsmq.clone(),
suspend_first,
)
.await?;
if job.is_none() {
return Ok(None);
@@ -952,14 +976,20 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
// concurrency check. If more than X jobs for this path are already running, we re-queue and pull another job from the queue
let pulled_job = job.unwrap();
if pulled_job.script_path.is_none() || pulled_job.concurrent_limit.is_none() {
if pulled_job.script_path.is_none()
|| pulled_job.concurrent_limit.is_none()
|| pulled_job.canceled
{
if *METRICS_ENABLED {
QUEUE_PULL_COUNT.inc();
}
tx.commit().await?;
return Ok(Option::Some(pulled_job));
}
let itx = db.begin().await?;
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), itx).into();
// Else the job is subject to concurrency limits
let job_script_path = pulled_job.script_path.clone().unwrap();
@@ -973,17 +1003,33 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
job_custom_concurrency_time_window_s
);
let running_job = sqlx::query_scalar!(
"INSERT INTO concurrency_counter VALUES ($1, 1)
ON CONFLICT (concurrency_id)
DO UPDATE SET counter = concurrency_counter.counter + 1
RETURNING concurrency_counter.counter",
pulled_job.full_path(),
)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!(
"Error getting concurrency count for script path {job_script_path}: {e}"
))
})?;
tracing::debug!("running_job: {}", running_job);
let script_path_live_stats = sqlx::query!(
"SELECT COALESCE(j.min_started_at, q.min_started_at) AS min_started_at, COALESCE(completed_count, 0) + COALESCE(running_count, 0) AS total_count
"SELECT COALESCE(j.min_started_at, q.min_started_at) AS min_started_at, COALESCE(completed_count, 0) AS completed_count
FROM
(SELECT script_path, MIN(started_at) as min_started_at, COUNT(*) as completed_count
FROM completed_job
WHERE script_path = $1 AND started_at + INTERVAL '1 MILLISECOND' * duration_ms > (now() - INTERVAL '1 second' * $2) AND workspace_id = $3
WHERE script_path = $1 AND job_kind != 'dependencies' AND started_at + INTERVAL '1 MILLISECOND' * duration_ms > (now() - INTERVAL '1 second' * $2) AND workspace_id = $3 AND canceled = false
GROUP BY script_path) as j
FULL OUTER JOIN
(SELECT script_path, MIN(started_at) as min_started_at, COUNT(*) as running_count
(SELECT script_path, MIN(started_at) as min_started_at
FROM queue
WHERE script_path = $1 AND running = true AND workspace_id = $3
WHERE script_path = $1 AND job_kind != 'dependencies' AND running = true AND workspace_id = $3 AND canceled = false
GROUP BY script_path) as q
ON q.script_path = j.script_path",
job_script_path,
@@ -998,20 +1044,31 @@ pub async fn pull<R: rsmq_async::RsmqConnection + Send + Clone>(
))
})?;
let concurrent_jobs_for_this_script: Option<i64> = script_path_live_stats.total_count;
let concurrent_jobs_for_this_script =
script_path_live_stats.completed_count.unwrap_or_default() as i32 + running_job;
tracing::debug!(
"Current concurrent jobs for this script: {}",
concurrent_jobs_for_this_script.unwrap_or(-1)
concurrent_jobs_for_this_script
);
if concurrent_jobs_for_this_script.is_none()
|| concurrent_jobs_for_this_script.unwrap() <= i64::from(job_custom_concurrent_limit)
{
if concurrent_jobs_for_this_script <= job_custom_concurrent_limit {
if *METRICS_ENABLED {
QUEUE_PULL_COUNT.inc();
}
tx.commit().await?;
return Ok(Option::Some(pulled_job));
}
let x = sqlx::query_scalar!(
"UPDATE concurrency_counter SET counter = counter - 1 WHERE concurrency_id = $1 RETURNING counter",
pulled_job.full_path()
)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!(
"Error decreasing concurrency count for script path {job_script_path}: {e}"
))
})?;
tracing::debug!("running_job after decrease: {}", x);
let job_uuid: Uuid = pulled_job.id;
let min_started_at: Option<DateTime<Utc>> = script_path_live_stats.min_started_at;
@@ -1085,24 +1142,35 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
'c,
R: rsmq_async::RsmqConnection + Send + Clone,
>(
mut tx: QueueTransaction<'c, R>,
db: &Pool<Postgres>,
rsmq: Option<R>,
) -> windmill_common::error::Result<(Option<QueuedJob>, QueueTransaction<'c, R>)> {
suspend_first: bool,
) -> windmill_common::error::Result<Option<QueuedJob>> {
let job: Option<QueuedJob> = if let Some(mut rsmq) = rsmq {
#[cfg(feature = "benchmark")]
let instant = Instant::now();
// TODO: REDIS: Race conditions / replace last_ping
// TODO: shuffle this list to have fairness
let mut all_tags = ACCEPTED_TAGS.clone();
let mut msg: Option<_> = None;
let mut tag = None;
while msg.is_none() && !all_tags.is_empty() {
let ntag = all_tags.pop().unwrap();
tag = Some(ntag.clone());
msg = rsmq
.pop_message::<Vec<u8>>(&all_tags.pop().unwrap())
.receive_message::<Vec<u8>>(&ntag, Some(10))
.await
.map_err(|e| anyhow::anyhow!(e))?;
}
// println!("3.1: {:?} {rs}", instant.elapsed());
// #[cfg(feature = "benchmark")]
// println!("rsmq 1: {:?}", instant.elapsed());
// println!("3.1: {:?} {rs}", instant.elapsed());
if let Some(msg) = msg {
let uuid = Uuid::from_bytes_le(
msg.message
@@ -1110,7 +1178,7 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
.map_err(|_| anyhow::anyhow!("Failed to parsed Redis message"))?,
);
sqlx::query_as::<_, QueuedJob>(
let m2 = sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
@@ -1120,8 +1188,17 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
RETURNING *",
)
.bind(uuid)
.fetch_optional(&mut tx)
.await?
.fetch_optional(db)
.await?;
rsmq.delete_message(&tag.unwrap(), &msg.id)
.await
.map_err(|e| anyhow::anyhow!(e))?;
#[cfg(feature = "benchmark")]
println!("rsmq 2: {:?}", instant.elapsed());
m2
} else {
None
}
@@ -1133,11 +1210,62 @@ async fn pull_single_job_and_mark_as_running_no_concurrency_limit<
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
sqlx::query_as::<_, QueuedJob>(&PULL_QUERY)
.fetch_optional(&mut tx)
.await?
let r = if suspend_first {
sqlx::query_as::<_, QueuedJob>("UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE suspend_until IS NOT NULL AND (suspend <= 0 OR suspend_until <= now()) AND tag = ANY($1)
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *")
.bind(ACCEPTED_TAGS.as_slice())
.fetch_optional(db)
.await?
} else {
None
};
if r.is_none() {
// #[cfg(feature = "benchmark")]
// let instant = Instant::now();
let r = sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
, last_ping = now()
, suspend_until = null
WHERE id = (
SELECT id
FROM queue
WHERE running = false AND scheduled_for <= now() AND tag = ANY($1)
ORDER BY scheduled_for, created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *",
)
.bind(ACCEPTED_TAGS.as_slice())
.fetch_optional(db)
.await?;
// #[cfg(feature = "benchmark")]
// println!("pull query: {:?}", instant.elapsed());
r
} else {
r
}
};
Ok((job, tx))
Ok(job)
}
#[async_recursion]
@@ -1403,30 +1531,29 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
language,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
dedicated_worker,
) = match job_payload {
JobPayload::ScriptHash { hash, path, concurrent_limit, concurrency_time_window_s } => {
let language = fetch_scalar_isolated!(sqlx::query_scalar!(
"SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND workspace_id = $2",
hash.0,
workspace_id
), tx)
.ok().flatten()
.ok_or_else(||{
Error::InternalErr(format!(
"fetching language for hash {hash} in {workspace_id}"
))
})?;
(
Some(hash.0),
Some(path),
None,
JobKind::Script,
None,
Some(language),
concurrent_limit,
concurrency_time_window_s,
)
}
JobPayload::ScriptHash {
hash,
path,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
} => (
Some(hash.0),
Some(path),
None,
JobKind::Script,
None,
Some(language),
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
dedicated_worker,
),
JobPayload::ScriptHub { path } => {
(
None,
@@ -1438,6 +1565,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
None,
None,
None,
None,
None,
)
}
JobPayload::Code(RawCode {
@@ -1447,6 +1576,7 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
lock,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
}) => (
None,
path,
@@ -1456,6 +1586,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
Some(language),
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
None,
),
JobPayload::Dependencies { hash, dependencies, language, path } => (
Some(hash.0),
@@ -1466,6 +1598,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
Some(language),
None,
None,
None,
None,
),
JobPayload::FlowDependencies { path } => {
let value_json = fetch_scalar_isolated!(
@@ -1491,6 +1625,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
None,
None,
None,
None,
None,
)
}
JobPayload::AppDependencies { path, version } => (
@@ -1502,15 +1638,19 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
None,
None,
None,
None,
None,
),
JobPayload::RawFlow { value, path } => (
None,
path,
None,
JobKind::FlowPreview,
Some(value),
None,
Some(value.clone()),
None,
value.concurrent_limit.clone(),
value.concurrency_time_window_s,
value.cache_ttl.map(|x| x as i32),
None,
),
JobPayload::Flow(flow) => {
@@ -1533,14 +1673,38 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
Some(flow),
None,
JobKind::Flow,
Some(value),
None,
Some(value.clone()),
None,
value.concurrent_limit.clone(),
value.concurrency_time_window_s,
value.cache_ttl.map(|x| x as i32),
None,
)
}
JobPayload::Identity => (None, None, None, JobKind::Identity, None, None, None, None),
JobPayload::Noop => (None, None, None, JobKind::Noop, None, None, None, None),
JobPayload::Identity => (
None,
None,
None,
JobKind::Identity,
None,
None,
None,
None,
None,
None,
),
JobPayload::Noop => (
None,
None,
None,
JobKind::Noop,
None,
None,
None,
None,
None,
None,
),
};
let is_running = same_worker;
@@ -1594,7 +1758,13 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
let tag = if job_kind == JobKind::Script_Hub {
let tag = if dedicated_worker.is_some_and(|x| x) {
format!(
"{}:{}",
workspace_id,
script_path.clone().expect("dedicated script has a path")
)
} else if job_kind == JobKind::Script_Hub {
"hub".to_string()
} else {
if tag == Some("".to_string()) {
@@ -1612,12 +1782,13 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
"deno".to_string()
}
};
tag.unwrap_or_else(|| {
language
.as_ref()
.map(|x| x.as_str().replace("$workspace", workspace_id))
.unwrap_or_else(default)
})
tag.map(|x| x.as_str().replace("$workspace", workspace_id).to_string())
.unwrap_or_else(|| {
language
.as_ref()
.map(|x| x.as_str().to_string())
.unwrap_or_else(default)
})
};
let mut tx = match tx {
@@ -1651,8 +1822,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
"INSERT INTO queue
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28) \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job, tag, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id, cache_ttl)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29) \
RETURNING id",
workspace_id,
job_id,
@@ -1681,7 +1852,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
concurrent_limit,
concurrency_time_window_s,
custom_timeout,
flow_step_id
flow_step_id,
cache_ttl
)
.fetch_one(&mut tx)
.await

View File

@@ -74,19 +74,29 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
let (payload, tag) = if schedule.is_flow {
(JobPayload::Flow(schedule.script_path), None)
} else {
let (hash, tag, concurrent_limit, concurrency_time_window_s) =
windmill_common::get_latest_hash_for_path(
tx.transaction_mut(),
&schedule.workspace_id,
&schedule.script_path,
)
.await?;
let (
hash,
tag,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
language,
dedicated_worker,
) = windmill_common::get_latest_hash_for_path(
tx.transaction_mut(),
&schedule.workspace_id,
&schedule.script_path,
)
.await?;
(
JobPayload::ScriptHash {
hash,
path: schedule.script_path,
concurrent_limit: concurrent_limit,
concurrency_time_window_s: concurrency_time_window_s,
cache_ttl: cache_ttl,
dedicated_worker,
language,
},
tag,
)

View File

@@ -11,6 +11,8 @@ path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-queue/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2"]
benchmark = ["windmill-queue/benchmark"]
flamegraph = []
[dependencies]
windmill-queue.workspace = true

View File

@@ -1,4 +1,4 @@
const p: BunPlugin = {
const p = {
name: "windmill-relative-resolver",
async setup(build) {
const { writeFileSync, readFileSync, mkdirSync } = await import("fs");
@@ -41,11 +41,3 @@ const p: BunPlugin = {
});
},
};
import { BunPlugin } from "bun";
Bun.build({
entrypoints: ["main.ts"],
outdir: "./out",
plugins: [p],
});

View File

@@ -14,6 +14,7 @@ use crate::{
transform_json_value, write_file,
},
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
TZ_ENV,
};
lazy_static::lazy_static! {
@@ -226,6 +227,7 @@ pub async fn handle_powershell_job(
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("TZ", TZ_ENV.as_str())
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(cmd_args)
@@ -239,6 +241,7 @@ pub async fn handle_powershell_job(
.env_clear()
.envs(envs)
.envs(reserved_variables)
.env("TZ", TZ_ENV.as_str())
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("HOME", HOME_ENV.as_str())

View File

@@ -6,7 +6,7 @@ use windmill_queue::HTTP_CLIENT;
use serde::Deserialize;
use crate::{common::transform_json_value, AuthedClient, JobCompleted};
use crate::{common::transform_json_value, AuthedClient};
use gcp_auth::{AuthenticationManager, CustomServiceAccount};
@@ -55,7 +55,7 @@ pub async fn do_bigquery(
job: QueuedJob,
client: &AuthedClient,
query: &str,
) -> windmill_common::error::Result<JobCompleted> {
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
} else {
@@ -176,12 +176,7 @@ pub async fn do_bigquery(
}
if result.rows.is_none() || result.rows.as_ref().unwrap().len() == 0 {
return Ok(JobCompleted {
job: job,
result: Value::Array(vec![]),
logs: "".to_string(),
success: true,
});
return Ok(Value::Array(vec![]));
}
if result.schema.is_none() {
@@ -223,12 +218,7 @@ pub async fn do_bigquery(
})
.collect();
return Ok(JobCompleted {
job: job,
result: rows,
logs: "".to_string(),
success: true,
});
return Ok(rows);
}
Err(e) => match response.json::<BigqueryErrorResponse>().await {
Ok(bq_err) => return Err(Error::ExecutionErr(bq_err.error.message)),

View File

@@ -1,21 +1,45 @@
use std::{collections::HashMap, process::Stdio};
#[cfg(feature = "enterprise")]
use std::collections::VecDeque;
#[cfg(feature = "enterprise")]
use anyhow::Context;
use base64::Engine;
use itertools::Itertools;
use uuid::Uuid;
#[cfg(feature = "enterprise")]
use crate::{common::build_envs_map, JobCompleted};
use crate::{
common::{
create_args_and_out_file, get_reserved_variables, handle_child, read_result, set_logs,
write_file, write_file_binary,
},
AuthedClientBackgroundTask, BUN_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL, DISABLE_NUSER,
NPM_CONFIG_REGISTRY, NSJAIL_PATH, PATH_ENV,
NPM_CONFIG_REGISTRY, NSJAIL_PATH, PATH_ENV, TZ_ENV,
};
use tokio::{fs::File, io::AsyncReadExt, process::Command};
use windmill_common::error::Result;
#[cfg(feature = "enterprise")]
use crate::MAX_BUFFERED_DEDICATED_JOBS;
use tokio::{fs::File, process::Command};
#[cfg(feature = "enterprise")]
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::io::AsyncReadExt;
#[cfg(feature = "enterprise")]
use tokio::sync::mpsc::{Receiver, Sender};
#[cfg(feature = "enterprise")]
use windmill_common::variables;
use windmill_common::{
error::{self},
error::{self, Result},
jobs::QueuedJob,
};
use windmill_parser::Typ;
@@ -26,8 +50,8 @@ const RELATIVE_BUN_BUILDER: &str = include_str!("../loader_builder.bun.ts");
const NSJAIL_CONFIG_RUN_BUN_CONTENT: &str = include_str!("../nsjail/run.bun.config.proto");
const BUN_LOCKB_SPLIT: &str = "\n//bun.lockb\n";
const EMPTY_FILE: &str = "<empty>";
pub const BUN_LOCKB_SPLIT: &str = "\n//bun.lockb\n";
pub const EMPTY_FILE: &str = "<empty>";
pub async fn gen_lockfile(
logs: &mut String,
@@ -71,8 +95,6 @@ pub async fn gen_lockfile(
.stderr(Stdio::piped())
.spawn()?;
// logs.push_str(format!("prepare: {:?}\n", start.elapsed().as_micros()).as_str());
// start = Instant::now();
handle_child(
job_id,
db,
@@ -139,8 +161,6 @@ pub async fn install_lockfile(
.stderr(Stdio::piped())
.spawn()?;
// logs.push_str(format!("prepare: {:?}\n", start.elapsed().as_micros()).as_str());
// start = Instant::now();
handle_child(
job_id,
db,
@@ -170,8 +190,6 @@ pub async fn handle_bun_job(
envs: HashMap<String, String>,
shared_mount: &str,
) -> error::Result<serde_json::Value> {
set_logs(&logs, &job.id, &db).await;
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url);
@@ -209,6 +227,7 @@ pub async fn handle_bun_job(
.await?;
} else if !*DISABLE_NSJAIL {
logs.push_str("\n\n--- BUN INSTALL ---\n");
set_logs(&logs, &job.id, &db).await;
let _ = gen_lockfile(
logs,
&job.id,
@@ -224,9 +243,12 @@ pub async fn handle_bun_job(
.await?;
}
// let mut start = Instant::now();
logs.push_str("\n\n--- BUN CODE EXECUTION ---\n");
set_logs(&logs, &job.id, &db).await;
let logs_f = async {
set_logs(&logs, &job.id, &db).await;
Ok(()) as error::Result<()>
};
let write_wrapper_f = async {
// let mut start = Instant::now();
@@ -291,27 +313,35 @@ run().catch(async (e) => {{
Ok(reserved_variables) as error::Result<HashMap<String, String>>
};
let (reserved_variables, _) = tokio::try_join!(reserved_variables_args_out_f, write_wrapper_f)?;
let _ = write_file(
&job_dir,
"loader.bun.ts",
&format!(
r#"
let write_loader_f = async {
write_file(
&job_dir,
"loader.bun.ts",
&format!(
r#"
import {{ plugin }} from "bun";
{}
plugin(p)
"#,
RELATIVE_BUN_LOADER
.replace("W_ID", &job.workspace_id)
.replace("BASE_INTERNAL_URL", base_internal_url)
.replace("TOKEN", &client.get_token().await)
.replace("CURRENT_PATH", job.script_path())
),
)
.await?;
RELATIVE_BUN_LOADER
.replace("W_ID", &job.workspace_id)
.replace("BASE_INTERNAL_URL", base_internal_url)
.replace("TOKEN", &client.get_token().await)
.replace("CURRENT_PATH", job.script_path())
),
)
.await?;
Ok(()) as error::Result<()>
};
let (reserved_variables, _, _, _) = tokio::try_join!(
reserved_variables_args_out_f,
write_wrapper_f,
logs_f,
write_loader_f
)?;
//do not cache local dependencies
let child = if !*DISABLE_NSJAIL {
@@ -370,8 +400,6 @@ plugin(p)
.spawn()?
};
// logs.push_str(format!("prepare: {:?}\n", start.elapsed().as_micros()).as_str());
// start = Instant::now();
handle_child(
&job.id,
db,
@@ -384,13 +412,13 @@ plugin(p)
job.timeout,
)
.await?;
// logs.push_str(format!("execute: {:?}\n", start.elapsed().as_millis()).as_str());
read_result(job_dir).await
}
fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String> {
pub fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String> {
let mut deno_envs: HashMap<String, String> = HashMap::from([
(String::from("PATH"), PATH_ENV.clone()),
(String::from("TZ"), TZ_ENV.clone()),
(String::from("DISABLE_COLORS"), "0".to_string()),
(String::from("DO_NOT_TRACK"), "1".to_string()),
(
@@ -410,3 +438,298 @@ fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String>
}
return deno_envs;
}
#[cfg(feature = "enterprise")]
pub async fn start_worker(
requirements_o: Option<String>,
db: &sqlx::Pool<sqlx::Postgres>,
inner_content: &str,
base_internal_url: &str,
job_dir: &str,
worker_name: &str,
envs: HashMap<String, String>,
w_id: &str,
script_path: &str,
token: &str,
job_completed_tx: Sender<JobCompleted>,
mut jobs_rx: Receiver<QueuedJob>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> Result<()> {
let mut logs = "".to_string();
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url);
let context = variables::get_reserved_variables(
w_id,
&token,
"dedicated_worker@windmill.dev",
"dedicated_worker",
"NOT_AVAILABLE",
"dedicated_worker",
Some(script_path.to_string()),
None,
None,
None,
None,
)
.to_vec();
let context_envs = build_envs_map(context);
if let Some(reqs) = requirements_o {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
if splitted.len() != 2 {
return Err(error::Error::ExecutionErr(
format!("Invalid requirements, expectd to find //bun.lockb split pattern in reqs. Found: |{reqs}|")
));
}
let _ = write_file(job_dir, "package.json", &splitted[0]).await?;
let lockb = splitted[1];
if lockb != EMPTY_FILE {
let _ = write_file_binary(
job_dir,
"bun.lockb",
&base64::engine::general_purpose::STANDARD
.decode(&splitted[1])
.map_err(|_| {
error::Error::InternalErr("Could not decode bun.lockb".to_string())
})?,
)
.await?;
}
install_lockfile(
&mut logs,
&Uuid::nil(),
&w_id,
db,
job_dir,
worker_name,
common_bun_proc_envs.clone(),
)
.await?;
} else if !*DISABLE_NSJAIL {
logs.push_str("\n\n--- BUN INSTALL ---\n");
let _ = gen_lockfile(
&mut logs,
&Uuid::nil(),
&w_id,
db,
token,
&script_path,
job_dir,
base_internal_url,
worker_name,
false,
)
.await?;
}
{
// let mut start = Instant::now();
let args = windmill_parser_ts::parse_deno_signature(inner_content, true)?.args;
let dates = args
.iter()
.enumerate()
.filter_map(|(i, x)| {
if matches!(x.typ, Typ::Datetime) {
Some(i)
} else {
None
}
})
.map(|x| return format!("args[{x}] = args[{x}] ? new Date(args[{x}]) : undefined"))
.join("\n");
let spread = args.into_iter().map(|x| x.name).join(",");
// logs.push_str(format!("infer args: {:?}\n", start.elapsed().as_micros()).as_str());
// we cannot use Bun.read and Bun.write because it results in an EBADF error on cloud
let wrapper_content: String = format!(
r#"
import {{ main }} from "./main.ts";
BigInt.prototype.toJSON = function () {{
return this.toString();
}};
{dates}
let stdout = Bun.stdout.writer();
// let stdout = Bun.file("output.txt").writer();
stdout.write('start\n');
for await (const chunk of Bun.stdin.stream()) {{
const lines = Buffer.from(chunk).toString();
let exit = false;
for (const line of lines.trim().split("\n")) {{
// stdout.write('s: ' + line + 'EE\n');
if (line === "end") {{
exit = true;
break;
}}
try {{
let {{ {spread} }} = JSON.parse(line)
let res: any = await main(...[ {spread} ]);
stdout.write(JSON.stringify(res ?? null, (key, value) => typeof value === 'undefined' ? null : value) + '\n');
}} catch (e) {{
stdout.write(JSON.stringify({{ error: {{ message: e.message, name: e.name, stack: e.stack, line: line }}}}) + '\n');
}}
stdout.flush();
}}
if (exit) {{
break;
}}
}}
"#,
);
write_file(job_dir, "wrapper.ts", &wrapper_content).await?;
}
let reserved_variables = windmill_common::variables::get_reserved_variables(
w_id,
token,
"dedicated_worker",
"dedicated_worker",
Uuid::nil().to_string().as_str(),
"dedicted_worker",
Some(script_path.to_string()),
None,
None,
None,
None,
);
let _ = write_file(
&job_dir,
"loader.bun.ts",
&format!(
r#"
import {{ plugin }} from "bun";
{}
plugin(p)
"#,
RELATIVE_BUN_LOADER
.replace("W_ID", &w_id)
.replace("BASE_INTERNAL_URL", base_internal_url)
.replace("TOKEN", token)
.replace("CURRENT_PATH", script_path)
),
)
.await?;
//do not cache local dependencies
let mut child = {
let script_path = format!("{job_dir}/wrapper.ts");
let args = vec![
"run",
"-i",
"--prefer-offline",
"-r",
"./loader.bun.ts",
&script_path,
];
Command::new(&*BUN_PATH)
.current_dir(job_dir)
.env_clear()
.envs(context_envs)
.envs(envs)
.envs(
reserved_variables
.iter()
.map(|x| (x.name.clone(), x.value.clone()))
.collect::<Vec<_>>(),
)
.envs(common_bun_proc_envs)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
let stdout = child
.stdout
.take()
.expect("child did not have a handle to stdout");
let mut reader = BufReader::new(stdout).lines();
let mut stdin = child
.stdin
.take()
.expect("child did not have a handle to stdin");
// Ensure the child process is spawned in the runtime so it can
// make progress on its own while we await for any output.
let child = tokio::spawn(async move {
let status = child
.wait()
.await
.expect("child process encountered an error");
println!("child status was: {}", status);
});
let mut jobs = VecDeque::with_capacity(MAX_BUFFERED_DEDICATED_JOBS);
// let mut i = 0;
// let mut j = 0;
let mut alive = true;
loop {
tokio::select! {
biased;
_ = killpill_rx.recv(), if alive => {
println!("received killpill for dedicated worker");
alive = false;
if let Err(e) = write_stdin(&mut stdin, "end").await {
tracing::info!("Could not write end message to stdin: {e:?}")
}
},
line = reader.next_line() => {
// j += 1;
if let Some(line) = line.expect("line is ok") {
if line == "start" {
tracing::info!("dedicated worker process started");
continue;
}
tracing::debug!("processed job");
let result = serde_json::from_str(&line).expect("json is ok");
let job: QueuedJob = jobs.pop_front().expect("pop");
job_completed_tx.send(JobCompleted { job , result, logs: "".to_string(), success: true, cached_res_path: None, token: token.to_string() }).await.unwrap();
} else {
tracing::info!("dedicated worker process exited");
break;
}
}
job = jobs_rx.recv(), if alive && jobs.len() < MAX_BUFFERED_DEDICATED_JOBS => {
// i += 1;
if let Some(job) = job {
tracing::debug!("received job");
jobs.push_back(job.clone());
// write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({"x": job.id}))).expect("serialize")).await?;
write_stdin(&mut stdin, &serde_json::to_string(&job.args.unwrap_or_else(|| serde_json::json!({}))).expect("serialize")).await?;
stdin.flush().await.context("stdin flush")?;
} else {
tracing::debug!("job channel closed");
alive = false;
if let Err(e) = write_stdin(&mut stdin, "end").await {
tracing::error!("Could not write end message to stdin: {e:?}")
}
}
}
}
}
child
.await
.map_err(|e| anyhow::anyhow!("child process encountered an error: {e}"))?;
tracing::info!("dedicated worker child process exited successfully");
Ok(())
}
#[cfg(feature = "enterprise")]
async fn write_stdin(stdin: &mut tokio::process::ChildStdin, s: &str) -> error::Result<()> {
let _ = &stdin.write_all(format!("{s}\n").as_bytes()).await?;
stdin.flush().await.context("stdin flush")?;
Ok(())
}

View File

@@ -2,15 +2,22 @@ use async_recursion::async_recursion;
use serde_json::{json, Value};
use sqlx::{Pool, Postgres};
use tokio::{fs::File, io::AsyncReadExt};
use windmill_api_client::{types::CreateResource, Client};
use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
variables::ContextualVariable,
};
use windmill_queue::CLOUD_HOSTED;
use anyhow::Result;
use std::{
borrow::Borrow, collections::HashMap, io, os::unix::process::ExitStatusExt, panic,
borrow::Borrow,
collections::{hash_map::DefaultHasher, HashMap},
hash::{Hash, Hasher},
io,
os::unix::process::ExitStatusExt,
panic,
time::Duration,
};
@@ -183,10 +190,12 @@ pub async fn get_reserved_variables(
)
.to_vec();
let mut r: HashMap<String, String> = variables
.into_iter()
.map(|rv| (rv.name, rv.value))
.collect();
Ok(build_envs_map(variables))
}
pub fn build_envs_map(context: Vec<ContextualVariable>) -> HashMap<String, String> {
let mut r: HashMap<String, String> =
context.into_iter().map(|rv| (rv.name, rv.value)).collect();
if let Some(ref envs) = *WHITELIST_ENVS {
for e in envs {
@@ -194,9 +203,8 @@ pub async fn get_reserved_variables(
}
}
Ok(r)
r
}
async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
if pid.is_none() {
return -1;
@@ -267,12 +275,15 @@ pub async fn handle_child(
/* the cancellation future is polled on by `wait_on_child` while
* waiting for the child to exit normally */
let update_job = async {
if job_id == Uuid::nil() {
return;
}
let db = db.clone();
let mut interval = interval(update_job_interval);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut i = 1;
let mut i = 0;
loop {
tokio::select!(
_ = rx.recv() => break,
@@ -289,7 +300,7 @@ pub async fn handle_child(
.expect("update worker ping");
}
let mem_peak = get_mem_peak(pid, nsjail).await;
tracing::info!("{job_id} still running. mem peak: {}kB", mem_peak);
tracing::info!("{job_id} in {} still running. mem peak: {}kB", _w_id, mem_peak);
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
if sqlx::query_scalar!("UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled", mem_peak, job_id)
.fetch_optional(&db)
@@ -351,7 +362,7 @@ pub async fn handle_child(
result = child.wait() => return result.map(Ok),
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
_ = sleep(timeout_duration) => KillReason::Timeout,
_ = update_job => KillReason::Cancelled,
_ = update_job, if job_id != Uuid::nil() => KillReason::Cancelled,
};
tx.send(()).expect("rx should never be dropped");
drop(tx);
@@ -532,7 +543,7 @@ fn child_joined_output_stream(
stream::select(lines_to_stream(stderr), lines_to_stream(stdout))
}
fn lines_to_stream<R: tokio::io::AsyncBufRead + Unpin>(
pub fn lines_to_stream<R: tokio::io::AsyncBufRead + Unpin>(
mut lines: tokio::io::Lines<R>,
) -> impl futures::Stream<Item = io::Result<String>> {
stream::poll_fn(move |cx| {
@@ -566,6 +577,32 @@ fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize) {
}
}
pub fn hash_args(v: &serde_json::Value) -> i64 {
let mut dh = DefaultHasher::new();
serde_json::to_string(v).unwrap().hash(&mut dh);
dh.finish() as i64
}
pub async fn save_in_cache(client: &AuthedClient, job: &QueuedJob, cached_path: String, r: &Value) {
let client: &Client = client.get_client();
let expire = chrono::Utc::now().timestamp() + job.cache_ttl.unwrap() as i64;
let cr = &CreateResource {
path: cached_path,
description: None,
resource_type: "cache".to_string(),
value: serde_json::json!({
"value": r,
"expire": expire
}),
};
if let Err(e) = client
.create_resource(&job.workspace_id, Some(true), cr)
.await
{
tracing::error!("Error creating cache resource {e}")
}
}
/* TODO retry this? */
#[tracing::instrument(level = "trace", skip_all)]
async fn append_logs(job_id: uuid::Uuid, logs: impl AsRef<str>, db: impl Borrow<Pool<Postgres>>) {

View File

@@ -0,0 +1,5 @@
// use tokio::sync::mpsc;
// pub fn create_dedicated_worker() {
// let (job_completed_tx, mut new_job) = mpsc::channel::<JobCompleted>(100);
// }

View File

@@ -9,7 +9,7 @@ use crate::{
write_file,
},
AuthedClientBackgroundTask, DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, NPM_CONFIG_REGISTRY,
PATH_ENV,
PATH_ENV, TZ_ENV,
};
use tokio::{fs::File, io::AsyncReadExt, process::Command};
use windmill_common::{error::Result, BASE_URL};
@@ -50,6 +50,7 @@ fn get_common_deno_proc_envs(token: &str, base_internal_url: &str) -> HashMap<St
let mut deno_envs: HashMap<String, String> = HashMap::from([
(String::from("PATH"), PATH_ENV.clone()),
(String::from("TZ"), TZ_ENV.clone()),
(String::from("DENO_AUTH_TOKENS"), deno_auth_tokens),
(
String::from("BASE_INTERNAL_URL"),
@@ -71,9 +72,21 @@ pub async fn generate_deno_lock(
db: &sqlx::Pool<sqlx::Postgres>,
w_id: &str,
worker_name: &str,
base_internal_url: &str,
) -> error::Result<String> {
let _ = write_file(job_dir, "main.ts", code).await?;
let import_map_path = format!("{job_dir}/import_map.json");
let import_map = format!(
r#"{{
"imports": {{
"/": "{base_internal_url}/api/scripts_u/empty_ts/"
}}
}}"#,
);
write_file(job_dir, "import_map.json", &import_map).await?;
write_file(job_dir, "empty.ts", "").await?;
let child = Command::new(DENO_PATH.as_str())
.current_dir(job_dir)
.args(vec![
@@ -81,6 +94,8 @@ pub async fn generate_deno_lock(
"--unstable",
"--lock=lock.json",
"--lock-write",
"--import-map",
&import_map_path,
"main.ts",
])
.stdout(Stdio::piped())
@@ -260,6 +275,7 @@ run().catch(async (e) => {{
if !reqs.is_empty() {
let _ = write_file(job_dir, "lock.json", &reqs).await?;
args.push("--lock=lock.json");
args.push("--lock-write");
}
}
if let Some(deno_flags) = DENO_FLAGS.as_ref() {

View File

@@ -6,6 +6,11 @@ use itertools::Itertools;
use rand::Rng;
#[cfg(feature = "enterprise")]
use std::process::Stdio;
#[cfg(feature = "enterprise")]
use windmill_common::DB;
#[cfg(feature = "enterprise")]
use windmill_common::global_settings::WORKER_S3_BUCKET_SYNC;
#[cfg(feature = "enterprise")]
use tokio::{process::Command, sync::mpsc::Sender, time::Instant};
@@ -219,6 +224,25 @@ pub async fn copy_cache_to_bucket(bucket: &str) -> error::Result<()> {
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn worker_s3_bucket_sync_enabled(db: &DB) -> bool {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
WORKER_S3_BUCKET_SYNC
)
.fetch_optional(db)
.await;
if let Ok(q) = q {
let r = q.map(|x| x.value.as_bool().unwrap_or(true)).unwrap_or(true);
tracing::info!("Got global setting {WORKER_S3_BUCKET_SYNC}: {}", r);
r
} else {
tracing::info!("Failed to get global setting {WORKER_S3_BUCKET_SYNC}");
false
}
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_to_bucket_as_tar(bucket: &str) {
tracing::info!("Copying cache to bucket {bucket} as tar");

View File

@@ -12,7 +12,7 @@ use windmill_common::{
jobs::QueuedJob,
utils::calculate_hash,
};
use windmill_parser_go::parse_go_imports;
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
use crate::{
common::{
@@ -20,7 +20,7 @@ use crate::{
set_logs, write_file,
},
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, GOPRIVATE, GOPROXY,
GO_BIN_CACHE_DIR, GO_CACHE_DIR, HOME_ENV, NSJAIL_PATH, PATH_ENV,
GO_BIN_CACHE_DIR, GO_CACHE_DIR, HOME_ENV, NSJAIL_PATH, PATH_ENV, TZ_ENV,
};
const GO_REQ_SPLITTER: &str = "//go.sum\n";
@@ -214,8 +214,7 @@ func Run(req Req) (interface{{}}, error){{
create_args_and_out_file(client, job, job_dir).await?;
}
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let child = if !*DISABLE_NSJAIL {
let _ = write_file(
@@ -234,6 +233,7 @@ func Run(req Req) (interface{{}}, error){{
.envs(envs)
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec!["--config", "run.config.proto", "--", "/tmp/go/main"])
.stdout(Stdio::piped())
@@ -247,6 +247,7 @@ func Run(req Req) (interface{{}}, error){{
.envs(envs)
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("GOPATH", GO_CACHE_DIR)
.env("HOME", HOME_ENV.as_str());
@@ -296,6 +297,9 @@ async fn gen_go_mod(
}
}
use std::fs::OpenOptions;
use std::io::prelude::*;
pub async fn install_go_dependencies(
job_id: &Uuid,
code: &str,
@@ -329,6 +333,16 @@ pub async fn install_go_dependencies(
None,
)
.await?;
for x in REQUIRE_PARSE.captures_iter(code) {
let mut file = OpenOptions::new()
.write(true)
.append(true)
.open(format!("{job_dir}/go.mod"))
.unwrap();
writeln!(file, "require {}\n", &x[1])?;
}
}
let mut new_lockfile = false;

View File

@@ -7,7 +7,7 @@ use windmill_queue::HTTP_CLIENT;
use serde::Deserialize;
use crate::{common::transform_json_value, AuthedClient, JobCompleted};
use crate::{common::transform_json_value, AuthedClient};
#[derive(Deserialize)]
struct GraphqlApi {
@@ -31,7 +31,7 @@ pub async fn do_graphql(
job: QueuedJob,
client: &AuthedClient,
query: &str,
) -> windmill_common::error::Result<JobCompleted> {
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
} else {
@@ -88,10 +88,5 @@ pub async fn do_graphql(
}
// And then check that we got back the same string we sent over.
return Ok(JobCompleted {
job: job,
result: result.data.unwrap_or(json!({})),
logs: "".to_string(),
success: true,
});
return Ok(result.data.unwrap_or(json!({})));
}

View File

@@ -67,6 +67,7 @@ impl TimersPermission for PermissionsContainer {
()
}
}
pub struct OptAuthedClient(Option<AuthedClient>);
pub async fn eval_timeout(
expr: String,
@@ -122,8 +123,6 @@ pub async fn eval_timeout(
.enable_all()
.build()?;
let re = Regex::new(r"import (.*)\n").unwrap();
let expr = re.replace_all(&expr, "").to_string();
// pretty frail but this it to make the expr more user friendly and not require the user to write await
let expr = ["variable", "step", "resource", "result_by_id"]
.into_iter()
@@ -442,7 +441,7 @@ pub async fn eval_fetch_timeout(
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
let ts_expr2 = ts_expr.clone();
timeout(
std::time::Duration::from_millis(100000),
std::time::Duration::from_secs(100),
tokio::task::spawn_blocking(move || {
let ops = vec![op_get_static_args::DECL, op_log::DECL];
let ext = Extension {
@@ -537,7 +536,7 @@ pub async fn eval_fetch_timeout(
isolate.terminate_execution();
};
Error::ExecutionErr(format!(
"The expression of evaluation `{ts_expr2}` took too long to execute (>10000ms)"
"The expression of evaluation `{ts_expr2}` took too long to execute (>100s)"
))
})??
}

View File

@@ -6,6 +6,7 @@ mod snowflake_executor;
mod bash_executor;
mod bun_executor;
mod common;
mod dedicated_worker;
mod deno_executor;
mod global_cache;
mod go_executor;

View File

@@ -10,7 +10,7 @@ use windmill_common::{
};
use windmill_parser_sql::parse_mysql_sig;
use crate::{common::transform_json_value, AuthedClient, JobCompleted};
use crate::{common::transform_json_value, AuthedClient};
#[derive(Deserialize)]
struct MysqlDatabase {
@@ -26,7 +26,7 @@ pub async fn do_mysql(
job: QueuedJob,
client: &AuthedClient,
query: &str,
) -> windmill_common::error::Result<JobCompleted> {
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
} else {
@@ -114,7 +114,7 @@ pub async fn do_mysql(
pool.disconnect().await.map_err(to_anyhow)?;
// And then check that we got back the same string we sent over.
return Ok(JobCompleted { job: job, result: json!(rows), logs: "".to_string(), success: true });
return Ok(json!(rows));
}
fn convert_row_to_value(row: Row) -> serde_json::Value {

View File

@@ -1,7 +1,7 @@
use anyhow::Context;
use chrono::Utc;
use futures::TryStreamExt;
use native_tls::TlsConnector;
use native_tls::{Certificate, TlsConnector};
use postgres_native_tls::MakeTlsConnector;
use rust_decimal::Decimal;
use serde::Deserialize;
@@ -13,12 +13,12 @@ use tokio_postgres::{
Column,
};
use uuid::Uuid;
use windmill_common::error::Error;
use windmill_common::error::{self, Error};
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser_sql::parse_pgsql_sig;
use crate::common::transform_json_value;
use crate::{AuthedClient, JobCompleted};
use crate::AuthedClient;
use urlencoding::encode;
#[derive(Deserialize)]
@@ -29,13 +29,14 @@ struct PgDatabase {
port: Option<u16>,
sslmode: Option<String>,
dbname: String,
root_certificate_pem: Option<String>,
}
pub async fn do_postgresql(
job: QueuedJob,
client: &AuthedClient,
query: &str,
) -> windmill_common::error::Result<JobCompleted> {
) -> error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
} else {
@@ -48,7 +49,7 @@ pub async fn do_postgresql(
serde_json::from_value::<PgDatabase>(pg_args.get("database").unwrap_or(&json!({})).clone())
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
let sslmode = database.sslmode.unwrap_or("prefer".to_string());
let database = format!(
let database_string = format!(
"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode={sslmode}",
user = encode(&database.user.unwrap_or("postgres".to_string())),
password = encode(&database.password.unwrap_or("".to_string())),
@@ -58,15 +59,26 @@ pub async fn do_postgresql(
sslmode = sslmode
);
let (client, handle) = if sslmode == "require" {
let mut connector = TlsConnector::builder();
if let Some(root_certificate_pem) = database.root_certificate_pem {
if !root_certificate_pem.is_empty() {
connector.add_root_certificate(
Certificate::from_pem(root_certificate_pem.as_bytes())
.map_err(|e| error::Error::BadConfig(format!("Invalid Certs: {e}")))?,
);
} else {
connector.danger_accept_invalid_certs(true);
connector.danger_accept_invalid_hostnames(true);
}
} else {
connector
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let (client, connection) = tokio_postgres::connect(
&database,
MakeTlsConnector::new(
TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.map_err(to_anyhow)?,
),
&database_string,
MakeTlsConnector::new(connector.build().map_err(to_anyhow)?),
)
.await
.map_err(to_anyhow)?;
@@ -78,7 +90,7 @@ pub async fn do_postgresql(
});
(client, handle)
} else {
let (client, connection) = tokio_postgres::connect(&database, NoTls)
let (client, connection) = tokio_postgres::connect(&database_string, NoTls)
.await
.map_err(to_anyhow)?;
let handle = tokio::spawn(async move {
@@ -169,7 +181,7 @@ pub async fn do_postgresql(
handle.abort();
// And then check that we got back the same string we sent over.
return Ok(JobCompleted { job: job, result, logs: "".to_string(), success: true });
return Ok(result);
}
pub fn pg_cell_to_json_value(

View File

@@ -62,7 +62,7 @@ use crate::{
write_file,
},
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HTTPS_PROXY, HTTP_PROXY,
LOCK_CACHE_DIR, NO_PROXY, NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR,
LOCK_CACHE_DIR, NO_PROXY, NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, TZ_ENV,
};
pub async fn create_dependencies_dir(job_dir: &str) {
@@ -280,8 +280,7 @@ pub async fn handle_python_job(
let name = &x.name;
format!(
"if \"{name}\" in kwargs and kwargs[\"{name}\"] is not None:\n \
kwargs[\"{name}\"] = datetime.strptime(kwargs[\"{name}\"], \
'%Y-%m-%dT%H:%M')\n",
kwargs[\"{name}\"] = datetime.fromisoformat(kwargs[\"{name}\"])\n",
)
}
_ => "".to_string(),
@@ -440,6 +439,7 @@ mount {{
// inject PYTHONPATH here - for some reason I had to do it in nsjail conf
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec![
"--config",
@@ -460,6 +460,7 @@ mount {{
.envs(envs)
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec!["-u", "-m", "wrapper"])
.stdout(Stdio::piped())

View File

@@ -12,7 +12,7 @@ use windmill_queue::HTTP_CLIENT;
use serde::{Deserialize, Serialize};
use crate::{common::transform_json_value, AuthedClient, JobCompleted};
use crate::{common::transform_json_value, AuthedClient};
#[derive(Serialize)]
struct Claims {
@@ -64,7 +64,7 @@ pub async fn do_snowflake(
job: QueuedJob,
client: &AuthedClient,
query: &str,
) -> windmill_common::error::Result<JobCompleted> {
) -> windmill_common::error::Result<serde_json::Value> {
let args = if let Some(args) = &job.args {
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
} else {
@@ -201,7 +201,7 @@ pub async fn do_snowflake(
})
.collect();
Ok(JobCompleted { job: job, result: rows, logs: "".to_string(), success: true })
Ok(rows)
}
Err(e) => {
let resp = response.text().await.unwrap_or("".to_string());

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More