Compare commits
66 Commits
v1.231.0
...
operator-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76266f3efb | ||
|
|
2cdda619de | ||
|
|
d340fbc02f | ||
|
|
bed2d1f417 | ||
|
|
4377cb3bcc | ||
|
|
269afe41f7 | ||
|
|
1d1f03691a | ||
|
|
6b075e92ef | ||
|
|
be28522eb7 | ||
|
|
5749a3083a | ||
|
|
2034aebb17 | ||
|
|
4384617f1e | ||
|
|
94493812ac | ||
|
|
2fc21d46fa | ||
|
|
2e023003da | ||
|
|
30a30f81e0 | ||
|
|
723744e41e | ||
|
|
fffc5338ce | ||
|
|
3c2f753eac | ||
|
|
aa3d802137 | ||
|
|
9efcb53bcd | ||
|
|
4385edc6ff | ||
|
|
3cadaa3f2b | ||
|
|
c05d119ae5 | ||
|
|
fb5cd6a429 | ||
|
|
0710ce6ef2 | ||
|
|
8619f3102d | ||
|
|
31d48533f4 | ||
|
|
6cd46ca7f8 | ||
|
|
2494171b9c | ||
|
|
965571853c | ||
|
|
37744ff583 | ||
|
|
dbe14757d4 | ||
|
|
486b3aeb17 | ||
|
|
5ee0040840 | ||
|
|
0f774b4485 | ||
|
|
0c40c1324b | ||
|
|
b17652ae60 | ||
|
|
b44618f35b | ||
|
|
821d2d7ea1 | ||
|
|
0299c656c3 | ||
|
|
e0140f175d | ||
|
|
65ad840dc4 | ||
|
|
f13fb4505c | ||
|
|
c80e4c5ffd | ||
|
|
787017f53c | ||
|
|
12bd4af3ee | ||
|
|
9cdaf9c201 | ||
|
|
ec094e8897 | ||
|
|
cf61e09b90 | ||
|
|
6a0ae83b49 | ||
|
|
2f4be74e94 | ||
|
|
28811023a4 | ||
|
|
e80534ca65 | ||
|
|
95be0155dc | ||
|
|
004781cd3d | ||
|
|
c085f5fa7e | ||
|
|
ad6c3e5cee | ||
|
|
74fc2d3f43 | ||
|
|
3d1f707f36 | ||
|
|
d316af993d | ||
|
|
263f41cda3 | ||
|
|
b66fcbf626 | ||
|
|
948b0da59e | ||
|
|
49355280f6 | ||
|
|
606b03d761 |
25
.github/change-versions-mac.sh
vendored
Executable file
25
.github/change-versions-mac.sh
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root_dirpath="$(cd "${script_dirpath}/.." && pwd)"
|
||||
|
||||
VERSION=$1
|
||||
echo "Updating versions to: $VERSION"
|
||||
|
||||
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/backend/Cargo.toml
|
||||
sed -i '' -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/cli/main.ts
|
||||
sed -i '' -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/benchmarks/lib.ts
|
||||
sed -i '' -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
|
||||
sed -i '' -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
|
||||
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
|
||||
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
|
||||
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
|
||||
# sed -i '' -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
|
||||
sed -i '' -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
sed -i '' -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
|
||||
sed -i '' -E "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock
|
||||
|
||||
cd ${root_dirpath}/frontend && npm i --package-lock-only
|
||||
32
.github/change-versions.sh
vendored
32
.github/change-versions.sh
vendored
@@ -1,23 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root_dirpath="$(cd "${script_dirpath}/.." && pwd)"
|
||||
|
||||
VERSION=$1
|
||||
echo "Updating versions to: $VERSION"
|
||||
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml
|
||||
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
|
||||
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" benchmarks/lib.ts
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" typescript-client/package.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" python-client/wmill/pyproject.toml
|
||||
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill/pyproject.toml
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" python-client/wmill_pg/pyproject.toml
|
||||
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
|
||||
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" lsp/Pipfile
|
||||
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" lsp/Pipfile
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/backend/Cargo.toml
|
||||
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/cli/main.ts
|
||||
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}/benchmarks/lib.ts
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
|
||||
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
|
||||
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
|
||||
|
||||
sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" backend/Cargo.lock
|
||||
sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock
|
||||
|
||||
cd frontend && npm i --package-lock-only
|
||||
cd ${root_dirpath}/frontend && npm i --package-lock-only
|
||||
|
||||
96
.github/workflows/docker-image.yml
vendored
96
.github/workflows/docker-image.yml
vendored
@@ -43,8 +43,9 @@ jobs:
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
${{ steps.meta-public.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.meta-public.outputs.labels }}
|
||||
@@ -79,8 +80,9 @@ jobs:
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
@@ -102,7 +104,7 @@ jobs:
|
||||
features=enterprise,enterprise_saml
|
||||
nsjail=true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
${{ steps.meta-ee-public.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.meta-ee-public.outputs.labels }}
|
||||
@@ -198,25 +200,25 @@ jobs:
|
||||
|
||||
- run: |
|
||||
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
|
||||
- run: |
|
||||
# Checks the image is in docker prior to running actions-docker-extract. It fails if not
|
||||
# Also useful to visually check that the arch is the right opencontainers
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
id: extract
|
||||
with:
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
path: "/usr/src/app/windmill"
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
id: extract-ee
|
||||
with:
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
path: "/usr/src/app/windmill"
|
||||
|
||||
- name: Rename binary with corresponding architecture
|
||||
@@ -242,25 +244,25 @@ jobs:
|
||||
|
||||
- run: |
|
||||
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
|
||||
- run: |
|
||||
# Checks the image is in docker prior to running actions-docker-extract. It fails if not
|
||||
# Also useful to visually check that the arch is the right opencontainers
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
docker image inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
id: extract
|
||||
with:
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||
path: "/usr/src/app/windmill"
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
id: extract-ee
|
||||
with:
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
path: "/usr/src/app/windmill"
|
||||
|
||||
- name: Rename binary with corresponding architecture
|
||||
@@ -305,7 +307,7 @@ jobs:
|
||||
git_hash=$(git rev-parse --short "$GITHUB_SHA")
|
||||
docker buildx imagetools create \
|
||||
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${git_hash:0:7} \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
echo "GIT_HASH=${git_hash:0:7}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: shrink/actions-docker-extract@v3
|
||||
@@ -322,3 +324,63 @@ jobs:
|
||||
folder: ${{ steps.extract.outputs.destination }}
|
||||
bucket: windmill-frontend
|
||||
bucket-region: us-east-1
|
||||
|
||||
run_integration_test:
|
||||
runs-on: ubicloud
|
||||
needs: [build_ee]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Prepare test run
|
||||
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
|
||||
run: cd integration_tests && ./build.sh
|
||||
- name: Test run
|
||||
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
run: cd integration_tests && ./run.sh
|
||||
- name: Archive logs
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: Windmill Integration Tests Logs
|
||||
path: |
|
||||
integration_tests/logs
|
||||
|
||||
tag_latest:
|
||||
runs-on: ubicloud
|
||||
needs: [run_integration_test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Tag main and latest
|
||||
run: |
|
||||
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
|
||||
|
||||
tag_latest_ee:
|
||||
runs-on: ubicloud
|
||||
needs: [run_integration_test]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Tag main and latest for ee
|
||||
run: |
|
||||
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||
docker buildx imagetools create ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:main
|
||||
|
||||
95
CHANGELOG.md
95
CHANGELOG.md
@@ -1,6 +1,101 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## [1.236.2](https://github.com/windmill-labs/windmill/compare/v1.236.1...v1.236.2) (2024-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle better non object result for app result loader ([269afe4](https://github.com/windmill-labs/windmill/commit/269afe41f7e02da8d3a364e649e5fef00847d24a))
|
||||
|
||||
## [1.236.1](https://github.com/windmill-labs/windmill/compare/v1.236.0...v1.236.1) (2024-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix app initialization ([6b075e9](https://github.com/windmill-labs/windmill/commit/6b075e92ef4e312e6a4ce582dc7c4d88456b2f5d))
|
||||
|
||||
## [1.236.0](https://github.com/windmill-labs/windmill/compare/v1.235.1...v1.236.0) (2024-01-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improve git sync and recompute dependents when common python script is redeployed ([#2967](https://github.com/windmill-labs/windmill/issues/2967)) ([9449381](https://github.com/windmill-labs/windmill/commit/94493812ac0030cc6bba468d8ee24a14661716a8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reduce stack size needed for workflow transitions ([4384617](https://github.com/windmill-labs/windmill/commit/4384617f1ec4c27de88ef6c895d6ef060a79f6bd))
|
||||
|
||||
## [1.235.1](https://github.com/windmill-labs/windmill/compare/v1.235.0...v1.235.1) (2024-01-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix resultjobloader ([2e02300](https://github.com/windmill-labs/windmill/commit/2e023003da8bea9efa86c0fbc52bf9723c73907d))
|
||||
|
||||
## [1.235.0](https://github.com/windmill-labs/windmill/compare/v1.234.1...v1.235.0) (2024-01-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** DB Explorer ([#2892](https://github.com/windmill-labs/windmill/issues/2892)) ([fffc533](https://github.com/windmill-labs/windmill/commit/fffc5338ce76fda9a68b19f03cefe199cc09a905))
|
||||
|
||||
## [1.234.1](https://github.com/windmill-labs/windmill/compare/v1.234.0...v1.234.1) (2024-01-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix double view runs button ([9efcb53](https://github.com/windmill-labs/windmill/commit/9efcb53bcd903bf756c4beb92a5c7634e8e016ad))
|
||||
* fix typo "Wourker" -> "Worker" ([4385edc](https://github.com/windmill-labs/windmill/commit/4385edc6ffdb1b2e4544347928b4cf08faf1eaa1))
|
||||
|
||||
## [1.234.0](https://github.com/windmill-labs/windmill/compare/v1.233.0...v1.234.0) (2024-01-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Detailed job memory footprint on job run page ([#2956](https://github.com/windmill-labs/windmill/issues/2956)) ([0710ce6](https://github.com/windmill-labs/windmill/commit/0710ce6ef25d8f2344ac1350e96aa906f16d1be1))
|
||||
* git sync can now push commits to individual branches ([#2959](https://github.com/windmill-labs/windmill/issues/2959)) ([fb5cd6a](https://github.com/windmill-labs/windmill/commit/fb5cd6a4298c88e26481b76258bf5b09a11cf4b0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Deleting workspace delete all workspace related values in DB ([#2961](https://github.com/windmill-labs/windmill/issues/2961)) ([c05d119](https://github.com/windmill-labs/windmill/commit/c05d119ae5dfb6f1bae88fc9d53b48a707cd2f15))
|
||||
|
||||
## [1.233.0](https://github.com/windmill-labs/windmill/compare/v1.232.0...v1.233.0) (2024-01-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add the view runs buttons for operators ([#2932](https://github.com/windmill-labs/windmill/issues/2932)) ([821d2d7](https://github.com/windmill-labs/windmill/commit/821d2d7ea15e828dbb3683c85025dbc09c7e8dc7))
|
||||
* Job caching now works with S3 files ([#2934](https://github.com/windmill-labs/windmill/issues/2934)) ([b44618f](https://github.com/windmill-labs/windmill/commit/b44618f35b6fe4558a5cd9fddec1e82216a2cf5e))
|
||||
* PIP_LOCAL_DEPENDENCIES now accepts regex and is configurable from the UI ([#2940](https://github.com/windmill-labs/windmill/issues/2940)) ([e0140f1](https://github.com/windmill-labs/windmill/commit/e0140f175d371e239f315613a537820b9a6ad25f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow extraConfig for agGrid to set onGridReady ([74fc2d3](https://github.com/windmill-labs/windmill/commit/74fc2d3f4397d976fec326893f8196008cb8b332))
|
||||
* decision tree fix wording ([#2941](https://github.com/windmill-labs/windmill/issues/2941)) ([0299c65](https://github.com/windmill-labs/windmill/commit/0299c656c368876581dc228c76513f1171ffd682))
|
||||
* decision tree nits ([#2936](https://github.com/windmill-labs/windmill/issues/2936)) ([787017f](https://github.com/windmill-labs/windmill/commit/787017f53c8227e1246b09e412e92316977937ae))
|
||||
* **frontend:** handle case where large json has a file and a filename ([#2951](https://github.com/windmill-labs/windmill/issues/2951)) ([2494171](https://github.com/windmill-labs/windmill/commit/2494171b9cb3d1857235b6558d276451d5550560))
|
||||
* persist state for agGrid ([c085f5f](https://github.com/windmill-labs/windmill/commit/c085f5fa7ec598d0abf6aad98de0e5c7b1c92985))
|
||||
* Tag all docker images with latest and main tags ([#2953](https://github.com/windmill-labs/windmill/issues/2953)) ([9655718](https://github.com/windmill-labs/windmill/commit/965571853ca253b130bcc4473190b6c9c98f44e1))
|
||||
|
||||
## [1.232.0](https://github.com/windmill-labs/windmill/compare/v1.231.0...v1.232.0) (2023-12-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add summary to schedules ([606b03d](https://github.com/windmill-labs/windmill/commit/606b03d7613cf15ab9a60ef34a82657ff49ccdbd))
|
||||
* no overlap for flows togglable on schedules ([4935528](https://github.com/windmill-labs/windmill/commit/49355280f6442c342a46f90b2fe2cb089b3f4eff))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix file input ([263f41c](https://github.com/windmill-labs/windmill/commit/263f41cda3dba24bcaa2bbabfc3f5bc54c70f9cb))
|
||||
* improve ata for bun ([b66fcbf](https://github.com/windmill-labs/windmill/commit/b66fcbf6261f69a57e6711fa8b9d1d499c1b47ed))
|
||||
* use bun install in every case ([948b0da](https://github.com/windmill-labs/windmill/commit/948b0da59e6bcd3b6312373a1bbe52d541e54d56))
|
||||
|
||||
## [1.231.0](https://github.com/windmill-labs/windmill/compare/v1.230.0...v1.231.0) (2023-12-26)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a href="https://www.windmill.dev/"><img src="./imgs/windmill-banner.png" alt="windmill.dev"></a>
|
||||
</p>
|
||||
<p align=center>
|
||||
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIsm and custom UIs to trigger workflows and scripts as internal apps.
|
||||
Open-source developer infrastructure for internal tools (APIs, background jobs, workflows and UIs). Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs and custom UIs to trigger workflows and scripts as internal apps.
|
||||
|
||||
<p align=center>
|
||||
Scripts are turned into sharable UIs automatically, and can be composed together into flows or used into richer apps built with low-code. Supported script languages supported are: Python, TypeScript, Go, Bash, SQL, and GraphQL.
|
||||
|
||||
14
backend/.sqlx/query-08c943c4b9b76da43b8b754e5c66ab3b6397e7edc82666662a8ea466496290fb.json
generated
Normal file
14
backend/.sqlx/query-08c943c4b9b76da43b8b754e5c66ab3b6397e7edc82666662a8ea466496290fb.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM account WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08c943c4b9b76da43b8b754e5c66ab3b6397e7edc82666662a8ea466496290fb"
|
||||
}
|
||||
@@ -32,7 +32,6 @@
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"scheduledscriptwithretry",
|
||||
"singlescriptflow"
|
||||
]
|
||||
}
|
||||
@@ -49,6 +48,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"scheduledscriptwithretry",
|
||||
"singlescriptflow"
|
||||
]
|
||||
}
|
||||
@@ -70,6 +69,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
22
backend/.sqlx/query-266741894b9a6ed650cf999b6e5b7c797424f8291517d3abf096fe45548a0273.json
generated
Normal file
22
backend/.sqlx/query-266741894b9a6ed650cf999b6e5b7c797424f8291517d3abf096fe45548a0273.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT git_sync FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "git_sync",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "266741894b9a6ed650cf999b6e5b7c797424f8291517d3abf096fe45548a0273"
|
||||
}
|
||||
18
backend/.sqlx/query-2ce7a562b1de63f767758642a4864ed982b74b53a143901480058f957b9641dc.json
generated
Normal file
18
backend/.sqlx/query-2ce7a562b1de63f767758642a4864ed982b74b53a143901480058f957b9641dc.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE job_stats SET timestamps = timestamps || $4, timeseries_float = timeseries_float || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"TimestamptzArray",
|
||||
"Float4Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2ce7a562b1de63f767758642a4864ed982b74b53a143901480058f957b9641dc"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
15
backend/.sqlx/query-30e9250edd0c4c6025322f51b758174435d6df4f835cb0883ba40897cb99a410.json
generated
Normal file
15
backend/.sqlx/query-30e9250edd0c4c6025322f51b758174435d6df4f835cb0883ba40897cb99a410.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM dependency_map\n WHERE importer_path = $1 AND importer_kind = 'script'\n AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "30e9250edd0c4c6025322f51b758174435d6df4f835cb0883ba40897cb99a410"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE\n completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL ORDER BY started_at DESC LIMIT 20) AS jobs ) t\n WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
|
||||
"query": "SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE\n completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL AND is_skipped = False ORDER BY started_at DESC LIMIT 20) AS jobs ) t\n WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -115,6 +115,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "jobs",
|
||||
"type_info": "JsonArray"
|
||||
}
|
||||
@@ -149,8 +159,10 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "7a00843921db4ac1eb20a1045e83363587c1a2cb62d2df25d5564ae78a03b097"
|
||||
"hash": "4422b7183ede17a9cbde4afae41be4da5447020e39398deedbcca9121492834a"
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
14
backend/.sqlx/query-4c5276bbf6aaad004a58aaff6b2abd7e78903a0354dd041fcc0cc7cca2b60cc1.json
generated
Normal file
14
backend/.sqlx/query-4c5276bbf6aaad004a58aaff6b2abd7e78903a0354dd041fcc0cc7cca2b60cc1.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM token WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4c5276bbf6aaad004a58aaff6b2abd7e78903a0354dd041fcc0cc7cca2b60cc1"
|
||||
}
|
||||
16
backend/.sqlx/query-5fbd5dfb8df9aecce023c5d482ee58efcbc23c8682a3f0ec94977eb56023ed72.json
generated
Normal file
16
backend/.sqlx/query-5fbd5dfb8df9aecce023c5d482ee58efcbc23c8682a3f0ec94977eb56023ed72.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path)\n VALUES ($1, $2, 'script', $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5fbd5dfb8df9aecce023c5d482ee58efcbc23c8682a3f0ec94977eb56023ed72"
|
||||
}
|
||||
14
backend/.sqlx/query-60cd5ea09b2eba9df97ab011dd8575940a9dd5e244f3e877d3802fc88087234b.json
generated
Normal file
14
backend/.sqlx/query-60cd5ea09b2eba9df97ab011dd8575940a9dd5e244f3e877d3802fc88087234b.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM input WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "60cd5ea09b2eba9df97ab011dd8575940a9dd5e244f3e877d3802fc88087234b"
|
||||
}
|
||||
@@ -42,7 +42,6 @@
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"scheduledscriptwithretry",
|
||||
"singlescriptflow"
|
||||
]
|
||||
}
|
||||
@@ -63,6 +62,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
24
backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json
generated
Normal file
24
backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT true FROM job_stats WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a"
|
||||
}
|
||||
17
backend/.sqlx/query-699427e7e3e6cd3ba4f61f77e171c5c27234cfa30460696ac619760667d7553c.json
generated
Normal file
17
backend/.sqlx/query-699427e7e3e6cd3ba4f61f77e171c5c27234cfa30460696ac619760667d7553c.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE job_stats SET scalar_int = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "699427e7e3e6cd3ba4f61f77e171c5c27234cfa30460696ac619760667d7553c"
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19 ) RETURNING *",
|
||||
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry, summary, no_flow_overlap ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 ) RETURNING *",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -134,7 +144,9 @@
|
||||
"Int4",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Jsonb"
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -159,8 +171,10 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b53442ca97ed9d25990405281c82c31062de24cbd2f99d1c4039965d861aafa9"
|
||||
"hash": "6bb9e6cab7034bf28ef2597445dc617b60c49b42568fdb975cf2d2dc8702167e"
|
||||
}
|
||||
18
backend/.sqlx/query-73a3a742702c1c8237a3216523dffe487146b14512d4882179978efe280956aa.json
generated
Normal file
18
backend/.sqlx/query-73a3a742702c1c8237a3216523dffe487146b14512d4882179978efe280956aa.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE job_stats SET timestamps = timestamps || $4, timeseries_int = timeseries_int || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"TimestamptzArray",
|
||||
"Int4Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "73a3a742702c1c8237a3216523dffe487146b14512d4882179978efe280956aa"
|
||||
}
|
||||
14
backend/.sqlx/query-74949628ea78a5fecfd415e6406636a9442edd5b7939f61894288103be0c03ef.json
generated
Normal file
14
backend/.sqlx/query-74949628ea78a5fecfd415e6406636a9442edd5b7939f61894288103be0c03ef.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_stats WHERE job_id IN (SELECT id FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now())",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "74949628ea78a5fecfd415e6406636a9442edd5b7939f61894288103be0c03ef"
|
||||
}
|
||||
14
backend/.sqlx/query-76d73f2fe5ac75e5033fb93d60f2ef5ab2b0b73a80dcb6a250c17c3b2f15c6b4.json
generated
Normal file
14
backend/.sqlx/query-76d73f2fe5ac75e5033fb93d60f2ef5ab2b0b73a80dcb6a250c17c3b2f15c6b4.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM dependency_map WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "76d73f2fe5ac75e5033fb93d60f2ef5ab2b0b73a80dcb6a250c17c3b2f15c6b4"
|
||||
}
|
||||
14
backend/.sqlx/query-813d3c65d4c4bc10f1ec16ed4ee0954f4b938f45751ac03fd45479e665db1d40.json
generated
Normal file
14
backend/.sqlx/query-813d3c65d4c4bc10f1ec16ed4ee0954f4b938f45751ac03fd45479e665db1d40.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM deployment_metadata WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "813d3c65d4c4bc10f1ec16ed4ee0954f4b938f45751ac03fd45479e665db1d40"
|
||||
}
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -143,7 +153,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90"
|
||||
|
||||
14
backend/.sqlx/query-96a77e839d6210629234879eeeaa23d9bddf5f751fe27d999659f6597f9759f1.json
generated
Normal file
14
backend/.sqlx/query-96a77e839d6210629234879eeeaa23d9bddf5f751fe27d999659f6597f9759f1.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM raw_app WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "96a77e839d6210629234879eeeaa23d9bddf5f751fe27d999659f6597f9759f1"
|
||||
}
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -144,7 +154,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "96dc1cd308f3c21cfb50b88048054dc03f93e261d25969d66aa48e9d0502960f"
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"scheduledscriptwithretry",
|
||||
"singlescriptflow"
|
||||
]
|
||||
}
|
||||
@@ -43,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -142,7 +152,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "aa2800113a8a8805f47cdc1dd0f29d94c546fe531e7edd3e91da4978af5442fb"
|
||||
|
||||
23
backend/.sqlx/query-aa44311f9bae8253c6c483d122597ce7e470c6ba1905e7e877dca35d9d68968f.json
generated
Normal file
23
backend/.sqlx/query-aa44311f9bae8253c6c483d122597ce7e470c6ba1905e7e877dca35d9d68968f.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT importer_path FROM dependency_map\n WHERE imported_path = $1 AND importer_kind = 'script'\n AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "importer_path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "aa44311f9bae8253c6c483d122597ce7e470c6ba1905e7e877dca35d9d68968f"
|
||||
}
|
||||
24
backend/.sqlx/query-aeedd3d92f7c50fe49d14053afebd82837093636da2950915ce4364106f5679c.json
generated
Normal file
24
backend/.sqlx/query-aeedd3d92f7c50fe49d14053afebd82837093636da2950915ce4364106f5679c.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT count(*) FROM queue WHERE schedule_path = $1 AND workspace_id = $2 AND id != $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "aeedd3d92f7c50fe49d14053afebd82837093636da2950915ce4364106f5679c"
|
||||
}
|
||||
17
backend/.sqlx/query-b56111c5b81de2449a1b687656a4636457da851a4db5e916cf4f6847c48c6bd3.json
generated
Normal file
17
backend/.sqlx/query-b56111c5b81de2449a1b687656a4636457da851a4db5e916cf4f6847c48c6bd3.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE job_stats SET scalar_float = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Float4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b56111c5b81de2449a1b687656a4636457da851a4db5e916cf4f6847c48c6bd3"
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12\n WHERE path = $13 AND workspace_id = $14 RETURNING *",
|
||||
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 WHERE path = $15 AND workspace_id = $16 RETURNING *",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -128,6 +138,8 @@
|
||||
"Json",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
@@ -154,8 +166,10 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e1673b211da54039c5cffa5a38fe23073149bc5bfcea1daccb657d5fa6892183"
|
||||
"hash": "b7ed211ec7611c994a49565e46e5893788bcf49b2c9357fbc45a8353a7c84f48"
|
||||
}
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -143,7 +153,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c10348d26e3e3dc9e345d9044db35db9906617eb9c98aaf58a55d9681ffb85e3"
|
||||
|
||||
14
backend/.sqlx/query-d77223fc94257711512b6ff21ce109b97324f2917113f04492f53b20860e9588.json
generated
Normal file
14
backend/.sqlx/query-d77223fc94257711512b6ff21ce109b97324f2917113f04492f53b20860e9588.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_stats WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d77223fc94257711512b6ff21ce109b97324f2917113f04492f53b20860e9588"
|
||||
}
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -143,7 +153,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dd74fa9468b5fe8c8ad657ded06076c11a78d0206af2af20685c691d4d9520bb"
|
||||
|
||||
23
backend/.sqlx/query-deb2e4c21d4f9d0b9439780ca6ebee13450609b578c469cb437c5420ab520258.json
generated
Normal file
23
backend/.sqlx/query-deb2e4c21d4f9d0b9439780ca6ebee13450609b578c469cb437c5420ab520258.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT content FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "content",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "deb2e4c21d4f9d0b9439780ca6ebee13450609b578c469cb437c5420ab520258"
|
||||
}
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -143,7 +153,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "eced0a09ba547ce1dccb54a5419b22373603c9d01f77047b3553bde125bf71e8"
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
23
backend/.sqlx/query-f2173822c1b3d82afedf0f7dba31e218539f0b259eb5f2c98e4e8ada6665f5ee.json
generated
Normal file
23
backend/.sqlx/query-f2173822c1b3d82afedf0f7dba31e218539f0b259eb5f2c98e4e8ada6665f5ee.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT no_flow_overlap FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f2173822c1b3d82afedf0f7dba31e218539f0b259eb5f2c98e4e8ada6665f5ee"
|
||||
}
|
||||
@@ -41,7 +41,6 @@
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"scheduledscriptwithretry",
|
||||
"singlescriptflow"
|
||||
]
|
||||
}
|
||||
@@ -66,7 +65,6 @@
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"scheduledscriptwithretry",
|
||||
"singlescriptflow"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -112,6 +112,16 @@
|
||||
"ordinal": 21,
|
||||
"name": "retry",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -141,7 +151,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fdd3710a381dac33ef5ee5ec5564a4874e6d7807d43fa6ea7b8408cad9e97480"
|
||||
|
||||
700
backend/Cargo.lock
generated
700
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.231.0"
|
||||
version = "1.236.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -11,6 +11,7 @@ members = [
|
||||
"./windmill-worker",
|
||||
"./windmill-common",
|
||||
"./windmill-audit",
|
||||
"./windmill-git-sync",
|
||||
"./parsers/windmill-parser",
|
||||
"./parsers/windmill-parser-ts",
|
||||
"./parsers/windmill-parser-wasm",
|
||||
@@ -21,7 +22,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.231.0"
|
||||
version = "1.236.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -34,7 +35,7 @@ opt-level = 0
|
||||
incremental = true
|
||||
|
||||
[features]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise"]
|
||||
enterprise_saml = ["windmill-api/enterprise_saml"]
|
||||
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark"]
|
||||
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
|
||||
@@ -46,6 +47,7 @@ anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
dotenv.workspace = true
|
||||
windmill-common = { workspace = true, features = ["tracing_init"] }
|
||||
windmill-git-sync.workspace = true
|
||||
windmill-api.workspace = true
|
||||
windmill-worker.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -66,7 +68,7 @@ uuid.workspace = true
|
||||
gethostname.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true}
|
||||
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false}
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
@@ -83,6 +85,7 @@ windmill-queue = { path = "./windmill-queue" }
|
||||
windmill-worker = { path = "./windmill-worker" }
|
||||
windmill-common = { path = "./windmill-common" }
|
||||
windmill-audit = { path = "./windmill-audit" }
|
||||
windmill-git-sync = { path = "./windmill-git-sync" }
|
||||
windmill-parser = { path = "./parsers/windmill-parser" }
|
||||
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
|
||||
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
|
||||
@@ -153,7 +156,7 @@ swc_ecma_visit = "0.93.7"
|
||||
base64 = "0.21.0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.6"
|
||||
sqlx = { version = "^0", features = [
|
||||
sqlx = { version = "0.7.3", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
"uuid",
|
||||
@@ -208,4 +211,7 @@ candle-transformers = "0.3.0"
|
||||
candle-nn = "0.3.0"
|
||||
tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono"] }
|
||||
aws-sdk-s3 = "1.5.0"
|
||||
aws-config = "1.0.3"
|
||||
aws-config = "1.0.3"
|
||||
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
|
||||
polars-io = { version = "0.35.2", features = ["csv"] }
|
||||
object_store = { version = "0.8.0", features = ["aws"] }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE schedule ADD COLUMN summary VARCHAR(512);
|
||||
ALTER TABLE schedule ADD COLUMN no_flow_overlap BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE dependency_map;
|
||||
14
backend/migrations/20231227014107_add_dependency_map.up.sql
Normal file
14
backend/migrations/20231227014107_add_dependency_map.up.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Add up migration script here
|
||||
|
||||
CREATE TYPE IMPORTER_KIND AS ENUM ('script', 'flow', 'app');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dependency_map (
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
importer_path VARCHAR(510) NOT NULL,
|
||||
importer_kind IMPORTER_KIND NOT NULL,
|
||||
imported_path VARCHAR(510) NOT NULL,
|
||||
PRIMARY KEY (workspace_id, importer_path, importer_kind, imported_path)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS dependency_map_imported_path_idx ON dependency_map (workspace_id, imported_path);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE job_stats;
|
||||
DROP TYPE METRIC_KIND;
|
||||
16
backend/migrations/20240102133412_job_stats_table.up.sql
Normal file
16
backend/migrations/20240102133412_job_stats_table.up.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Add up migration script here
|
||||
CREATE TYPE METRIC_KIND AS ENUM ('scalar_int', 'scalar_float', 'timeseries_int', 'timeseries_float');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job_stats (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
|
||||
job_id UUID NOT NULL,
|
||||
metric_id VARCHAR(50) NOT NULL,
|
||||
metric_name VARCHAR(255),
|
||||
metric_kind METRIC_KIND NOT NULL,
|
||||
scalar_int INTEGER,
|
||||
scalar_float REAL,
|
||||
timestamps TIMESTAMP WITH TIME ZONE[],
|
||||
timeseries_int INTEGER[],
|
||||
timeseries_float REAL[],
|
||||
PRIMARY KEY (workspace_id, job_id, metric_id)
|
||||
);
|
||||
@@ -63,7 +63,7 @@ lazy_static! {
|
||||
|
||||
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<String> {
|
||||
if level > 0 {
|
||||
let mut imports = vec!["requests".to_string()];
|
||||
let mut imports = vec![];
|
||||
let splitted_path = path.split("/");
|
||||
let base = splitted_path
|
||||
.clone()
|
||||
@@ -78,10 +78,7 @@ fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<Strin
|
||||
} else if let Some(module) = module {
|
||||
let imprt = module.split('.').next().unwrap_or("").replace("_", "-");
|
||||
if imprt == "u" || imprt == "f" {
|
||||
vec![
|
||||
"requests".to_string(),
|
||||
format!("relative:{}", module.replace(".", "/")),
|
||||
]
|
||||
vec![format!("relative:{}", module.replace(".", "/"))]
|
||||
} else {
|
||||
vec![imprt]
|
||||
}
|
||||
@@ -90,6 +87,57 @@ fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<Strin
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_relative_imports(code: &str, path: &str) -> error::Result<Vec<String>> {
|
||||
let nimports = parse_code_for_imports(code, path)?;
|
||||
return Ok(nimports
|
||||
.into_iter()
|
||||
.filter_map(|x| {
|
||||
if x.starts_with("relative:") {
|
||||
Some(x.replace("relative:", ""))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect());
|
||||
}
|
||||
|
||||
fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<String>> {
|
||||
let code = code.split(DEF_MAIN).next().unwrap_or("");
|
||||
let ast = Suite::parse(code, "main.py").map_err(|e| {
|
||||
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
|
||||
})?;
|
||||
let nimports: Vec<String> = ast
|
||||
.into_iter()
|
||||
.filter_map(|x| match x {
|
||||
Stmt::Import(StmtImport { names, .. }) => Some(
|
||||
names
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let name = x.name.to_string();
|
||||
process_import(Some(name), path, 0)
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<String>>(),
|
||||
),
|
||||
Stmt::ImportFrom(StmtImportFrom { level: Some(i), module, .. }) if i.to_u32() > 0 => {
|
||||
Some(process_import(
|
||||
module.map(|x| x.to_string()),
|
||||
path,
|
||||
i.to_usize(),
|
||||
))
|
||||
}
|
||||
Stmt::ImportFrom(StmtImportFrom { level: _, module, .. }) => {
|
||||
Some(process_import(module.map(|x| x.to_string()), path, 0))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
|
||||
.unique()
|
||||
.collect();
|
||||
return Ok(nimports);
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
pub async fn parse_python_imports(
|
||||
code: &str,
|
||||
@@ -127,41 +175,7 @@ pub async fn parse_python_imports(
|
||||
imports.extend(lines);
|
||||
}
|
||||
|
||||
let code = code.split(DEF_MAIN).next().unwrap_or("");
|
||||
let ast = Suite::parse(code, "main.py").map_err(|e| {
|
||||
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
|
||||
})?;
|
||||
let nimports: Vec<String> = ast
|
||||
.into_iter()
|
||||
.filter_map(|x| match x {
|
||||
Stmt::Import(StmtImport { names, .. }) => Some(
|
||||
names
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let name = x.name.to_string();
|
||||
process_import(Some(name), path, 0)
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<String>>(),
|
||||
),
|
||||
Stmt::ImportFrom(StmtImportFrom { level: Some(i), module, .. })
|
||||
if i.to_u32() > 0 =>
|
||||
{
|
||||
Some(process_import(
|
||||
module.map(|x| x.to_string()),
|
||||
path,
|
||||
i.to_usize(),
|
||||
))
|
||||
}
|
||||
Stmt::ImportFrom(StmtImportFrom { level: _, module, .. }) => {
|
||||
Some(process_import(module.map(|x| x.to_string()), path, 0))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
|
||||
.unique()
|
||||
.collect();
|
||||
let nimports = parse_code_for_imports(code, path)?;
|
||||
for n in nimports.iter() {
|
||||
let nested = if n.starts_with("relative:") {
|
||||
let rpath = n.replace("relative:", "");
|
||||
|
||||
@@ -20,7 +20,7 @@ def main():
|
||||
";
|
||||
let r = parse_python_imports(code, "test-workspace", "f/foo/bar", &db).await?;
|
||||
// println!("{}", serde_json::to_string(&r)?);
|
||||
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]);
|
||||
assert_eq!(r, vec!["matplotlib", "wmill", "zanzibar"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ def main():
|
||||
"numpy",
|
||||
"pandas",
|
||||
"pandas2",
|
||||
"requests"
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -431,7 +431,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
},
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING => {
|
||||
load_require_preexisting_user(&db).await;
|
||||
}
|
||||
},
|
||||
EXPOSE_METRICS_SETTING | EXPOSE_DEBUG_METRICS_SETTING => {
|
||||
if n.payload() != EXPOSE_DEBUG_METRICS_SETTING || worker_mode {
|
||||
tracing::info!("Metrics setting changed, restarting");
|
||||
|
||||
@@ -35,8 +35,8 @@ use windmill_common::{
|
||||
BASE_URL, DB, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
};
|
||||
use windmill_worker::{
|
||||
create_token_for_owner, handle_job_error, AuthedClient, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR,
|
||||
NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
|
||||
create_token_for_owner, handle_job_error, AuthedClient, SendResult, JOB_DEFAULT_TIMEOUT,
|
||||
KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
|
||||
};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -167,6 +167,7 @@ pub async fn load_metrics_debug_enabled(db: &DB) -> error::Result<()> {
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_keep_job_dir(db: &DB) {
|
||||
let value = sqlx::query_scalar!(
|
||||
"SELECT value FROM global_settings WHERE name = $1",
|
||||
@@ -250,25 +251,52 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
|
||||
let job_retention_secs = *JOB_RETENTION_SECS.read().await;
|
||||
if job_retention_secs > 0 {
|
||||
let deleted_jobs = sqlx::query_scalar!(
|
||||
"DELETE FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now() RETURNING id",
|
||||
job_retention_secs
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await;
|
||||
match db.begin().await {
|
||||
Ok(mut tx) => {
|
||||
let r = sqlx::query!(
|
||||
"DELETE FROM job_stats WHERE job_id IN (SELECT id FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now())",
|
||||
job_retention_secs
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await;
|
||||
match r {
|
||||
Ok(_) => {
|
||||
let deleted_jobs = sqlx::query_scalar!(
|
||||
"DELETE FROM completed_job WHERE created_at <= now() - ($1::bigint::text || ' s')::interval AND started_at + ((duration_ms/1000 + $1::bigint) || ' s')::interval <= now() RETURNING id",
|
||||
job_retention_secs
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await;
|
||||
|
||||
match deleted_jobs {
|
||||
Ok(deleted_jobs) => {
|
||||
if deleted_jobs.len() > 0 {
|
||||
tracing::info!(
|
||||
"deleted {} jobs completed JOB_RETENTION_SECS {} ago: {:?}",
|
||||
deleted_jobs.len(),
|
||||
job_retention_secs,
|
||||
deleted_jobs,
|
||||
)
|
||||
match deleted_jobs {
|
||||
Ok(deleted_jobs) => {
|
||||
if deleted_jobs.len() > 0 {
|
||||
tracing::info!(
|
||||
"deleted {} jobs completed JOB_RETENTION_SECS {} ago: {:?}",
|
||||
deleted_jobs.len(),
|
||||
job_retention_secs,
|
||||
deleted_jobs,
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error deleting expired jobs: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error deleting expired job stats: {:?}", err)
|
||||
}
|
||||
}
|
||||
|
||||
match tx.commit().await {
|
||||
Ok(_) => (),
|
||||
Err(err) => tracing::error!("Error deleting expired jobs: {:?}", err),
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting jobs: {}", e.to_string()),
|
||||
Err(err) => {
|
||||
tracing::error!("Error deleting expired jobs: {:?}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -729,6 +757,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
|
||||
// since the job is unrecoverable, the same worker queue should never be sent anything
|
||||
let (same_worker_tx_never_used, _same_worker_rx_never_used) = mpsc::channel::<Uuid>(1);
|
||||
let (send_result_never_used, _send_result_rx_never_used) = mpsc::channel::<SendResult>(1);
|
||||
|
||||
let token = create_token_for_owner(
|
||||
&db,
|
||||
@@ -767,6 +796,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
"",
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
send_result_never_used,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1805,7 +1805,7 @@ def main():
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_empty_loop(db: Pool<Postgres>) {
|
||||
async fn test_empty_loop_1(db: Pool<Postgres>) {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await;
|
||||
let port = server.addr.port();
|
||||
@@ -2871,6 +2871,9 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
|
||||
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
|
||||
ws_error_handler_muted: None,
|
||||
retry: None,
|
||||
no_flow_overlap: None,
|
||||
summary: None,
|
||||
|
||||
};
|
||||
|
||||
let _ = client.create_schedule("test-workspace", &schedule).await;
|
||||
@@ -2932,6 +2935,8 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
|
||||
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
|
||||
ws_error_handler_muted: None,
|
||||
retry: None,
|
||||
summary: None,
|
||||
no_flow_overlap: None
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -3009,6 +3014,8 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
|
||||
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
|
||||
ws_error_handler_muted: None,
|
||||
retry: None,
|
||||
no_flow_overlap: None,
|
||||
summary: None,
|
||||
};
|
||||
|
||||
let _ = client.create_schedule("test-workspace", &schedule).await;
|
||||
@@ -3071,6 +3078,8 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
|
||||
schedule: format!("{} {} * * * *", then.second(), then.minute()).to_string(),
|
||||
ws_error_handler_muted: None,
|
||||
retry: None,
|
||||
summary: None,
|
||||
no_flow_overlap: None
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "windmill_api"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise"]
|
||||
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
|
||||
enterprise_saml = ["samael"]
|
||||
benchmark = []
|
||||
|
||||
@@ -27,6 +27,7 @@ windmill-common = { workspace = true, features = [
|
||||
windmill-audit.workspace = true
|
||||
windmill-parser.workspace = true
|
||||
windmill-parser-py-imports.workspace = true
|
||||
windmill-git-sync.workspace = true
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
argon2.workspace = true
|
||||
@@ -84,7 +85,6 @@ candle-core.workspace = true
|
||||
candle-transformers.workspace = true
|
||||
candle-nn.workspace = true
|
||||
aws-sdk-s3.workspace = true
|
||||
aws-config.workspace = true
|
||||
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
|
||||
polars-io = { version = "0.35.2", features = ["csv"] }
|
||||
object_store = { version = "0.8.0", features = ["aws"] }
|
||||
polars.workspace = true
|
||||
polars-io.workspace = true
|
||||
object_store.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.230.0
|
||||
version: 1.233.0
|
||||
title: Windmill API
|
||||
contact:
|
||||
name: Windmill Team
|
||||
@@ -1531,6 +1531,8 @@ paths:
|
||||
type: string
|
||||
git_repo_resource_path:
|
||||
type: string
|
||||
use_individual_branch:
|
||||
type: boolean
|
||||
required: &ref_17
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
@@ -3352,32 +3354,32 @@ paths:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
oneOf: &ref_196
|
||||
oneOf: &ref_204
|
||||
- type: object
|
||||
properties: &ref_180
|
||||
properties: &ref_188
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
oneOf: &ref_26
|
||||
- type: object
|
||||
properties: &ref_176
|
||||
properties: &ref_184
|
||||
value: {}
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- javascript
|
||||
required: &ref_177
|
||||
required: &ref_185
|
||||
- expr
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_178
|
||||
properties: &ref_186
|
||||
expr:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- javascript
|
||||
required: &ref_179
|
||||
required: &ref_187
|
||||
- expr
|
||||
- type
|
||||
discriminator: &ref_27
|
||||
@@ -3417,13 +3419,13 @@ paths:
|
||||
type: number
|
||||
concurrency_time_window_s:
|
||||
type: number
|
||||
required: &ref_181
|
||||
required: &ref_189
|
||||
- type
|
||||
- content
|
||||
- language
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_182
|
||||
properties: &ref_190
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3437,12 +3439,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
required: &ref_183
|
||||
required: &ref_191
|
||||
- type
|
||||
- path
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_184
|
||||
properties: &ref_192
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3454,12 +3456,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- flow
|
||||
required: &ref_185
|
||||
required: &ref_193
|
||||
- type
|
||||
- path
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_186
|
||||
properties: &ref_194
|
||||
modules:
|
||||
type: array
|
||||
items:
|
||||
@@ -3481,13 +3483,13 @@ paths:
|
||||
type: boolean
|
||||
parallelism:
|
||||
type: integer
|
||||
required: &ref_187
|
||||
required: &ref_195
|
||||
- modules
|
||||
- iterator
|
||||
- skip_failures
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_188
|
||||
properties: &ref_196
|
||||
branches:
|
||||
type: array
|
||||
items:
|
||||
@@ -3518,12 +3520,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- branchone
|
||||
required: &ref_189
|
||||
required: &ref_197
|
||||
- branches
|
||||
- default
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_190
|
||||
properties: &ref_198
|
||||
branches:
|
||||
type: array
|
||||
items:
|
||||
@@ -3548,28 +3550,28 @@ paths:
|
||||
- branchall
|
||||
parallel:
|
||||
type: boolean
|
||||
required: &ref_191
|
||||
required: &ref_199
|
||||
- branches
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_192
|
||||
properties: &ref_200
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- identity
|
||||
flow:
|
||||
type: boolean
|
||||
required: &ref_193
|
||||
required: &ref_201
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_194
|
||||
properties: &ref_202
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- graphql
|
||||
required: &ref_195
|
||||
required: &ref_203
|
||||
- type
|
||||
discriminator: &ref_197
|
||||
discriminator: &ref_205
|
||||
propertyName: type
|
||||
mapping:
|
||||
rawscript: '#/components/schemas/RawScript'
|
||||
@@ -8240,6 +8242,10 @@ paths:
|
||||
retry:
|
||||
type: object
|
||||
properties: *ref_78
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
required: &ref_148
|
||||
- path
|
||||
- schedule
|
||||
@@ -8305,6 +8311,10 @@ paths:
|
||||
retry:
|
||||
type: object
|
||||
properties: *ref_78
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
required: &ref_150
|
||||
- schedule
|
||||
- timezone
|
||||
@@ -8446,6 +8456,10 @@ paths:
|
||||
retry:
|
||||
type: object
|
||||
properties: *ref_78
|
||||
summary:
|
||||
type: string
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
required: &ref_80
|
||||
- path
|
||||
- edited_by
|
||||
@@ -10309,6 +10323,91 @@ paths:
|
||||
- upload_id
|
||||
- parts
|
||||
- is_done
|
||||
/w/{workspace}/job_metrics/get/{id}:
|
||||
post:
|
||||
summary: get job metrics
|
||||
operationId: getJobMetrics
|
||||
tags:
|
||||
- metrics
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_74
|
||||
requestBody:
|
||||
description: parameters for statistics retrieval
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
timeseries_max_datapoints:
|
||||
type: integer
|
||||
from_timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
to_timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
responses:
|
||||
'200':
|
||||
description: job details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
metrics_metadata:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_176
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required: &ref_177
|
||||
- id
|
||||
scalar_metrics:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_178
|
||||
metric_id:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
required: &ref_179
|
||||
- id
|
||||
- value
|
||||
timeseries_metrics:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_180
|
||||
metric_id:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_182
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
value:
|
||||
type: number
|
||||
required: &ref_183
|
||||
- timestamp
|
||||
- value
|
||||
required: &ref_181
|
||||
- id
|
||||
- values
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -10960,56 +11059,72 @@ components:
|
||||
type: object
|
||||
properties: *ref_96
|
||||
required: *ref_97
|
||||
StaticTransform:
|
||||
MetricMetadata:
|
||||
type: object
|
||||
properties: *ref_176
|
||||
required: *ref_177
|
||||
JavascriptTransform:
|
||||
ScalarMetric:
|
||||
type: object
|
||||
properties: *ref_178
|
||||
required: *ref_179
|
||||
TimeseriesMetric:
|
||||
type: object
|
||||
properties: *ref_180
|
||||
required: *ref_181
|
||||
MetricDataPoint:
|
||||
type: object
|
||||
properties: *ref_182
|
||||
required: *ref_183
|
||||
StaticTransform:
|
||||
type: object
|
||||
properties: *ref_184
|
||||
required: *ref_185
|
||||
JavascriptTransform:
|
||||
type: object
|
||||
properties: *ref_186
|
||||
required: *ref_187
|
||||
InputTransform:
|
||||
oneOf: *ref_26
|
||||
discriminator: *ref_27
|
||||
RawScript:
|
||||
type: object
|
||||
properties: *ref_180
|
||||
required: *ref_181
|
||||
properties: *ref_188
|
||||
required: *ref_189
|
||||
PathScript:
|
||||
type: object
|
||||
properties: *ref_182
|
||||
required: *ref_183
|
||||
properties: *ref_190
|
||||
required: *ref_191
|
||||
PathFlow:
|
||||
type: object
|
||||
properties: *ref_184
|
||||
required: *ref_185
|
||||
properties: *ref_192
|
||||
required: *ref_193
|
||||
FlowModule:
|
||||
type: object
|
||||
properties: *ref_28
|
||||
required: *ref_29
|
||||
ForloopFlow:
|
||||
type: object
|
||||
properties: *ref_186
|
||||
required: *ref_187
|
||||
BranchOne:
|
||||
type: object
|
||||
properties: *ref_188
|
||||
required: *ref_189
|
||||
BranchAll:
|
||||
type: object
|
||||
properties: *ref_190
|
||||
required: *ref_191
|
||||
Identity:
|
||||
type: object
|
||||
properties: *ref_192
|
||||
required: *ref_193
|
||||
Graphql:
|
||||
type: object
|
||||
properties: *ref_194
|
||||
required: *ref_195
|
||||
BranchOne:
|
||||
type: object
|
||||
properties: *ref_196
|
||||
required: *ref_197
|
||||
BranchAll:
|
||||
type: object
|
||||
properties: *ref_198
|
||||
required: *ref_199
|
||||
Identity:
|
||||
type: object
|
||||
properties: *ref_200
|
||||
required: *ref_201
|
||||
Graphql:
|
||||
type: object
|
||||
properties: *ref_202
|
||||
required: *ref_203
|
||||
FlowModuleValue:
|
||||
oneOf: *ref_196
|
||||
discriminator: *ref_197
|
||||
oneOf: *ref_204
|
||||
discriminator: *ref_205
|
||||
Retry:
|
||||
type: object
|
||||
properties: *ref_78
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.231.0
|
||||
version: 1.236.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -6674,14 +6674,17 @@ paths:
|
||||
- storage_options
|
||||
/w/{workspace}/job_helpers/v2/s3_resource_info:
|
||||
post:
|
||||
summary: Returns the s3 resource associated to the provided path, or the workspace default S3 resource
|
||||
summary: Returns the s3 resource associated to the provided path, or the
|
||||
workspace default S3 resource
|
||||
operationId: s3ResourceInfo
|
||||
tags:
|
||||
- helpers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: S3 resource path to use. If empty, the S3 resource defined in the workspace settings will be used
|
||||
description:
|
||||
S3 resource path to use. If empty, the S3 resource defined in the
|
||||
workspace settings will be used
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
@@ -6917,6 +6920,53 @@ paths:
|
||||
- parts
|
||||
- is_done
|
||||
|
||||
/w/{workspace}/job_metrics/get/{id}:
|
||||
post:
|
||||
summary: get job metrics
|
||||
operationId: getJobMetrics
|
||||
tags:
|
||||
- metrics
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
requestBody:
|
||||
description: parameters for statistics retrieval
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
timeseries_max_datapoints:
|
||||
type: integer
|
||||
from_timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
to_timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: job details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
metrics_metadata:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/MetricMetadata"
|
||||
scalar_metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ScalarMetric"
|
||||
timeseries_metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/TimeseriesMetric"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
@@ -8399,6 +8449,10 @@ components:
|
||||
type: boolean
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
summary:
|
||||
type: string
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- edited_by
|
||||
@@ -8468,6 +8522,10 @@ components:
|
||||
type: boolean
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- schedule
|
||||
@@ -8504,6 +8562,10 @@ components:
|
||||
type: boolean
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
required:
|
||||
- schedule
|
||||
- timezone
|
||||
@@ -9050,6 +9112,8 @@ components:
|
||||
type: string
|
||||
git_repo_resource_path:
|
||||
type: string
|
||||
use_individual_branch:
|
||||
type: boolean
|
||||
required:
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
@@ -9064,3 +9128,49 @@ components:
|
||||
required:
|
||||
- part_number
|
||||
- tag
|
||||
|
||||
MetricMetadata:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
|
||||
ScalarMetric:
|
||||
type: object
|
||||
properties:
|
||||
metric_id:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
required:
|
||||
- id
|
||||
- value
|
||||
|
||||
TimeseriesMetric:
|
||||
type: object
|
||||
properties:
|
||||
metric_id:
|
||||
type: string
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/MetricDataPoint"
|
||||
required:
|
||||
- id
|
||||
- values
|
||||
|
||||
MetricDataPoint:
|
||||
type: object
|
||||
properties:
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
value:
|
||||
type: number
|
||||
required:
|
||||
- timestamp
|
||||
- value
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::collections::HashMap;
|
||||
*/
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
deployment_metadata_helpers,
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
variables::build_crypt,
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
@@ -588,12 +587,16 @@ async fn create_app(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut tx = PushIsolationLevel::Transaction(tx);
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
let (dependency_job_uuid, new_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
|
||||
JobPayload::AppDependencies {
|
||||
path: app.path.clone(),
|
||||
version: v_id,
|
||||
deployment_message: app.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
@@ -615,25 +618,7 @@ async fn create_app(
|
||||
.await?;
|
||||
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
|
||||
|
||||
tx = PushIsolationLevel::Transaction(new_tx);
|
||||
tx = deployment_metadata_helpers::handle_deployment_metadata(
|
||||
tx,
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
deployment_metadata_helpers::DeployedObject::App { path: app.path.clone(), version: v_id },
|
||||
app.deployment_message,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
|
||||
_ => {
|
||||
return Err(Error::InternalErr(
|
||||
"Expected a transaction here".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
new_tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
@@ -849,14 +834,18 @@ async fn update_app(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
|
||||
let tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
|
||||
PushIsolationLevel::Transaction(tx);
|
||||
if let Some(v_id) = v_id {
|
||||
let (dependency_job_uuid, new_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
|
||||
JobPayload::AppDependencies {
|
||||
path: npath.clone(),
|
||||
version: v_id,
|
||||
deployment_message: ns.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
@@ -877,26 +866,7 @@ async fn update_app(
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
|
||||
|
||||
tx = PushIsolationLevel::Transaction(new_tx);
|
||||
tx = deployment_metadata_helpers::handle_deployment_metadata(
|
||||
tx,
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
deployment_metadata_helpers::DeployedObject::App { path: npath.clone(), version: v_id },
|
||||
ns.deployment_message,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
|
||||
_ => {
|
||||
return Err(Error::InternalErr(
|
||||
"Expected a transaction here".to_string(),
|
||||
));
|
||||
}
|
||||
new_tx.commit().await?;
|
||||
}
|
||||
|
||||
webhook.send_message(
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::deployment_metadata_helpers;
|
||||
use crate::{
|
||||
db::DB,
|
||||
schedule::clear_schedule,
|
||||
@@ -355,7 +354,7 @@ async fn create_flow(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut tx = PushIsolationLevel::Transaction(tx);
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
let (dependency_job_uuid, mut new_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
@@ -363,6 +362,7 @@ async fn create_flow(
|
||||
JobPayload::FlowDependencies {
|
||||
path: nf.path.clone(),
|
||||
dedicated_worker: nf.dedicated_worker,
|
||||
deployment_message: nf.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
@@ -393,26 +393,7 @@ async fn create_flow(
|
||||
.execute(&mut new_tx)
|
||||
.await?;
|
||||
|
||||
tx = PushIsolationLevel::Transaction(new_tx);
|
||||
tx = deployment_metadata_helpers::handle_deployment_metadata(
|
||||
tx,
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
deployment_metadata_helpers::DeployedObject::Flow { path: nf.path.clone() },
|
||||
nf.deployment_message,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
|
||||
_ => {
|
||||
return Err(Error::InternalErr(
|
||||
"Expected a transaction here".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
new_tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
|
||||
@@ -585,7 +566,7 @@ async fn update_flow(
|
||||
},
|
||||
);
|
||||
|
||||
let mut tx = PushIsolationLevel::Transaction(tx);
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
|
||||
let (dependency_job_uuid, mut new_tx) = push(
|
||||
&db,
|
||||
@@ -594,6 +575,7 @@ async fn update_flow(
|
||||
JobPayload::FlowDependencies {
|
||||
path: nf.path.clone(),
|
||||
dedicated_worker: nf.dedicated_worker,
|
||||
deployment_message: nf.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
@@ -631,25 +613,7 @@ async fn update_flow(
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx = PushIsolationLevel::Transaction(new_tx);
|
||||
tx = deployment_metadata_helpers::handle_deployment_metadata(
|
||||
tx,
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
deployment_metadata_helpers::DeployedObject::Flow { path: nf.path.clone() },
|
||||
nf.deployment_message,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
|
||||
_ => {
|
||||
return Err(Error::InternalErr(
|
||||
"Expected a transaction here".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
new_tx.commit().await?;
|
||||
|
||||
Ok(nf.path.to_string())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use std::{cmp, time::Duration};
|
||||
|
||||
use crate::{
|
||||
db::DB, resources::get_resource_value_interpolated_internal, users::Tokened,
|
||||
workspaces::LargeFileStorage,
|
||||
};
|
||||
use crate::{db::DB, resources::get_resource_value_interpolated_internal, users::Tokened};
|
||||
use anyhow::Context;
|
||||
use aws_sdk_s3::{
|
||||
config::{BehaviorVersion, Credentials, Region},
|
||||
presigning::PresigningConfig,
|
||||
primitives::ByteStream,
|
||||
types::{CompletedMultipartUpload, CompletedPart},
|
||||
@@ -32,7 +28,11 @@ use polars::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use windmill_common::{db::UserDB, error};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error,
|
||||
s3_helpers::{build_s3_client, render_endpoint, LargeFileStorage, S3Resource},
|
||||
};
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
@@ -87,23 +87,6 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
struct S3Resource {
|
||||
#[serde(rename = "bucket")]
|
||||
bucket: String,
|
||||
region: String,
|
||||
#[serde(rename = "endPoint")]
|
||||
endpoint: String,
|
||||
#[serde(rename = "useSSL")]
|
||||
use_ssl: bool,
|
||||
#[serde(rename = "accessKey")]
|
||||
access_key: Option<String>,
|
||||
#[serde(rename = "secretKey")]
|
||||
secret_key: Option<String>,
|
||||
#[serde(rename = "pathStyle")]
|
||||
path_style: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DuckdbConnectionSettingsQuery {
|
||||
s3_resource: S3Resource,
|
||||
@@ -346,7 +329,10 @@ async fn test_connection(
|
||||
.max_keys(1)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
|
||||
.map_err(|err| {
|
||||
tracing::error!("Error testing connection to S3 bucket: {:?}", err);
|
||||
error::Error::InternalErr(err.to_string())
|
||||
})?;
|
||||
return Ok(Json(()));
|
||||
}
|
||||
|
||||
@@ -911,6 +897,11 @@ async fn multipart_upload_s3_file(
|
||||
}));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct S3Object {
|
||||
pub s3: String,
|
||||
}
|
||||
|
||||
async fn get_workspace_s3_resource<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: &UserDB,
|
||||
@@ -983,30 +974,6 @@ async fn get_s3_resource<'c>(
|
||||
return Ok(Some(s3_resource));
|
||||
}
|
||||
|
||||
fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
|
||||
let s3_resource = s3_resource_ref.clone();
|
||||
|
||||
let endpoint = render_endpoint(&s3_resource);
|
||||
let mut s3_config_builder = aws_sdk_s3::Config::builder()
|
||||
.endpoint_url(endpoint)
|
||||
.behavior_version(BehaviorVersion::latest())
|
||||
.region(Region::new(s3_resource.region));
|
||||
if s3_resource.access_key.is_some() {
|
||||
s3_config_builder = s3_config_builder.credentials_provider(Credentials::new(
|
||||
s3_resource.access_key.unwrap_or_default(),
|
||||
s3_resource.secret_key.unwrap_or_default(),
|
||||
None,
|
||||
None,
|
||||
"s3_storage",
|
||||
));
|
||||
}
|
||||
if s3_resource.path_style {
|
||||
s3_config_builder = s3_config_builder.force_path_style(true);
|
||||
}
|
||||
let s3_config = s3_config_builder.build();
|
||||
return aws_sdk_s3::Client::from_conf(s3_config);
|
||||
}
|
||||
|
||||
fn build_polars_s3_config(s3_resource_ref: &S3Resource) -> CloudOptions {
|
||||
let s3_resource = s3_resource_ref.to_owned();
|
||||
let mut s3_configs: Vec<(AmazonS3ConfigKey, String)> = vec![
|
||||
@@ -1034,16 +1001,6 @@ fn build_polars_s3_config(s3_resource_ref: &S3Resource) -> CloudOptions {
|
||||
return CloudOptions::default().with_aws(s3_configs);
|
||||
}
|
||||
|
||||
fn render_endpoint(s3_resource: &S3Resource) -> String {
|
||||
if s3_resource.endpoint.starts_with("http://") || s3_resource.endpoint.starts_with("https://") {
|
||||
s3_resource.endpoint.clone()
|
||||
} else if s3_resource.use_ssl {
|
||||
format!("https://{}", s3_resource.endpoint)
|
||||
} else {
|
||||
format!("http://{}", s3_resource.endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_s3_text_object_head(
|
||||
s3_client: &aws_sdk_s3::Client,
|
||||
s3_bucket: &str,
|
||||
|
||||
165
backend/windmill-api/src/job_metrics.rs
Normal file
165
backend/windmill-api/src/job_metrics.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use crate::db::DB;
|
||||
|
||||
use axum::{extract::Path, routing::post, Extension, Json, Router};
|
||||
use hyper::http;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::{
|
||||
error,
|
||||
job_metrics::{JobStatsRecord, MetricKind},
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([http::Method::GET, http::Method::POST])
|
||||
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
|
||||
.allow_origin(Any);
|
||||
|
||||
Router::new().route("/get/:id", post(get_job_metrics).layer(cors.clone()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JobStatsRequest {
|
||||
from_timestamp: Option<chrono::DateTime<chrono::Utc>>,
|
||||
to_timestamp: Option<chrono::DateTime<chrono::Utc>>,
|
||||
timeseries_max_datapoints: Option<u32>, // default to 100, any value lower than that will be ignored. Set to 0 to retrieve all
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JobStatsResponse {
|
||||
metrics_metadata: Vec<MetricsMetadata>,
|
||||
scalar_metrics: Vec<ScalarMetric>,
|
||||
timeseries_metrics: Vec<TimeseriesMetric>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MetricsMetadata {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ScalarMetric {
|
||||
metric_id: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TimeseriesMetric {
|
||||
metric_id: String,
|
||||
values: Vec<DataPoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DataPoint {
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
async fn get_job_metrics(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, job_id)): Path<(String, Uuid)>,
|
||||
Json(JobStatsRequest { from_timestamp, to_timestamp, timeseries_max_datapoints }): Json<
|
||||
JobStatsRequest,
|
||||
>,
|
||||
) -> error::JsonResult<JobStatsResponse> {
|
||||
let records = sqlx::query_as::<_, JobStatsRecord>(
|
||||
"SELECT * FROM job_stats where workspace_id = $1 and job_id = $2",
|
||||
)
|
||||
.bind(w_id)
|
||||
.bind(job_id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let mut metrics_metadata: Vec<MetricsMetadata> = vec![];
|
||||
let mut scalar_metrics: Vec<ScalarMetric> = vec![];
|
||||
let mut timeseries_metrics: Vec<TimeseriesMetric> = vec![];
|
||||
|
||||
for record in records {
|
||||
let metric_id = record.metric_id;
|
||||
match record.metric_kind {
|
||||
MetricKind::ScalarInt => {
|
||||
let value = record.scalar_int.unwrap_or_default() as f64;
|
||||
scalar_metrics.push(ScalarMetric { metric_id: metric_id.clone(), value });
|
||||
}
|
||||
MetricKind::ScalarFloat => {
|
||||
let value = record.scalar_float.unwrap_or_default() as f64;
|
||||
scalar_metrics.push(ScalarMetric { metric_id: metric_id.clone(), value });
|
||||
}
|
||||
MetricKind::TimeseriesInt => {
|
||||
if record.timestamps.clone().unwrap_or_default().len()
|
||||
!= record.timeseries_int.clone().unwrap_or_default().len()
|
||||
{
|
||||
tracing::warn!("Timeseries metric {} has an invalid shape. It doesn't have one timestamp per measurement. (timestamps: {:?}, measurements: {:?})", metric_id, record.timestamps, record.timeseries_int)
|
||||
}
|
||||
let (timestamps, timeseries_int) = timeseries_sample(
|
||||
from_timestamp,
|
||||
to_timestamp,
|
||||
timeseries_max_datapoints,
|
||||
record.timestamps.unwrap_or_default(),
|
||||
record.timeseries_int.unwrap_or_default(),
|
||||
);
|
||||
let mut values: Vec<DataPoint> = vec![];
|
||||
for (idx, value) in timeseries_int.iter().enumerate() {
|
||||
values.push(DataPoint {
|
||||
timestamp: timestamps[idx],
|
||||
value: value.to_owned() as f64,
|
||||
});
|
||||
}
|
||||
timeseries_metrics.push(TimeseriesMetric { metric_id: metric_id.clone(), values });
|
||||
}
|
||||
MetricKind::TimeseriesFloat => {
|
||||
if record.timestamps.clone().unwrap_or_default().len()
|
||||
!= record.timeseries_int.clone().unwrap_or_default().len()
|
||||
{
|
||||
tracing::warn!("Timeseries metric {} has an invalid shape. It doesn't have one timestamp per measurement. (timestamps: {:?}, measurements: {:?})", metric_id, record.timestamps, record.timeseries_float)
|
||||
}
|
||||
let (timestamps, timeseries_float) = timeseries_sample(
|
||||
from_timestamp,
|
||||
to_timestamp,
|
||||
timeseries_max_datapoints,
|
||||
record.timestamps.unwrap_or_default(),
|
||||
record.timeseries_float.unwrap_or_default(),
|
||||
);
|
||||
let mut values: Vec<DataPoint> = vec![];
|
||||
for (idx, value) in timeseries_float.iter().enumerate() {
|
||||
values.push(DataPoint {
|
||||
timestamp: timestamps[idx],
|
||||
value: value.to_owned() as f64,
|
||||
});
|
||||
}
|
||||
timeseries_metrics.push(TimeseriesMetric { metric_id: metric_id.clone(), values });
|
||||
}
|
||||
};
|
||||
metrics_metadata.push(MetricsMetadata { id: metric_id, name: record.metric_name });
|
||||
}
|
||||
|
||||
let response = JobStatsResponse { metrics_metadata, scalar_metrics, timeseries_metrics };
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
fn timeseries_sample<T: Copy>(
|
||||
from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
to: Option<chrono::DateTime<chrono::Utc>>,
|
||||
_datapoints: Option<u32>,
|
||||
timestamps: Vec<chrono::DateTime<chrono::Utc>>,
|
||||
values: Vec<T>,
|
||||
) -> (Vec<chrono::DateTime<chrono::Utc>>, Vec<T>) {
|
||||
if timestamps.len() != values.len() {
|
||||
tracing::warn!("Timeseries metric has an invalid shape. It doesn't have one timestamp per measurement. (timestamps: {:?}, measurements: {:?})", timestamps.len(), values.len());
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
let mut filtered_timestamp: Vec<chrono::DateTime<chrono::Utc>> = vec![];
|
||||
let mut filtered_values: Vec<T> = vec![];
|
||||
for (idx, timestamp) in timestamps.iter().enumerate() {
|
||||
if *timestamp > from.unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC)
|
||||
&& *timestamp < to.unwrap_or(chrono::DateTime::<chrono::Utc>::MAX_UTC)
|
||||
{
|
||||
filtered_timestamp.push(timestamps[idx]);
|
||||
filtered_values.push(values[idx]);
|
||||
}
|
||||
}
|
||||
// TODO: implement sampling
|
||||
return (filtered_timestamp, filtered_values);
|
||||
}
|
||||
@@ -2826,14 +2826,14 @@ pub struct JobUpdate {
|
||||
|
||||
async fn get_job_update(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
Path((w_id, job_id)): Path<(String, Uuid)>,
|
||||
Query(JobUpdateQuery { running, log_offset }): Query<JobUpdateQuery>,
|
||||
) -> error::JsonResult<JobUpdate> {
|
||||
let record = sqlx::query!(
|
||||
"SELECT running, substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
|
||||
log_offset,
|
||||
&w_id,
|
||||
&id
|
||||
&job_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
@@ -2855,11 +2855,11 @@ async fn get_job_update(
|
||||
$3",
|
||||
log_offset,
|
||||
&w_id,
|
||||
&id
|
||||
&job_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
let logs = not_found_if_none(logs, "Job Update", id.to_string())?;
|
||||
let logs = not_found_if_none(logs, "Job Update", job_id.to_string())?;
|
||||
Ok(Json(JobUpdate {
|
||||
running: Some(false),
|
||||
completed: Some(true),
|
||||
|
||||
@@ -49,7 +49,6 @@ mod audit;
|
||||
mod capture;
|
||||
mod configs;
|
||||
mod db;
|
||||
mod deployment_metadata_helpers;
|
||||
mod drafts;
|
||||
pub mod ee;
|
||||
pub mod embeddings;
|
||||
@@ -61,6 +60,7 @@ mod groups;
|
||||
mod inputs;
|
||||
mod integration;
|
||||
pub mod job_helpers;
|
||||
pub mod job_metrics;
|
||||
pub mod jobs;
|
||||
pub mod oauth2;
|
||||
mod openai;
|
||||
@@ -198,6 +198,7 @@ pub async fn run_server(
|
||||
.nest("/folders", folders::workspaced_service())
|
||||
.nest("/groups", groups::workspaced_service())
|
||||
.nest("/inputs", inputs::workspaced_service())
|
||||
.nest("/job_metrics", job_metrics::workspaced_service())
|
||||
.nest("/job_helpers", job_helpers::workspaced_service())
|
||||
.nest("/jobs", jobs::workspaced_service())
|
||||
.nest("/oauth", oauth2::workspaced_service())
|
||||
|
||||
@@ -54,6 +54,8 @@ pub struct NewSchedule {
|
||||
pub path: String,
|
||||
pub schedule: String,
|
||||
pub timezone: String,
|
||||
pub summary: Option<String>,
|
||||
pub no_flow_overlap: Option<bool>,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
@@ -153,9 +155,9 @@ async fn create_schedule(
|
||||
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
|
||||
is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, \
|
||||
on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, \
|
||||
ws_error_handler_muted, retry \
|
||||
ws_error_handler_muted, retry, summary, no_flow_overlap \
|
||||
) VALUES ( \
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19 \
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 \
|
||||
) RETURNING *",
|
||||
w_id,
|
||||
ns.path,
|
||||
@@ -176,6 +178,8 @@ async fn create_schedule(
|
||||
ns.on_recovery_extra_args,
|
||||
ns.ws_error_handler_muted.unwrap_or(false),
|
||||
ns.retry,
|
||||
ns.summary,
|
||||
ns.no_flow_overlap.unwrap_or(false),
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
@@ -229,8 +233,8 @@ async fn edit_schedule(
|
||||
Schedule,
|
||||
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, \
|
||||
on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, \
|
||||
on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12
|
||||
WHERE path = $13 AND workspace_id = $14 RETURNING *",
|
||||
on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 \
|
||||
WHERE path = $15 AND workspace_id = $16 RETURNING *",
|
||||
es.schedule,
|
||||
es.timezone,
|
||||
es.args,
|
||||
@@ -243,6 +247,8 @@ async fn edit_schedule(
|
||||
es.on_recovery_extra_args,
|
||||
es.ws_error_handler_muted.unwrap_or(false),
|
||||
es.retry,
|
||||
es.summary,
|
||||
es.no_flow_overlap.unwrap_or(false),
|
||||
path,
|
||||
w_id,
|
||||
)
|
||||
@@ -336,6 +342,8 @@ pub struct ScheduleWJobs {
|
||||
pub ws_error_handler_muted: bool,
|
||||
pub retry: Option<serde_json::Value>,
|
||||
pub jobs: Option<Vec<serde_json::Value>>,
|
||||
pub summary: Option<String>,
|
||||
pub no_flow_overlap: bool,
|
||||
}
|
||||
|
||||
async fn list_schedule_with_jobs(
|
||||
@@ -348,7 +356,7 @@ async fn list_schedule_with_jobs(
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
let rows = sqlx::query_as!(ScheduleWJobs,
|
||||
"SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE
|
||||
completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL ORDER BY started_at DESC LIMIT 20) AS jobs ) t
|
||||
completed_job.schedule_path = schedule.path AND completed_job.workspace_id = $1 AND parent_job IS NULL AND is_skipped = False ORDER BY started_at DESC LIMIT 20) AS jobs ) t
|
||||
WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
|
||||
w_id,
|
||||
per_page as i64,
|
||||
@@ -673,6 +681,7 @@ pub struct EditSchedule {
|
||||
pub schedule: String,
|
||||
pub timezone: String,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub summary: Option<String>,
|
||||
pub on_failure: Option<String>,
|
||||
pub on_failure_times: Option<i32>,
|
||||
pub on_failure_exact: Option<bool>,
|
||||
@@ -682,6 +691,7 @@ pub struct EditSchedule {
|
||||
pub on_recovery_extra_args: Option<serde_json::Value>,
|
||||
pub ws_error_handler_muted: Option<bool>,
|
||||
pub retry: Option<serde_json::Value>,
|
||||
pub no_flow_overlap: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn clear_schedule<'c>(
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
deployment_metadata_helpers,
|
||||
schedule::clear_schedule,
|
||||
users::{maybe_refresh_folders, require_owner_of_path, AuthCache},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
@@ -47,6 +46,7 @@ use windmill_common::{
|
||||
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
|
||||
},
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{
|
||||
self, schedule::push_scheduled_job, PushArgs, PushIsolationLevel, QueueTransaction,
|
||||
};
|
||||
@@ -325,7 +325,7 @@ async fn create_script(
|
||||
let script_path = ns.path.clone();
|
||||
let hash = ScriptHash(hash_script(&ns));
|
||||
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
|
||||
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||
let mut tx: QueueTransaction<'_, _> = (rsmq.clone(), user_db.begin(&authed).await?).into();
|
||||
|
||||
if sqlx::query_scalar!(
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
@@ -604,37 +604,29 @@ async fn create_script(
|
||||
);
|
||||
}
|
||||
|
||||
let mut tx = PushIsolationLevel::Transaction(tx);
|
||||
|
||||
let permissioned_as = username_to_permissioned_as(&authed.username);
|
||||
if needs_lock_gen {
|
||||
let dependencies = match ns.language {
|
||||
ScriptLang::Python3 => {
|
||||
windmill_parser_py_imports::parse_python_imports(&ns.content, &w_id, &ns.path, &db)
|
||||
.await?
|
||||
.join("\n")
|
||||
}
|
||||
_ => ns.content,
|
||||
};
|
||||
let tag = if ns.dedicated_worker.is_some_and(|x| x) {
|
||||
Some(format!("{}:{}", &w_id, &ns.path,))
|
||||
} else {
|
||||
ns.tag
|
||||
};
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
let (_, new_tx) = windmill_queue::push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::Dependencies {
|
||||
hash,
|
||||
dependencies,
|
||||
language: ns.language,
|
||||
path: ns.path,
|
||||
dedicated_worker: ns.dedicated_worker,
|
||||
deployment_message: ns.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
permissioned_as,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -650,26 +642,19 @@ async fn create_script(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx = PushIsolationLevel::Transaction(new_tx);
|
||||
}
|
||||
|
||||
tx = deployment_metadata_helpers::handle_deployment_metadata(
|
||||
tx,
|
||||
&authed,
|
||||
&db,
|
||||
&w_id,
|
||||
deployment_metadata_helpers::DeployedObject::Script { hash: hash, path: script_path },
|
||||
ns.deployment_message,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => tx.commit().await?,
|
||||
_ => {
|
||||
return Err(Error::InternalErr(
|
||||
"Expected a transaction here".to_string(),
|
||||
));
|
||||
}
|
||||
new_tx.commit().await?;
|
||||
} else {
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script { hash: hash, path: script_path },
|
||||
ns.deployment_message,
|
||||
rsmq,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
|
||||
Ok((StatusCode::CREATED, format!("{}", hash)))
|
||||
|
||||
@@ -40,9 +40,11 @@ use stripe::CustomerId;
|
||||
use uuid::Uuid;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::s3_helpers::LargeFileStorage;
|
||||
use windmill_common::schedule::Schedule;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
use windmill_common::workspaces::WorkspaceGitRepo;
|
||||
use windmill_common::{
|
||||
error::{to_anyhow, Error, JsonResult, Result},
|
||||
flows::Flow,
|
||||
@@ -281,18 +283,6 @@ pub struct EditErrorHandler {
|
||||
pub error_handler_muted_on_cancel: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum LargeFileStorage {
|
||||
S3Storage(S3Storage),
|
||||
// TODO: Add a filesystem type here in the future if needed
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct S3Storage {
|
||||
pub s3_resource_path: String,
|
||||
}
|
||||
|
||||
async fn list_pending_invites(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -1056,12 +1046,6 @@ struct EditGitSyncConfig {
|
||||
git_sync_settings: Option<WorkspaceGitRepo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct WorkspaceGitRepo {
|
||||
pub script_path: String,
|
||||
pub git_repo_resource_path: String,
|
||||
}
|
||||
|
||||
async fn edit_git_sync_config(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -1515,6 +1499,9 @@ async fn delete_workspace(
|
||||
let mut tx = db.begin().await?;
|
||||
require_super_admin(&db, &email).await?;
|
||||
|
||||
sqlx::query!("DELETE FROM dependency_map WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!("DELETE FROM queue WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -1533,6 +1520,12 @@ async fn delete_workspace(
|
||||
sqlx::query!("DELETE FROM app WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!("DELETE FROM raw_app WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!("DELETE FROM input WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!("DELETE FROM variable WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -1548,6 +1541,17 @@ async fn delete_workspace(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM job_stats WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM deployment_metadata WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM usr WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -1575,6 +1579,10 @@ async fn delete_workspace(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM account WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM workspace_key WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -1586,6 +1594,10 @@ async fn delete_workspace(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM token WHERE workspace_id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM workspace WHERE id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -48,4 +48,6 @@ itertools.workspace = true
|
||||
regex.workspace = true
|
||||
git-version.workspace = true
|
||||
cron.workspace = true
|
||||
tracing-loki = { version = "^0", optional = true }
|
||||
tracing-loki = { version = "^0", optional = true }
|
||||
aws-sdk-s3.workspace = true
|
||||
aws-config.workspace = true
|
||||
187
backend/windmill-common/src/job_metrics.rs
Normal file
187
backend/windmill-common/src/job_metrics.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use crate::{db::DB, error};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct JobStatsRecord {
|
||||
pub workspace_id: String,
|
||||
pub job_id: Uuid,
|
||||
pub metric_id: String,
|
||||
pub metric_name: Option<String>,
|
||||
pub metric_kind: MetricKind,
|
||||
pub scalar_int: Option<i32>,
|
||||
pub scalar_float: Option<f32>,
|
||||
pub timestamps: Option<Vec<chrono::DateTime<chrono::Utc>>>,
|
||||
pub timeseries_int: Option<Vec<i32>>,
|
||||
pub timeseries_float: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[sqlx(type_name = "METRIC_KIND", rename_all = "snake_case")]
|
||||
pub enum MetricKind {
|
||||
ScalarInt,
|
||||
ScalarFloat,
|
||||
TimeseriesInt,
|
||||
TimeseriesFloat,
|
||||
}
|
||||
|
||||
pub enum MetricNumericValue {
|
||||
Integer(i32),
|
||||
Float(f32),
|
||||
}
|
||||
|
||||
pub async fn register_metric_for_job(
|
||||
db: &DB,
|
||||
workspace_id: String,
|
||||
job_id: Uuid,
|
||||
metric_id: String,
|
||||
metric_kind: MetricKind,
|
||||
metric_name: Option<String>,
|
||||
) -> error::Result<String> {
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT true FROM job_stats WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
workspace_id,
|
||||
job_id,
|
||||
metric_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten();
|
||||
if exists.unwrap_or(false) {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Metric {} is already registered for job {}",
|
||||
metric_id, job_id
|
||||
)));
|
||||
}
|
||||
|
||||
let (scalar_int, scalar_float, timestamps, timeseries_int, timeseries_float) = match metric_kind
|
||||
{
|
||||
MetricKind::ScalarInt | MetricKind::ScalarFloat => {
|
||||
(None as Option<i32>, None as Option<f32>, None, None, None)
|
||||
}
|
||||
MetricKind::TimeseriesInt => (
|
||||
None,
|
||||
None,
|
||||
Some(&[] as &[chrono::DateTime<chrono::Utc>]),
|
||||
Some(&[] as &[i32]),
|
||||
None,
|
||||
),
|
||||
MetricKind::TimeseriesFloat => (
|
||||
None,
|
||||
None,
|
||||
Some(&[] as &[chrono::DateTime<chrono::Utc>]),
|
||||
None,
|
||||
Some(&[] as &[f32]),
|
||||
),
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO job_stats (workspace_id, job_id, metric_id, metric_name, metric_kind, scalar_int, scalar_float, timestamps, timeseries_int, timeseries_float) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
|
||||
)
|
||||
.bind(workspace_id)
|
||||
.bind(job_id)
|
||||
.bind(&metric_id)
|
||||
.bind(metric_name)
|
||||
.bind(metric_kind)
|
||||
.bind(scalar_int)
|
||||
.bind(scalar_float)
|
||||
.bind(timestamps)
|
||||
.bind(timeseries_int)
|
||||
.bind(timeseries_float)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
Ok(metric_id)
|
||||
}
|
||||
|
||||
pub async fn record_metric(
|
||||
db: &DB,
|
||||
workspace_id: String,
|
||||
job_id: Uuid,
|
||||
metric_id: String,
|
||||
value: MetricNumericValue,
|
||||
) -> error::Result<()> {
|
||||
let metric_kind_opt: Option<MetricKind> = sqlx::query_scalar(
|
||||
"SELECT metric_kind FROM job_stats WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
)
|
||||
.bind(&workspace_id)
|
||||
.bind(&job_id)
|
||||
.bind(&metric_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
if metric_kind_opt.is_none() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Metric {} not yet registered for job {}.",
|
||||
metric_id, job_id
|
||||
)));
|
||||
}
|
||||
let metric_kind = metric_kind_opt.unwrap();
|
||||
|
||||
let (value_int, value_float) = match value {
|
||||
MetricNumericValue::Integer(val) => {
|
||||
if metric_kind != MetricKind::TimeseriesInt && metric_kind != MetricKind::ScalarInt {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Metric {} is not a timeseries int metric.",
|
||||
metric_id
|
||||
)));
|
||||
}
|
||||
(val, 0 as f32)
|
||||
}
|
||||
MetricNumericValue::Float(val) => {
|
||||
if metric_kind != MetricKind::TimeseriesFloat && metric_kind != MetricKind::ScalarFloat
|
||||
{
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Metric {} is not a timeseries float metric.",
|
||||
metric_id
|
||||
)));
|
||||
}
|
||||
(0 as i32, val)
|
||||
}
|
||||
};
|
||||
|
||||
let timestamp = chrono::Utc::now();
|
||||
match metric_kind {
|
||||
MetricKind::ScalarInt => {
|
||||
sqlx::query!(
|
||||
"UPDATE job_stats SET scalar_int = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
&workspace_id,
|
||||
&job_id,
|
||||
&metric_id,
|
||||
value_int,
|
||||
).execute(db).await?;
|
||||
}
|
||||
MetricKind::ScalarFloat => {
|
||||
sqlx::query!(
|
||||
"UPDATE job_stats SET scalar_float = $4 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
&workspace_id,
|
||||
&job_id,
|
||||
&metric_id,
|
||||
value_float,
|
||||
).execute(db).await?;
|
||||
}
|
||||
MetricKind::TimeseriesInt => {
|
||||
sqlx::query!(
|
||||
"UPDATE job_stats SET timestamps = timestamps || $4, timeseries_int = timeseries_int || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
&workspace_id,
|
||||
&job_id,
|
||||
&metric_id,
|
||||
&[timestamp],
|
||||
&[value_int]
|
||||
).execute(db).await?;
|
||||
}
|
||||
MetricKind::TimeseriesFloat => {
|
||||
sqlx::query!(
|
||||
"UPDATE job_stats SET timestamps = timestamps || $4, timeseries_float = timeseries_float || $5 WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
|
||||
&workspace_id,
|
||||
&job_id,
|
||||
&metric_id,
|
||||
&[timestamp],
|
||||
&[value_float]
|
||||
).execute(db).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -283,17 +283,19 @@ pub enum JobPayload {
|
||||
Dependencies {
|
||||
path: String,
|
||||
hash: ScriptHash,
|
||||
dependencies: String,
|
||||
language: ScriptLang,
|
||||
dedicated_worker: Option<bool>,
|
||||
deployment_message: Option<String>,
|
||||
},
|
||||
FlowDependencies {
|
||||
path: String,
|
||||
dedicated_worker: Option<bool>,
|
||||
deployment_message: Option<String>,
|
||||
},
|
||||
AppDependencies {
|
||||
path: String,
|
||||
version: i64,
|
||||
deployment_message: Option<String>,
|
||||
},
|
||||
Flow {
|
||||
path: String,
|
||||
|
||||
@@ -23,9 +23,11 @@ pub mod external_ip;
|
||||
pub mod flow_status;
|
||||
pub mod flows;
|
||||
pub mod global_settings;
|
||||
pub mod job_metrics;
|
||||
pub mod jobs;
|
||||
pub mod more_serde;
|
||||
pub mod oauth2;
|
||||
pub mod s3_helpers;
|
||||
pub mod schedule;
|
||||
pub mod scripts;
|
||||
pub mod server;
|
||||
@@ -34,6 +36,7 @@ pub mod users;
|
||||
pub mod utils;
|
||||
pub mod variables;
|
||||
pub mod worker;
|
||||
pub mod workspaces;
|
||||
|
||||
#[cfg(feature = "tracing_init")]
|
||||
pub mod tracing_init;
|
||||
|
||||
96
backend/windmill-common/src/s3_helpers.rs
Normal file
96
backend/windmill-common/src/s3_helpers.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
use aws_sdk_s3::config::Credentials;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum LargeFileStorage {
|
||||
S3Storage(S3Storage),
|
||||
// TODO: Add a filesystem type here in the future if needed
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct S3Storage {
|
||||
pub s3_resource_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct S3Resource {
|
||||
#[serde(rename = "bucket")]
|
||||
pub bucket: String,
|
||||
pub region: String,
|
||||
#[serde(rename = "endPoint")]
|
||||
pub endpoint: String,
|
||||
#[serde(rename = "useSSL")]
|
||||
pub use_ssl: bool,
|
||||
#[serde(rename = "accessKey")]
|
||||
pub access_key: Option<String>,
|
||||
#[serde(rename = "secretKey")]
|
||||
pub secret_key: Option<String>,
|
||||
#[serde(rename = "pathStyle")]
|
||||
pub path_style: bool,
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct S3Object {
|
||||
pub s3: String,
|
||||
}
|
||||
|
||||
pub async fn get_etag_or_empty(s3_resource: &S3Resource, s3_object: S3Object) -> Option<String> {
|
||||
let s3_client = build_s3_client(s3_resource);
|
||||
|
||||
let s3_bucket = s3_resource.bucket.clone();
|
||||
let s3_key = s3_object.s3;
|
||||
let s3_object_metadata = s3_client
|
||||
.head_object()
|
||||
.bucket(s3_bucket)
|
||||
.key(s3_key)
|
||||
.send()
|
||||
.await;
|
||||
s3_object_metadata
|
||||
.ok()
|
||||
.map(|res| res.e_tag().map(|et| et.to_string()))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn render_endpoint(s3_resource: &S3Resource) -> String {
|
||||
let url_with_prefix = if s3_resource.endpoint.starts_with("http://")
|
||||
|| s3_resource.endpoint.starts_with("https://")
|
||||
{
|
||||
s3_resource.endpoint.clone()
|
||||
} else if s3_resource.use_ssl {
|
||||
format!("https://{}", s3_resource.endpoint)
|
||||
} else {
|
||||
format!("http://{}", s3_resource.endpoint)
|
||||
};
|
||||
if s3_resource.port.is_some() {
|
||||
format!("{}:{}", url_with_prefix, s3_resource.port.unwrap())
|
||||
} else {
|
||||
url_with_prefix
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
|
||||
let s3_resource = s3_resource_ref.clone();
|
||||
|
||||
let endpoint = render_endpoint(&s3_resource);
|
||||
let mut s3_config_builder = aws_sdk_s3::Config::builder()
|
||||
.endpoint_url(endpoint)
|
||||
.behavior_version(BehaviorVersion::latest())
|
||||
.region(Region::new(s3_resource.region));
|
||||
if s3_resource.access_key.is_some() {
|
||||
s3_config_builder = s3_config_builder.credentials_provider(Credentials::new(
|
||||
s3_resource.access_key.unwrap_or_default(),
|
||||
s3_resource.secret_key.unwrap_or_default(),
|
||||
None,
|
||||
None,
|
||||
"s3_storage",
|
||||
));
|
||||
}
|
||||
if s3_resource.path_style {
|
||||
s3_config_builder = s3_config_builder.force_path_style(true);
|
||||
}
|
||||
let s3_config = s3_config_builder.build();
|
||||
return aws_sdk_s3::Client::from_conf(s3_config);
|
||||
}
|
||||
@@ -37,6 +37,8 @@ pub struct Schedule {
|
||||
pub on_recovery_extra_args: Option<serde_json::Value>,
|
||||
pub ws_error_handler_muted: bool,
|
||||
pub retry: Option<serde_json::Value>,
|
||||
pub no_flow_overlap: bool,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
pub const SUPERADMIN_SECRET_EMAIL: &str = "superadmin_secret@windmill.dev";
|
||||
pub const SUPERADMIN_NOTIFICATION_EMAIL: &str = "superadmin_notification@windmill.dev";
|
||||
pub const SUPERADMIN_SYNC_EMAIL: &str = "superadmin_sync@windmill.dev";
|
||||
|
||||
pub fn username_to_permissioned_as(user: &str) -> String {
|
||||
if user.contains('@') {
|
||||
|
||||
8
backend/windmill-common/src/workspaces.rs
Normal file
8
backend/windmill-common/src/workspaces.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct WorkspaceGitRepo {
|
||||
pub script_path: String,
|
||||
pub git_repo_resource_path: String,
|
||||
pub use_individual_branch: Option<bool>,
|
||||
}
|
||||
22
backend/windmill-git-sync/Cargo.toml
Normal file
22
backend/windmill-git-sync/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "windmill-git-sync"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_git_sync"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
enterprise = ["windmill-queue/enterprise"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
windmill-common = { workspace = true, features = ["axum"] }
|
||||
windmill-queue.workspace = true
|
||||
rsmq_async.workspace = true
|
||||
@@ -8,17 +8,18 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::db::DB;
|
||||
use crate::workspaces::{WorkspaceGitRepo, WorkspaceSettings};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
|
||||
use windmill_common::workspaces::WorkspaceGitRepo;
|
||||
|
||||
use serde_json::json;
|
||||
use windmill_common::error::{Error, Result};
|
||||
use windmill_common::jobs::JobPayload;
|
||||
use windmill_common::scripts::ScriptHash;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_queue::PushIsolationLevel;
|
||||
|
||||
pub type DB = Pool<Postgres>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DeployedObject {
|
||||
Script { hash: ScriptHash, path: String },
|
||||
@@ -37,13 +38,14 @@ impl DeployedObject {
|
||||
}
|
||||
|
||||
pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
mut tx: PushIsolationLevel<'c, R>,
|
||||
authed: &ApiAuthed,
|
||||
email: &str,
|
||||
created_by: &str,
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
obj: DeployedObject,
|
||||
deployment_message: Option<String>,
|
||||
) -> Result<PushIsolationLevel<'c, R>> {
|
||||
rsmq: Option<R>,
|
||||
) -> Result<()> {
|
||||
let skip_git_sync = if obj.get_path().starts_with("u/") {
|
||||
tracing::debug!(
|
||||
"Ignoring {} from git sync as it's in a private user folder",
|
||||
@@ -54,10 +56,10 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
false
|
||||
};
|
||||
|
||||
let workspace_git_repo_setting = sqlx::query_as::<_, WorkspaceSettings>(
|
||||
"SELECT * FROM workspace_settings WHERE workspace_id = $1",
|
||||
let workspace_git_repo_setting = sqlx::query_scalar!(
|
||||
"SELECT git_sync FROM workspace_settings WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
@@ -69,11 +71,10 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
|
||||
let workspace_git_repo = workspace_git_repo_setting
|
||||
.unwrap()
|
||||
.git_sync
|
||||
.map(|conf| serde_json::from_value::<WorkspaceGitRepo>(conf).ok())
|
||||
.flatten();
|
||||
|
||||
let (git_sync_job_uuid, mut new_tx) = if !skip_git_sync && workspace_git_repo.is_some() {
|
||||
let git_sync_job_uuid = if !skip_git_sync && workspace_git_repo.is_some() {
|
||||
let workspace_git_repo = workspace_git_repo.unwrap();
|
||||
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
@@ -84,35 +85,56 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
.strip_prefix("$res:")),
|
||||
);
|
||||
|
||||
let default_commit_msg: String;
|
||||
match obj.clone() {
|
||||
let message = match obj.clone() {
|
||||
DeployedObject::Script { path, .. } => {
|
||||
args.insert("path".to_string(), json!(path.to_string()));
|
||||
default_commit_msg = format!("Script '{}' deployed", path);
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("Script '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
DeployedObject::Flow { path } => {
|
||||
args.insert("path".to_string(), json!(path.to_string()));
|
||||
default_commit_msg = format!("Flow '{}' deployed", path);
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("Flow '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
DeployedObject::App { path, .. } => {
|
||||
args.insert("path".to_string(), json!(path.to_string()));
|
||||
default_commit_msg = format!("App '{}' deployed", path);
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("App '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
args.insert("commit_msg".to_string(), json!(message));
|
||||
args.insert(
|
||||
"commit_msg".to_string(),
|
||||
json!(deployment_message.clone().unwrap_or(default_commit_msg)),
|
||||
"use_individual_branch".to_string(),
|
||||
json!(workspace_git_repo.use_individual_branch.unwrap_or(false)),
|
||||
);
|
||||
|
||||
let tx: PushIsolationLevel<'_, R> = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
|
||||
|
||||
let (job_uuid, new_tx) = windmill_queue::push(
|
||||
&db,
|
||||
db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::DeploymentCallback { path: workspace_git_repo.script_path.clone() },
|
||||
args,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
created_by,
|
||||
&email,
|
||||
SUPERADMIN_SYNC_EMAIL.to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -128,16 +150,10 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
(Some(job_uuid), new_tx)
|
||||
new_tx.commit().await?;
|
||||
Some(job_uuid)
|
||||
} else {
|
||||
let new_tx = match tx {
|
||||
PushIsolationLevel::Isolated(user_db, authed, rsmq) => {
|
||||
(rsmq, user_db.begin(&authed).await?).into()
|
||||
}
|
||||
PushIsolationLevel::IsolatedRoot(db, rsmq) => (rsmq, db.begin().await?).into(),
|
||||
PushIsolationLevel::Transaction(tx) => tx,
|
||||
};
|
||||
(None, new_tx)
|
||||
None
|
||||
};
|
||||
|
||||
// We're not persisting the default commit msg as it's pretty useless. We will persist the ones manually set by users
|
||||
@@ -149,28 +165,27 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
if deployment_message.is_some() || job_uuids.len() > 0 {
|
||||
// if the git sync job hasn't been triggered, and there is not custom deployment message, there's not point adding an entry to the table
|
||||
match obj.clone() {
|
||||
DeployedObject::Script { path, hash, .. } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id, path, hash.0, &job_uuids, deployment_message,
|
||||
)
|
||||
},
|
||||
DeployedObject::Flow { path } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path) WHERE script_hash IS NULL AND app_version IS NULL DO UPDATE SET callback_job_ids = $3, deployment_msg = $4",
|
||||
w_id, path, &job_uuids, deployment_message,
|
||||
)
|
||||
}
|
||||
DeployedObject::App { path, version } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id, path, version, &job_uuids, deployment_message,
|
||||
)
|
||||
}
|
||||
}.execute(&mut new_tx)
|
||||
.await?;
|
||||
DeployedObject::Script { path, hash, .. } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id, path, hash.0, &job_uuids, deployment_message,
|
||||
)
|
||||
},
|
||||
DeployedObject::Flow { path } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path) WHERE script_hash IS NULL AND app_version IS NULL DO UPDATE SET callback_job_ids = $3, deployment_msg = $4",
|
||||
w_id, path, &job_uuids, deployment_message,
|
||||
)
|
||||
}
|
||||
DeployedObject::App { path, version } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id, path, version, &job_uuids, deployment_message,
|
||||
)
|
||||
}
|
||||
}.execute(db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx = PushIsolationLevel::Transaction(new_tx);
|
||||
return Ok(tx);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1003,6 +1003,8 @@ pub async fn handle_maybe_scheduled_job<'c, R: rsmq_async::RsmqConnection + Clon
|
||||
on_recovery_extra_args: schedule.on_recovery_extra_args,
|
||||
ws_error_handler_muted: schedule.ws_error_handler_muted,
|
||||
retry: schedule.retry,
|
||||
summary: schedule.summary,
|
||||
no_flow_overlap: schedule.no_flow_overlap,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -2494,21 +2496,23 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
dedicated_worker,
|
||||
None,
|
||||
),
|
||||
JobPayload::Dependencies { hash, dependencies, language, path, dedicated_worker } => (
|
||||
Some(hash.0),
|
||||
Some(path),
|
||||
Some((dependencies, None)),
|
||||
JobKind::Dependencies,
|
||||
None,
|
||||
None,
|
||||
Some(language),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
dedicated_worker,
|
||||
None,
|
||||
),
|
||||
JobPayload::FlowDependencies { path, dedicated_worker } => {
|
||||
JobPayload::Dependencies { hash, language, path, dedicated_worker, deployment_message } => {
|
||||
(
|
||||
Some(hash.0),
|
||||
Some(path),
|
||||
Some((deployment_message.unwrap_or_else(String::new), None)),
|
||||
JobKind::Dependencies,
|
||||
None,
|
||||
None,
|
||||
Some(language),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
dedicated_worker,
|
||||
None,
|
||||
)
|
||||
}
|
||||
JobPayload::FlowDependencies { path, dedicated_worker, deployment_message } => {
|
||||
let value_json = fetch_scalar_isolated!(
|
||||
sqlx::query_scalar!(
|
||||
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
@@ -2526,7 +2530,7 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
(
|
||||
None,
|
||||
Some(path),
|
||||
None,
|
||||
Some((deployment_message.unwrap_or_else(String::new), None)),
|
||||
JobKind::FlowDependencies,
|
||||
Some(value.clone()),
|
||||
Some(FlowStatus::new(&value)), // this is a new flow being pushed, flow_status is set to flow_value
|
||||
@@ -2538,10 +2542,10 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
None,
|
||||
)
|
||||
}
|
||||
JobPayload::AppDependencies { path, version } => (
|
||||
JobPayload::AppDependencies { path, version, deployment_message } => (
|
||||
Some(version),
|
||||
Some(path),
|
||||
None,
|
||||
Some((deployment_message.unwrap_or_else(String::new), None)),
|
||||
JobKind::AppDependencies,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -10,7 +10,7 @@ path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
enterprise = ["windmill-queue/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"]
|
||||
benchmark = ["windmill-queue/benchmark"]
|
||||
flamegraph = []
|
||||
|
||||
@@ -31,6 +31,7 @@ windmill-parser-py-imports.workspace = true
|
||||
windmill-parser-bash.workspace = true
|
||||
windmill-parser-sql.workspace = true
|
||||
windmill-parser-graphql.workspace = true
|
||||
windmill-git-sync.workspace = true
|
||||
sqlx.workspace = true
|
||||
uuid.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -28,22 +28,23 @@ class WindmillFinder(MetaPathFinder):
|
||||
return ModuleSpec(name, WindmillLoader(name))
|
||||
elif l > 2:
|
||||
script_path = "/".join(splitted)
|
||||
import requests
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
headers = {"Authorization": f"Bearer {os.environ.get('WM_TOKEN')}"}
|
||||
url = f"{os.environ.get('BASE_INTERNAL_URL')}/api/w/{os.environ.get('WM_WORKSPACE')}/scripts/raw/p/{script_path}.py"
|
||||
|
||||
r = requests.get(
|
||||
url, headers={"Authorization": f"Bearer {os.environ.get('WM_TOKEN')}"}
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
folder = os.getcwd() + "/tmp/" + "/".join(splitted[:-1])
|
||||
fullpath = folder + "/" + splitted[-1] + ".py"
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
with open(fullpath, "w+") as f:
|
||||
f.write(r.text)
|
||||
return ModuleSpec(name, SourceFileLoader(name, fullpath))
|
||||
else:
|
||||
req = urllib.request.Request(url, None, headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
r = response.read().decode("utf-8")
|
||||
folder = os.getcwd() + "/tmp/" + "/".join(splitted[:-1])
|
||||
fullpath = folder + "/" + splitted[-1] + ".py"
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
with open(fullpath, "w+") as f:
|
||||
f.write(r)
|
||||
return ModuleSpec(name, SourceFileLoader(name, fullpath))
|
||||
except:
|
||||
# raise ImportError(f"Script {script_path} not found")
|
||||
return ModuleSpec(name, WindmillLoader(name))
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ pub async fn gen_lockfile(
|
||||
|
||||
// if custom NPM registry is being used, write bunfig.toml at the root of the job dir
|
||||
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
|
||||
let (raw_url, token_opt) = if s.contains(":_authToken=") {
|
||||
let (url, token_opt) = if s.contains(":_authToken=") {
|
||||
let split_url = s.split(":_authToken=").collect::<Vec<&str>>();
|
||||
let url = split_url
|
||||
.get(0)
|
||||
@@ -100,12 +100,6 @@ pub async fn gen_lockfile(
|
||||
} else {
|
||||
(s.to_owned(), None)
|
||||
};
|
||||
// somehow bun fails to resolve deps if the url does not end with a slash ...
|
||||
let url = if !raw_url.ends_with("/") {
|
||||
format!("{raw_url}/")
|
||||
} else {
|
||||
raw_url.to_string()
|
||||
};
|
||||
let registry_toml_string = if let Some(token) = token_opt {
|
||||
format!("{{ url = \"{url}\", token = \"{token}\" }}")
|
||||
} else {
|
||||
@@ -342,26 +336,26 @@ pub async fn handle_bun_job(
|
||||
let trusted_deps = get_trusted_deps(inner_content);
|
||||
let empty_trusted_deps = trusted_deps.len() == 0;
|
||||
let has_custom_config_registry = common_bun_proc_envs.contains_key("NPM_CONFIG_REGISTRY");
|
||||
if !*DISABLE_NSJAIL || !empty_trusted_deps || has_custom_config_registry {
|
||||
logs.push_str("\n\n--- BUN INSTALL ---\n");
|
||||
set_logs(&logs, &job.id, &db).await;
|
||||
let _ = gen_lockfile(
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
db,
|
||||
&client.get_token().await,
|
||||
&job.script_path(),
|
||||
job_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
false,
|
||||
trusted_deps,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
// if !*DISABLE_NSJAIL || !empty_trusted_deps || has_custom_config_registry {
|
||||
logs.push_str("\n\n--- BUN INSTALL ---\n");
|
||||
set_logs(&logs, &job.id, &db).await;
|
||||
let _ = gen_lockfile(
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
db,
|
||||
&client.get_token().await,
|
||||
&job.script_path(),
|
||||
job_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
false,
|
||||
trusted_deps,
|
||||
)
|
||||
.await?;
|
||||
// }
|
||||
|
||||
if empty_trusted_deps && !has_custom_config_registry {
|
||||
let node_modules_path = format!("{}/node_modules", job_dir);
|
||||
|
||||
@@ -3,12 +3,13 @@ use itertools::Itertools;
|
||||
use nix::sys::signal::{self, Signal};
|
||||
use nix::unistd::Pid;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio::process::Command;
|
||||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
use windmill_common::s3_helpers::{get_etag_or_empty, LargeFileStorage, S3Object, S3Resource};
|
||||
use windmill_common::worker::{CLOUD_HOSTED, WORKER_CONFIG};
|
||||
use windmill_common::{
|
||||
error::{self, Error},
|
||||
@@ -31,7 +32,7 @@ use std::{
|
||||
|
||||
use tracing::{trace_span, Instrument};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::variables;
|
||||
use windmill_common::{job_metrics, variables, DB};
|
||||
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
@@ -407,6 +408,7 @@ async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
|
||||
}
|
||||
-2
|
||||
} else {
|
||||
// rand::random::<i32>() % 100 // to remove - used to fake memory data on MacOS
|
||||
-3
|
||||
}
|
||||
}
|
||||
@@ -426,7 +428,7 @@ pub async fn handle_child(
|
||||
mut child: Child,
|
||||
nsjail: bool,
|
||||
worker_name: &str,
|
||||
_w_id: &str,
|
||||
w_id: &str,
|
||||
child_name: &str,
|
||||
custom_timeout: Option<i32>,
|
||||
sigterm: bool,
|
||||
@@ -464,6 +466,8 @@ pub async fn handle_child(
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let mut i = 0;
|
||||
let mut memory_metric_id: Result<String, Error> =
|
||||
Err(Error::NotFound("not yet initialized".to_string()));
|
||||
|
||||
loop {
|
||||
tokio::select!(
|
||||
@@ -484,7 +488,29 @@ pub async fn handle_child(
|
||||
if current_mem > *mem_peak {
|
||||
*mem_peak = current_mem
|
||||
}
|
||||
tracing::info!("{worker_name}/{job_id} in {_w_id} still running. mem: {current_mem}kB, peak mem: {mem_peak}kB");
|
||||
tracing::info!("{worker_name}/{job_id} in {w_id} still running. mem: {current_mem}kB, peak mem: {mem_peak}kB");
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
// tracking metric starting at i >= 2 b/c first point it useless and we don't want to track metric for super fast jobs
|
||||
if i == 2 {
|
||||
memory_metric_id = job_metrics::register_metric_for_job(
|
||||
&db,
|
||||
w_id.to_string(),
|
||||
job_id,
|
||||
"memory_kb".to_string(),
|
||||
job_metrics::MetricKind::TimeseriesInt,
|
||||
Some("Job Memory Footprint (kB)".to_string()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if let Ok(ref metric_id) = memory_metric_id {
|
||||
if let Err(err) = job_metrics::record_metric(&db, w_id.to_string(), job_id, metric_id.to_owned(), job_metrics::MetricNumericValue::Integer(current_mem)).await {
|
||||
tracing::error!("Unable to save memory stat for job {} in workspace {}. Error was: {:?}", job_id, w_id, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (canceled, canceled_by, canceled_reason) = sqlx::query_as::<_, (bool, Option<String>, Option<String>)>("UPDATE queue SET mem_peak = $1, last_ping = now() WHERE id = $2 RETURNING canceled, canceled_by, canceled_reason")
|
||||
.bind(*mem_peak)
|
||||
.bind(job_id)
|
||||
@@ -515,7 +541,7 @@ pub async fn handle_child(
|
||||
}
|
||||
|
||||
let (timeout_duration, timeout_warn_msg) =
|
||||
resolve_job_timeout(&db, _w_id, job_id, custom_timeout).await;
|
||||
resolve_job_timeout(&db, w_id, job_id, custom_timeout).await;
|
||||
if let Some(msg) = timeout_warn_msg {
|
||||
logs.push_str(msg.as_str());
|
||||
append_logs(job_id, msg.as_str(), db).await;
|
||||
@@ -841,13 +867,26 @@ fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize, remove_x00:
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_args(v: &Option<sqlx::types::Json<HashMap<String, Box<RawValue>>>>) -> String {
|
||||
pub async fn hash_args(
|
||||
db: &DB,
|
||||
client: &AuthedClient,
|
||||
workspace_id: &str,
|
||||
job_id: &Uuid,
|
||||
v: &Option<sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
|
||||
) -> String {
|
||||
if let Some(vs) = v {
|
||||
let mut dh = DefaultHasher::new();
|
||||
let hm = &vs.0;
|
||||
for k in hm.keys().sorted() {
|
||||
k.hash(&mut dh);
|
||||
hm.get(k).unwrap().get().hash(&mut dh);
|
||||
let arg_value = hm.get(k).unwrap();
|
||||
let arg_additions =
|
||||
arg_value_hash_additions(db, client, workspace_id, job_id, hm.get(k).unwrap())
|
||||
.await;
|
||||
arg_value.get().hash(&mut dh);
|
||||
for (_, arg_addition) in arg_additions {
|
||||
arg_addition.hash(&mut dh);
|
||||
}
|
||||
}
|
||||
hex::encode(dh.finish().to_be_bytes())
|
||||
} else {
|
||||
@@ -855,21 +894,152 @@ pub fn hash_args(v: &Option<sqlx::types::Json<HashMap<String, Box<RawValue>>>>)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StoreCachedResource<'a> {
|
||||
expire: i64,
|
||||
value: &'a RawValue,
|
||||
async fn get_workspace_s3_resource_path(
|
||||
db: &DB,
|
||||
client: &AuthedClient,
|
||||
workspace_id: &str,
|
||||
job_id: &Uuid,
|
||||
) -> Option<S3Resource> {
|
||||
let raw_lfs_opt = sqlx::query_scalar!(
|
||||
"SELECT large_file_storage FROM workspace_settings WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|val| serde_json::from_value::<LargeFileStorage>(val).ok())
|
||||
.flatten();
|
||||
|
||||
if let Some(LargeFileStorage::S3Storage(s3_storage)) = raw_lfs_opt {
|
||||
let resource_path = s3_storage.s3_resource_path.trim_start_matches("$res:");
|
||||
client
|
||||
.get_resource_value_interpolated::<S3Resource>(&resource_path, Some(job_id.to_string()))
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_in_cache<'a>(
|
||||
async fn arg_value_hash_additions(
|
||||
db: &DB,
|
||||
client: &AuthedClient,
|
||||
workspace_id: &str,
|
||||
job_id: &Uuid,
|
||||
raw_value: &Box<RawValue>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut result: HashMap<String, String> = HashMap::new();
|
||||
let parsed_s3_values: &mut Vec<S3Object> = &mut Vec::new();
|
||||
extract_all_s3_object_from_raw_value(raw_value, parsed_s3_values);
|
||||
if parsed_s3_values.is_empty() {
|
||||
// no s3 object, nothing to return
|
||||
return result;
|
||||
}
|
||||
|
||||
let s3_resource_opt = get_workspace_s3_resource_path(db, client, workspace_id, job_id).await;
|
||||
if let Some(s3_resource) = s3_resource_opt {
|
||||
for s3_object in parsed_s3_values {
|
||||
let etag = get_etag_or_empty(&s3_resource, s3_object.clone()).await;
|
||||
tracing::warn!("Enriching s3 arg value with etag: {:?}", etag);
|
||||
result.insert(s3_object.s3.clone(), etag.unwrap_or_default()); // TODO: maybe inject a random value to invalidate the cache?
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn extract_all_s3_object_from_raw_value(raw_value: &Box<RawValue>, result: &mut Vec<S3Object>) {
|
||||
let parsed_value = serde_json::from_str::<S3Object>(raw_value.get());
|
||||
if let Ok(parsed_value) = parsed_value {
|
||||
result.push(parsed_value);
|
||||
} else {
|
||||
let parsed_value = serde_json::from_str::<HashMap<String, Box<RawValue>>>(raw_value.get());
|
||||
if let Ok(parsed_value) = parsed_value {
|
||||
for (_, v) in parsed_value {
|
||||
extract_all_s3_object_from_raw_value(&v, result);
|
||||
}
|
||||
} else {
|
||||
let parsed_value = serde_json::from_str::<Vec<Box<RawValue>>>(raw_value.get());
|
||||
if let Ok(parsed_value) = parsed_value {
|
||||
for v in parsed_value {
|
||||
extract_all_s3_object_from_raw_value(&v, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct CachedResource {
|
||||
expire: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
s3_etags: Option<HashMap<String, String>>,
|
||||
value: Box<RawValue>,
|
||||
}
|
||||
|
||||
pub async fn get_cached_resource_value_if_valid(
|
||||
db: &DB,
|
||||
client: &AuthedClient,
|
||||
job_id: &Uuid,
|
||||
workspace_id: &str,
|
||||
cached_res_path: &str,
|
||||
) -> Option<Box<RawValue>> {
|
||||
let resource_opt = client
|
||||
.get_resource_value::<CachedResource>(cached_res_path)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(cached_resource) = resource_opt {
|
||||
if cached_resource.expire <= chrono::Utc::now().timestamp() {
|
||||
// cache expired
|
||||
return None;
|
||||
}
|
||||
let s3_etags = cached_resource.s3_etags.unwrap_or_default();
|
||||
let s3_resource_opt: Option<S3Resource> = if s3_etags.is_empty() {
|
||||
None
|
||||
} else {
|
||||
get_workspace_s3_resource_path(db, &client, workspace_id, job_id).await
|
||||
};
|
||||
if !s3_etags.is_empty() && s3_resource_opt.is_none() {
|
||||
tracing::warn!("Cached result references s3 files that are not retrievable anymore because the workspace S3 resource can't be fetched. Cache will be invalidated");
|
||||
return None;
|
||||
}
|
||||
for (s3_file_key, s3_file_etag) in s3_etags {
|
||||
if let Some(s3_resource) = s3_resource_opt.clone() {
|
||||
let etag =
|
||||
get_etag_or_empty(&s3_resource, S3Object { s3: s3_file_key.clone() }).await;
|
||||
if etag.is_none() || etag.clone().unwrap() != s3_file_etag {
|
||||
tracing::warn!("S3 file etag for '{}' has changed. Value from cache is {:?} while current value from S3 is {:?}. Cache will be invalidated", s3_file_key.clone(), s3_file_etag, etag);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(cached_resource.value);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub async fn save_in_cache(
|
||||
db: &Pool<Postgres>,
|
||||
client: &AuthedClient,
|
||||
job: &QueuedJob,
|
||||
cached_path: String,
|
||||
r: &'a RawValue,
|
||||
r: &Box<RawValue>,
|
||||
) {
|
||||
let expire = chrono::Utc::now().timestamp() + job.cache_ttl.unwrap() as i64;
|
||||
|
||||
let store_cache_resource = StoreCachedResource { expire, value: r };
|
||||
let s3_etags =
|
||||
arg_value_hash_additions(db, client, job.workspace_id.as_str(), &job.id, r).await;
|
||||
|
||||
let store_cache_resource = CachedResource {
|
||||
expire,
|
||||
s3_etags: if s3_etags.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s3_etags)
|
||||
},
|
||||
value: r.clone(),
|
||||
};
|
||||
let raw_json = sqlx::types::Json(store_cache_resource);
|
||||
|
||||
if let Err(e) = sqlx::query!(
|
||||
@@ -879,7 +1049,7 @@ pub async fn save_in_cache<'a>(
|
||||
DO UPDATE SET value = $3",
|
||||
job.workspace_id,
|
||||
cached_path,
|
||||
raw_json as sqlx::types::Json<StoreCachedResource>,
|
||||
raw_json as sqlx::types::Json<CachedResource>,
|
||||
"cache"
|
||||
)
|
||||
.execute(db)
|
||||
|
||||
@@ -322,7 +322,13 @@ fn convert_val(value: &Value, arg_t: &String) -> windmill_common::error::Result<
|
||||
Value::Number(n) if n.is_i64() && (arg_t == "smallint" || arg_t == "smallserial") => {
|
||||
Ok(PgType::I16(n.as_i64().unwrap() as i16))
|
||||
}
|
||||
Value::Number(n) if n.is_i64() && (arg_t == "int" || arg_t == "serial") => {
|
||||
Value::Number(n)
|
||||
if n.is_i64()
|
||||
&& (arg_t == "int"
|
||||
|| arg_t == "integer"
|
||||
|| arg_t == "int4"
|
||||
|| arg_t == "serial") =>
|
||||
{
|
||||
Ok(PgType::I32(n.as_i64().unwrap() as i32))
|
||||
}
|
||||
Value::Number(n) if n.is_i64() && (arg_t == "numeric" || arg_t == "decimal") => Ok(
|
||||
@@ -429,7 +435,6 @@ pub fn pg_cell_to_json_value(
|
||||
Type::TS_VECTOR => get_basic(row, column, column_i, |a: StringCollector| {
|
||||
Ok(JSONValue::String(a.0))
|
||||
})?,
|
||||
|
||||
// array types
|
||||
Type::BOOL_ARRAY => get_array(row, column, column_i, |a: bool| Ok(JSONValue::Bool(a)))?,
|
||||
Type::INT2_ARRAY => get_array(row, column, column_i, |a: i16| {
|
||||
|
||||
@@ -83,14 +83,24 @@ pub async fn pip_compile(
|
||||
WORKER_CONFIG.read().await.pip_local_dependencies.as_ref()
|
||||
{
|
||||
let deps = pip_local_dependencies.clone();
|
||||
let compiled_deps = deps.iter().map(|dep| {
|
||||
let compiled_dep = Regex::new(dep);
|
||||
match compiled_dep {
|
||||
Ok(compiled_dep) => Some(compiled_dep),
|
||||
Err(e) => {
|
||||
tracing::warn!("regex compilation failed for Python local dependency: '{}' - it will be ignored", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}).filter(|dep_maybe| dep_maybe.is_some()).map(|dep| dep.unwrap()).collect::<Vec<Regex>>();
|
||||
requirements
|
||||
.lines()
|
||||
.filter(|s| {
|
||||
if !deps.contains(&s.to_string()) {
|
||||
return true;
|
||||
} else {
|
||||
if compiled_deps.iter().any(|dep| dep.is_match(s)) {
|
||||
logs.push_str(&format!("\nignoring local dependency: {}", s));
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
@@ -566,7 +576,7 @@ async fn handle_python_deps(
|
||||
};
|
||||
|
||||
if requirements.len() > 0 {
|
||||
additional_python_paths = handle_python_reqs(
|
||||
let mut venv_path = handle_python_reqs(
|
||||
requirements
|
||||
.split("\n")
|
||||
.filter(|x| !x.starts_with("--"))
|
||||
@@ -582,6 +592,7 @@ async fn handle_python_deps(
|
||||
worker_dir,
|
||||
)
|
||||
.await?;
|
||||
additional_python_paths.append(&mut venv_path);
|
||||
}
|
||||
Ok(additional_python_paths)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ use std::{
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_parser_py_imports::parse_relative_imports;
|
||||
|
||||
use uuid::Uuid;
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -33,9 +35,9 @@ use windmill_common::{
|
||||
error::{self, to_anyhow, Error},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue},
|
||||
get_latest_deployed_hash_for_path,
|
||||
jobs::{JobKind, QueuedJob},
|
||||
jobs::{JobKind, JobPayload, QueuedJob},
|
||||
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
|
||||
utils::{rd_string, StripPath},
|
||||
worker::{
|
||||
to_raw_value, to_raw_value_owned, update_ping, CLOUD_HOSTED, WORKER_CONFIG, WORKER_GROUP,
|
||||
@@ -76,7 +78,10 @@ use windmill_queue::{add_completed_job, add_completed_job_error};
|
||||
use crate::{
|
||||
bash_executor::{handle_bash_job, handle_powershell_job, ANSI_ESCAPE_RE},
|
||||
bun_executor::{gen_lockfile, get_trusted_deps, handle_bun_job},
|
||||
common::{build_args_map, hash_args, read_result, save_in_cache, write_file},
|
||||
common::{
|
||||
build_args_map, get_cached_resource_value_if_valid, hash_args, read_result, save_in_cache,
|
||||
write_file,
|
||||
},
|
||||
deno_executor::{generate_deno_lock, handle_deno_job},
|
||||
go_executor::{handle_go_job, install_go_dependencies},
|
||||
graphql_executor::do_graphql,
|
||||
@@ -146,7 +151,8 @@ pub async fn create_token_for_owner(
|
||||
.await?
|
||||
.unwrap_or(false)
|
||||
|| email == SUPERADMIN_SECRET_EMAIL
|
||||
|| email == SUPERADMIN_NOTIFICATION_EMAIL;
|
||||
|| email == SUPERADMIN_NOTIFICATION_EMAIL
|
||||
|| owner == SUPERADMIN_SYNC_EMAIL;
|
||||
|
||||
sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
@@ -469,6 +475,7 @@ async fn handle_receive_completed_job<
|
||||
worker_name: &str,
|
||||
worker_save_completed_job_duration: Option<Arc<prometheus::Histogram>>,
|
||||
worker_flow_transition_duration: Option<Arc<prometheus::Histogram>>,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) {
|
||||
let token = jc.token.clone();
|
||||
let workspace = jc.job.workspace_id.clone();
|
||||
@@ -491,6 +498,7 @@ async fn handle_receive_completed_job<
|
||||
worker_name,
|
||||
worker_save_completed_job_duration,
|
||||
worker_flow_transition_duration,
|
||||
job_completed_tx.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -506,6 +514,7 @@ async fn handle_receive_completed_job<
|
||||
&worker_dir,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -513,7 +522,7 @@ async fn handle_receive_completed_job<
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobCompletedSender(
|
||||
Sender<JobCompleted>,
|
||||
Sender<SendResult>,
|
||||
Option<Arc<GenericGauge<AtomicI64>>>,
|
||||
Option<Arc<prometheus::Histogram>>,
|
||||
);
|
||||
@@ -522,12 +531,12 @@ impl JobCompletedSender {
|
||||
pub async fn send(
|
||||
&self,
|
||||
jc: JobCompleted,
|
||||
) -> Result<(), tokio::sync::mpsc::error::SendError<JobCompleted>> {
|
||||
) -> Result<(), tokio::sync::mpsc::error::SendError<SendResult>> {
|
||||
if let Some(wj) = self.1.as_ref() {
|
||||
wj.inc()
|
||||
}
|
||||
let timer = self.2.as_ref().map(|x| x.start_timer());
|
||||
let r = self.0.send(jc).await;
|
||||
let r = self.0.send(SendResult::JobCompleted(jc)).await;
|
||||
timer.map(|x| x.stop_and_record());
|
||||
r
|
||||
}
|
||||
@@ -873,7 +882,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
|
||||
let (same_worker_tx, mut same_worker_rx) = mpsc::channel::<Uuid>(5);
|
||||
|
||||
let (job_completed_tx, mut job_completed_rx) = mpsc::channel::<JobCompleted>(3);
|
||||
let (job_completed_tx, mut job_completed_rx) = mpsc::channel::<SendResult>(3);
|
||||
|
||||
let job_completed_tx = JobCompletedSender(
|
||||
job_completed_tx,
|
||||
@@ -961,119 +970,167 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
|
||||
let worker_name2 = worker_name.clone();
|
||||
let killpill_tx2 = killpill_tx.clone();
|
||||
let job_completed_sender = job_completed_tx.0.clone();
|
||||
let send_result = tokio::spawn(async move {
|
||||
while let Some(jc) = job_completed_rx.recv().await {
|
||||
if let Some(wj) = worker_job_completed_channel_queue2.as_ref() {
|
||||
wj.dec();
|
||||
}
|
||||
let base_internal_url2 = base_internal_url2.clone();
|
||||
let worker_dir2 = worker_dir2.clone();
|
||||
let db2 = db2.clone();
|
||||
let same_worker_tx2 = same_worker_tx2.clone();
|
||||
let rsmq2 = rsmq2.clone();
|
||||
let worker_name = worker_name2.clone();
|
||||
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
|
||||
thread_count.fetch_add(1, Ordering::SeqCst);
|
||||
let thread_count = thread_count.clone();
|
||||
|
||||
loop {
|
||||
if thread_count.load(Ordering::Relaxed) < 4 {
|
||||
break;
|
||||
while let Some(sr) = job_completed_rx.recv().await {
|
||||
match sr {
|
||||
SendResult::JobCompleted(jc) => {
|
||||
if let Some(wj) = worker_job_completed_channel_queue2.as_ref() {
|
||||
wj.dec();
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(3)).await;
|
||||
}
|
||||
let base_internal_url2 = base_internal_url2.clone();
|
||||
let worker_dir2 = worker_dir2.clone();
|
||||
let db2 = db2.clone();
|
||||
let same_worker_tx2 = same_worker_tx2.clone();
|
||||
let rsmq2 = rsmq2.clone();
|
||||
let worker_name = worker_name2.clone();
|
||||
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
|
||||
thread_count.fetch_add(1, Ordering::SeqCst);
|
||||
let thread_count = thread_count.clone();
|
||||
|
||||
#[cfg(feature = "benchmark")]
|
||||
let send_duration = send_duration2.clone();
|
||||
#[cfg(feature = "benchmark")]
|
||||
let process_duration = process_duration.clone();
|
||||
#[cfg(feature = "benchmark")]
|
||||
let completed_jobs = completed_jobs.clone();
|
||||
#[cfg(feature = "benchmark")]
|
||||
let main_duration = main_duration2.clone();
|
||||
|
||||
let worker_save_completed_job_duration2 =
|
||||
worker_save_completed_job_duration.clone();
|
||||
let worker_flow_transition_duration2 = worker_flow_transition_duration.clone();
|
||||
let killpill_tx = killpill_tx2.clone();
|
||||
tokio::spawn(async move {
|
||||
#[cfg(feature = "benchmark")]
|
||||
let process_start = Instant::now();
|
||||
|
||||
let is_dependency_job = matches!(
|
||||
jc.job.job_kind,
|
||||
JobKind::Dependencies | JobKind::FlowDependencies
|
||||
);
|
||||
handle_receive_completed_job(
|
||||
jc,
|
||||
base_internal_url2,
|
||||
db2.clone(),
|
||||
worker_dir2,
|
||||
same_worker_tx2,
|
||||
rsmq2,
|
||||
&worker_name,
|
||||
worker_save_completed_job_duration2.clone(),
|
||||
worker_flow_transition_duration2.clone(),
|
||||
)
|
||||
.await;
|
||||
#[cfg(feature = "benchmark")]
|
||||
{
|
||||
let n = completed_jobs.fetch_add(1, Ordering::SeqCst);
|
||||
if (n + 1) % 1000 == 0 || n == (jobs - 1) as usize {
|
||||
let duration_s = start.elapsed().as_secs_f64();
|
||||
let jobs_per_sec = n as f64 / duration_s;
|
||||
tracing::info!(
|
||||
"completed {} jobs in {}s, {} jobs/s",
|
||||
n + 1,
|
||||
duration_s,
|
||||
jobs_per_sec
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"main loop without send {}s",
|
||||
main_duration.load(Ordering::SeqCst) as f64 / 1000.0
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"send job completed / send dedicated job duration {}s",
|
||||
send_duration.load(Ordering::SeqCst) as f64 / 1000.0
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"job completed process duration {}s",
|
||||
process_duration.load(Ordering::SeqCst) as f64 / 1000.0
|
||||
);
|
||||
loop {
|
||||
if thread_count.load(Ordering::Relaxed) < 4 {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(3)).await;
|
||||
}
|
||||
|
||||
process_duration.fetch_add(
|
||||
process_start.elapsed().as_millis() as usize,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "benchmark")]
|
||||
let send_duration = send_duration2.clone();
|
||||
#[cfg(feature = "benchmark")]
|
||||
let process_duration = process_duration.clone();
|
||||
#[cfg(feature = "benchmark")]
|
||||
let completed_jobs = completed_jobs.clone();
|
||||
#[cfg(feature = "benchmark")]
|
||||
let main_duration = main_duration2.clone();
|
||||
|
||||
thread_count.fetch_sub(1, Ordering::SeqCst);
|
||||
if is_dependency_job && is_dedicated_worker {
|
||||
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
|
||||
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
|
||||
let worker_save_completed_job_duration2 =
|
||||
worker_save_completed_job_duration.clone();
|
||||
let worker_flow_transition_duration2 =
|
||||
worker_flow_transition_duration.clone();
|
||||
let killpill_tx = killpill_tx2.clone();
|
||||
let job_completed_sender = job_completed_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
#[cfg(feature = "benchmark")]
|
||||
let process_start = Instant::now();
|
||||
|
||||
let is_dependency_job = matches!(
|
||||
jc.job.job_kind,
|
||||
JobKind::Dependencies | JobKind::FlowDependencies
|
||||
);
|
||||
handle_receive_completed_job(
|
||||
jc,
|
||||
base_internal_url2,
|
||||
db2.clone(),
|
||||
worker_dir2,
|
||||
same_worker_tx2,
|
||||
rsmq2,
|
||||
&worker_name,
|
||||
worker_save_completed_job_duration2.clone(),
|
||||
worker_flow_transition_duration2.clone(),
|
||||
job_completed_sender.clone(),
|
||||
)
|
||||
.await;
|
||||
#[cfg(feature = "benchmark")]
|
||||
{
|
||||
let n = completed_jobs.fetch_add(1, Ordering::SeqCst);
|
||||
if (n + 1) % 1000 == 0 || n == (jobs - 1) as usize {
|
||||
let duration_s = start.elapsed().as_secs_f64();
|
||||
let jobs_per_sec = n as f64 / duration_s;
|
||||
tracing::info!(
|
||||
"completed {} jobs in {}s, {} jobs/s",
|
||||
n + 1,
|
||||
duration_s,
|
||||
jobs_per_sec
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"main loop without send {}s",
|
||||
main_duration.load(Ordering::SeqCst) as f64 / 1000.0
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"send job completed / send dedicated job duration {}s",
|
||||
send_duration.load(Ordering::SeqCst) as f64 / 1000.0
|
||||
);
|
||||
|
||||
tracing::info!(
|
||||
"job completed process duration {}s",
|
||||
process_duration.load(Ordering::SeqCst) as f64 / 1000.0
|
||||
);
|
||||
}
|
||||
|
||||
process_duration.fetch_add(
|
||||
process_start.elapsed().as_millis() as usize,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
|
||||
thread_count.fetch_sub(1, Ordering::SeqCst);
|
||||
if is_dependency_job && is_dedicated_worker {
|
||||
tracing::error!("Dedicated worker executed a dependency job, a new script has been deployed. Exiting expecting to be restarted.");
|
||||
sqlx::query!("UPDATE config SET config = config WHERE name = $1", format!("worker__{}", *WORKER_GROUP))
|
||||
.execute(&db2)
|
||||
.await
|
||||
.expect("update config to trigger restart of all dedicated workers at that config");
|
||||
killpill_tx.send(()).unwrap_or_default();
|
||||
killpill_tx.send(()).unwrap_or_default();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handle_receive_completed_job(
|
||||
jc,
|
||||
base_internal_url2,
|
||||
db2,
|
||||
worker_dir2,
|
||||
same_worker_tx2,
|
||||
rsmq2,
|
||||
&worker_name,
|
||||
worker_save_completed_job_duration2.clone(),
|
||||
worker_flow_transition_duration2.clone(),
|
||||
job_completed_sender.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handle_receive_completed_job(
|
||||
jc,
|
||||
base_internal_url2,
|
||||
db2,
|
||||
worker_dir2,
|
||||
same_worker_tx2,
|
||||
rsmq2,
|
||||
&worker_name,
|
||||
worker_save_completed_job_duration2.clone(),
|
||||
worker_flow_transition_duration2.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
SendResult::UpdateFlow {
|
||||
flow,
|
||||
w_id,
|
||||
success,
|
||||
result,
|
||||
worker_dir,
|
||||
stop_early_override,
|
||||
token,
|
||||
} => {
|
||||
// let r;
|
||||
if let Err(e) = update_flow_status_after_job_completion(
|
||||
&db2,
|
||||
&AuthedClient {
|
||||
base_internal_url: base_internal_url2.to_string(),
|
||||
workspace: w_id.clone(),
|
||||
token: token.clone(),
|
||||
force_client: None,
|
||||
},
|
||||
flow,
|
||||
&Uuid::nil(),
|
||||
&w_id,
|
||||
success,
|
||||
&result,
|
||||
true,
|
||||
same_worker_tx2.clone(),
|
||||
&worker_dir,
|
||||
stop_early_override,
|
||||
rsmq2.clone(),
|
||||
&worker_name2,
|
||||
job_completed_sender.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error updating flow status after job completion: {e}");
|
||||
}
|
||||
}
|
||||
SendResult::Kill => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1330,6 +1387,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
}
|
||||
}
|
||||
println!("received killpill for worker {}", i_worker);
|
||||
job_completed_tx.0.send(SendResult::Kill).await.unwrap();
|
||||
break
|
||||
},
|
||||
_ = copy_to_bucket_rx.recv() => {
|
||||
@@ -1597,6 +1655,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
&worker_dir,
|
||||
rsmq.clone(),
|
||||
&worker_name,
|
||||
(&job_completed_tx.0).clone(),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
@@ -2080,11 +2139,12 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
worker_name: &str,
|
||||
worker_save_completed_job_duration: Option<Arc<prometheus::Histogram>>,
|
||||
worker_flow_transition_duration: Option<Arc<prometheus::Histogram>>,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
if success {
|
||||
// println!("bef completed job{:?}", SystemTime::now());
|
||||
if let Some(cached_path) = cached_res_path {
|
||||
save_in_cache(db, &job, cached_path.to_string(), &result).await;
|
||||
save_in_cache(db, client, &job, cached_path.to_string(), &result).await;
|
||||
}
|
||||
|
||||
let timer = worker_save_completed_job_duration
|
||||
@@ -2123,6 +2183,7 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
None,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await?;
|
||||
timer.map(|x| x.stop_and_record());
|
||||
@@ -2158,6 +2219,7 @@ pub async fn process_completed_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
None,
|
||||
rsmq,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -2213,6 +2275,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
|
||||
worker_dir: &str,
|
||||
rsmq: Option<R>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) {
|
||||
let err = match err {
|
||||
Error::JsonErr(err) => err,
|
||||
@@ -2260,6 +2323,7 @@ pub async fn handle_job_error<R: rsmq_async::RsmqConnection + Send + Sync + Clon
|
||||
None,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2302,6 +2366,34 @@ fn extract_error_value(log_lines: &str, i: i32) -> Box<RawValue> {
|
||||
);
|
||||
}
|
||||
|
||||
pub enum SendResult {
|
||||
JobCompleted(JobCompleted),
|
||||
UpdateFlow {
|
||||
flow: Uuid,
|
||||
w_id: String,
|
||||
success: bool,
|
||||
result: Box<RawValue>,
|
||||
worker_dir: String,
|
||||
stop_early_override: Option<bool>,
|
||||
token: String,
|
||||
},
|
||||
Kill,
|
||||
}
|
||||
|
||||
// db: &DB,
|
||||
// client: &AuthedClient,
|
||||
// flow: uuid::Uuid,
|
||||
// job_id_for_status: &Uuid,
|
||||
// w_id: &str,
|
||||
// success: bool,
|
||||
// result: &'a RawValue,
|
||||
// unrecoverable: bool,
|
||||
// same_worker_tx: Sender<Uuid>,
|
||||
// worker_dir: &str,
|
||||
// stop_early_override: Option<bool>,
|
||||
// rsmq: Option<R>,
|
||||
// worker_name: &str,
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JobCompleted {
|
||||
pub job: Arc<QueuedJob>,
|
||||
@@ -2332,12 +2424,6 @@ async fn do_nativets(
|
||||
Ok((result.0, [logs, result.1].join("\n\n")))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CachedResource {
|
||||
expire: i64,
|
||||
value: Box<RawValue>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
pub struct PreviousResult<'a> {
|
||||
#[serde(borrow)]
|
||||
@@ -2399,7 +2485,14 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
} else {
|
||||
"none".to_string()
|
||||
};
|
||||
let args_hash = hash_args(&job.args);
|
||||
let args_hash = hash_args(
|
||||
db,
|
||||
&client.get_authed().await,
|
||||
&job.workspace_id,
|
||||
&job.id,
|
||||
&job.args,
|
||||
)
|
||||
.await;
|
||||
if job.is_flow_step {
|
||||
let flow_path = sqlx::query_scalar!(
|
||||
"SELECT script_path FROM queue WHERE id = $1",
|
||||
@@ -2424,33 +2517,32 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
|
||||
if let Some(cached_res_path) = cached_res_path.clone() {
|
||||
let authed_client = client.get_authed().await;
|
||||
let resource = authed_client
|
||||
.get_resource_value::<CachedResource>(&cached_res_path)
|
||||
.await;
|
||||
|
||||
if let Ok(resource) = resource {
|
||||
let expire = resource.expire;
|
||||
if expire > chrono::Utc::now().timestamp() {
|
||||
let result = resource.value;
|
||||
let logs =
|
||||
"Job skipped because args & path found in cache and not expired".to_string();
|
||||
let cached_resource_value_maybe = get_cached_resource_value_if_valid(
|
||||
db,
|
||||
&authed_client,
|
||||
&job.id,
|
||||
&job.workspace_id,
|
||||
&cached_res_path,
|
||||
)
|
||||
.await;
|
||||
if let Some(cached_resource_value) = cached_resource_value_maybe {
|
||||
let logs = "Job skipped because args & path found in cache and not expired".to_string();
|
||||
job_completed_tx
|
||||
.send(JobCompleted {
|
||||
job: job,
|
||||
result: cached_resource_value,
|
||||
logs,
|
||||
mem_peak: 0,
|
||||
canceled_by: None,
|
||||
success: true,
|
||||
cached_res_path: None,
|
||||
token: authed_client.token,
|
||||
})
|
||||
.await
|
||||
.expect("send job completed");
|
||||
|
||||
job_completed_tx
|
||||
.send(JobCompleted {
|
||||
job: job,
|
||||
result,
|
||||
logs,
|
||||
mem_peak: 0,
|
||||
canceled_by: None,
|
||||
success: true,
|
||||
cached_res_path: None,
|
||||
token: authed_client.token,
|
||||
})
|
||||
.await
|
||||
.expect("send job completed");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
match job.job_kind {
|
||||
@@ -2464,7 +2556,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
rsmq,
|
||||
worker_name,
|
||||
job_completed_tx.0.clone(),
|
||||
)
|
||||
.await?;
|
||||
timer.map(|x| x.stop_and_record());
|
||||
@@ -2512,6 +2604,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
&client.get_token().await,
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2526,6 +2619,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
&client.get_token().await,
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await
|
||||
.map(|()| serde_json::from_str("{}").unwrap()),
|
||||
@@ -2540,6 +2634,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
&client.get_token().await,
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await
|
||||
.map(|()| serde_json::from_str("{}").unwrap()),
|
||||
@@ -3041,7 +3136,7 @@ mount {{
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_dependency_job(
|
||||
async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
job: &QueuedJob,
|
||||
logs: &mut String,
|
||||
mem_peak: &mut i32,
|
||||
@@ -3052,7 +3147,17 @@ async fn handle_dependency_job(
|
||||
worker_dir: &str,
|
||||
base_internal_url: &str,
|
||||
token: &str,
|
||||
rsmq: Option<R>,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let raw_code = sqlx::query_scalar!(
|
||||
"SELECT content FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
&job.script_hash.unwrap_or(ScriptHash(0)).0,
|
||||
&job.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or_else(|| "No script found at this hash".to_string());
|
||||
let script_path = job.script_path();
|
||||
let content = capture_dependency_job(
|
||||
&job.id,
|
||||
job.language.as_ref().map(|v| Ok(v)).unwrap_or_else(|| {
|
||||
@@ -3060,10 +3165,7 @@ async fn handle_dependency_job(
|
||||
"Job Language required for dependency jobs".to_owned(),
|
||||
))
|
||||
})?,
|
||||
job.raw_code
|
||||
.as_ref()
|
||||
.map(|a| a.as_str())
|
||||
.unwrap_or_else(|| "no raw code"),
|
||||
&raw_code,
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
@@ -3074,19 +3176,83 @@ async fn handle_dependency_job(
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
token,
|
||||
job.script_path(),
|
||||
script_path,
|
||||
)
|
||||
.await;
|
||||
match content {
|
||||
Ok(content) => {
|
||||
let hash = job.script_hash.unwrap_or(ScriptHash(0));
|
||||
let w_id = &job.workspace_id;
|
||||
sqlx::query!(
|
||||
"UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
|
||||
&content,
|
||||
&job.script_hash.unwrap_or(ScriptHash(0)).0,
|
||||
&job.workspace_id
|
||||
&hash.0,
|
||||
w_id
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
let deployment_message = job.raw_code.as_ref().map(|x| x.clone());
|
||||
|
||||
if let Err(e) = handle_deployment_metadata(
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script { hash, path: script_path.to_string() },
|
||||
deployment_message.clone(),
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%e, "error handling deployment metadata");
|
||||
}
|
||||
|
||||
if &job.language == &Some(ScriptLang::Python3) {
|
||||
if let Ok(relative_imports) = parse_relative_imports(&raw_code, script_path) {
|
||||
logs.push_str("\n--- RELATIVE IMPORTS ---\n\n");
|
||||
logs.push_str(&relative_imports.join("\n"));
|
||||
if !relative_imports.is_empty() {
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM dependency_map
|
||||
WHERE importer_path = $1 AND importer_kind = 'script'
|
||||
AND workspace_id = $2",
|
||||
script_path,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
for import in relative_imports {
|
||||
sqlx::query!(
|
||||
"INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path)
|
||||
VALUES ($1, $2, 'script', $3)",
|
||||
w_id,
|
||||
script_path,
|
||||
import
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
logs.push_str(&format!("{}\n", import));
|
||||
}
|
||||
tx.commit().await?;
|
||||
}
|
||||
if let Err(e) = trigger_python_dependents_to_recompute_dependencies(
|
||||
w_id,
|
||||
script_path,
|
||||
deployment_message,
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&job.permissioned_as,
|
||||
db,
|
||||
rsmq,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%e, "error triggering python dependents to recompute dependencies");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(to_raw_value_owned(
|
||||
json!({ "success": "Successful lock file generation", "lock": content }),
|
||||
))
|
||||
@@ -3105,7 +3271,80 @@ async fn handle_dependency_job(
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_flow_dependency_job(
|
||||
async fn trigger_python_dependents_to_recompute_dependencies<
|
||||
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
|
||||
>(
|
||||
w_id: &str,
|
||||
script_path: &str,
|
||||
deployment_message: Option<String>,
|
||||
email: &str,
|
||||
created_by: &str,
|
||||
permissioned_as: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
rsmq: Option<R>,
|
||||
) -> error::Result<()> {
|
||||
let script_importers = sqlx::query_scalar!(
|
||||
"SELECT importer_path FROM dependency_map
|
||||
WHERE imported_path = $1 AND importer_kind = 'script'
|
||||
AND workspace_id = $2",
|
||||
script_path,
|
||||
w_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
for s in script_importers.iter() {
|
||||
let tx: PushIsolationLevel<'_, R> =
|
||||
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq.clone());
|
||||
let r = get_latest_deployed_hash_for_path(db, w_id, s.as_str()).await;
|
||||
if let Ok(r) = r {
|
||||
let (job_uuid, new_tx) = windmill_queue::push(
|
||||
db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::Dependencies {
|
||||
path: s.clone(),
|
||||
hash: r.0,
|
||||
language: r.5,
|
||||
dedicated_worker: r.6,
|
||||
deployment_message: deployment_message.clone(),
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&created_by,
|
||||
email,
|
||||
permissioned_as.to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
"pushed dependency job due to common python path: {job_uuid} for path {path} with hash {hash}",
|
||||
path = s,
|
||||
hash = r.0
|
||||
);
|
||||
new_tx.commit().await?;
|
||||
} else {
|
||||
tracing::error!(
|
||||
"error getting latest deployed hash for path {path}: {err}",
|
||||
path = s,
|
||||
err = r.unwrap_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_flow_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
job: &QueuedJob,
|
||||
logs: &mut String,
|
||||
mem_peak: &mut i32,
|
||||
@@ -3116,6 +3355,7 @@ async fn handle_flow_dependency_job(
|
||||
worker_dir: &str,
|
||||
base_internal_url: &str,
|
||||
token: &str,
|
||||
rsmq: Option<R>,
|
||||
) -> error::Result<()> {
|
||||
let job_path = job.script_path.clone().ok_or_else(|| {
|
||||
error::Error::InternalErr(
|
||||
@@ -3167,6 +3407,21 @@ async fn handle_flow_dependency_job(
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
if let Err(e) = handle_deployment_metadata(
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&db,
|
||||
&job.workspace_id,
|
||||
DeployedObject::Flow { path: job_path },
|
||||
job.raw_code.as_ref().map(|x| x.clone()),
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%e, "error handling deployment metadata");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3292,22 +3547,11 @@ async fn lock_modules(
|
||||
new_flow_modules.push(e);
|
||||
continue;
|
||||
};
|
||||
// sync with windmill-api/scripts
|
||||
let dependencies = match language {
|
||||
ScriptLang::Python3 => windmill_parser_py_imports::parse_python_imports(
|
||||
&content,
|
||||
&job.workspace_id,
|
||||
&path.clone().unwrap_or_else(|| job_path.to_string()),
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
.join("\n"),
|
||||
_ => content.clone(),
|
||||
};
|
||||
|
||||
let new_lock = capture_dependency_job(
|
||||
&job.id,
|
||||
&language,
|
||||
&dependencies,
|
||||
&content,
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
@@ -3318,7 +3562,7 @@ async fn lock_modules(
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
token,
|
||||
job.script_path(),
|
||||
&path.clone().unwrap_or_else(|| job_path.to_string()),
|
||||
)
|
||||
.await;
|
||||
match new_lock {
|
||||
@@ -3393,24 +3637,11 @@ async fn lock_modules_app(
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let dependencies = match language {
|
||||
ScriptLang::Python3 => {
|
||||
windmill_parser_py_imports::parse_python_imports(
|
||||
&content,
|
||||
&job.workspace_id,
|
||||
job_path,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
.join("\n")
|
||||
}
|
||||
_ => content.clone(),
|
||||
};
|
||||
logs.push_str("Found lockable inline script. Generating lock...\n");
|
||||
let new_lock = capture_dependency_job(
|
||||
&job.id,
|
||||
&language,
|
||||
&dependencies,
|
||||
&content,
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
@@ -3495,7 +3726,7 @@ async fn lock_modules_app(
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_app_dependency_job(
|
||||
async fn handle_app_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
job: &QueuedJob,
|
||||
logs: &mut String,
|
||||
mem_peak: &mut i32,
|
||||
@@ -3506,6 +3737,7 @@ async fn handle_app_dependency_job(
|
||||
worker_dir: &str,
|
||||
base_internal_url: &str,
|
||||
token: &str,
|
||||
rsmq: Option<R>,
|
||||
) -> error::Result<()> {
|
||||
let job_path = job.script_path.clone().ok_or_else(|| {
|
||||
error::Error::InternalErr(
|
||||
@@ -3555,6 +3787,41 @@ async fn handle_app_dependency_job(
|
||||
sqlx::query!("UPDATE app_version SET value = $1 WHERE id = $2", value, id,)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
if let Err(e) = handle_deployment_metadata(
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&db,
|
||||
&job.workspace_id,
|
||||
DeployedObject::App { path: job_path, version: id },
|
||||
job.raw_code.as_ref().map(|x| x.clone()),
|
||||
rsmq.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%e, "error handling deployment metadata");
|
||||
}
|
||||
|
||||
// tx = PushIsolationLevel::Transaction(new_tx);
|
||||
// tx = handle_deployment_metadata(
|
||||
// tx,
|
||||
// &authed,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::App { path: app.path.clone(), version: v_id },
|
||||
// app.deployment_message,
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// match tx {
|
||||
// PushIsolationLevel::Transaction(tx) => tx.commit().await?,
|
||||
// _ => {
|
||||
// return Err(Error::InternalErr(
|
||||
// "Expected a transaction here".to_string(),
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
@@ -3579,10 +3846,18 @@ async fn capture_dependency_job(
|
||||
) -> error::Result<String> {
|
||||
match job_language {
|
||||
ScriptLang::Python3 => {
|
||||
let reqs = windmill_parser_py_imports::parse_python_imports(
|
||||
job_raw_code,
|
||||
&w_id,
|
||||
script_path,
|
||||
&db,
|
||||
)
|
||||
.await?
|
||||
.join("\n");
|
||||
create_dependencies_dir(job_dir).await;
|
||||
let req: std::result::Result<String, Error> = pip_compile(
|
||||
job_id,
|
||||
job_raw_code,
|
||||
&reqs,
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
|
||||
@@ -15,9 +15,8 @@ use std::time::Duration;
|
||||
|
||||
use crate::common::{hash_args, save_in_cache};
|
||||
use crate::js_eval::{eval_timeout, IdContext};
|
||||
use crate::{AuthedClient, PreviousResult, KEEP_JOB_DIR};
|
||||
use crate::{AuthedClient, PreviousResult, SendResult, KEEP_JOB_DIR};
|
||||
use anyhow::Context;
|
||||
use async_recursion::async_recursion;
|
||||
use serde::Serialize;
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::{json, Value};
|
||||
@@ -70,8 +69,10 @@ pub async fn update_flow_status_after_job_completion<
|
||||
stop_early_override: Option<bool>,
|
||||
rsmq: Option<R>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> error::Result<()> {
|
||||
// this is manual tailrecursion because async_recursion blows up the stack
|
||||
// todo!();
|
||||
let mut rec = update_flow_status_after_job_completion_internal(
|
||||
db,
|
||||
client,
|
||||
@@ -87,6 +88,7 @@ pub async fn update_flow_status_after_job_completion<
|
||||
false,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
)
|
||||
.await?;
|
||||
while let Some(nrec) = rec {
|
||||
@@ -105,6 +107,7 @@ pub async fn update_flow_status_after_job_completion<
|
||||
nrec.skip_error_handler,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -125,6 +128,7 @@ pub async fn update_flow_status_after_job_completion<
|
||||
nrec.skip_error_handler,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx.clone(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -167,6 +171,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
skip_error_handler: bool,
|
||||
rsmq: Option<R>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> error::Result<Option<RecUpdateFlowStatusAfterJobCompletion>> {
|
||||
let (
|
||||
should_continue_flow,
|
||||
@@ -575,6 +580,8 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
tracing::debug!(id = %flow_job.id, root_id = %job_root, "flow status updated");
|
||||
|
||||
(
|
||||
should_continue_flow,
|
||||
flow_job,
|
||||
@@ -633,7 +640,8 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
} else {
|
||||
if flow_job.cache_ttl.is_some() {
|
||||
let cached_res_path = {
|
||||
let args_hash = hash_args(&flow_job.args);
|
||||
let args_hash =
|
||||
hash_args(db, client, w_id, job_id_for_status, &flow_job.args).await;
|
||||
let flow_path = flow_job.script_path();
|
||||
let version_hash = if let Some(rc) = flow_job.raw_flow.as_ref() {
|
||||
use std::hash::Hasher;
|
||||
@@ -648,7 +656,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
format!("{flow_path}/cache/{version_hash}/{args_hash}")
|
||||
};
|
||||
|
||||
save_in_cache(db, &flow_job, cached_res_path, &nresult).await;
|
||||
save_in_cache(db, client, &flow_job, cached_res_path, &nresult).await;
|
||||
}
|
||||
let success = success && !is_failure_step && !skip_error_handler;
|
||||
if success {
|
||||
@@ -685,6 +693,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
}
|
||||
true
|
||||
} else {
|
||||
tracing::debug!(id = %flow_job.id, "start handle flow");
|
||||
match handle_flow(
|
||||
&flow_job,
|
||||
db,
|
||||
@@ -693,7 +702,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
same_worker_tx.clone(),
|
||||
worker_dir,
|
||||
rsmq.clone(),
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1036,7 +1045,7 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
same_worker_tx: Sender<Uuid>,
|
||||
worker_dir: &str,
|
||||
rsmq: Option<R>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> anyhow::Result<()> {
|
||||
let flow = flow_job
|
||||
.parse_raw_flow()
|
||||
@@ -1055,7 +1064,7 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
rsmq,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1088,7 +1097,7 @@ pub struct RawArgs {
|
||||
pub args: Option<Json<HashMap<String, Box<RawValue>>>>,
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
// #[async_recursion]
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
flow_job: &QueuedJob,
|
||||
@@ -1100,7 +1109,7 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
same_worker_tx: Sender<Uuid>,
|
||||
worker_dir: &str,
|
||||
rsmq: Option<R>,
|
||||
worker_name: &str,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> error::Result<()> {
|
||||
let job_root = flow_job
|
||||
.root_job
|
||||
@@ -1121,34 +1130,114 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
|
||||
// if this is an empty module of if the module has already been completed, successfully, update the parent flow
|
||||
if flow.modules.is_empty() || matches!(status_module, FlowStatusModule::Success { .. }) {
|
||||
let r;
|
||||
return update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
flow_job.id,
|
||||
&Uuid::nil(),
|
||||
flow_job.workspace_id.as_str(),
|
||||
true,
|
||||
if flow.modules.is_empty() {
|
||||
r = to_raw_value(&flow_job_args);
|
||||
&r
|
||||
} else {
|
||||
// it has to be an empty for loop event
|
||||
serde_json::from_str("[]").unwrap()
|
||||
},
|
||||
true,
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
None,
|
||||
rsmq,
|
||||
worker_name,
|
||||
)
|
||||
.await;
|
||||
job_completed_tx
|
||||
.send(SendResult::UpdateFlow {
|
||||
flow: flow_job.id,
|
||||
success: true,
|
||||
result: if flow.modules.is_empty() {
|
||||
to_raw_value(&flow_job_args)
|
||||
} else {
|
||||
// it has to be an empty for loop event
|
||||
serde_json::from_str("[]").unwrap()
|
||||
},
|
||||
stop_early_override: None,
|
||||
w_id: flow_job.workspace_id.clone(),
|
||||
worker_dir: worker_dir.to_string(),
|
||||
token: client.token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error sending update flow message to job completed channel: {e}"
|
||||
))
|
||||
})?;
|
||||
// let r;
|
||||
// return update_flow_status_after_job_completion(
|
||||
// db,
|
||||
// client,
|
||||
// flow_job.id,
|
||||
// &Uuid::nil(),
|
||||
// flow_job.workspace_id.as_str(),
|
||||
// true,
|
||||
// if flow.modules.is_empty() {
|
||||
// r = to_raw_value(&flow_job_args);
|
||||
// &r
|
||||
// } else {
|
||||
// // it has to be an empty for loop event
|
||||
// serde_json::from_str("[]").unwrap()
|
||||
// },
|
||||
// true,
|
||||
// same_worker_tx,
|
||||
// worker_dir,
|
||||
// None,
|
||||
// rsmq,
|
||||
// worker_name,
|
||||
// )
|
||||
// .await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let arc_flow_job_args = Arc::new(flow_job_args.clone());
|
||||
|
||||
if i == 0 {
|
||||
if !flow_job.is_flow_step && flow_job.schedule_path.is_some() {
|
||||
let no_flow_overlap = sqlx::query_scalar!(
|
||||
"SELECT no_flow_overlap FROM schedule WHERE path = $1 AND workspace_id = $2",
|
||||
flow_job.schedule_path.as_ref().unwrap(),
|
||||
flow_job.workspace_id.as_str()
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if no_flow_overlap {
|
||||
let count = sqlx::query_scalar!(
|
||||
"SELECT count(*) FROM queue WHERE schedule_path = $1 AND workspace_id = $2 AND id != $3",
|
||||
flow_job.schedule_path.as_ref().unwrap(),
|
||||
flow_job.workspace_id.as_str(),
|
||||
flow_job.id
|
||||
).fetch_one(db).await?.unwrap_or(0);
|
||||
if count > 0 {
|
||||
job_completed_tx
|
||||
.send(SendResult::UpdateFlow {
|
||||
flow: flow_job.id,
|
||||
success: true,
|
||||
result: serde_json::from_str(
|
||||
"\"not allowed to overlap, scheduling next iteration\"",
|
||||
)
|
||||
.unwrap(),
|
||||
stop_early_override: Some(true),
|
||||
w_id: flow_job.workspace_id.clone(),
|
||||
worker_dir: worker_dir.to_string(),
|
||||
token: client.token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error sending update flow message to job completed channel: {e}"
|
||||
))
|
||||
})?;
|
||||
// return update_flow_status_after_job_completion(
|
||||
// db,
|
||||
// client,
|
||||
// flow_job.id,
|
||||
// &Uuid::nil(),
|
||||
// flow_job.workspace_id.as_str(),
|
||||
// true,
|
||||
// serde_json::from_str(
|
||||
// "\"not allowed to overlap, scheduling next iteration\"",
|
||||
// )
|
||||
// .unwrap(),
|
||||
// true,
|
||||
// same_worker_tx,
|
||||
// worker_dir,
|
||||
// Some(true),
|
||||
// rsmq,
|
||||
// worker_name,
|
||||
// )
|
||||
// .await;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(skip_expr) = &flow.skip_expr {
|
||||
let skip = compute_bool_from_expr(
|
||||
skip_expr.to_string(),
|
||||
@@ -1160,22 +1249,40 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
)
|
||||
.await?;
|
||||
if skip {
|
||||
return update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
flow_job.id,
|
||||
&Uuid::nil(),
|
||||
flow_job.workspace_id.as_str(),
|
||||
true,
|
||||
serde_json::from_str("\"stopped early\"").unwrap(),
|
||||
true,
|
||||
same_worker_tx,
|
||||
worker_dir,
|
||||
Some(true),
|
||||
rsmq,
|
||||
worker_name,
|
||||
)
|
||||
.await;
|
||||
job_completed_tx
|
||||
.send(SendResult::UpdateFlow {
|
||||
flow: flow_job.id,
|
||||
success: true,
|
||||
result: serde_json::from_str("\"stopped early\"").unwrap(),
|
||||
stop_early_override: Some(true),
|
||||
w_id: flow_job.workspace_id.clone(),
|
||||
worker_dir: worker_dir.to_string(),
|
||||
token: client.token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error sending update flow message to job completed channel: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// return update_flow_status_after_job_completion(
|
||||
// db,
|
||||
// client,
|
||||
// flow_job.id,
|
||||
// &Uuid::nil(),
|
||||
// flow_job.workspace_id.as_str(),
|
||||
// true,
|
||||
// serde_json::from_str("\"stopped early\"").unwrap(),
|
||||
// true,
|
||||
// same_worker_tx,
|
||||
// worker_dir,
|
||||
// Some(true),
|
||||
// rsmq,
|
||||
// worker_name,
|
||||
// )
|
||||
// .await;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1397,22 +1504,39 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
.await?;
|
||||
if flow_job.is_flow_step {
|
||||
if let Some(parent_job) = flow_job.parent_job {
|
||||
update_flow_status_after_job_completion(
|
||||
db,
|
||||
client,
|
||||
parent_job,
|
||||
&flow_job.id,
|
||||
&flow_job.workspace_id,
|
||||
true,
|
||||
&to_raw_value(&result),
|
||||
false,
|
||||
same_worker_tx.clone(),
|
||||
&worker_dir,
|
||||
None,
|
||||
rsmq,
|
||||
worker_name,
|
||||
)
|
||||
.await?;
|
||||
job_completed_tx
|
||||
.send(SendResult::UpdateFlow {
|
||||
flow: parent_job,
|
||||
success: true,
|
||||
result: to_raw_value(&result),
|
||||
stop_early_override: Some(true),
|
||||
w_id: flow_job.workspace_id.clone(),
|
||||
worker_dir: worker_dir.to_string(),
|
||||
token: client.token.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error sending update flow message to job completed channel: {e}"
|
||||
))
|
||||
})?;
|
||||
// update_flow_status_after_job_completion(
|
||||
// db,
|
||||
// client,
|
||||
// parent_job,
|
||||
// &flow_job.id,
|
||||
// &flow_job.workspace_id,
|
||||
// true,
|
||||
// &to_raw_value(&result),
|
||||
// false,
|
||||
// same_worker_tx.clone(),
|
||||
// &worker_dir,
|
||||
// None,
|
||||
// rsmq,
|
||||
// worker_name,
|
||||
// )
|
||||
// .await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
@@ -1650,6 +1774,7 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
_ => Ok(flow_job_args),
|
||||
}
|
||||
};
|
||||
tracing::debug!(id = %flow_job.id, root_id = %job_root, "flow job args computed");
|
||||
|
||||
let next_flow_transform = compute_next_flow_transform(
|
||||
arc_flow_job_args.clone(),
|
||||
@@ -1850,6 +1975,8 @@ async fn push_next_flow_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::debug!(id = %flow_job.id, root_id = %job_root, "pushed next flow job: {uuid}");
|
||||
|
||||
if let FlowModuleValue::ForloopFlow { parallelism: Some(p), .. } = &module.value {
|
||||
if i as u16 >= *p {
|
||||
sqlx::query!(
|
||||
@@ -2172,6 +2299,8 @@ async fn compute_next_flow_transform(
|
||||
))
|
||||
};
|
||||
let delete_after_use = module.delete_after_use.unwrap_or(false);
|
||||
|
||||
tracing::debug!(id = %flow_job.id, "computing next flow transform for {:?}", &module.value);
|
||||
match &module.value {
|
||||
FlowModuleValue::Identity => trivial_next_job(JobPayload::Identity),
|
||||
FlowModuleValue::Flow { path, .. } => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.231.0";
|
||||
export const VERSION = "v1.236.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const VERSION = "v1.231.0";
|
||||
export const VERSION = "v1.236.2";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
writeAllSync,
|
||||
yamlParse,
|
||||
} from "./deps.ts";
|
||||
import { deepEqual } from "./utils.ts";
|
||||
|
||||
export interface ScriptFile {
|
||||
parent_hash?: string;
|
||||
@@ -66,7 +67,7 @@ export async function handleFile(
|
||||
path: string,
|
||||
workspace: string,
|
||||
alreadySynced: string[],
|
||||
message?: string,
|
||||
message?: string
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!path.includes(".inline_script.") &&
|
||||
@@ -125,8 +126,9 @@ export async function handleFile(
|
||||
typed.is_template === remote.is_template &&
|
||||
typed.kind == remote.kind &&
|
||||
!remote.archived &&
|
||||
(remote?.lock ?? "") == (typed.lock?.join("\n") ?? "") &&
|
||||
JSON.stringify(typed.schema) == JSON.stringify(remote.schema) &&
|
||||
(remote?.lock ?? "").trim() ==
|
||||
(typed.lock?.join("\n") ?? "").trim() &&
|
||||
deepEqual(typed.schema, remote.schema) &&
|
||||
typed.tag == remote.tag &&
|
||||
(typed.ws_error_handler_muted ?? false) ==
|
||||
remote.ws_error_handler_muted &&
|
||||
@@ -140,6 +142,7 @@ export async function handleFile(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
colors.yellow.bold(`Creating script with a parent ${remotePath}`)
|
||||
);
|
||||
|
||||
29
cli/sync.ts
29
cli/sync.ts
@@ -478,7 +478,7 @@ async function pull(
|
||||
!opts.json
|
||||
);
|
||||
const local = opts.raw
|
||||
? undefined
|
||||
? await FSFSElement(Deno.cwd())
|
||||
: await FSFSElement(path.join(Deno.cwd(), ".wmill"));
|
||||
const changes = await compareDynFSElement(
|
||||
remote,
|
||||
@@ -685,19 +685,18 @@ async function push(
|
||||
"Computing the files to update on the remote to match local (taking .wmillignore into account)"
|
||||
)
|
||||
);
|
||||
const remote = opts.raw
|
||||
? undefined
|
||||
: ZipFSElement(
|
||||
(await downloadZip(
|
||||
workspace,
|
||||
opts.plainSecrets,
|
||||
opts.skipVariables,
|
||||
opts.skipResources,
|
||||
opts.skipSecrets,
|
||||
opts.includeSchedules
|
||||
))!,
|
||||
!opts.json
|
||||
);
|
||||
const remote = ZipFSElement(
|
||||
(await downloadZip(
|
||||
workspace,
|
||||
opts.plainSecrets,
|
||||
opts.skipVariables,
|
||||
opts.skipResources,
|
||||
opts.skipSecrets,
|
||||
opts.includeSchedules
|
||||
))!,
|
||||
!opts.json
|
||||
);
|
||||
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), ""));
|
||||
const changes = await compareDynFSElement(
|
||||
local,
|
||||
@@ -850,7 +849,7 @@ async function push(
|
||||
case "flow":
|
||||
await FlowService.deleteFlowByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".flow.json"),
|
||||
path: removeSuffix(change.path, ".flow/flow.json"),
|
||||
});
|
||||
break;
|
||||
case "app":
|
||||
|
||||
124
examples/deploy/job-monitoring-prometheus/README.md
Normal file
124
examples/deploy/job-monitoring-prometheus/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
Windmill monitoring with Prometheus
|
||||
===================================
|
||||
|
||||
## Intro
|
||||
|
||||
Windmill servers and workers both produces metrics Prometheus can scrap.
|
||||
|
||||
Individual jobs producing their own metric can be difficult to achieve due to the ephemeral nature of a job. Prometheus might not have the time to scrape the metric if the job is too quick. Prometheus has developed the [Prometheus Gateway](https://prometheus.io/docs/practices/pushing/) exactly for this usecase, and we're going to go through the initial setup. By the end of this example, you'll have a Windmill stack running a job that produces its own metric, and a Grafana simple dashboard displaying it.
|
||||
|
||||
Prometheus push gateway is hosted in [this repository](https://github.com/prometheus/pushgateway)
|
||||
|
||||
## Setup
|
||||
|
||||
First, we need to setup an entire stack composed of:
|
||||
- Windmill: a database, a server, and at least one worker
|
||||
- Prometheus: the server (if you don't already have one), and the gateway to which metrics will be pushed
|
||||
- Grafana to visualize the metrics (optional)
|
||||
|
||||
We have assembled a docker compose with all those services. You can run it with a simple:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
Once all the services are running, Windmill can be accessed at http://localhost:8000, Prometheus at http://localhost:9090, Grafana at http://localhost:3000 (credentials are set in [grafana.ini](./grafana/grafana.ini) config file).
|
||||
|
||||
The Gateway exposes the metrics on http://localhost:9091/metrics and also has an interface at http://localhost:9091 to manage metric groups (see the Gateway documentation for what a metric group is). You can check that Prometheus can collect them directly on [Prometheus status page](http://localhost:9090/targets)
|
||||
|
||||
## Monitoring Windmill servers and workers
|
||||
|
||||
Metrics need to be enabled on Windmill server and workers. They can be enabled only on Windmill Enterprise Edition by setting the environment variable `METRICS_ADDR` to `1` on each container or toggle on "Expose Metrics" in instance settings -> core (additional "debug-level" metrics can be enabled in the instance settings > debug menu).
|
||||
|
||||
Once metrics are enabled, Prometheus needs to discover Windmill service containers. Using docker-compose, it requires a few adjustments:
|
||||
- Each Windmill containers need to expose the metrics port (`8001` by default) so that Prometheus discovery knows which ports to scrap. This is done by adding the value `8001` to the `expose` block to both `windmill_server` and `windmill_worker`.
|
||||
- Each Windmill container needs to be labelled so that Prometheus service discovery filters out other services. Here we're using the `prometheus-job=windmill_server` and `prometheus-job=windmill_worker` in the docker compose to differentiate between server and worker
|
||||
- The Prometheus container needs to have access to the docker socket to discover other containers. To achieve this, Prometheus container needs to be run as root and the docker socket needs to be mounted on the container (see `user: root` and `/var/run/docker.sock:/var/run/docker.sock` in the `prometheus` block of docker-compose.yml)
|
||||
- Finally, the following block should be added to `prometheus.yml` config file:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: "windmill_server"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_server
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
|
||||
- job_name: "windmill_worker"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_worker
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
```
|
||||
|
||||
With all this in place, Windmill servers and workers metrics will be scrapped by Prometheus and can be visualized in Grafana. We've added a simple dashboard in [grafana/dashboards/](./grafana/dashboards) showing the most interesting metrics.
|
||||
|
||||

|
||||
|
||||
Note: If your Windmill containers are split onto multiple docker daemon, the above still works. Instead of pointing to the docker socket in Prometheus' `docker_sd_config` block, simply expose docker daemon to http or https and point to the remote docker daemon host: `http://remote_docker_daemon:2375`. Multiple remote docker daemons can be listed.
|
||||
|
||||
## Producing metrics from individual Windmill jobs
|
||||
|
||||
Here is an example of how a job can "push" its own metrics to Prometheus via Prometheus Gateway
|
||||
|
||||
### Windmill script
|
||||
|
||||
Now we will create a script in Windmill that pushes a value to the Prometheus Gateway. We will do it in Python using [prometheus-client](https://github.com/prometheus/client_python), but other clients are available for [Golang](https://github.com/prometheus/client_golang) or [Typescript](https://github.com/siimon/prom-client), and for bash metrics can be pushed via [simple CURL commands](https://github.com/prometheus/pushgateway?tab=readme-ov-file#command-line).
|
||||
|
||||
In Windmill, create a new Python script (named `u/admin/random_number_metric_script` in this tutorial) with the following content:
|
||||
|
||||
```python
|
||||
import os
|
||||
import random
|
||||
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
|
||||
|
||||
PROMETHEUS_GATEWAY_URL = "prometheus_gateway:9091"
|
||||
|
||||
def main():
|
||||
job_path = os.environ.get("WM_JOB_PATH")
|
||||
registry = CollectorRegistry()
|
||||
gauge = Gauge(
|
||||
"job_records_processed",
|
||||
"Number of records processed for {}".format(job_path),
|
||||
registry=registry,
|
||||
)
|
||||
val = random.randint(0, 100)
|
||||
print("Storing metrics value: ", val)
|
||||
gauge.set(val)
|
||||
push_to_gateway(PROMETHEUS_GATEWAY_URL, job=job_path, registry=registry)
|
||||
```
|
||||
|
||||
It is quite simple. It generate a random number and pushed it via a Gauge metric named `job_records_processed` to the Prometheus Gateway. Note that we use the script path as the `job` label, so that all runs of this script will push to the same metric. Other labels (such as the job ID for example) can also be used via the `grouping_key` argument that `push_to_gateway` accepts.
|
||||
|
||||
Once the script is created, you can create a schedule that runs is every 5 seconds for example, such that a new value is pushed regularly.
|
||||
|
||||
### Visualizing the metric
|
||||
|
||||
If you chose to not run Grafana, the raw metric value can be seen in Prometheus UI directly. Go to http://localhost:9090/graph and input the following in the Expression bar:
|
||||
```promql
|
||||
job_records_processed{exported_job="u/admin/random_number_metric_script"}
|
||||
```
|
||||
|
||||

|
||||
|
||||
In Grafana, a similar visualization can be achieved. We've added a dashboard JSON in [grafana/dashboards](./grafana/dashboards/) and the easiest is to import it directly in your Grafana.
|
||||
|
||||
The first panel is a simple representation of the `job_records_processed` metric. We've adjusted the `Min Step` to 5 seconds because we know our job runs every 5 seconds and we ideally want to display all the measurement points.
|
||||
|
||||

|
||||
|
||||
The second panel is here to highlight a limitation to be aware of when using the Prometheus Gateway. It displays the instant value of the `job_records_processed` metric. The main issue with using the Gateway is that the value pushed to it will persist until a new value is pushed. Which means that if the schedule is somehow stopped, or if the script starts failing and stops pushing to Prometheus Gateway, the value will remain constant. It might not be ideal to spot any kind of undesired behavior. Instead what would be nice is to have the metric drop to a default value, like `0`. Thankfully the Gateway comes with a built-in metric called `push_time_seconds` which stores the timestamp of the last successful push for any given metric and label. You can combine the 2 metrics using basic functions to easily create an "instant" representation of signal. This is what we've done in the second panel:
|
||||
|
||||

|
||||
|
||||
We simply multiply the `job_records_processed` with the `irate` of the `push_time_seconds` metric for the same label, and we set the `Min Step` to `1s` for the `irate` to properly work.
|
||||
|
||||
We obtain the following simple yet useful dashboard:
|
||||
|
||||

|
||||
|
||||
In between the 2 light blue vertical lines, we manually stopped the Windmill schedule to show that the raw metric (at the top) remains constant, while its instant representation drops to zero.
|
||||
100
examples/deploy/job-monitoring-prometheus/docker-compose.yml
Normal file
100
examples/deploy/job-monitoring-prometheus/docker-compose.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:14
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
windmill_server:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
deploy:
|
||||
replicas: 1
|
||||
labels:
|
||||
- prometheus-job=windmill_server
|
||||
expose:
|
||||
- 8001
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
|
||||
- MODE=server
|
||||
- METRICS_ADDR=1
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
windmill_worker:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
pull_policy: always
|
||||
deploy:
|
||||
replicas: 3
|
||||
labels:
|
||||
- prometheus-job=windmill_worker
|
||||
expose:
|
||||
- 8001
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
- METRICS_ADDR=1
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
user: root
|
||||
volumes:
|
||||
- prometheus_data:/prometheus
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # for service discovery
|
||||
ports:
|
||||
- 9090:9090
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=365d"
|
||||
|
||||
prometheus_gateway:
|
||||
image: prom/pushgateway
|
||||
ports:
|
||||
- 9091:9091
|
||||
command:
|
||||
- "--persistence.file=/data/persistence.dat"
|
||||
volumes:
|
||||
- prometheus_gateway_data:/data
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
container_name: grafana
|
||||
environment:
|
||||
- GF_PATHS_CONFIG=/etc/grafana/grafana.ini
|
||||
deploy:
|
||||
replicas: 1
|
||||
depends_on:
|
||||
- prometheus
|
||||
ports:
|
||||
- 3030:3000
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
|
||||
- ./grafana/grafana.ini:/etc/grafana/grafana.ini
|
||||
|
||||
volumes:
|
||||
db_data: null
|
||||
prometheus_data: null
|
||||
prometheus_gateway_data: null
|
||||
grafana_data: null
|
||||
@@ -0,0 +1,319 @@
|
||||
{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus"
|
||||
}
|
||||
],
|
||||
"__elements": {},
|
||||
"__requires": [
|
||||
{
|
||||
"type": "grafana",
|
||||
"id": "grafana",
|
||||
"name": "Grafana",
|
||||
"version": "10.2.3"
|
||||
},
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "timeseries",
|
||||
"name": "Time series",
|
||||
"version": ""
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "builder",
|
||||
"expr": "job_records_processed{exported_job=\"$script_path\"}",
|
||||
"fullMetaSearch": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": false,
|
||||
"interval": "5s",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "A",
|
||||
"useBackend": false
|
||||
}
|
||||
],
|
||||
"title": "Processed records (raw)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 100,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 17,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 2,
|
||||
"interval": "15s",
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "code",
|
||||
"expr": "irate(push_time_seconds{exported_job=\"$script_path\"}[$__interval]) * job_records_processed{exported_job=\"$script_path\"} / irate(push_time_seconds{exported_job=\"$script_path\"}[$__interval])",
|
||||
"fullMetaSearch": false,
|
||||
"hide": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": true,
|
||||
"interval": "1s",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "B",
|
||||
"useBackend": false
|
||||
}
|
||||
],
|
||||
"title": "Processed records (instant)",
|
||||
"transformations": [],
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": "Prometheus",
|
||||
"value": "PBFA97CFB590B2093"
|
||||
},
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "prometheus",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"queryValue": "",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"definition": "label_values(exported_job)",
|
||||
"hide": 1,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "script_path",
|
||||
"options": [],
|
||||
"query": {
|
||||
"qryType": 1,
|
||||
"query": "label_values(exported_job)",
|
||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Job metrics Dashboard",
|
||||
"uid": "a31424a8-5eaa-4d26-bded-19570145b7bd",
|
||||
"version": 8,
|
||||
"weekStart": ""
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
@@ -0,0 +1,3 @@
|
||||
[security]
|
||||
admin_user = windmill
|
||||
admin_password = changeme
|
||||
@@ -0,0 +1,28 @@
|
||||
global:
|
||||
scrape_interval: 1s
|
||||
external_labels:
|
||||
env: "infrastructure"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "windmill_server"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_server
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
|
||||
- job_name: "windmill_worker"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_worker
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
|
||||
- job_name: "prometheus_gateway"
|
||||
static_configs:
|
||||
- targets: ["prometheus_gateway:9091"]
|
||||
scrape_interval: 1s
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user