From 8c47321e9b3f643eb70cad99f4e9502cbad2660d Mon Sep 17 00:00:00 2001 From: geofmureithi Date: Wed, 16 Sep 2026 05:14:35 +0300 Subject: [PATCH] bump: to v1.0.0-rc.9 --- .cargo/audit.toml | 1 + .github/workflows/ci.yml | 2 +- .github/workflows/coverage.yml | 11 + .github/workflows/update-changelog.yml | 130 ++ ...2acf90e2a3710690f398f9ffaa71e180f1d9e.json | 12 +- ...06a42fe6918792ae9bea8848a6ec1a3895a76.json | 32 - ...1eaa74232f6f6f3121c7086a0b138195e9b0d.json | 112 +- ...f77683c7bf0eb4711c57e3196820d18991cfa.json | 12 + ...df63b808a98a67431073781b67161487ab243.json | 12 + ...2d6063f186242ca1808cfae9d6477c7d8687b.json | 112 +- ...9af4519adf779487e4d2e8b7137b79db1b5e7.json | 12 - ...4dd6e8a3e7ea31ef8d622534a9fa0093c31e2.json | 48 +- ...29a245d0bb2b84595f87a883c60d6f120cf63.json | 23 +- ...8edce306564754651c4098f8dfe693195f6ca.json | 12 +- ...5a78685df4c262c2a13eafd9803c3672dcd10.json | 48 +- ...010eebb0551753ee259a623a98b886ef13d52.json | 62 + ...85ad7b0a3ddc713341adcbbc959021082c8e9.json | 12 - ...5653f4fe06d10361c555f8ade4b65589520bc.json | 112 +- ...1aa417b28a37e6612f8de7e5525ebdcdab373.json | 182 +++ ...0ff587b8d8981a7fbe522ffd5667fdc11fd2.json} | 6 +- ...90f217b575f0ca464a5faddd0fe2d51793983.json | 98 -- ...dc012ed036e4e0fadde45dfe544fc95d75466.json | 12 + ...0fa7bdc9acef7704fe86466d28fc85126b8e3.json | 114 +- CHANGELOG.md | 1 + Cargo.lock | 1097 +++++++---------- Cargo.toml | 73 +- README.md | 80 +- deny.toml | 5 +- examples/codec.rs | 2 +- examples/config.rs | 66 + examples/etl_workflow.rs | 25 +- examples/order_workflow.rs | 18 +- examples/realtime.rs | 47 +- examples/shared.rs | 12 +- examples/unique_jobs.rs | 8 +- .../20260726081359_add_fetch_next_indices.sql | 11 + .../20260803223031_add_lock_indices.sql | 3 + .../20260804103122_drop_unneeded_indices.sql | 11 + queries/backend/fetch_completed_tasks.sql | 3 +- queries/backend/fetch_next.sql | 81 +- queries/backend/reenqueue_abandoned.sql | 27 + queries/backend/reenqueue_orphaned.sql | 2 + queries/task/ack.sql | 21 +- queries/task/lock.sql | 7 +- src/ack.rs | 135 -- src/callback.rs | 48 +- src/config.rs | 246 ++++ src/error.rs | 26 + src/fetcher.rs | 209 ---- src/from_row.rs | 99 +- src/lib.rs | 659 ++++------ src/persist.rs | 160 +++ src/queries/ack_task.rs | 62 +- src/queries/fetch_by_id.rs | 39 +- src/queries/fetch_next.rs | 37 + src/queries/keep_alive.rs | 50 +- src/queries/list_queues.rs | 37 +- src/queries/list_tasks.rs | 59 +- src/queries/list_workers.rs | 20 +- src/queries/lock_task.rs | 26 +- src/queries/metrics.rs | 26 +- src/queries/mod.rs | 50 +- src/queries/reenqueue_orphaned.rs | 106 +- src/queries/register_worker.rs | 23 +- src/queries/vacuum.rs | 13 +- src/queries/wait_for.rs | 66 +- src/shared.rs | 384 +++--- src/sink.rs | 147 +-- supply-chain/config.toml | 672 ++++------ supply-chain/imports.lock | 4 + 70 files changed, 3211 insertions(+), 2969 deletions(-) create mode 100644 .github/workflows/update-changelog.yml delete mode 100644 .sqlx/query-1356c17313c5f3491e384d1667106a42fe6918792ae9bea8848a6ec1a3895a76.json create mode 100644 .sqlx/query-372f1019c59dddf3cf6389f5a3bf77683c7bf0eb4711c57e3196820d18991cfa.json create mode 100644 .sqlx/query-390dc1621e4fc1474d101b067e5df63b808a98a67431073781b67161487ab243.json delete mode 100644 .sqlx/query-41198057d67f5386d366f332c349af4519adf779487e4d2e8b7137b79db1b5e7.json create mode 100644 .sqlx/query-7f32c6cbf20be0cd6e20510d04b010eebb0551753ee259a623a98b886ef13d52.json delete mode 100644 .sqlx/query-90406ca41cb79e68f66a300730f85ad7b0a3ddc713341adcbbc959021082c8e9.json create mode 100644 .sqlx/query-add94a70c96b1ca5c6c8d65ee201aa417b28a37e6612f8de7e5525ebdcdab373.json rename .sqlx/{query-d2a2123b8ede2014f106836cf41b7ec3470a737fa5174e7a2aa16dcf16fc5576.json => query-c894aafd6bd61741339f3494ce7e0ff587b8d8981a7fbe522ffd5667fdc11fd2.json} (70%) delete mode 100644 .sqlx/query-d7aefe54cd7388c208fff5b946390f217b575f0ca464a5faddd0fe2d51793983.json create mode 100644 .sqlx/query-e913ec096a6da5c915d86fdc0b6dc012ed036e4e0fadde45dfe544fc95d75466.json create mode 100644 examples/config.rs create mode 100644 migrations/20260726081359_add_fetch_next_indices.sql create mode 100644 migrations/20260803223031_add_lock_indices.sql create mode 100644 migrations/20260804103122_drop_unneeded_indices.sql create mode 100644 queries/backend/reenqueue_abandoned.sql delete mode 100644 src/ack.rs create mode 100644 src/config.rs create mode 100644 src/error.rs delete mode 100644 src/fetcher.rs create mode 100644 src/persist.rs create mode 100644 src/queries/fetch_next.rs diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 77b2027..7c597f9 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -4,6 +4,7 @@ ignore = [ "RUSTSEC-2023-0071", "RUSTSEC-2025-0052", "RUSTSEC-2024-0384", + "RUSTSEC-2025-0141", ] [output] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cc6a1e..0366ef9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: rust: [stable, beta] include: - os: ubuntu-latest - rust: nightly-2026-04-16 + rust: nightly steps: - name: Checkout sources uses: actions/checkout@v6 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ad82e88..a782804 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,8 +3,19 @@ name: Coverage on: push: branches: [main] + paths: + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/*.rs" + - "queries/**/*.sql" + pull_request: branches: [main] + paths: + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/*.rs" + - "queries/**/*.sql" env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 0000000..843651c --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,130 @@ +name: Update Changelog + +on: + pull_request: + types: [opened, edited] + +permissions: + contents: write + pull-requests: write + +jobs: + update-changelog: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + - name: Check if CHANGELOG already modified in this PR + id: check + env: + GH_TOKEN: ${{ github.token }} + run: | + FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only) + if echo "$FILES" | grep -qx "CHANGELOG.md"; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Update CHANGELOG + if: steps.check.outputs.changed == 'false' + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + REPO="${{ github.repository }}" + EVENT="${{ github.event.action }}" + + FORMATTED_TITLE="$(echo "$PR_TITLE" | sed -E 's/^([a-zA-Z0-9_-]+):/*\1*:/')" + CHANGELOG_ENTRY="- ${FORMATTED_TITLE} ([#${PR_NUMBER}](https://github.com/${REPO}/pull/${PR_NUMBER}))" + + # Pattern to match an existing entry for this PR number (anywhere in the file) + PR_PATTERN="(#${PR_NUMBER})" + + if [ "$EVENT" = "opened" ]; then + # On open: only insert if no entry for this PR number already exists + if grep -qF "$PR_PATTERN" CHANGELOG.md; then + echo "Changelog entry for #${PR_NUMBER} already exists, skipping." + else + awk -v entry="$CHANGELOG_ENTRY" ' + /^## \[Unreleased\]$/ { + print + print "" + print entry + skip_blank=1 + next + } + skip_blank && /^$/ { skip_blank=0; next } + { skip_blank=0; print } + ' CHANGELOG.md > CHANGELOG.md.tmp + mv CHANGELOG.md.tmp CHANGELOG.md + fi + + elif [ "$EVENT" = "edited" ]; then + # On edit: replace the existing entry, or insert if it doesn't exist yet + if grep -qF "$PR_PATTERN" CHANGELOG.md; then + awk -v entry="$CHANGELOG_ENTRY" -v pattern="$PR_PATTERN" ' + index($0, pattern) { print entry; next } + { print } + ' CHANGELOG.md > CHANGELOG.md.tmp + mv CHANGELOG.md.tmp CHANGELOG.md + else + echo "No existing entry for #${PR_NUMBER}, inserting under [Unreleased]." + awk -v entry="$CHANGELOG_ENTRY" ' + /^## \[Unreleased\]$/ { + print + print "" + print entry + skip_blank=1 + next + } + skip_blank && /^$/ { skip_blank=0; next } + { skip_blank=0; print } + ' CHANGELOG.md > CHANGELOG.md.tmp + mv CHANGELOG.md.tmp CHANGELOG.md + fi + fi + + - name: Commit via GitHub API + if: steps.check.outputs.changed == 'false' + env: + GH_TOKEN: ${{ github.token }} + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + REPO="${{ github.repository }}" + + # Bail if nothing actually changed (e.g. entry already matched) + if git diff --quiet CHANGELOG.md; then + echo "No changes to commit." + exit 0 + fi + + LATEST_SHA=$(gh api "repos/${REPO}/git/refs/heads/${BRANCH}" --jq .object.sha) + BASE_TREE=$(gh api "repos/${REPO}/git/commits/${LATEST_SHA}" --jq .tree.sha) + + BLOB_SHA=$(gh api "repos/${REPO}/git/blobs" \ + -f content="$(base64 -w0 CHANGELOG.md)" \ + -f encoding="base64" \ + --jq .sha) + + NEW_TREE=$(gh api "repos/${REPO}/git/trees" \ + -f base_tree="$BASE_TREE" \ + -f "tree[][path]=CHANGELOG.md" \ + -f "tree[][mode]=100644" \ + -f "tree[][type]=blob" \ + -f "tree[][sha]=$BLOB_SHA" \ + --jq .sha) + + NEW_COMMIT=$(gh api "repos/${REPO}/git/commits" \ + -f message="docs: update CHANGELOG for PR #${{ github.event.pull_request.number }}" \ + -f tree="$NEW_TREE" \ + -f "parents[]=$LATEST_SHA" \ + --jq .sha) + + gh api "repos/${REPO}/git/refs/heads/${BRANCH}" \ + -X PATCH \ + -f sha="$NEW_COMMIT" \ + -f force=false diff --git a/.sqlx/query-0c51f68a6dc3a43da8d955530e02acf90e2a3710690f398f9ffaa71e180f1d9e.json b/.sqlx/query-0c51f68a6dc3a43da8d955530e02acf90e2a3710690f398f9ffaa71e180f1d9e.json index c9b9a22..c8fdf29 100644 --- a/.sqlx/query-0c51f68a6dc3a43da8d955530e02acf90e2a3710690f398f9ffaa71e180f1d9e.json +++ b/.sqlx/query-0c51f68a6dc3a43da8d955530e02acf90e2a3710690f398f9ffaa71e180f1d9e.json @@ -6,22 +6,26 @@ { "name": "priority", "ordinal": 0, - "type_info": "Integer" + "type_info": "Integer", + "origin": "Expression" }, { "name": "type", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" }, { "name": "statistic", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" }, { "name": "value", "ordinal": 3, - "type_info": "Float" + "type_info": "Float", + "origin": "Expression" } ], "parameters": { diff --git a/.sqlx/query-1356c17313c5f3491e384d1667106a42fe6918792ae9bea8848a6ec1a3895a76.json b/.sqlx/query-1356c17313c5f3491e384d1667106a42fe6918792ae9bea8848a6ec1a3895a76.json deleted file mode 100644 index 06b7454..0000000 --- a/.sqlx/query-1356c17313c5f3491e384d1667106a42fe6918792ae9bea8848a6ec1a3895a76.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n id,\n status,\n last_result as result\nFROM\n Jobs\nWHERE\n id IN (\n SELECT\n value\n FROM\n json_each(?)\n )\n AND (\n status = 'Done'\n OR (\n status = 'Failed'\n AND attempts >= max_attempts\n )\n OR status = 'Killed'\n )\n", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "status", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "result", - "ordinal": 2, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true - ] - }, - "hash": "1356c17313c5f3491e384d1667106a42fe6918792ae9bea8848a6ec1a3895a76" -} diff --git a/.sqlx/query-1cd760004a2341bbded38a4fa431eaa74232f6f6f3121c7086a0b138195e9b0d.json b/.sqlx/query-1cd760004a2341bbded38a4fa431eaa74232f6f6f3121c7086a0b138195e9b0d.json index c930097..19400f7 100644 --- a/.sqlx/query-1cd760004a2341bbded38a4fa431eaa74232f6f6f3121c7086a0b138195e9b0d.json +++ b/.sqlx/query-1cd760004a2341bbded38a4fa431eaa74232f6f6f3121c7086a0b138195e9b0d.json @@ -6,72 +6,156 @@ { "name": "job", "ordinal": 0, - "type_info": "Blob" + "type_info": "Blob", + "origin": { + "Table": { + "table": "Jobs", + "name": "job" + } + } }, { "name": "id", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "id" + } + } }, { "name": "job_type", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "job_type" + } + } }, { "name": "status", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "status" + } + } }, { "name": "attempts", "ordinal": 4, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "attempts" + } + } }, { "name": "max_attempts", "ordinal": 5, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "max_attempts" + } + } }, { "name": "run_at", "ordinal": 6, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "run_at" + } + } }, { "name": "last_result", "ordinal": 7, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "last_result" + } + } }, { "name": "lock_at", "ordinal": 8, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_at" + } + } }, { "name": "lock_by", "ordinal": 9, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_by" + } + } }, { "name": "done_at", "ordinal": 10, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "done_at" + } + } }, { "name": "priority", "ordinal": 11, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "priority" + } + } }, { "name": "metadata", "ordinal": 12, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "metadata" + } + } }, { "name": "idempotency_key", "ordinal": 13, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "idempotency_key" + } + } } ], "parameters": { diff --git a/.sqlx/query-372f1019c59dddf3cf6389f5a3bf77683c7bf0eb4711c57e3196820d18991cfa.json b/.sqlx/query-372f1019c59dddf3cf6389f5a3bf77683c7bf0eb4711c57e3196820d18991cfa.json new file mode 100644 index 0000000..fd4c1b0 --- /dev/null +++ b/.sqlx/query-372f1019c59dddf3cf6389f5a3bf77683c7bf0eb4711c57e3196820d18991cfa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "WITH j AS (\n SELECT\n value ->> 'task_id' AS task_id,\n value ->> 'attempt' AS attempt,\n value ->> 'result' AS result,\n value ->> 'status' AS status\n FROM\n json_each(?1)\n)\nUPDATE\n Jobs\nSET\n status = j.status,\n attempts = j.attempt,\n last_result = j.result,\n done_at = strftime('%s', 'now')\nFROM\n j\nWHERE\n Jobs.id = j.task_id\n AND Jobs.lock_by = ?2\n", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "372f1019c59dddf3cf6389f5a3bf77683c7bf0eb4711c57e3196820d18991cfa" +} diff --git a/.sqlx/query-390dc1621e4fc1474d101b067e5df63b808a98a67431073781b67161487ab243.json b/.sqlx/query-390dc1621e4fc1474d101b067e5df63b808a98a67431073781b67161487ab243.json new file mode 100644 index 0000000..5a59b0f --- /dev/null +++ b/.sqlx/query-390dc1621e4fc1474d101b067e5df63b808a98a67431073781b67161487ab243.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE\n Jobs\nSET\n status = 'Running',\n lock_at = strftime('%s', 'now'),\n lock_by = ?2\nWHERE\n id IN (\n SELECT\n value\n FROM\n json_each(?1)\n )\n AND (\n status = 'Queued'\n OR status = 'Pending'\n OR (\n status = 'Failed'\n AND attempts < max_attempts\n )\n )\n", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "390dc1621e4fc1474d101b067e5df63b808a98a67431073781b67161487ab243" +} diff --git a/.sqlx/query-40a420986a37c9db2fc35b161092d6063f186242ca1808cfae9d6477c7d8687b.json b/.sqlx/query-40a420986a37c9db2fc35b161092d6063f186242ca1808cfae9d6477c7d8687b.json index ee53bfb..abc056c 100644 --- a/.sqlx/query-40a420986a37c9db2fc35b161092d6063f186242ca1808cfae9d6477c7d8687b.json +++ b/.sqlx/query-40a420986a37c9db2fc35b161092d6063f186242ca1808cfae9d6477c7d8687b.json @@ -6,72 +6,156 @@ { "name": "job", "ordinal": 0, - "type_info": "Blob" + "type_info": "Blob", + "origin": { + "Table": { + "table": "Jobs", + "name": "job" + } + } }, { "name": "id", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "id" + } + } }, { "name": "job_type", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "job_type" + } + } }, { "name": "status", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "status" + } + } }, { "name": "attempts", "ordinal": 4, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "attempts" + } + } }, { "name": "max_attempts", "ordinal": 5, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "max_attempts" + } + } }, { "name": "run_at", "ordinal": 6, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "run_at" + } + } }, { "name": "last_result", "ordinal": 7, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "last_result" + } + } }, { "name": "lock_at", "ordinal": 8, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_at" + } + } }, { "name": "lock_by", "ordinal": 9, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_by" + } + } }, { "name": "done_at", "ordinal": 10, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "done_at" + } + } }, { "name": "priority", "ordinal": 11, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "priority" + } + } }, { "name": "metadata", "ordinal": 12, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "metadata" + } + } }, { "name": "idempotency_key", "ordinal": 13, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "idempotency_key" + } + } } ], "parameters": { diff --git a/.sqlx/query-41198057d67f5386d366f332c349af4519adf779487e4d2e8b7137b79db1b5e7.json b/.sqlx/query-41198057d67f5386d366f332c349af4519adf779487e4d2e8b7137b79db1b5e7.json deleted file mode 100644 index f0c561c..0000000 --- a/.sqlx/query-41198057d67f5386d366f332c349af4519adf779487e4d2e8b7137b79db1b5e7.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE\n Jobs\nSET\n status = 'Running',\n lock_at = strftime('%s', 'now'),\n lock_by = ?2\nWHERE\n id = ?1\n AND (\n status = 'Queued'\n OR status = 'Pending'\n OR (\n status = 'Failed'\n AND attempts < max_attempts\n )\n )\n", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "41198057d67f5386d366f332c349af4519adf779487e4d2e8b7137b79db1b5e7" -} diff --git a/.sqlx/query-41ce7e67750c304b39bb88def9c4dd6e8a3e7ea31ef8d622534a9fa0093c31e2.json b/.sqlx/query-41ce7e67750c304b39bb88def9c4dd6e8a3e7ea31ef8d622534a9fa0093c31e2.json index 5c528f8..96551cb 100644 --- a/.sqlx/query-41ce7e67750c304b39bb88def9c4dd6e8a3e7ea31ef8d622534a9fa0093c31e2.json +++ b/.sqlx/query-41ce7e67750c304b39bb88def9c4dd6e8a3e7ea31ef8d622534a9fa0093c31e2.json @@ -6,32 +6,68 @@ { "name": "id", "ordinal": 0, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "id" + } + } }, { "name": "worker_type", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "worker_type" + } + } }, { "name": "storage_name", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "storage_name" + } + } }, { "name": "layers", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "layers" + } + } }, { "name": "last_seen", "ordinal": 4, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Workers", + "name": "last_seen" + } + } }, { "name": "started_at", "ordinal": 5, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Workers", + "name": "started_at" + } + } } ], "parameters": { diff --git a/.sqlx/query-42042a81a76b1f366a6f421acc329a245d0bb2b84595f87a883c60d6f120cf63.json b/.sqlx/query-42042a81a76b1f366a6f421acc329a245d0bb2b84595f87a883c60d6f120cf63.json index 9aef9ee..f1e37ba 100644 --- a/.sqlx/query-42042a81a76b1f366a6f421acc329a245d0bb2b84595f87a883c60d6f120cf63.json +++ b/.sqlx/query-42042a81a76b1f366a6f421acc329a245d0bb2b84595f87a883c60d6f120cf63.json @@ -6,22 +6,31 @@ { "name": "name", "ordinal": 0, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "job_type" + } + } }, { "name": "stats", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" }, { "name": "workers", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" }, { "name": "activity", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" } ], "parameters": { @@ -29,9 +38,9 @@ }, "nullable": [ false, - false, - false, - false + null, + null, + null ] }, "hash": "42042a81a76b1f366a6f421acc329a245d0bb2b84595f87a883c60d6f120cf63" diff --git a/.sqlx/query-75c53a93669a7ac2141b1bb62af8edce306564754651c4098f8dfe693195f6ca.json b/.sqlx/query-75c53a93669a7ac2141b1bb62af8edce306564754651c4098f8dfe693195f6ca.json index 2d8ca07..200ed3e 100644 --- a/.sqlx/query-75c53a93669a7ac2141b1bb62af8edce306564754651c4098f8dfe693195f6ca.json +++ b/.sqlx/query-75c53a93669a7ac2141b1bb62af8edce306564754651c4098f8dfe693195f6ca.json @@ -6,22 +6,26 @@ { "name": "priority", "ordinal": 0, - "type_info": "Integer" + "type_info": "Integer", + "origin": "Expression" }, { "name": "type", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" }, { "name": "statistic", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": "Expression" }, { "name": "value", "ordinal": 3, - "type_info": "Float" + "type_info": "Float", + "origin": "Expression" } ], "parameters": { diff --git a/.sqlx/query-7bd7de027caa72c93f7f89bd24a5a78685df4c262c2a13eafd9803c3672dcd10.json b/.sqlx/query-7bd7de027caa72c93f7f89bd24a5a78685df4c262c2a13eafd9803c3672dcd10.json index 8be601e..914af95 100644 --- a/.sqlx/query-7bd7de027caa72c93f7f89bd24a5a78685df4c262c2a13eafd9803c3672dcd10.json +++ b/.sqlx/query-7bd7de027caa72c93f7f89bd24a5a78685df4c262c2a13eafd9803c3672dcd10.json @@ -6,32 +6,68 @@ { "name": "id", "ordinal": 0, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "id" + } + } }, { "name": "worker_type", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "worker_type" + } + } }, { "name": "storage_name", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "storage_name" + } + } }, { "name": "layers", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Workers", + "name": "layers" + } + } }, { "name": "last_seen", "ordinal": 4, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Workers", + "name": "last_seen" + } + } }, { "name": "started_at", "ordinal": 5, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Workers", + "name": "started_at" + } + } } ], "parameters": { diff --git a/.sqlx/query-7f32c6cbf20be0cd6e20510d04b010eebb0551753ee259a623a98b886ef13d52.json b/.sqlx/query-7f32c6cbf20be0cd6e20510d04b010eebb0551753ee259a623a98b886ef13d52.json new file mode 100644 index 0000000..991421f --- /dev/null +++ b/.sqlx/query-7f32c6cbf20be0cd6e20510d04b010eebb0551753ee259a623a98b886ef13d52.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id,\n status,\n last_result as result,\n attempts as attempt\nFROM\n Jobs\nWHERE\n id IN (\n SELECT\n value\n FROM\n json_each(?)\n )\n AND (\n status = 'Done'\n OR (\n status = 'Failed'\n AND attempts >= max_attempts\n )\n OR status = 'Killed'\n )\n", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "id" + } + } + }, + { + "name": "status", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "status" + } + } + }, + { + "name": "result", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "last_result" + } + } + }, + { + "name": "attempt", + "ordinal": 3, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "attempts" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false + ] + }, + "hash": "7f32c6cbf20be0cd6e20510d04b010eebb0551753ee259a623a98b886ef13d52" +} diff --git a/.sqlx/query-90406ca41cb79e68f66a300730f85ad7b0a3ddc713341adcbbc959021082c8e9.json b/.sqlx/query-90406ca41cb79e68f66a300730f85ad7b0a3ddc713341adcbbc959021082c8e9.json deleted file mode 100644 index 6dbad87..0000000 --- a/.sqlx/query-90406ca41cb79e68f66a300730f85ad7b0a3ddc713341adcbbc959021082c8e9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE\n Jobs\nSET\n status = ?4,\n attempts = ?2,\n last_result = ?3,\n done_at = strftime('%s', 'now')\nWHERE\n id = ?1\n AND lock_by = ?5\n", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "90406ca41cb79e68f66a300730f85ad7b0a3ddc713341adcbbc959021082c8e9" -} diff --git a/.sqlx/query-a43052e877930a522d6a63789935653f4fe06d10361c555f8ade4b65589520bc.json b/.sqlx/query-a43052e877930a522d6a63789935653f4fe06d10361c555f8ade4b65589520bc.json index d225792..ceef92b 100644 --- a/.sqlx/query-a43052e877930a522d6a63789935653f4fe06d10361c555f8ade4b65589520bc.json +++ b/.sqlx/query-a43052e877930a522d6a63789935653f4fe06d10361c555f8ade4b65589520bc.json @@ -6,72 +6,156 @@ { "name": "job", "ordinal": 0, - "type_info": "Blob" + "type_info": "Blob", + "origin": { + "Table": { + "table": "Jobs", + "name": "job" + } + } }, { "name": "id", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "id" + } + } }, { "name": "job_type", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "job_type" + } + } }, { "name": "status", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "status" + } + } }, { "name": "attempts", "ordinal": 4, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "attempts" + } + } }, { "name": "max_attempts", "ordinal": 5, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "max_attempts" + } + } }, { "name": "run_at", "ordinal": 6, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "run_at" + } + } }, { "name": "last_result", "ordinal": 7, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "last_result" + } + } }, { "name": "lock_at", "ordinal": 8, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_at" + } + } }, { "name": "lock_by", "ordinal": 9, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_by" + } + } }, { "name": "done_at", "ordinal": 10, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "done_at" + } + } }, { "name": "priority", "ordinal": 11, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "priority" + } + } }, { "name": "metadata", "ordinal": 12, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "metadata" + } + } }, { "name": "idempotency_key", "ordinal": 13, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "idempotency_key" + } + } } ], "parameters": { diff --git a/.sqlx/query-add94a70c96b1ca5c6c8d65ee201aa417b28a37e6612f8de7e5525ebdcdab373.json b/.sqlx/query-add94a70c96b1ca5c6c8d65ee201aa417b28a37e6612f8de7e5525ebdcdab373.json new file mode 100644 index 0000000..36a9a1d --- /dev/null +++ b/.sqlx/query-add94a70c96b1ca5c6c8d65ee201aa417b28a37e6612f8de7e5525ebdcdab373.json @@ -0,0 +1,182 @@ +{ + "db_name": "SQLite", + "query": "UPDATE\n Jobs\nSET\n status = 'Queued',\n lock_by = ?1,\n lock_at = strftime('%s', 'now')\nWHERE\n ROWID IN (\n WITH candidates AS (\n SELECT\n *\n FROM\n (\n SELECT\n ROWID,\n priority,\n run_at,\n id\n FROM\n Jobs\n WHERE\n job_type = ?2\n AND status = 'Pending'\n AND lock_by IS NULL\n AND (\n run_at IS NULL\n OR run_at <= ?4\n )\n ORDER BY\n priority DESC,\n run_at ASC,\n id ASC\n LIMIT\n ?3\n )\n UNION\n ALL\n SELECT\n *\n FROM\n (\n SELECT\n ROWID,\n priority,\n run_at,\n id\n FROM\n Jobs\n WHERE\n job_type = ?2\n AND status = 'Failed'\n AND attempts < max_attempts\n AND (\n run_at IS NULL\n OR run_at <= ?4\n )\n ORDER BY\n priority DESC,\n run_at ASC,\n id ASC\n LIMIT\n ?3\n )\n )\n SELECT\n ROWID\n FROM\n candidates\n ORDER BY\n priority DESC,\n run_at ASC,\n id ASC\n LIMIT\n ?3\n ) RETURNING *\n", + "describe": { + "columns": [ + { + "name": "job", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "Jobs", + "name": "job" + } + } + }, + { + "name": "id", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "id" + } + } + }, + { + "name": "job_type", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "job_type" + } + } + }, + { + "name": "status", + "ordinal": 3, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "status" + } + } + }, + { + "name": "attempts", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "attempts" + } + } + }, + { + "name": "max_attempts", + "ordinal": 5, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "max_attempts" + } + } + }, + { + "name": "run_at", + "ordinal": 6, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "run_at" + } + } + }, + { + "name": "last_result", + "ordinal": 7, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "last_result" + } + } + }, + { + "name": "lock_at", + "ordinal": 8, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_at" + } + } + }, + { + "name": "lock_by", + "ordinal": 9, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_by" + } + } + }, + { + "name": "done_at", + "ordinal": 10, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "done_at" + } + } + }, + { + "name": "priority", + "ordinal": 11, + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "priority" + } + } + }, + { + "name": "metadata", + "ordinal": 12, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "metadata" + } + } + }, + { + "name": "idempotency_key", + "ordinal": 13, + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "idempotency_key" + } + } + } + ], + "parameters": { + "Right": 4 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + true, + true + ] + }, + "hash": "add94a70c96b1ca5c6c8d65ee201aa417b28a37e6612f8de7e5525ebdcdab373" +} diff --git a/.sqlx/query-d2a2123b8ede2014f106836cf41b7ec3470a737fa5174e7a2aa16dcf16fc5576.json b/.sqlx/query-c894aafd6bd61741339f3494ce7e0ff587b8d8981a7fbe522ffd5667fdc11fd2.json similarity index 70% rename from .sqlx/query-d2a2123b8ede2014f106836cf41b7ec3470a737fa5174e7a2aa16dcf16fc5576.json rename to .sqlx/query-c894aafd6bd61741339f3494ce7e0ff587b8d8981a7fbe522ffd5667fdc11fd2.json index d2a9814..056058a 100644 --- a/.sqlx/query-d2a2123b8ede2014f106836cf41b7ec3470a737fa5174e7a2aa16dcf16fc5576.json +++ b/.sqlx/query-c894aafd6bd61741339f3494ce7e0ff587b8d8981a7fbe522ffd5667fdc11fd2.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "UPDATE\n Jobs\nSET\n status = \"Pending\",\n done_at = NULL,\n lock_by = NULL,\n lock_at = NULL,\n attempts = attempts + 1,\n last_result = '{\"Err\": \"Re-enqueued due to worker heartbeat timeout.\"}'\nWHERE\n id IN (\n SELECT\n Jobs.id\n FROM\n Jobs\n INNER JOIN Workers ON lock_by = Workers.id\n WHERE\n (\n status = \"Running\"\n OR status = \"Queued\"\n )\n AND strftime('%s', 'now') - Workers.last_seen >= ?1\n AND Workers.worker_type = ?2\n );\n", + "query": "UPDATE\n Jobs\nSET\n status = \"Pending\",\n done_at = NULL,\n lock_by = NULL,\n lock_at = NULL,\n attempts = attempts + 1,\n last_result = '{\"Err\": \"Re-enqueued due to worker heartbeat timeout.\"}'\nWHERE\n id IN (\n SELECT\n Jobs.id\n FROM\n Jobs\n INNER JOIN Workers ON lock_by = Workers.id\n WHERE\n (\n status = \"Running\"\n OR status = \"Queued\"\n )\n AND strftime('%s', 'now') - Workers.last_seen >= ?1\n AND Workers.worker_type = ?2\n AND (?3 IS NULL OR Workers.id != ?3) -- exclude this worker\n AND (?4 IS NULL OR Workers.id = ?4) -- only this worker\n );\n", "describe": { "columns": [], "parameters": { - "Right": 2 + "Right": 4 }, "nullable": [] }, - "hash": "d2a2123b8ede2014f106836cf41b7ec3470a737fa5174e7a2aa16dcf16fc5576" + "hash": "c894aafd6bd61741339f3494ce7e0ff587b8d8981a7fbe522ffd5667fdc11fd2" } diff --git a/.sqlx/query-d7aefe54cd7388c208fff5b946390f217b575f0ca464a5faddd0fe2d51793983.json b/.sqlx/query-d7aefe54cd7388c208fff5b946390f217b575f0ca464a5faddd0fe2d51793983.json deleted file mode 100644 index cf2c0aa..0000000 --- a/.sqlx/query-d7aefe54cd7388c208fff5b946390f217b575f0ca464a5faddd0fe2d51793983.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE Jobs\nSET\n status = 'Queued',\n lock_by = ?1,\n lock_at = strftime('%s', 'now')\nWHERE\n ROWID IN (\n SELECT ROWID\n FROM Jobs\n WHERE job_type = ?2\n AND (\n (status = 'Pending' AND lock_by IS NULL) \n OR \n (status = 'Failed' AND attempts < max_attempts)\n )\n AND (run_at IS NULL OR run_at <= strftime('%s', 'now'))\n ORDER BY priority DESC, run_at ASC, id ASC\n LIMIT ?3\n )\nRETURNING *\n", - "describe": { - "columns": [ - { - "name": "job", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "job_type", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "status", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "attempts", - "ordinal": 4, - "type_info": "Integer" - }, - { - "name": "max_attempts", - "ordinal": 5, - "type_info": "Integer" - }, - { - "name": "run_at", - "ordinal": 6, - "type_info": "Integer" - }, - { - "name": "last_result", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "lock_at", - "ordinal": 8, - "type_info": "Integer" - }, - { - "name": "lock_by", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "done_at", - "ordinal": 10, - "type_info": "Integer" - }, - { - "name": "priority", - "ordinal": 11, - "type_info": "Integer" - }, - { - "name": "metadata", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "idempotency_key", - "ordinal": 13, - "type_info": "Text" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - true - ] - }, - "hash": "d7aefe54cd7388c208fff5b946390f217b575f0ca464a5faddd0fe2d51793983" -} diff --git a/.sqlx/query-e913ec096a6da5c915d86fdc0b6dc012ed036e4e0fadde45dfe544fc95d75466.json b/.sqlx/query-e913ec096a6da5c915d86fdc0b6dc012ed036e4e0fadde45dfe544fc95d75466.json new file mode 100644 index 0000000..28b7dec --- /dev/null +++ b/.sqlx/query-e913ec096a6da5c915d86fdc0b6dc012ed036e4e0fadde45dfe544fc95d75466.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE\n Jobs\nSET\n status = \"Pending\",\n done_at = NULL,\n lock_by = NULL,\n lock_at = NULL,\n attempts = attempts + 1,\n last_result = '{\"Err\": \"Re-enqueued due to worker shutdown\"}'\nWHERE\n id IN (\n SELECT\n Jobs.id\n FROM\n Jobs\n INNER JOIN Workers ON lock_by = Workers.id\n WHERE\n status = \"Queued\"\n AND Workers.worker_type = ?1\n AND Workers.id = ?2\n AND Jobs.id IN (\n SELECT\n value\n FROM\n json_each(?3)\n )\n );\n", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "e913ec096a6da5c915d86fdc0b6dc012ed036e4e0fadde45dfe544fc95d75466" +} diff --git a/.sqlx/query-eed1a436f9bad0f9ee6c46b47580fa7bdc9acef7704fe86466d28fc85126b8e3.json b/.sqlx/query-eed1a436f9bad0f9ee6c46b47580fa7bdc9acef7704fe86466d28fc85126b8e3.json index 4aec34e..1723e10 100644 --- a/.sqlx/query-eed1a436f9bad0f9ee6c46b47580fa7bdc9acef7704fe86466d28fc85126b8e3.json +++ b/.sqlx/query-eed1a436f9bad0f9ee6c46b47580fa7bdc9acef7704fe86466d28fc85126b8e3.json @@ -6,72 +6,156 @@ { "name": "job", "ordinal": 0, - "type_info": "Blob" + "type_info": "Blob", + "origin": { + "Table": { + "table": "Jobs", + "name": "job" + } + } }, { "name": "id", "ordinal": 1, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "id" + } + } }, { "name": "job_type", "ordinal": 2, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "job_type" + } + } }, { "name": "status", "ordinal": 3, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "status" + } + } }, { "name": "attempts", "ordinal": 4, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "attempts" + } + } }, { "name": "max_attempts", "ordinal": 5, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "max_attempts" + } + } }, { "name": "run_at", "ordinal": 6, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "run_at" + } + } }, { "name": "last_result", "ordinal": 7, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "last_result" + } + } }, { "name": "lock_at", "ordinal": 8, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_at" + } + } }, { "name": "lock_by", "ordinal": 9, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "lock_by" + } + } }, { "name": "done_at", "ordinal": 10, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "done_at" + } + } }, { "name": "priority", "ordinal": 11, - "type_info": "Integer" + "type_info": "Integer", + "origin": { + "Table": { + "table": "Jobs", + "name": "priority" + } + } }, { "name": "metadata", "ordinal": 12, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "metadata" + } + } }, { "name": "idempotency_key", "ordinal": 13, - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "Jobs", + "name": "idempotency_key" + } + } } ], "parameters": { @@ -88,7 +172,7 @@ true, true, true, - true, + false, false, true, true diff --git a/CHANGELOG.md b/CHANGELOG.md index f5101e9..c3c0e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- *bump*: to v1.0.0-rc.9 ([#110](https://github.com/apalis-dev/apalis-sqlite/pull/110)) ## [1.0.0-rc.7] - 2026-05-07 - bump: to v1.0.0-rc.8 (#78) diff --git a/Cargo.lock b/Cargo.lock index 4146f52..5e184f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,35 +3,30 @@ version = 4 [[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" +name = "aho-corasick" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ - "libc", + "memchr", ] [[package]] -name = "anyhow" -version = "1.0.102" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "apalis" -version = "1.0.0-rc.9" +version = "1.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7780d1e7082500a4fdb463b0a6fc1c00e4012cd9b2af101c26fcabbb2f390f2c" +checksum = "cc96f8162b465147c3bbf0fe27b3803d468456823d1e2e5db17a3cc9206f1ccc" dependencies = [ "apalis-core", "futures-util", "pin-project", + "serde", "thiserror", "tower", "tracing", @@ -39,11 +34,12 @@ dependencies = [ [[package]] name = "apalis-codec" -version = "0.1.0-rc.9" +version = "0.1.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c506e7f00c7c9c38eeb02290b3ec6328695f0614a257faefaeb8e8286746a665" +checksum = "81f5cac07535fe1fd62ed316a2068c0efd886f41c2b1bf326f5d0d774fa523a3" dependencies = [ "apalis-core", + "bincode", "rmp-serde", "serde", "serde_json", @@ -52,45 +48,34 @@ dependencies = [ [[package]] name = "apalis-core" -version = "1.0.0-rc.9" +version = "1.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797af42a40f6bc297365f2fed187b74d089c63641f57ce2a5e0f629db560cb47" +checksum = "736bb41e6475dafb9489fe67302f787381d5a511dbfa039cfe69e585bda6956a" dependencies = [ + "dashmap", "futures-channel", "futures-core", "futures-sink", "futures-timer", "futures-util", "pin-project", + "rand", "serde", "thiserror", "tower-layer", "tower-service", "tracing", -] - -[[package]] -name = "apalis-sql" -version = "1.0.0-rc.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b555912820da093b004a055105af258df116edcea761db6b759d01aabac2ec" -dependencies = [ - "apalis-core", - "chrono", - "serde", - "serde_json", - "thiserror", - "time", + "ulid", + "uuid", ] [[package]] name = "apalis-sqlite" -version = "1.0.0-rc.8" +version = "1.0.0-rc.9" dependencies = [ "apalis", "apalis-codec", "apalis-core", - "apalis-sql", "apalis-workflow", "async-std", "futures", @@ -102,23 +87,29 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "tracing-subscriber", "ulid", ] [[package]] name = "apalis-workflow" -version = "0.1.0-rc.9" +version = "0.1.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9392c07db462a5c0befec5d6685a37d9ec2424a244b7e7e021208c5c9544cef0" +checksum = "018253861e1cc1c9ec33d7b89bcfc60b38588d653aaab85555b122184cae5faa" dependencies = [ + "apalis-codec", "apalis-core", - "futures", + "dashmap", + "futures-channel", + "futures-core", + "futures-sink", + "futures-util", "petgraph", "serde", + "serde_json", "thiserror", "tower", "tracing", - "ulid", ] [[package]] @@ -208,7 +199,7 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "event-listener-strategy", "pin-project-lite", ] @@ -262,9 +253,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -272,11 +263,31 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "serde_core", ] @@ -292,18 +303,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "blocking" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" dependencies = [ "async-channel 2.5.0", "async-task", @@ -314,9 +325,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -326,15 +337,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.61" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "shlex", @@ -348,27 +359,13 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", + "cpufeatures 0.3.1", + "rand_core", ] [[package]] @@ -413,9 +410,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -437,24 +434,24 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -479,13 +476,17 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.5.8" +name = "dashmap" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ - "powerfmt", - "serde_core", + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] @@ -495,7 +496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "crypto-common 0.1.7", + "crypto-common 0.1.6", ] [[package]] @@ -504,20 +505,20 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] @@ -528,9 +529,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" dependencies = [ "serde", ] @@ -569,11 +570,10 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -584,21 +584,21 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener 5.4.2", "pin-project-lite", ] [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "fixedbitset" @@ -655,9 +655,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -670,9 +670,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -680,15 +680,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -708,9 +708,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -727,38 +727,38 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -773,9 +773,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -794,28 +794,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", + "r-efi", + "rand_core", ] [[package]] @@ -830,6 +816,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -852,15 +844,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ "hashbrown 0.16.1", ] @@ -873,9 +865,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -903,42 +895,18 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -950,9 +918,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -963,9 +931,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -977,16 +945,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -997,15 +966,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1016,12 +985,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -1045,14 +1008,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown 0.17.1", ] [[package]] @@ -1063,13 +1024,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1083,22 +1043,22 @@ dependencies = [ ] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -1113,9 +1073,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -1128,13 +1088,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" dependencies = [ "value-bag", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "md-5" version = "0.11.0" @@ -1147,15 +1116,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -1180,10 +1149,13 @@ dependencies = [ ] [[package]] -name = "num-conv" -version = "0.2.1" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] name = "num-traits" @@ -1202,9 +1174,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags", "cfg-if", @@ -1222,7 +1194,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1233,9 +1205,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -1293,22 +1265,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1336,9 +1308,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "polling" @@ -1356,62 +1328,31 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -1420,58 +1361,46 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.4" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha", - "rand_core 0.9.5", + "chacha20", + "getrandom 0.4.3", + "rand_core", ] [[package]] -name = "rand" +name = "rand_core" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "bitflags", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "regex-automata" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ - "getrandom 0.3.4", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" +name = "regex-syntax" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "ring" @@ -1521,9 +1450,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -1535,18 +1464,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -1555,9 +1484,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "schannel" @@ -1597,17 +1526,11 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1615,29 +1538,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1653,10 +1576,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -1675,15 +1604,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "slab" @@ -1693,18 +1631,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -1712,9 +1650,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -1744,11 +1682,10 @@ dependencies = [ "base64", "bytes", "cfg-if", - "chrono", "crc", "crossbeam-queue", "either", - "event-listener 5.4.1", + "event-listener 5.4.2", "futures-core", "futures-intrusive", "futures-io", @@ -1766,7 +1703,6 @@ dependencies = [ "sha2 0.10.9", "smallvec", "thiserror", - "time", "tokio", "tokio-stream", "tracing", @@ -1784,7 +1720,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn", + "syn 2.0.119", ] [[package]] @@ -1808,7 +1744,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn", + "syn 2.0.119", "thiserror", "tokio", "url", @@ -1823,7 +1759,6 @@ dependencies = [ "bitflags", "byteorder", "bytes", - "chrono", "crc", "digest 0.11.3", "dotenvy", @@ -1838,7 +1773,6 @@ dependencies = [ "sha2 0.11.0", "sqlx-core", "thiserror", - "time", "tracing", ] @@ -1852,7 +1786,6 @@ dependencies = [ "base64", "bitflags", "byteorder", - "chrono", "crc", "dotenvy", "etcetera", @@ -1866,7 +1799,7 @@ dependencies = [ "log", "md-5", "memchr", - "rand 0.10.1", + "rand", "serde", "serde_json", "sha2 0.11.0", @@ -1874,7 +1807,6 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time", "tracing", "whoami", ] @@ -1886,7 +1818,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", - "chrono", "flume", "form_urlencoded", "futures-channel", @@ -1900,7 +1831,6 @@ dependencies = [ "serde", "sqlx-core", "thiserror", - "time", "tracing", "url", ] @@ -1930,9 +1860,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -1947,13 +1888,13 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] @@ -1963,7 +1904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1971,60 +1912,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", + "syn 3.0.5", ] [[package]] -name = "time-core" -version = "0.1.8" +name = "thread_local" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ - "num-conv", - "time-core", + "cfg-if", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -2032,24 +1951,15 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.52.2" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -2062,20 +1972,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -2084,9 +1994,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -2144,7 +2054,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2154,22 +2064,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ulid" -version = "1.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +checksum = "947dde63b6d514cc5e044edad4e0ca7261afd1099d16c83d942cb2b2f348689c" dependencies = [ - "rand 0.9.4", + "rand", "serde", + "uuid", "web-time", ] @@ -2200,18 +2141,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -2230,11 +2171,30 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "value-bag" -version = "1.12.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" [[package]] name = "vcpkg" @@ -2249,34 +2209,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" +name = "virtue" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -2287,9 +2235,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -2297,9 +2245,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2307,60 +2255,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-time" version = "1.1.0" @@ -2373,53 +2287,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" - -[[package]] -name = "windows-core" -version = "0.62.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" [[package]] name = "windows-link" @@ -2427,24 +2306,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2527,111 +2388,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -2640,68 +2407,48 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -2710,9 +2457,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -2721,17 +2468,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 6da01a0..73cbe67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apalis-sqlite" -version = "1.0.0-rc.8" +version = "1.0.0-rc.9" authors = ["Njuguna Mureithi "] readme = "README.md" edition = "2024" @@ -11,19 +11,17 @@ keywords = ["apalis", "background-jobs", "task-queue", "sqlite", "async"] categories = ["asynchronous", "database"] [features] -default = ["tokio-comp", "migrate", "json", "chrono"] +default = ["tokio-comp", "migrate", "json"] migrate = ["sqlx/migrate", "sqlx/macros"] async-std-comp = ["async-std", "sqlx/runtime-async-std", "sqlx/tls-rustls"] async-std-comp-native-tls = [ - "async-std", - "sqlx/runtime-async-std", - "sqlx/tls-native-tls", + "async-std", + "sqlx/runtime-async-std", + "sqlx/tls-native-tls", ] tokio-comp = ["tokio", "sqlx/runtime-tokio", "sqlx/tls-rustls"] tokio-comp-native-tls = ["tokio", "sqlx/runtime-tokio", "sqlx/tls-native-tls"] json = ["apalis-codec/json", "sqlx/json"] -chrono = ["apalis-sql/chrono", "sqlx/chrono"] -time = ["apalis-sql/time", "sqlx/time"] [dependencies.sqlx] version = "0.9.0" @@ -33,37 +31,55 @@ features = ["sqlite"] [dependencies] serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } -apalis-codec = { version = "0.1.0-rc.9", default-features = false } -apalis-core = { version = "1.0.0-rc.9", features = ["sleep"] } -apalis-sql = { version = "1.0.0-rc.9", default-features = false } -log = "0.4.21" -futures = "0.3.30" +apalis-codec = { version = "0.1.0-rc.10", default-features = false } +apalis-core = { version = "1.0.0-rc.10", features = ["sleep", "ulid"] } +log = "0.4.34" +futures = "0.3.34" tokio = { version = "1", features = ["rt", "net"], optional = true } -async-std = { version = "1.13.0", optional = true } -thiserror = "2.0.0" -pin-project = "1.1.10" -ulid = { version = "1", features = ["serde"] } +async-std = { version = "1.13.2", optional = true } +thiserror = "2.0.20" +pin-project = "1.1.13" +ulid = { version = "3", features = ["serde"] } [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -apalis = { version = "1.0.0-rc.9" } -apalis-workflow = { version = "0.1.0-rc.9" } -apalis-codec = { version = "0.1.0-rc.9", features = ["msgpack"] } -futures-util = "0.3.31" +apalis = { version = "1.0.0-rc.10", features = ["retry"] } +apalis-workflow = { version = "0.1.0-rc.10" } +apalis-codec = { version = "0.1.0-rc.10", features = ["msgpack", "bincode"] } +futures-util = "0.3.34" +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } [package.metadata.docs.rs] # defines the configuration attribute `docsrs` rustdoc-args = ["--cfg", "docsrs"] all-features = true -[lints.rust] +[workspace.lints.rust] unsafe_code = "forbid" + rust_2018_idioms = { level = "warn", priority = -1 } missing_debug_implementations = "warn" +missing_docs = "warn" unreachable_pub = "warn" -[lints.clippy] +[workspace.lints.clippy] type_complexity = "allow" + +await_holding_lock = "warn" +dbg_macro = "warn" +empty_enums = "warn" +enum_glob_use = "warn" +equatable_if_let = "warn" +exit = "warn" +filter_map_next = "warn" +fn_params_excessive_bools = "warn" +if_let_mutex = "warn" +implicit_clone = "warn" +imprecise_flops = "warn" +inefficient_to_string = "warn" +linkedlist = "warn" +lossy_float_literal = "warn" +macro_use_imports = "warn" manual_let_else = "warn" match_same_arms = "warn" match_wildcard_for_single_variants = "warn" @@ -75,3 +91,16 @@ needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" option_option = "warn" redundant_clone = "warn" +ref_option = "warn" +rest_pat_in_fully_bound_structs = "warn" +return_self_not_must_use = "warn" +single_match_else = "warn" +str_to_string = "warn" +suboptimal_flops = "warn" +todo = "warn" +trivially_copy_pass_by_ref = "warn" +uninlined_format_args = "warn" +unnested_or_patterns = "warn" +unused_self = "warn" +use_self = "warn" +verbose_file_reads = "warn" diff --git a/README.md b/README.md index 1c678cf..0b64e61 100644 --- a/README.md +++ b/README.md @@ -19,24 +19,12 @@ - **Priority queues** — assign integer priorities so high-urgency jobs are always picked up first. - **Multiple polling strategies** — choose between standard polling and event-driven (hooked) storage to trade latency for CPU usage. - **Multi-step workflows** — chain async steps into pipelines with `apalis-workflow`; each stage only runs if the previous one succeeds. -- **Shared storage** — multiplex multiple job types over a single SQLite connection. +- **Shared storage** — multiple job types over a single SQLite connection. - **Custom codecs** — pluggable serialization/deserialization of job payloads as raw bytes. - **First-class `apalis` integration** — works seamlessly with workers, tower layers, and middleware. --- -## Storage Types - -| Type | Description | -| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| [`SqliteStorage`](https://docs.rs/apalis-sqlite/latest/apalis_sqlite/struct.SqliteStorage.html) | Standard polling-based storage. | -| [`SqliteStorageWithHook`](https://docs.rs/apalis-sqlite/latest/apalis_sqlite/struct.SqliteStorageWithHook.html) | Event-driven storage using SQLite update hooks for low-latency job fetching. | -| [`SharedSqliteStorage`](https://docs.rs/apalis-sqlite/latest/apalis_sqlite/struct.SharedSqliteStorage.html) | Shared storage supporting multiple job types over a single connection. | - -> All types are built on top of `SqliteStorage` with different configurations applied under the hood. - ---- - ## Examples ### Basic Worker @@ -59,7 +47,7 @@ async fn main() { let mut start = 0; let mut items = stream::repeat_with(move || { start += 1; - Task::builder(start) + TaskBuilder::new(start) .run_after(Duration::from_secs(1)) .priority(1) .max_attempts(5) @@ -92,26 +80,29 @@ use futures::stream::{self, StreamExt}; #[tokio::main] async fn main() { - let lazy_strategy = StrategyBuilder::new() - .apply(IntervalStrategy::new(Duration::from_secs(5))) - .build(); - let config = Config::new("queue") - .with_poll_interval(lazy_strategy) - .set_buffer_size(5); - let backend = SqliteStorage::new_with_callback(":memory:", &config); - - let pool = backend.pool(); + let (pool, listener) = SqliteStorage::connect_with_callback(":memory:").unwrap(); + + let config = Config::default() + .queue("queue") + .batch_size(5); + SqliteStorage::setup(&pool).await.unwrap(); + let strategy = Strategy::new() + .stream(listener.ready_chunks(config.batch_size)) + .interval(Duration::from_millis(500)); + + tokio::spawn({ let pool = pool.clone(); let config = config.clone(); + let queue = config.queue.to_string(); async move { tokio::time::sleep(Duration::from_secs(2)).await; let mut start = 0; let items = stream::repeat_with(move || { start += 1; - Task::builder(serde_json::to_vec(&start).unwrap()) + TaskBuilder::new(serde_json::to_vec(&start).unwrap()) .run_after(Duration::from_secs(1)) .priority(start) .build() @@ -119,10 +110,15 @@ async fn main() { .take(20) .collect::>() .await; - apalis_sqlite::sink::push_tasks(pool, config, items).await.unwrap(); + let mut conn = pool.acquire().await.unwrap(); + apalis_sqlite::queries::push_tasks(&mut conn, &queue, &items).await.unwrap(); } }); + let backend = SqliteStorage::new(&pool) + .with_config(config) + .poll_with_strategy(strategy); + async fn send_reminder(item: usize, wrk: WorkerContext) -> Result<(), BoxDynError> { Ok(()) } @@ -134,7 +130,7 @@ async fn main() { } ``` -### Sequential Workflow (Order Fulfilment Pipeline) +### Sequential Workflow (Order Fulfillment Pipeline) Chain async steps to model a real-world order processing pipeline — validating payment, reserving inventory, dispatching a shipment notification, and emailing the customer. @@ -192,7 +188,7 @@ async fn check_stock(item: StockedItem) -> Option { #[tokio::main] async fn main() { - let workflow = Workflow::new("order-fulfilment") + let workflow = SteppedFlow::new("order-fulfillment") // Step 1: Charge payment and return a record of the charged order. .and_then(|order: Order| async move { println!( @@ -272,7 +268,9 @@ async fn main() { let pool = SqlitePool::connect(":memory:").await.unwrap(); SqliteStorage::setup(&pool).await.unwrap(); - let mut sqlite = SqliteStorage::new_in_queue(&pool, "order-fulfilment"); + + let config = Config::default().queue("order-fulfillment"); + let mut sqlite = SqliteStorage::new(&pool).with_config(config); sqlite .push_start(Order { @@ -284,7 +282,7 @@ async fn main() { .await .unwrap(); - let worker = WorkerBuilder::new("fulfilment-worker") + let worker = WorkerBuilder::new("fulfillment-worker") .backend(sqlite) .on_event(|ctx, ev| { println!("Event: {:?}", ev); @@ -338,15 +336,15 @@ async fn collector( async fn main() { let pool = SqlitePool::connect(":memory:").await.unwrap(); SqliteStorage::setup(&pool).await.unwrap(); - let mut backend = SqliteStorage::new(&pool).with_codec::(); + let mut backend = SqliteStorage::new(&pool).with_codec(MsgPackCodec); backend.start_fan_out(vec![42, 43, 44]).await.unwrap(); - let dag_flow = DagFlow::new("user-etl-workflow"); - let get_name = dag_flow.node(get_name); - let get_age = dag_flow.node(get_age); - let get_address = dag_flow.node(get_address); + let dag_flow = GraphFlow::new("user-etl-workflow"); + let get_name = dag_flow.add_task(get_name); + let get_age = dag_flow.add_task(get_age); + let get_address = dag_flow.add_task(get_address); dag_flow - .node(collector) + .add_task(collector) .depends_on((&get_name, &get_age, &get_address)); // Order and types matters here dag_flow.validate().unwrap(); // Ensure DAG is valid @@ -369,16 +367,16 @@ Run multiple job types over a single SQLite connection: use std::{collections::HashMap, time::Duration}; use apalis::prelude::*; -use apalis_sqlite::{SharedSqliteStorage, SqliteStorage}; +use apalis_sqlite::{shared::SqliteStorageFactory, SqliteStorage}; use futures::stream; #[tokio::main] async fn main() { - let mut store = SharedSqliteStorage::new(":memory:"); + let mut store = SqliteStorageFactory::new(":memory:"); SqliteStorage::setup(store.pool()).await.unwrap(); - let mut map_store = store.make_shared().unwrap(); - let mut int_store = store.make_shared().unwrap(); + let mut map_store = store.create().unwrap(); + let mut int_store = store.create().unwrap(); map_store .push_stream(&mut stream::iter(vec![HashMap::::new()])) @@ -386,9 +384,9 @@ async fn main() { .unwrap(); int_store.push(99).await.unwrap(); - async fn send_reminder( + async fn send_reminder( _: T, - _task_id: TaskId, + _task_id: TaskId, wrk: WorkerContext, ) -> Result<(), BoxDynError> { tokio::time::sleep(Duration::from_secs(2)).await; diff --git a/deny.toml b/deny.toml index f555596..69a2328 100644 --- a/deny.toml +++ b/deny.toml @@ -35,8 +35,11 @@ skip-tree = [ { name = "getrandom" }, { name = "hashbrown" }, { name = "rand" }, - { name = "rand_chacha" }, + { name = "async-channel" }, { name = "rand_core" }, + { name = "event-listener" }, + { name = "syn" }, + ] [sources] diff --git a/examples/codec.rs b/examples/codec.rs index 7aedaf7..cecf338 100644 --- a/examples/codec.rs +++ b/examples/codec.rs @@ -7,7 +7,7 @@ use sqlx::SqlitePool; async fn main() { let pool = SqlitePool::connect(":memory:").await.unwrap(); SqliteStorage::setup(&pool).await.unwrap(); - let mut backend = SqliteStorage::new(&pool).with_codec::(); + let mut backend = SqliteStorage::new(&pool).with_codec(MsgPackCodec); backend.push(42).await.unwrap(); async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> { diff --git a/examples/config.rs b/examples/config.rs new file mode 100644 index 0000000..eef803e --- /dev/null +++ b/examples/config.rs @@ -0,0 +1,66 @@ +use std::time::Duration; + +use apalis::{config::WorkerConfig, config::WorkerFromConfig, prelude::*}; +use apalis_sqlite::SqliteStorage; +use sqlx::Row; + +const CONFIG: &str = r#" +{ + "name": "simple-worker", + "backend": { + "queue": "example-queue", + "lock_task": false, + "database_url": ":memory:" + }, + "middleware": [ + "CatchPanic", + "Tracing" + ] +} +"#; + +type Config = WorkerConfig>; + +async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> { + apalis_core::timer::sleep(std::time::Duration::from_secs(1)).await; + assert_eq!(task, 42); + worker.stop()?; + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), BoxDynError> { + let config: Config = serde_json::from_str(CONFIG).unwrap(); + + let worker = WorkerBuilder::try_config(config)? + .map_backend(|backend| { + backend + .after_start(|b| { + let mut b = b.clone(); + async move { + b.push(42).await.unwrap(); + Ok(()) + } + }) + .after_stop(|b| { + let pool = b.pool().clone(); + async move { + let jobs_count = sqlx::query("SELECT count(*) FROM Jobs") + .fetch_one(&pool) + .await?; + assert_eq!( + jobs_count.get::(0), + 1, + "There should be only one job" + ); + Ok(()) + } + }) + .poll_with_interval(Duration::from_millis(100)) + }) + .build(task); + + worker.run().await?; + + Ok(()) +} diff --git a/examples/etl_workflow.rs b/examples/etl_workflow.rs index cf08cb8..74307fd 100644 --- a/examples/etl_workflow.rs +++ b/examples/etl_workflow.rs @@ -2,7 +2,7 @@ use std::time::Duration; use apalis::prelude::*; use apalis_codec::msgpack::MsgPackCodec; -use apalis_sqlite::SqliteStorage; +use apalis_sqlite::{Config, SqliteStorage}; use apalis_workflow::*; use sqlx::SqlitePool; @@ -33,24 +33,29 @@ async fn collector( async fn main() { let pool = SqlitePool::connect(":memory:").await.unwrap(); SqliteStorage::setup(&pool).await.unwrap(); - let mut backend = SqliteStorage::new(&pool).with_codec::(); + let config = Config::default().queue("user-etl-workflow"); + let mut backend = SqliteStorage::new(&pool) + .with_config(config) + .with_codec(MsgPackCodec) + .poll_with_interval(Duration::from_secs(1)); + backend.start_fan_out(vec![42, 43, 44]).await.unwrap(); - let dag_flow = DagFlow::new("user-etl-workflow"); - let get_name = dag_flow.node(get_name); - let get_age = dag_flow.node(get_age); - let get_address = dag_flow.node(get_address); - dag_flow - .node(collector) + let graph = GraphFlow::new("user-etl-workflow"); + let get_name = graph.add_task(get_name); + let get_age = graph.add_task(get_age); + let get_address = graph.add_task(get_address); + graph + .add_task(collector) .depends_on((&get_name, &get_age, &get_address)); // Order and types matters here - dag_flow.validate().unwrap(); // Ensure DAG is valid + graph.validate().unwrap(); // Ensure DAG is valid let worker = WorkerBuilder::new("rango-tango") .backend(backend) .on_event(|_c, e| { println!("{e:?},"); }) - .build(dag_flow); + .build(graph); worker.run().await.unwrap(); } diff --git a/examples/order_workflow.rs b/examples/order_workflow.rs index f54d67a..9c53655 100644 --- a/examples/order_workflow.rs +++ b/examples/order_workflow.rs @@ -65,7 +65,6 @@ struct ShipmentService { impl ShipmentService { async fn dispatch(&self, order_id: i64, item_count: i64) -> Result { - // Submit shipment and return a tracking number. Ok(format!("dispatch {item_count} items for order {order_id}")) } } @@ -82,7 +81,6 @@ impl EmailService { order_id: i64, tracking_number: &str, ) -> Result<(), BoxDynError> { - // Send a transactional email via your provider. println!("email {to} about order {order_id} with tracking {tracking_number}"); Ok(()) } @@ -167,7 +165,7 @@ async fn refund_if_all_unavailable( .execute(&*db) .await?; - Ok(OrderJob { order_id: order_id }) + Ok(OrderJob { order_id }) } async fn dispatch_shipment( @@ -206,6 +204,7 @@ async fn notify_customer( job: OrderJob, db: Data, email: Data, + worker: WorkerContext, ) -> Result<(), BoxDynError> { let order: OrderRecord = sqlx::query_as("SELECT * FROM orders WHERE id = ?") .bind(job.order_id) @@ -227,6 +226,8 @@ async fn notify_customer( .execute(&*db) .await?; + worker.stop()?; + Ok(()) } @@ -303,17 +304,14 @@ async fn main() { .await .unwrap(); - let mut sqlite = SqliteStorage::new_in_queue(&pool, "order-fulfilment"); - sqlite - .push_start(OrderJob { order_id: 1001 }) - .await - .unwrap(); + let mut sqlite = SqliteStorage::new(&pool).poll_with_interval(Duration::from_secs(1)); + sqlite.push(OrderJob { order_id: 1001 }).await.unwrap(); - let workflow = Workflow::new("order-fulfilment") + let workflow = SteppedFlow::new("order-fulfilment") .and_then(charge_payment) .filter_map(filter_unavailable) .and_then(refund_if_all_unavailable) - .delay_for(Duration::from_millis(500)) + .delay_for(Duration::from_millis(5000)) .and_then(dispatch_shipment) .and_then(notify_customer); diff --git a/examples/realtime.rs b/examples/realtime.rs index 21b6907..a284102 100644 --- a/examples/realtime.rs +++ b/examples/realtime.rs @@ -1,24 +1,53 @@ -use apalis::prelude::*; +use std::time::Duration; + +use apalis::{layers::tracing::info, prelude::*}; use apalis_sqlite::{Config, SqliteStorage}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() { - let config = Config::new("realtime-queue"); - let mut backend = SqliteStorage::new_with_callback(":memory:", &config); + use tracing_subscriber::{EnvFilter, fmt}; + let fmt_layer = fmt::layer(); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("debug,sqlx=off")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .init(); + + let config = Config::default().queue("realtime-queue").lock_tasks(false); + let (pool, listener) = SqliteStorage::connect_with_callback(":memory:").unwrap(); + SqliteStorage::setup(&pool).await.unwrap(); - let pool = backend.pool(); - SqliteStorage::setup(pool).await.unwrap(); + let backend = SqliteStorage::new(&pool) + .with_config(config) + .poll_with_stream(listener); - backend.push(42).await.unwrap(); + let mut b = backend.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(1000)).await; + let mut iters = 0; + loop { + iters += 1; + tokio::time::sleep(Duration::from_millis(1000)).await; + b.push(iters).await.unwrap(); + if iters == 10 { + break; + } + } + }); async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> { - apalis_core::timer::sleep(std::time::Duration::from_secs(1)).await; - assert_eq!(task, 42); - worker.stop()?; + if task == 10 { + worker.stop()?; + } Ok(()) } let worker = WorkerBuilder::new("rango-tango") .backend(backend) + .on_event(|_, ev| info!("{ev:?}")) .build(task); worker.run().await.unwrap(); } diff --git a/examples/shared.rs b/examples/shared.rs index c05b72b..53cb5ec 100644 --- a/examples/shared.rs +++ b/examples/shared.rs @@ -1,19 +1,19 @@ use std::{collections::HashMap, time::Duration}; use apalis::prelude::*; -use apalis_sqlite::{SharedSqliteStorage, SqliteStorage}; +use apalis_sqlite::{SqliteStorage, shared::SqliteStorageFactory}; use futures::stream; #[tokio::main] async fn main() { - let mut store = SharedSqliteStorage::new(":memory:"); + let mut store = SqliteStorageFactory::new(":memory:"); SqliteStorage::setup(store.pool()).await.unwrap(); - let mut map_store = store.make_shared().unwrap(); + let mut map_store = store.create().unwrap(); - let mut int_store = store.make_shared().unwrap(); + let mut int_store = store.create().unwrap(); map_store .push_stream(&mut stream::iter(vec![HashMap::::new()])) @@ -21,9 +21,9 @@ async fn main() { .unwrap(); int_store.push(99).await.unwrap(); - async fn send_reminder( + async fn send_reminder( _: T, - _task_id: TaskId, + _task_id: TaskId, wrk: WorkerContext, ) -> Result<(), BoxDynError> { tokio::time::sleep(Duration::from_secs(2)).await; diff --git a/examples/unique_jobs.rs b/examples/unique_jobs.rs index e491566..b300e7b 100644 --- a/examples/unique_jobs.rs +++ b/examples/unique_jobs.rs @@ -10,13 +10,9 @@ async fn main() { SqliteStorage::setup(&pool).await.unwrap(); let mut backend = SqliteStorage::new(&pool); - let task_1 = TaskBuilder::new(42) - .with_idempotency_key(dedupe_key) - .build(); + let task_1 = TaskBuilder::new(42).idempotency_key(dedupe_key).build(); - let task_2 = TaskBuilder::new(43) - .with_idempotency_key(dedupe_key) - .build(); + let task_2 = TaskBuilder::new(43).idempotency_key(dedupe_key).build(); backend.push_task(task_1).await.unwrap(); backend.push_task(task_2).await.unwrap(); diff --git a/migrations/20260726081359_add_fetch_next_indices.sql b/migrations/20260726081359_add_fetch_next_indices.sql new file mode 100644 index 0000000..574973d --- /dev/null +++ b/migrations/20260726081359_add_fetch_next_indices.sql @@ -0,0 +1,11 @@ +DROP INDEX IF EXISTS idx_jobs_pending; + +CREATE INDEX IF NOT EXISTS idx_jobs_pending ON Jobs (job_type, priority DESC, run_at ASC, id ASC) +WHERE + status = 'Pending' + AND lock_by IS NULL; + +CREATE INDEX IF NOT EXISTS idx_jobs_failed ON Jobs (job_type, priority DESC, run_at ASC, id ASC) +WHERE + status = 'Failed' + AND attempts < max_attempts; diff --git a/migrations/20260803223031_add_lock_indices.sql b/migrations/20260803223031_add_lock_indices.sql new file mode 100644 index 0000000..32a050e --- /dev/null +++ b/migrations/20260803223031_add_lock_indices.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS idx_jobs_claimable ON Jobs(status, attempts) +WHERE + status IN ('Queued', 'Pending', 'Failed'); diff --git a/migrations/20260804103122_drop_unneeded_indices.sql b/migrations/20260804103122_drop_unneeded_indices.sql new file mode 100644 index 0000000..76f0d33 --- /dev/null +++ b/migrations/20260804103122_drop_unneeded_indices.sql @@ -0,0 +1,11 @@ +DROP INDEX IF EXISTS TIdx; + +DROP INDEX IF EXISTS SIdx; + +DROP INDEX IF EXISTS JTIdx; + +DROP INDEX IF EXISTS idx_jobs_run_at_status; + +DROP INDEX IF EXISTS idx_jobs_job_type_run_at; + +DROP INDEX IF EXISTS idx_jobs_job_type_covering; diff --git a/queries/backend/fetch_completed_tasks.sql b/queries/backend/fetch_completed_tasks.sql index bc27097..3cf8e5e 100644 --- a/queries/backend/fetch_completed_tasks.sql +++ b/queries/backend/fetch_completed_tasks.sql @@ -1,7 +1,8 @@ SELECT id, status, - last_result as result + last_result as result, + attempts as attempt FROM Jobs WHERE diff --git a/queries/backend/fetch_next.sql b/queries/backend/fetch_next.sql index f2552f9..2087b5e 100644 --- a/queries/backend/fetch_next.sql +++ b/queries/backend/fetch_next.sql @@ -1,20 +1,75 @@ -UPDATE Jobs +UPDATE + Jobs SET status = 'Queued', lock_by = ?1, lock_at = strftime('%s', 'now') WHERE ROWID IN ( - SELECT ROWID - FROM Jobs - WHERE job_type = ?2 - AND ( - (status = 'Pending' AND lock_by IS NULL) - OR - (status = 'Failed' AND attempts < max_attempts) + WITH candidates AS ( + SELECT + * + FROM + ( + SELECT + ROWID, + priority, + run_at, + id + FROM + Jobs + WHERE + job_type = ?2 + AND status = 'Pending' + AND lock_by IS NULL + AND ( + run_at IS NULL + OR run_at <= ?4 + ) + ORDER BY + priority DESC, + run_at ASC, + id ASC + LIMIT + ?3 ) - AND (run_at IS NULL OR run_at <= strftime('%s', 'now')) - ORDER BY priority DESC, run_at ASC, id ASC - LIMIT ?3 - ) -RETURNING * + UNION + ALL + SELECT + * + FROM + ( + SELECT + ROWID, + priority, + run_at, + id + FROM + Jobs + WHERE + job_type = ?2 + AND status = 'Failed' + AND attempts < max_attempts + AND ( + run_at IS NULL + OR run_at <= ?4 + ) + ORDER BY + priority DESC, + run_at ASC, + id ASC + LIMIT + ?3 + ) + ) + SELECT + ROWID + FROM + candidates + ORDER BY + priority DESC, + run_at ASC, + id ASC + LIMIT + ?3 + ) RETURNING * diff --git a/queries/backend/reenqueue_abandoned.sql b/queries/backend/reenqueue_abandoned.sql new file mode 100644 index 0000000..a1beeb6 --- /dev/null +++ b/queries/backend/reenqueue_abandoned.sql @@ -0,0 +1,27 @@ +UPDATE + Jobs +SET + status = "Pending", + done_at = NULL, + lock_by = NULL, + lock_at = NULL, + attempts = attempts + 1, + last_result = '{"Err": "Re-enqueued due to worker shutdown"}' +WHERE + id IN ( + SELECT + Jobs.id + FROM + Jobs + INNER JOIN Workers ON lock_by = Workers.id + WHERE + status = "Queued" + AND Workers.worker_type = ?1 + AND Workers.id = ?2 + AND Jobs.id IN ( + SELECT + value + FROM + json_each(?3) + ) + ); diff --git a/queries/backend/reenqueue_orphaned.sql b/queries/backend/reenqueue_orphaned.sql index 3e09593..d13fc82 100644 --- a/queries/backend/reenqueue_orphaned.sql +++ b/queries/backend/reenqueue_orphaned.sql @@ -21,4 +21,6 @@ WHERE ) AND strftime('%s', 'now') - Workers.last_seen >= ?1 AND Workers.worker_type = ?2 + AND (?3 IS NULL OR Workers.id != ?3) -- exclude this worker + AND (?4 IS NULL OR Workers.id = ?4) -- only this worker ); diff --git a/queries/task/ack.sql b/queries/task/ack.sql index 3089f74..c4a9f7d 100644 --- a/queries/task/ack.sql +++ b/queries/task/ack.sql @@ -1,10 +1,21 @@ +WITH j AS ( + SELECT + value ->> 'task_id' AS task_id, + value ->> 'attempt' AS attempt, + value ->> 'result' AS result, + value ->> 'status' AS status + FROM + json_each(?1) +) UPDATE Jobs SET - status = ?4, - attempts = ?2, - last_result = ?3, + status = j.status, + attempts = j.attempt, + last_result = j.result, done_at = strftime('%s', 'now') +FROM + j WHERE - id = ?1 - AND lock_by = ?5 + Jobs.id = j.task_id + AND Jobs.lock_by = ?2 diff --git a/queries/task/lock.sql b/queries/task/lock.sql index d2807b6..dc56c84 100644 --- a/queries/task/lock.sql +++ b/queries/task/lock.sql @@ -5,7 +5,12 @@ SET lock_at = strftime('%s', 'now'), lock_by = ?2 WHERE - id = ?1 + id IN ( + SELECT + value + FROM + json_each(?1) + ) AND ( status = 'Queued' OR status = 'Pending' diff --git a/src/ack.rs b/src/ack.rs deleted file mode 100644 index 7c11d2b..0000000 --- a/src/ack.rs +++ /dev/null @@ -1,135 +0,0 @@ -use apalis_core::{ - error::BoxDynError, - layers::{Layer, Service}, - task::Parts, - worker::{context::WorkerContext, ext::ack::Acknowledge}, -}; -use futures::{FutureExt, future::BoxFuture}; -use serde::Serialize; -use sqlx::SqlitePool; -use ulid::Ulid; - -use crate::{ - SqliteContext, SqliteTask, - queries::{ - ack_task::{ack_task, calculate_status}, - lock_task::lock_task, - }, -}; - -#[derive(Clone, Debug)] -pub struct SqliteAck { - pool: SqlitePool, -} -impl SqliteAck { - pub fn new(pool: SqlitePool) -> Self { - Self { pool } - } -} - -impl Acknowledge for SqliteAck { - type Error = sqlx::Error; - type Future = BoxFuture<'static, Result<(), Self::Error>>; - fn ack( - &mut self, - res: &Result, - parts: &Parts, - ) -> Self::Future { - let task_id = parts.task_id; - let worker_id = parts.ctx.lock_by().clone(); - - // Workflows need special handling to serialize the response correctly - let response = serde_json::to_string(&res.as_ref().map_err(|e| e.to_string())); - - let status = calculate_status(parts, res); - parts.status.store(status.clone()); - let attempt = parts.attempt.current() as i32; - let pool = self.pool.clone(); - let res = response.map_err(|e| sqlx::Error::Decode(e.into())); - async move { - let task_id = task_id - .ok_or(sqlx::Error::ColumnNotFound("TASK_ID_FOR_ACK".to_owned()))? - .to_string(); - let worker_id = - worker_id.ok_or(sqlx::Error::ColumnNotFound("WORKER_ID_LOCK_BY".to_owned()))?; - let res_ok = res?; - ack_task(&pool, &task_id, &worker_id, &res_ok, &status, attempt).await?; - Ok(()) - } - .boxed() - } -} - -#[derive(Clone, Debug)] -pub struct LockTaskLayer { - pool: SqlitePool, -} - -impl LockTaskLayer { - pub fn new(pool: SqlitePool) -> Self { - Self { pool } - } -} - -impl Layer for LockTaskLayer { - type Service = LockTaskService; - - fn layer(&self, inner: S) -> Self::Service { - LockTaskService { - inner, - pool: self.pool.clone(), - } - } -} - -#[derive(Clone, Debug)] -pub struct LockTaskService { - inner: S, - pool: SqlitePool, -} - -impl Service> for LockTaskService -where - S: Service> + Send + 'static, - S::Future: Send + 'static, - S::Error: Into, - Args: Send + 'static, -{ - type Response = S::Response; - type Error = BoxDynError; - type Future = BoxFuture<'static, Result>; - - fn poll_ready( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.inner.poll_ready(cx).map_err(|e| e.into()) - } - - fn call(&mut self, mut req: SqliteTask) -> Self::Future { - let pool = self.pool.clone(); - let worker_id = req - .parts - .data - .get::() - .map(|w| w.name().to_owned()) - .unwrap(); - let parts = &req.parts; - let task_id = match &parts.task_id { - Some(id) => *id.inner(), - None => { - return async { - Err(sqlx::Error::ColumnNotFound("TASK_ID_FOR_LOCK".to_owned()).into()) - } - .boxed(); - } - }; - req.parts.ctx = req.parts.ctx.with_lock_by(Some(worker_id.clone())); - let fut = self.inner.call(req); - async move { - lock_task(&pool, &task_id.to_string(), &worker_id).await?; - fut.await.map_err(|e| e.into()) - } - .boxed() - } -} diff --git a/src/callback.rs b/src/callback.rs index 6691de0..2183fb2 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -1,10 +1,27 @@ +//! Callbacks allow realtime listeners for new jobs +//! +//! ## Example usage +//! +//! ```ignore +//! let (pool, callback) = SqliteStorage::connect_with_callback(url).unwrap(); +//! SqliteStorage::setup(&pool).await.unwrap(); +//! let backend = SqliteStorage::new(&pool).with_callback(callback); +//! ``` +use apalis_core::backend::ext::poll_strategy::{PollWith, StreamStrategy}; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::{Stream, StreamExt}; use sqlx::sqlite::{SqliteOperation, UpdateHookResult}; + use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; +use crate::{JOBS_TABLE, SqliteStorage}; + +/// An [SqliteStorage] that polls when [HookCallbackListener] is invoked +pub type SqliteStorageWithHook = + PollWith, StreamStrategy>; + /// Database event emitted by SQLite update hook #[derive(Debug)] pub struct DbEvent { @@ -42,13 +59,14 @@ impl DbEvent { // Callback for SQLite update hook pub(crate) fn update_hook_callback(event: UpdateHookResult<'_>, tx: &mut UnboundedSender) { - tx.start_send(DbEvent { - op: event.operation, - db_name: event.database.to_owned(), - table_name: event.table.to_owned(), - rowid: event.rowid, - }) - .unwrap(); + if event.operation == SqliteOperation::Insert && event.table == JOBS_TABLE { + let _ = tx.start_send(DbEvent { + op: event.operation, + db_name: event.database.to_owned(), + table_name: event.table.to_owned(), + rowid: event.rowid, + }); + } } /// Listener for database events emitted by SQLite update hook @@ -68,9 +86,21 @@ impl HookCallbackListener { } impl Stream for HookCallbackListener { - type Item = DbEvent; + type Item = (); fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.rx.lock().unwrap().poll_next_unpin(cx) + log::trace!("HookCallbackListener: poll_next"); + match self.rx.lock().unwrap().poll_next_unpin(cx) { + Poll::Ready(Some(_)) => Poll::Ready(Some(())), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + .map(|s| { + log::trace!( + "HookCallbackListener: poll_ready: {ready}", + ready = s.is_some() + ); + s + }) } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..23300d5 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,246 @@ +use std::time::Duration; + +use apalis_core::backend::queue::Queue; +use serde::{Deserialize, Serialize}; + +/// Configuration for a worker's queue, batching, and liveness detection. +/// +/// `Config` controls how jobs are fetched from a queue and how worker +/// liveness is monitored. +/// +/// # Defaults +/// +/// - `batch_size`: `10` +/// - `heartbeat_interval`: `30` seconds +/// - `missed_heartbeats`: `2` +/// - `queue`: `"default"` +/// - `database_url`: `None` +/// - `lock_tasks`: `true` +/// - `persist_results`: `true` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Config { + /// The maximum number of jobs fetched in a single batch. + /// + /// Must be greater than zero. + #[serde(default = "default_batch_size")] + pub batch_size: usize, + + /// The interval between worker heartbeats. + #[serde(default = "default_heartbeat_interval")] + pub heartbeat_interval: Duration, + + /// The number of missed heartbeats allowed before a worker is + /// considered dead. + #[serde(default = "default_missed_heartbeats")] + pub missed_heartbeats: usize, + + /// The queue from which jobs are consumed. + pub queue: Queue, + + /// An optional database URL used by the worker. + pub database_url: Option, + + /// Whether tasks should be locked while being processed. + #[serde(default = "default_events")] + pub lock_tasks: bool, + + /// Whether job results should be persisted. + #[serde(default = "default_events")] + pub persist_results: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + batch_size: 10, + heartbeat_interval: Duration::from_secs(30), + missed_heartbeats: 2, + queue: Queue::from("default"), + database_url: None, + lock_tasks: true, + persist_results: true, + } + } +} + +fn default_batch_size() -> usize { + 10 +} + +fn default_heartbeat_interval() -> Duration { + Duration::from_secs(30) +} + +fn default_missed_heartbeats() -> usize { + 2 +} + +fn default_events() -> bool { + true +} + +impl Config { + /// Sets the maximum number of jobs to fetch in a single batch. + /// + /// Larger batches can improve throughput by reducing the number of + /// queue operations, while smaller batches can reduce memory usage + /// and improve job distribution between workers. + /// + /// # Panics + /// + /// Panics if `size` is `0`. + /// + /// # Examples + /// + /// ``` + /// use apalis_sqlite::Config; + /// + /// let config = Config::default().batch_size(50); + /// + /// assert_eq!(config.batch_size, 50); + /// ``` + #[must_use] + pub fn batch_size(mut self, size: usize) -> Self { + assert!(size > 0, "batch size cannot be 0"); + self.batch_size = size; + self + } + + /// Sets the interval between worker heartbeats. + /// + /// A shorter interval detects failed workers sooner but produces + /// heartbeat activity more frequently. + /// + /// # Examples + /// + /// ``` + /// use std::time::Duration; + /// use apalis_sqlite::Config; + /// + /// let config = Config::default() + /// .heartbeat_interval(Duration::from_secs(15)); + /// + /// assert_eq!(config.heartbeat_interval, Duration::from_secs(15)); + /// ``` + #[must_use] + pub fn heartbeat_interval(mut self, interval: Duration) -> Self { + self.heartbeat_interval = interval; + self + } + + /// Sets the queue from which jobs are consumed. + /// + /// # Examples + /// + /// ``` + /// # use apalis_sqlite::Config; + /// let config = Config::default() + /// .queue("high-priority"); + /// + /// assert_eq!(config.queue.as_ref(), "high-priority"); + /// ``` + #[must_use] + pub fn queue(mut self, queue: impl AsRef) -> Self { + self.queue = Queue::from(queue.as_ref()); + self + } + + /// Sets the number of missed heartbeats allowed before a worker is + /// considered dead. + /// + /// This value works together with [`Self::heartbeat_interval`]. + /// For example, a 30-second heartbeat interval with `2` missed + /// heartbeats results in an orphan timeout of 60 seconds. + /// + /// # Examples + /// + /// ``` + /// use apalis_sqlite::Config; + /// + /// let config = Config::default().missed_heartbeats(3); + /// + /// assert_eq!(config.missed_heartbeats, 3); + /// assert_eq!( + /// config.orphaned_duration(), + /// std::time::Duration::from_secs(90) + /// ); + /// ``` + #[must_use] + pub fn missed_heartbeats(mut self, missed_heartbeats: usize) -> Self { + self.missed_heartbeats = missed_heartbeats; + self + } + + /// Sets the database URL used by the worker. + /// + /// # Examples + /// + /// ``` + /// use apalis_sqlite::Config; + /// + /// let config = Config::default() + /// .database_url(":memory:"); + /// + /// assert_eq!( + /// config.database_url.as_deref(), + /// Some(":memory:") + /// ); + /// ``` + #[must_use] + pub fn database_url(mut self, database_url: impl Into) -> Self { + self.database_url = Some(database_url.into()); + self + } + + /// Enables or disables task locking. + /// + /// When enabled, tasks are locked while being processed to prevent + /// multiple workers from processing the same task concurrently. + /// + /// # Examples + /// + /// ``` + /// use apalis_sqlite::Config; + /// + /// let config = Config::default().lock_tasks(false); + /// + /// assert!(!config.lock_tasks); + /// ``` + #[must_use] + pub fn lock_tasks(mut self, lock_tasks: bool) -> Self { + self.lock_tasks = lock_tasks; + self + } + + /// Enables or disables result persistence. + /// + /// When enabled, results produced by completed jobs are persisted. + /// + /// # Examples + /// + /// ``` + /// use apalis_sqlite::Config; + /// + /// let config = Config::default().persist_results(false); + /// + /// assert!(!config.persist_results); + /// ``` + #[must_use] + pub fn persist_results(mut self, persist_results: bool) -> Self { + self.persist_results = persist_results; + self + } + + /// Returns the amount of time after which a worker may be considered + /// orphaned. + /// + /// The duration is calculated as: + /// + /// ```text + /// heartbeat_interval × missed_heartbeats + /// ``` + #[must_use] + pub fn orphaned_duration(&self) -> Duration { + self.heartbeat_interval * self.missed_heartbeats as u32 + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..70de026 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,26 @@ +use super::SqlxError; +use apalis_core::task::{status::StatusError, task_id::TaskIdError}; +/// Represents a wrapper for errors encountered on this crate +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Inner engine error + #[error(transparent)] + Database(#[from] SqlxError), + /// Error handling json + #[error("JsonError: {0}")] + JsonError(serde_json::Error), + /// Reenqueue Mismatch error + #[error("ReenqueueMismatch: Queued [{queued}] , Abandoned[{abandoned}] ")] + ReenqueueMismatch { + /// The db count + queued: usize, + /// The workers count + abandoned: usize, + }, + /// Error decoding the task_id + #[error("TaskIdError: {0}")] + TaskIdError(TaskIdError), + /// Error decoding the task status + #[error("StatusError: {0}")] + StatusError(StatusError), +} diff --git a/src/fetcher.rs b/src/fetcher.rs deleted file mode 100644 index 97952d9..0000000 --- a/src/fetcher.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::{ - collections::VecDeque, - marker::PhantomData, - pin::Pin, - sync::{Arc, atomic::AtomicUsize}, - task::{Context, Poll}, -}; - -use apalis_core::{ - backend::poll_strategy::{PollContext, PollStrategyExt}, - task::Task, - worker::context::WorkerContext, -}; -use apalis_sql::TaskRow; -use futures::{FutureExt, future::BoxFuture, stream::Stream}; -use pin_project::pin_project; -use sqlx::{Pool, Sqlite, SqlitePool}; -use ulid::Ulid; - -use crate::{CompactType, Config, SqliteContext, SqliteTask, from_row::SqliteTaskRow}; - -/// Fetch the next batch of tasks from the sqlite backend -pub async fn fetch_next( - pool: SqlitePool, - config: Config, - worker: WorkerContext, -) -> Result>, sqlx::Error> -where -{ - let job_type = config.queue().to_string(); - let buffer_size = config.buffer_size() as i32; - let worker = worker.name().clone(); - sqlx::query_file_as!( - SqliteTaskRow, - "queries/backend/fetch_next.sql", - worker, - job_type, - buffer_size - ) - .fetch_all(&pool) - .await? - .into_iter() - .map(|r| { - let row: TaskRow = r.try_into()?; - row.try_into_task_compact() - .map_err(|e| sqlx::Error::Protocol(e.to_string())) - }) - .collect() -} - -enum StreamState { - Ready, - Delay, - Fetch(BoxFuture<'static, Result>, sqlx::Error>>), - Buffered(VecDeque>), - Empty, -} - -/// Dispatcher for fetching tasks from a SQLite backend via [SqlitePollFetcher] -#[derive(Clone, Debug)] -pub struct SqliteFetcher; -/// Polling-based fetcher for retrieving tasks from a SQLite backend -#[pin_project] -pub struct SqlitePollFetcher { - pool: SqlitePool, - config: Config, - wrk: WorkerContext, - _marker: PhantomData<(Compact, Decode)>, - #[pin] - state: StreamState, - - #[pin] - delay_stream: Option + Send>>>, - - prev_count: Arc, -} - -impl std::fmt::Debug for SqlitePollFetcher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SqlitePollFetcher") - .field("pool", &self.pool) - .field("config", &self.config) - .field("wrk", &self.wrk) - .field("_marker", &self._marker) - .field("prev_count", &self.prev_count) - .finish() - } -} - -impl Clone for SqlitePollFetcher { - fn clone(&self) -> Self { - Self { - pool: self.pool.clone(), - config: self.config.clone(), - wrk: self.wrk.clone(), - _marker: PhantomData, - state: StreamState::Ready, - delay_stream: None, - prev_count: Arc::new(AtomicUsize::new(0)), - } - } -} - -impl SqlitePollFetcher { - /// Create a new SqlitePollFetcher - #[must_use] - pub fn new(pool: &Pool, config: &Config, wrk: &WorkerContext) -> Self { - Self { - pool: pool.clone(), - config: config.clone(), - wrk: wrk.clone(), - _marker: PhantomData, - state: StreamState::Ready, - delay_stream: None, - prev_count: Arc::new(AtomicUsize::new(0)), - } - } -} - -impl Stream for SqlitePollFetcher { - type Item = Result>, sqlx::Error>; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - if this.delay_stream.is_none() { - let strategy = this - .config - .poll_strategy() - .clone() - .build_stream(&PollContext::new(this.wrk.clone(), this.prev_count.clone())); - this.delay_stream = Some(Box::pin(strategy)); - } - - loop { - match this.state { - StreamState::Ready => { - let stream = - fetch_next(this.pool.clone(), this.config.clone(), this.wrk.clone()); - this.state = StreamState::Fetch(stream.boxed()); - } - StreamState::Delay => { - if let Some(delay_stream) = this.delay_stream.as_mut() { - match delay_stream.as_mut().poll_next(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Some(_)) => { - this.state = StreamState::Ready; - } - Poll::Ready(None) => { - this.state = StreamState::Empty; - return Poll::Ready(None); - } - } - } else { - this.state = StreamState::Empty; - return Poll::Ready(None); - } - } - - StreamState::Fetch(ref mut fut) => match fut.poll_unpin(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(item) => match item { - Ok(requests) => { - if requests.is_empty() { - this.state = StreamState::Delay; - } else { - let mut buffer = VecDeque::new(); - for request in requests { - buffer.push_back(request); - } - - this.state = StreamState::Buffered(buffer); - } - } - Err(e) => { - this.state = StreamState::Empty; - return Poll::Ready(Some(Err(e))); - } - }, - }, - - StreamState::Buffered(ref mut buffer) => { - if let Some(request) = buffer.pop_front() { - // Yield the next buffered item - if buffer.is_empty() { - // Buffer is now empty, transition to ready for next fetch - this.state = StreamState::Ready; - } - return Poll::Ready(Some(Ok(Some(request)))); - } else { - // Buffer is empty, transition to ready - this.state = StreamState::Ready; - } - } - - StreamState::Empty => return Poll::Ready(None), - } - } - } -} - -impl SqlitePollFetcher { - /// Take pending tasks from the fetcher - pub fn take_pending(&mut self) -> VecDeque>> { - match &mut self.state { - StreamState::Buffered(tasks) => std::mem::take(tasks), - _ => VecDeque::new(), - } - } -} diff --git a/src/from_row.rs b/src/from_row.rs index 371eab7..a8462db 100644 --- a/src/from_row.rs +++ b/src/from_row.rs @@ -1,4 +1,9 @@ -use apalis_sql::{DateTime, DateTimeExt, TaskRow}; +use std::str::FromStr; + +use apalis_core::task::{builder::TaskBuilder, status::Status, task_id::TaskId}; +use ulid::Ulid; + +use crate::{Error, SqliteTask}; #[derive(Debug)] pub(crate) struct SqliteTaskRow { @@ -9,53 +14,67 @@ pub(crate) struct SqliteTaskRow { pub(crate) attempts: Option, pub(crate) max_attempts: Option, pub(crate) run_at: Option, - pub(crate) last_result: Option, pub(crate) lock_at: Option, pub(crate) lock_by: Option, pub(crate) done_at: Option, pub(crate) priority: Option, pub(crate) metadata: Option, pub(crate) idempotency_key: Option, + #[allow(unused)] + pub(crate) last_result: Option, } -impl TryInto for SqliteTaskRow { - type Error = sqlx::Error; +impl TryInto for SqliteTaskRow { + type Error = Error; + + fn try_into(self) -> Result { + let mut task = TaskBuilder::new(self.job) + .task_id({ + let task_id = self + .id + .ok_or_else(|| sqlx::Error::ColumnNotFound("task_id".into()))?; + TaskId::from_ulid( + Ulid::from_string(&task_id).map_err(|e| sqlx::Error::Decode(e.into()))?, + ) + }) + .queue( + self.job_type + .ok_or_else(|| sqlx::Error::ColumnNotFound("job_type".into()))? + .into(), + ) + .status( + Status::from_str( + &self + .status + .ok_or_else(|| sqlx::Error::ColumnNotFound("status".into()))?, + ) + .map_err(|e| sqlx::Error::Decode(e.into()))?, + ) + .attempt( + self.attempts + .ok_or_else(|| sqlx::Error::ColumnNotFound("attempts".into()))? + as usize, + ) + .max_attempts(self.max_attempts.map(|v| v as usize).unwrap_or(25)) + .run_at_timestamp( + self.run_at + .ok_or(sqlx::Error::ColumnNotFound("run_at".to_owned()))? + as u64, + ) + .lock_at(self.lock_at.map(|dt| dt as u64)) + .done_at(self.done_at.map(|dt| dt as u64)) + .lock_by(self.lock_by) + .priority(self.priority.map(|v| v as usize).unwrap_or_default()) + .with_metadata( + self.metadata + .map(|meta| serde_json::from_str(&meta).unwrap_or_default()) + .unwrap_or_default(), + ); + + if let Some(idempotency_key) = self.idempotency_key { + task = task.idempotency_key(idempotency_key); + } - fn try_into(self) -> Result { - Ok(TaskRow { - job: self.job, - id: self - .id - .ok_or_else(|| sqlx::Error::Protocol("Missing id".into()))?, - job_type: self - .job_type - .ok_or_else(|| sqlx::Error::Protocol("Missing job_type".into()))?, - status: self - .status - .ok_or_else(|| sqlx::Error::Protocol("Missing status".into()))?, - attempts: self - .attempts - .ok_or_else(|| sqlx::Error::Protocol("Missing attempts".into()))? - as usize, - max_attempts: self.max_attempts.map(|v| v as usize), - run_at: self - .run_at - .map(::from_unix_timestamp), - last_result: self - .last_result - .map(|res| serde_json::from_str(&res).unwrap_or(serde_json::Value::Null)), - lock_at: self - .lock_at - .map(::from_unix_timestamp), - lock_by: self.lock_by, - done_at: self - .done_at - .map(::from_unix_timestamp), - priority: self.priority.map(|v| v as usize), - metadata: self - .metadata - .map(|meta| serde_json::from_str(&meta).unwrap_or(serde_json::Value::Null)), - idempotency_key: self.idempotency_key, - }) + Ok(task.build()) } } diff --git a/src/lib.rs b/src/lib.rs index 0a72699..bf176ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,68 +1,82 @@ +#![warn( + missing_debug_implementations, + missing_docs, + rust_2018_idioms, + unreachable_pub, + bad_style, + dead_code, + improper_ctypes, + non_shorthand_field_patterns, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + unconditional_recursion, + unused, + unused_allocation, + unused_comparisons, + unused_parens, + while_true +)] #![doc = include_str!("../README.md")] -//! -//! [`SqliteStorageWithHook`]: crate::SqliteStorage -use std::{fmt, marker::PhantomData}; -use apalis_codec::json::JsonCodec; +use std::{ + fmt::Debug, + marker::PhantomData, + task::{Context, Poll}, +}; + +pub use apalis_codec::json::JsonCodec; + use apalis_core::{ - backend::{Backend, BackendExt, TaskStream, codec::Codec, queue::Queue}, + backend::{ + Backend, BackendConfig, TryNewBackend, WireFormatBackend, + ext::{ + BackendExt, + lifecycle::BeforeStart, + poll_strategy::{PollWith, StreamStrategy}, + }, + finalize::Durable, + persistence::{Persisted, TaskPersistLayer}, + }, features_table, - layers::Stack, task::Task, - worker::{context::WorkerContext, ext::ack::AcknowledgeLayer}, + worker::context::WorkerContext, }; -use apalis_sql::context::SqlContext; use futures::{ - FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, + FutureExt, channel::mpsc::{self}, - future::ready, - stream::{self, BoxStream, select}, }; +use serde_json::Value; +use sqlx::{Sqlite, pool::PoolOptions}; pub use sqlx::{ - Connection, Pool, Sqlite, SqliteConnection, SqlitePool, + SqliteConnection, SqlitePool, error::Error as SqlxError, - pool::{PoolConnection, PoolOptions}, - sqlite::{SqliteConnectOptions, SqliteOperation}, + sqlite::{SqliteConnectOptions, SqlitePoolOptions}, }; use ulid::Ulid; -use crate::{ - ack::{LockTaskLayer, SqliteAck}, - callback::update_hook_callback, - fetcher::{SqliteFetcher, SqlitePollFetcher, fetch_next}, - queries::{ - keep_alive::{initial_heartbeat, keep_alive, keep_alive_stream}, - reenqueue_orphaned::reenqueue_orphaned_stream, - }, - sink::SqliteSink, -}; - -mod ack; -mod callback; -/// Fetcher module for retrieving tasks from sqlite backend -pub mod fetcher; +pub mod callback; mod from_row; -/// Queries module for sqlite backend +mod persist; pub mod queries; pub mod shared; /// Sink module for pushing tasks to sqlite backend -pub mod sink; +mod sink; + +use persist::SqlxPersistence; +const JOBS_TABLE: &str = "Jobs"; -/// Type alias for sqlite context -pub type SqliteContext = SqlContext; +/// An alias for [`Task`] specialized for Sqlite +pub type SqliteTask> = Task; -/// Type alias for a task stored in sqlite backend -pub type SqliteTask = Task; -pub use apalis_sql::config::Config; -pub use apalis_sql::ext::TaskBuilderExt; -pub use callback::{DbEvent, HookCallbackListener}; -pub use shared::{SharedSqliteError, SharedSqliteStorage}; +mod error; -/// CompactType is the type used for compact serialization in sqlite backend -pub type CompactType = Vec; +mod config; -const JOBS_TABLE: &str = "Jobs"; +pub use config::Config; +pub use error::Error; +use crate::callback::{DbEvent, HookCallbackListener, update_hook_callback}; /// SqliteStorage is a storage backend for apalis using sqlite as the database. /// /// It supports both standard polling and event-driven (hooked) storage mechanisms. @@ -74,7 +88,7 @@ const JOBS_TABLE: &str = "Jobs"; # use sqlx::SqlitePool; # let pool = SqlitePool::connect(":memory:").await.unwrap(); # SqliteStorage::setup(&pool).await.unwrap(); - # SqliteStorage::new(&pool) + # SqliteStorage::::new(&pool) # }; "#, @@ -85,64 +99,74 @@ const JOBS_TABLE: &str = "Jobs"; WebUI => supported("Expose a web interface for monitoring tasks", true), FetchById => supported("Allow fetching a task by its ID", false), RegisterWorker => supported("Allow registering a worker with the backend", false), - MakeShared => supported("Share one connection across multiple workers via [`SharedSqliteStorage`]", false), + BackendFactory => supported("Share one connection across multiple workers via [`SqliteStorageFactory`]", false), WaitForCompletion => supported("Wait for tasks to complete without blocking", true), ResumeById => supported("Resume a task by its ID", false), ResumeAbandoned => supported("Resume abandoned tasks", false), ListWorkers => supported("List all workers registered with the backend", false), ListTasks => supported("List all tasks in the backend", false), }] +/// +/// [`SqliteStorageFactory`]: crate::shared::SqliteStorageFactory #[pin_project::pin_project] -pub struct SqliteStorage { - pool: Pool, - job_type: PhantomData, - config: Config, - codec: PhantomData, - #[pin] - sink: SqliteSink, +#[derive(Debug)] +pub struct SqliteStorage { #[pin] - fetcher: Fetcher, + persistence: Persisted, + job_type: PhantomData, + codec: JsonCodec, } -impl fmt::Debug for SqliteStorage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SqliteStorage") - .field("pool", &self.pool) - .field("job_type", &"PhantomData") - .field("config", &self.config) - .field("codec", &std::any::type_name::()) - .finish() - } -} - -impl Clone for SqliteStorage { +impl Clone for SqliteStorage { fn clone(&self) -> Self { Self { - sink: self.sink.clone(), - pool: self.pool.clone(), + persistence: self.persistence.clone(), job_type: PhantomData, - config: self.config.clone(), - codec: self.codec, - fetcher: self.fetcher.clone(), + codec: self.codec.clone(), } } } -impl SqliteStorage<(), (), ()> { +impl SqliteStorage<()> { + /// Connects to a database returning a pool and listener + pub fn connect_with_callback(url: &str) -> Result<(SqlitePool, HookCallbackListener), Error> { + let (tx, rx) = mpsc::unbounded::(); + let listener = HookCallbackListener::new(rx); + let pool = PoolOptions::::new() + .after_connect(move |conn, _| { + let mut tx = tx.clone(); + Box::pin(async move { + let mut lock_handle = conn.lock_handle().await?; + lock_handle.set_update_hook(move |ev| update_hook_callback(ev, &mut tx)); + Ok(()) + }) + }) + .connect_lazy(url)?; + Ok((pool, listener)) + } /// Perform migrations for storage #[cfg(feature = "migrate")] - pub async fn setup(pool: &Pool) -> Result<(), sqlx::Error> { + pub async fn setup(pool: &SqlitePool) -> Result<(), Error> { sqlx::query("PRAGMA journal_mode = 'WAL';") .execute(pool) .await?; - sqlx::query("PRAGMA temp_store = 2;").execute(pool).await?; - sqlx::query("PRAGMA synchronous = NORMAL;") + sqlx::query("PRAGMA temp_store = MEMORY;") + .execute(pool) + .await?; + sqlx::query("PRAGMA synchronous = OFF;") .execute(pool) .await?; sqlx::query("PRAGMA cache_size = 64000;") .execute(pool) .await?; - Self::migrations().run(pool).await?; + sqlx::query("PRAGMA journal_size_limit = 67108864;") + .execute(pool) + .await?; + sqlx::query("PRAGMA optimize;").execute(pool).await?; + Self::migrations() + .run(pool) + .await + .map_err(sqlx::Error::from)?; Ok(()) } @@ -154,347 +178,153 @@ impl SqliteStorage<(), (), ()> { } } -impl SqliteStorage { +impl SqliteStorage { /// Create a new SqliteStorage #[must_use] - pub fn new( - pool: &Pool, - ) -> SqliteStorage, fetcher::SqliteFetcher> { - let config = Config::new(std::any::type_name::()); + pub fn new(pool: &SqlitePool) -> SqliteStorage { + let config = Config::default().queue(std::any::type_name::()); SqliteStorage { - pool: pool.clone(), job_type: PhantomData, - sink: SqliteSink::new(pool, &config), - config, - codec: PhantomData, - fetcher: fetcher::SqliteFetcher, + codec: JsonCodec::default(), + persistence: Persisted::new(SqlxPersistence { + pool: pool.clone(), + config, + }), } } - /// Create a new SqliteStorage for a specific queue + /// Create a new SqliteStorage with a custom configuration #[must_use] - pub fn new_in_queue( - pool: &Pool, - queue: &str, - ) -> SqliteStorage, fetcher::SqliteFetcher> { - let config = Config::new(queue); - SqliteStorage { - pool: pool.clone(), - job_type: PhantomData, - sink: SqliteSink::new(pool, &config), - config, - codec: PhantomData, - fetcher: fetcher::SqliteFetcher, - } - } - - /// Create a new SqliteStorage with config - #[must_use] - pub fn new_with_config( - pool: &Pool, - config: &Config, - ) -> SqliteStorage, fetcher::SqliteFetcher> { - SqliteStorage { - pool: pool.clone(), - job_type: PhantomData, - config: config.clone(), - codec: PhantomData, - sink: SqliteSink::new(pool, config), - fetcher: fetcher::SqliteFetcher, - } - } - - /// Create a new SqliteStorage with hook callback listener - #[must_use] - pub fn new_with_callback( - url: &str, - config: &Config, - ) -> SqliteStorage, HookCallbackListener> { - let (tx, rx) = mpsc::unbounded::(); - - let listener = HookCallbackListener::new(rx); - let pool = PoolOptions::::new() - .after_connect(move |conn, _meta| { - let mut tx = tx.clone(); - Box::pin(async move { - let mut lock_handle = conn.lock_handle().await?; - lock_handle.set_update_hook(move |ev| update_hook_callback(ev, &mut tx)); - Ok(()) - }) - }) - .connect_lazy(url) - .expect("Failed to create Sqlite pool"); - SqliteStorage { - pool: pool.clone(), - job_type: PhantomData, - config: config.clone(), - codec: PhantomData, - sink: SqliteSink::new(&pool, config), - fetcher: listener, - } - } -} - -impl SqliteStorage { - /// Change the codec used for serialization/deserialization - pub fn with_codec(self) -> SqliteStorage { - SqliteStorage { - sink: SqliteSink::new(&self.pool, &self.config), - pool: self.pool, - job_type: PhantomData, - config: self.config, - codec: PhantomData, - fetcher: self.fetcher, - } + pub fn with_config(mut self, config: Config) -> SqliteStorage { + self.persistence.config = config; + self } - /// Get the config used by the storage - pub fn config(&self) -> &Config { - &self.config + /// Attach a callback to an instance + pub fn with_callback( + self, + callback: HookCallbackListener, + ) -> PollWith> { + self.poll_with_stream(callback) } - /// Get the connection pool used by the storage - pub fn pool(&self) -> &Pool { - &self.pool + /// Get the underlying pool + pub fn pool(&self) -> &SqlitePool { + &self.persistence.pool } } -impl Backend for SqliteStorage -where - Args: Send + 'static + Unpin, - Decode: Codec + 'static + Send, - Decode::Error: std::error::Error + Send + Sync + 'static, -{ - type Args = Args; - type IdType = Ulid; - - type Context = SqliteContext; - - type Error = sqlx::Error; - - type Stream = TaskStream, sqlx::Error>; - - type Beat = BoxStream<'static, Result<(), sqlx::Error>>; - - type Layer = Stack>; - - fn heartbeat(&self, worker: &WorkerContext) -> Self::Beat { - let pool = self.pool.clone(); - let config = self.config.clone(); - let worker = worker.clone(); - let keep_alive = keep_alive_stream(pool, config, worker); - let reenqueue = reenqueue_orphaned_stream( - self.pool.clone(), - self.config.clone(), - *self.config.keep_alive(), - ) - .map_ok(|_| ()); - futures::stream::select(keep_alive, reenqueue).boxed() - } +impl Backend for SqliteStorage { + type Task = Task>; - fn middleware(&self) -> Self::Layer { - let lock = LockTaskLayer::new(self.pool.clone()); - let ack = AcknowledgeLayer::new(SqliteAck::new(self.pool.clone())); - Stack::new(lock, ack) - } + type Error = Error; - fn poll(self, worker: &WorkerContext) -> Self::Stream { - self.poll_default(worker) - .map(|a| match a { - Ok(Some(task)) => Ok(Some( - task.try_map(|t| Decode::decode(&t)) - .map_err(|e| sqlx::Error::Decode(e.into()))?, - )), - Ok(None) => Ok(None), - Err(e) => Err(e), - }) - .boxed() + fn poll_ready( + &mut self, + cx: &mut Context<'_>, + worker: &WorkerContext, + ) -> Poll> { + self.persistence + .poll_ready(cx, worker, self.persistence.config.heartbeat_interval) } -} -impl BackendExt for SqliteStorage -where - Self: Backend, - Decode: Codec + Send + 'static, - Decode::Error: std::error::Error + Send + Sync + 'static, - Args: Send + 'static + Unpin, -{ - type Codec = Decode; - type Compact = CompactType; - type CompactStream = TaskStream, sqlx::Error>; - - fn get_queue(&self) -> Queue { - self.config.queue().clone() + fn poll_next( + &mut self, + cx: &mut Context<'_>, + worker: &WorkerContext, + ) -> Poll>> { + self.persistence.poll_next(cx, worker) } - fn poll_compact(self, worker: &WorkerContext) -> Self::CompactStream { - self.poll_default(worker).boxed() + fn poll_close( + &mut self, + cx: &mut Context<'_>, + worker: &WorkerContext, + ) -> Poll> { + self.persistence.poll_close(cx, worker) } } -impl Backend for SqliteStorage -where - Args: Send + 'static + Unpin, - Decode: Codec + Send + 'static, - Decode::Error: std::error::Error + Send + Sync + 'static, -{ +impl BackendConfig for SqliteStorage { type Args = Args; - type IdType = Ulid; - type Context = SqliteContext; + type Id = Ulid; - type Error = sqlx::Error; + type Kind = Durable; - type Stream = TaskStream, sqlx::Error>; + type Config = Config; - type Beat = BoxStream<'static, Result<(), sqlx::Error>>; + type Layer = TaskPersistLayer, Value>; - type Layer = Stack>; - - fn heartbeat(&self, worker: &WorkerContext) -> Self::Beat { - let pool = self.pool.clone(); - let config = self.config.clone(); - let worker = worker.clone(); - let keep_alive = keep_alive_stream(pool, config, worker); - let reenqueue = reenqueue_orphaned_stream( - self.pool.clone(), - self.config.clone(), - *self.config.keep_alive(), - ) - .map_ok(|_| ()); - futures::stream::select(keep_alive, reenqueue).boxed() - } - - fn middleware(&self) -> Self::Layer { - let lock = LockTaskLayer::new(self.pool.clone()); - let ack = AcknowledgeLayer::new(SqliteAck::new(self.pool.clone())); - Stack::new(lock, ack) + fn config(&self) -> &Self::Config { + &self.persistence.config } - fn poll(self, worker: &WorkerContext) -> Self::Stream { - self.poll_with_listener(worker) - .map(|a| match a { - Ok(Some(task)) => Ok(Some( - task.try_map(|t| Decode::decode(&t)) - .map_err(|e| sqlx::Error::Decode(e.into()))?, - )), - Ok(None) => Ok(None), - Err(e) => Err(e), - }) - .boxed() + fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer { + self.persistence + .layer(JsonCodec::default(), self.config().batch_size) + .persist_results(self.config().persist_results) + .lock_tasks(self.config().lock_tasks) } } -impl BackendExt for SqliteStorage -where - Self: Backend, - Decode: Codec + Send + 'static, - Decode::Error: std::error::Error + Send + Sync + 'static, - Args: Send + 'static + Unpin, -{ - type Codec = Decode; - type Compact = CompactType; - type CompactStream = TaskStream, sqlx::Error>; - - fn get_queue(&self) -> Queue { - self.config.queue().clone() - } - - fn poll_compact(self, worker: &WorkerContext) -> Self::CompactStream { - self.poll_with_listener(worker).boxed() - } -} - -impl SqliteStorage { - fn poll_with_listener( - self, - worker: &WorkerContext, - ) -> impl Stream>, sqlx::Error>> + Send + 'static - { - let pool = self.pool.clone(); - let config = self.config.clone(); - let worker = worker.clone(); - let register_worker = initial_heartbeat( - self.pool.clone(), - self.config.clone(), - worker.clone(), - "SqliteStorageWithHook", - ); - let register_worker = stream::once(register_worker.map_ok(|_| None)); - let eager_fetcher: SqlitePollFetcher = - SqlitePollFetcher::new(&self.pool, &self.config, &worker); - let lazy_fetcher = self - .fetcher - .filter(|a| { - ready(a.operation() == &SqliteOperation::Insert && a.table_name() == JOBS_TABLE) - }) - .inspect(|db_event| { - log::debug!("Received new job event: {db_event:?}"); - }) - .ready_chunks(self.config.buffer_size()) - .then(move |_| fetch_next(pool.clone(), config.clone(), worker.clone())) - .flat_map(|res| match res { - Ok(tasks) => stream::iter(tasks).map(Ok).boxed(), - Err(e) => stream::iter(vec![Err(e)]).boxed(), - }) - .map(|res| match res { - Ok(task) => Ok(Some(task)), - Err(e) => Err(e), - }); +impl WireFormatBackend for SqliteStorage { + type Codec = JsonCodec>; - register_worker.chain(select(lazy_fetcher, eager_fetcher)) + type Compact = Vec; + fn codec(&self) -> &Self::Codec { + &self.codec } } -impl SqliteStorage { - fn poll_default( - self, - worker: &WorkerContext, - ) -> impl Stream>, sqlx::Error>> + Send + 'static - { - let fut = initial_heartbeat( - self.pool.clone(), - self.config().clone(), - worker.clone(), - "SqliteStorage", - ); - let register = stream::once(fut.map(|_| Ok(None))); - register.chain(SqlitePollFetcher::::new( - &self.pool, - &self.config, - worker, - )) +impl TryNewBackend for SqliteStorage { + type Backend = BeforeStart; + fn try_new(config: Self::Config) -> Result { + let pool = SqlitePoolOptions::new() + .connect_lazy(config.database_url.as_deref().unwrap_or(":memory:"))?; + + Ok(SqliteStorage::new(&pool).before_start(|s| { + let pool = s.persistence.pool.clone(); + async move { + SqliteStorage::setup(&pool).await?; + Ok(()) + } + .boxed() + })) } } - #[cfg(test)] mod tests { - use std::time::Duration; - use apalis::prelude::*; + use apalis_codec::bincode::BincodeCodec; + use apalis_core::backend::ext::BackendExt; use apalis_workflow::*; + use futures::{StreamExt, future::ready, stream}; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; + use std::time::{Duration, Instant}; use super::*; #[tokio::test] async fn basic_worker() { - const ITEMS: usize = 10; - let pool = SqlitePool::connect(std::env::var("DATABASE_URL").unwrap().as_str()) - .await - .unwrap(); - SqliteStorage::setup(&pool).await.unwrap(); + const ITEMS: usize = 3; + let url = std::env::var("DATABASE_URL").unwrap_or(":memory:".to_owned()); + let pool = SqlitePool::connect(&url).await.unwrap(); - let mut backend = SqliteStorage::new(&pool); + let mut backend = SqliteStorage::new(&pool).before_start(|b| { + let pool = b.pool().clone(); + async move { SqliteStorage::setup(&pool).await } + }); - let mut start = 0; + let mut start: usize = 0; let mut items = stream::repeat_with(move || { start += 1; start }) .take(ITEMS); + backend.push_stream(&mut items).await.unwrap(); async fn send_reminder(item: usize, wrk: WorkerContext) -> Result<(), BoxDynError> { @@ -503,53 +333,53 @@ mod tests { } Ok(()) } - - let worker = WorkerBuilder::new("rango-tango-basic") + let inst = Instant::now(); + let worker = WorkerBuilder::new("rango-tango") .backend(backend) + .retry(RetryPolicy::retries(3)) .build(send_reminder); worker.run().await.unwrap(); + + println!("Done in {:?}", inst.elapsed()); } #[tokio::test] async fn hooked_worker() { - const ITEMS: usize = 20; - - let lazy_strategy = StrategyBuilder::new() - .apply(IntervalStrategy::new(Duration::from_secs(5))) - .build(); - let config = Config::new("rango-tango-queue") - .with_poll_interval(lazy_strategy) - .set_buffer_size(5); - let backend = SqliteStorage::new_with_callback( - std::env::var("DATABASE_URL").unwrap().as_str(), - &config, - ); - let pool = backend.pool().clone(); + const ITEMS: usize = 10; + let url = &std::env::var("DATABASE_URL").unwrap_or(":memory:".to_owned()); + let (pool, callback) = SqliteStorage::connect_with_callback(url).unwrap(); SqliteStorage::setup(&pool).await.unwrap(); + let backend = SqliteStorage::new(&pool).with_callback(callback); + let queue = backend.config().queue.to_string(); + tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(2)).await; let mut start = 0; - - let items = stream::repeat_with(move || { + tokio::time::sleep(Duration::from_secs(5)).await; + loop { start += 1; + tokio::time::sleep(Duration::from_secs(1)).await; - Task::builder(serde_json::to_vec(&start).unwrap()) - .with_ctx(SqliteContext::new().with_priority(start)) - .build() - }) - .take(ITEMS) - .collect::>() - .await; - crate::sink::push_tasks(pool.clone(), config, items) - .await - .unwrap(); + let items = stream::repeat_with(move || { + TaskBuilder::new(serde_json::to_vec(&start).unwrap()).build() + }) + .take(1) + .collect::>() + .await; + let mut tx = pool.begin().await.unwrap(); + let _ = crate::sink::push_tasks(&mut tx, &queue, &items).await; + tx.commit().await.unwrap(); + } }); - async fn send_reminder(item: usize, wrk: WorkerContext) -> Result<(), BoxDynError> { - // Priority is in reverse order - if item == 1 { - apalis_core::timer::sleep(Duration::from_secs(1)).await; + async fn send_reminder( + item: usize, + wrk: WorkerContext, + token: TaskContext, + ) -> Result<(), BoxDynError> { + let _ctx = token.execution_context().unwrap(); + if item == ITEMS { + wrk.emit(format!("Processed {} tasks", ITEMS)); wrk.stop().unwrap(); } Ok(()) @@ -557,35 +387,34 @@ mod tests { let worker = WorkerBuilder::new("rango-tango-hooked") .backend(backend) + .on_event(|_, ev| println!("{ev}")) .build(send_reminder); worker.run().await.unwrap(); } #[tokio::test] async fn test_workflow() { - let workflow = Workflow::new("odd-numbers-workflow") + let workflow = SteppedFlow::new("odd-numbers-workflow") .and_then(|a: usize| async move { Ok::<_, BoxDynError>((0..=a).collect::>()) }) - .filter_map(|x| async move { if x % 2 != 0 { Some(x) } else { None } }) - .filter_map(|x| async move { if x % 3 != 0 { Some(x) } else { None } }) - .filter_map(|x| async move { if x % 5 != 0 { Some(x) } else { None } }) - .delay_for(Duration::from_millis(1000)) + .delay_for(Duration::from_millis(5000)) + .filter_map(|x| ready(if x % 2 != 0 { Some(x) } else { None })) .and_then(|a: Vec| async move { println!("Sum: {}", a.iter().sum::()); Err::<(), BoxDynError>("Intentional Error".into()) }); - let mut sqlite = SqliteStorage::new_with_callback( - std::env::var("DATABASE_URL").unwrap().as_str(), - &Config::new("workflow-queue").with_poll_interval( - StrategyBuilder::new() - .apply(IntervalStrategy::new(Duration::from_millis(100))) - .build(), - ), - ); + let pool = + SqlitePool::connect(&std::env::var("DATABASE_URL").unwrap_or(":memory:".to_owned())) + .await + .unwrap(); + SqliteStorage::setup(&pool).await.unwrap(); - SqliteStorage::setup(sqlite.pool()).await.unwrap(); + let mut sqlite = SqliteStorage::new(&pool) + .with_codec(BincodeCodec) + // Our worker may sleep at the delay, (our heartbeat is 30s, so we wake the worker every second) + .poll_with_interval(Duration::from_secs(1)); - sqlite.push_start(100usize).await.unwrap(); + sqlite.push_start(42).await.unwrap(); let worker = WorkerBuilder::new("rango-tango-workflow") .backend(sqlite) @@ -625,13 +454,10 @@ mod tests { sentiment: Option, } - let workflow = Workflow::new("text-pipeline") + let workflow = SteppedFlow::new("text-pipeline") // Step 1: Preprocess input (e.g., tokenize, lowercase) - .and_then(|input: UserInput, mut worker: WorkerContext| async move { - worker.emit(&Event::custom(format!( - "Preprocessing input: {}", - input.text - ))); + .and_then(|input: UserInput, worker: WorkerContext| async move { + worker.emit(format!("Preprocessing input: {}", input.text)); let processed = input.text.to_lowercase(); Ok::<_, BoxDynError>(processed) }) @@ -654,6 +480,7 @@ mod tests { .collect::>(); Ok::<_, BoxDynError>(results) }) + .delay_for(Duration::from_millis(5000)) // Step 3: Filter out low-confidence predictions .filter_map( |c: Classified| async move { if c.confidence >= 0.6 { Some(c) } else { None } }, @@ -680,18 +507,18 @@ mod tests { }) } }) - .and_then(|a: Vec, mut worker: WorkerContext| async move { - worker.emit(&Event::Custom(Box::new(format!( - "Generated {} summaries", - a.len() - )))); + .and_then(|a: Vec, worker: WorkerContext| async move { + worker.emit(format!("Generated {} summaries", a.len())); worker.stop() }); - let mut sqlite = - SqliteStorage::new_with_callback(":memory:", &Config::new("text-pipeline")); + let pool = SqlitePool::connect(":memory:").await.unwrap(); - SqliteStorage::setup(sqlite.pool()).await.unwrap(); + SqliteStorage::setup(&pool).await.unwrap(); + + let backoff = BackoffConfig::new(Duration::from_millis(5000)); + let mut sqlite = + SqliteStorage::new(&pool).poll_with_backoff(Duration::from_millis(200), backoff); let input = UserInput { text: "Rust makes systems programming delightful!".to_string(), diff --git a/src/persist.rs b/src/persist.rs new file mode 100644 index 0000000..47cf687 --- /dev/null +++ b/src/persist.rs @@ -0,0 +1,160 @@ +use apalis_core::{ + backend::{ + WorkerFilter, + persistence::{Persistence, TaskEvent}, + }, + task::Task, + worker::context::WorkerContext, +}; +use serde_json::Value; +use sqlx::SqlitePool; + +use crate::{ + error::Error, + queries::{ + self, fetch_next, keep_alive, push_tasks, reenqueue_abandoned, reenqueue_orphaned, + register_worker, + }, +}; + +use crate::config::Config; + +#[derive(Debug, Clone)] +pub(crate) struct SqlxPersistence { + pub(crate) pool: SqlitePool, + pub(crate) config: Config, +} + +impl Persistence for SqlxPersistence { + type Compact = Vec; + type Error = Error; + type Response = Value; + async fn register(&mut self, worker: &WorkerContext) -> Result<(), Error> { + let dead_for = self.config.heartbeat_interval.as_secs() as i64; + let mut tx = self.pool.begin().await?; + let count = reenqueue_orphaned( + &mut *tx, + dead_for, + self.config.queue.as_ref(), + &WorkerFilter::Only(worker.name().to_owned()), + ) + .await?; + if count > 0 { + log::debug!( + "{count} Re-enqueued orphaned tasks by worker {}", + worker.name() + ); + } + register_worker(&mut *tx, &self.config, worker, "SqliteStorage").await?; + tx.commit().await?; + log::debug!("Registered Worker: {}", worker.name()); + Ok(()) + } + async fn heartbeat(&mut self, worker: &WorkerContext) -> Result<(), Error> { + let config = &self.config; + let mut txn = self.pool.begin().await?; + keep_alive(&mut *txn, config, worker).await?; + let count = reenqueue_orphaned( + &mut *txn, + config.orphaned_duration().as_secs() as i64, + config.queue.as_ref(), + &WorkerFilter::AllExcept(worker.name().to_owned()), + ) + .await?; + txn.commit().await?; + if count > 0 { + log::debug!( + "Re-enqueued {count} orphaned tasks by worker {}", + worker.name() + ); + } + Ok(()) + } + async fn fetch_next(&mut self, worker: &WorkerContext) -> Result>>, Error> { + let mut tx = self.pool.begin().await?; + let res = fetch_next(&mut *tx, &self.config, worker).await?; + tx.commit().await?; + Ok(res) + } + + async fn handle_events( + &mut self, + messages: Vec>, + worker: &WorkerContext, + ) -> Result<(), Error> { + let pool = &self.pool; + let lock_ids = messages + .iter() + .filter_map(|msg| { + if let TaskEvent::Lock { task_id, .. } = msg { + Some(task_id.to_string()) + } else { + None + } + }) + .collect::>(); + + let ack_payloads = messages + .iter() + .filter_map(|msg| { + if let TaskEvent::Complete(payload) = msg { + Some(payload) + } else { + None + } + }) + .collect::>(); + + if lock_ids.is_empty() && ack_payloads.is_empty() { + return Ok(()); + } + + log::debug!( + "Processing {} messages ({} locks, {} acks)", + messages.len(), + lock_ids.len(), + ack_payloads.len() + ); + + let mut tx = pool.begin().await?; + if !lock_ids.is_empty() { + queries::lock_tasks(&mut *tx, &lock_ids, worker.name()).await?; + } + if !ack_payloads.is_empty() { + queries::ack_tasks(&mut *tx, &ack_payloads, worker.name()).await?; + } + tx.commit().await?; + Ok(()) + } + + async fn reenqueue_abandoned( + &mut self, + tasks: Vec>>, + worker: &WorkerContext, + ) -> Result { + let config = &self.config; + let pool = &self.pool; + let mut txn = pool.begin().await?; + let task_ids = tasks + .iter() + .map(|t| t.task_id().unwrap().to_string()) + .collect(); + let count = + reenqueue_abandoned(&mut *txn, config.queue.as_ref(), worker.name(), &task_ids).await?; + if count as usize != tasks.len() { + return Err(Error::ReenqueueMismatch { + queued: tasks.len(), + abandoned: count as usize, + }); + } + txn.commit().await?; + Ok(count) + } + async fn push_tasks(&mut self, tasks: Vec>) -> Result<(), Self::Error> { + let queue = self.config.queue.as_ref(); + let mut tx = self.pool.begin().await?; + push_tasks(&mut tx, queue, &tasks).await?; + tx.commit().await?; + Ok(()) + } +} diff --git a/src/queries/ack_task.rs b/src/queries/ack_task.rs index 07ed017..f4eb7fd 100644 --- a/src/queries/ack_task.rs +++ b/src/queries/ack_task.rs @@ -1,50 +1,24 @@ -use apalis_core::{ - error::{AbortError, BoxDynError}, - task::{Parts, status::Status}, -}; +use apalis_core::backend::TaskResult; +use sqlx::Executor; -use sqlx::{Executor, Sqlite}; -use ulid::Ulid; +/// Represents the result of a `SqliteTask` execution +pub type AckPayload = TaskResult; -use crate::SqliteContext; - -/// Lock a task, given a worker -pub async fn ack_task Executor<'a, Database = Sqlite>>( - pool: E, - task_id: &str, +/// Ack multiple tasks, given a worker +pub async fn ack_tasks<'a, E>( + executor: E, + messages: &[&AckPayload], worker_id: &str, - res: &str, - status: &Status, - attempt: i32, -) -> Result<(), sqlx::Error> { - let status = status.to_string(); - let res = sqlx::query_file!( - "queries/task/ack.sql", - task_id, - attempt, - res, - status, - worker_id - ) - .execute(pool) - .await?; +) -> Result +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ + let payload_json = + serde_json::to_string(&messages).map_err(|e| sqlx::Error::Encode(Box::new(e)))?; - if res.rows_affected() == 0 { - return Err(sqlx::Error::RowNotFound); - } - Ok(()) -} + let result = sqlx::query_file!("queries/task/ack.sql", payload_json, worker_id) + .execute(executor) + .await?; -pub fn calculate_status( - parts: &Parts, - res: &Result, -) -> Status { - match &res { - Ok(_) => Status::Done, - Err(e) => match e { - _ if parts.ctx.max_attempts() as usize <= parts.attempt.current() => Status::Killed, - e if e.downcast_ref::().is_some() => Status::Killed, - _ => Status::Failed, - }, - } + Ok(result.rows_affected()) } diff --git a/src/queries/fetch_by_id.rs b/src/queries/fetch_by_id.rs index 2162622..057ee37 100644 --- a/src/queries/fetch_by_id.rs +++ b/src/queries/fetch_by_id.rs @@ -1,46 +1,27 @@ use apalis_core::{ - backend::{BackendExt, FetchById, codec::Codec}, - task::task_id::TaskId, + backend::{Backend, FetchById}, + task::{Task, task_id::TaskId}, }; -use apalis_sql::{TaskRow, from_row::FromRowError}; -use ulid::Ulid; -use crate::{CompactType, SqliteContext, SqliteStorage, SqliteTask, from_row::SqliteTaskRow}; +use crate::{SqliteStorage, SqliteTask, error::Error, from_row::SqliteTaskRow}; -impl FetchById for SqliteStorage +impl FetchById for SqliteStorage where - Self: BackendExt< - Context = SqliteContext, - Compact = CompactType, - IdType = Ulid, - Error = sqlx::Error, - >, - D: Codec, - D::Error: std::error::Error + Send + Sync + 'static, + Self: Backend>>, Args: 'static, { fn fetch_by_id( &mut self, - id: &TaskId, - ) -> impl Future>, Self::Error>> + Send { - let pool = self.pool.clone(); + id: &TaskId, + ) -> impl Future, Self::Error>> + Send { + let pool = self.persistence.pool.clone(); let id = id.to_string(); async move { let task = sqlx::query_file_as!(SqliteTaskRow, "queries/task/find_by_id.sql", id) .fetch_optional(&pool) .await? - .map(|r| { - let row: TaskRow = r - .try_into() - .map_err(|e: sqlx::Error| FromRowError::DecodeError(e.into()))?; - row.try_into_task_compact().and_then(|t| { - t.try_map(|a| { - D::decode(&a).map_err(|e| FromRowError::DecodeError(e.into())) - }) - }) - }) - .transpose() - .map_err(|e| sqlx::Error::Protocol(e.to_string()))?; + .map(|r| r.try_into()) + .transpose()?; Ok(task) } } diff --git a/src/queries/fetch_next.rs b/src/queries/fetch_next.rs new file mode 100644 index 0000000..5553217 --- /dev/null +++ b/src/queries/fetch_next.rs @@ -0,0 +1,37 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use apalis_core::{task::Task, worker::context::WorkerContext}; +use sqlx::Executor; + +use crate::{Error, config::Config, from_row::SqliteTaskRow}; + +/// Fetch the next batch of tasks from the sqlite backend +pub async fn fetch_next<'a, E>( + executor: E, + config: &Config, + worker: &WorkerContext, +) -> Result>>, Error> +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ + let job_type = config.queue.as_ref(); + let buffer_size = config.batch_size as i32; + let worker = worker.name(); + let now: i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + sqlx::query_file_as!( + SqliteTaskRow, + "queries/backend/fetch_next.sql", + worker, + job_type, + buffer_size, + now + ) + .fetch_all(executor) + .await? + .into_iter() + .map(|r| r.try_into()) + .collect() +} diff --git a/src/queries/keep_alive.rs b/src/queries/keep_alive.rs index f543fd4..c14f30a 100644 --- a/src/queries/keep_alive.rs +++ b/src/queries/keep_alive.rs @@ -1,22 +1,21 @@ use apalis_core::worker::context::WorkerContext; -use futures::{FutureExt, Stream, stream}; -use sqlx::SqlitePool; +use sqlx::Executor; -use crate::{ - Config, - queries::{reenqueue_orphaned::reenqueue_orphaned, register_worker::register_worker}, -}; +use crate::config::Config; /// Send a keep-alive signal to the database to indicate that the worker is still active -pub async fn keep_alive( - pool: SqlitePool, - config: Config, - worker: WorkerContext, -) -> Result<(), sqlx::Error> { +pub async fn keep_alive<'a, E>( + executor: E, + config: &Config, + worker: &WorkerContext, +) -> Result<(), sqlx::Error> +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ let worker = worker.name().to_owned(); - let queue = config.queue().to_string(); + let queue = config.queue.to_string(); let res = sqlx::query_file!("queries/backend/keep_alive.sql", worker, queue) - .execute(&pool) + .execute(executor) .await?; if res.rows_affected() == 0 { return Err(sqlx::Error::Io(std::io::Error::new( @@ -26,28 +25,3 @@ pub async fn keep_alive( } Ok(()) } - -/// Perform the initial heartbeat and registration of the worker -pub async fn initial_heartbeat( - pool: SqlitePool, - config: Config, - worker: WorkerContext, - storage_type: &str, -) -> Result<(), sqlx::Error> { - reenqueue_orphaned(pool.clone(), &config).await?; - register_worker(pool, config, worker, storage_type).await?; - Ok(()) -} - -/// Create a stream that sends keep-alive signals at regular intervals -pub fn keep_alive_stream( - pool: SqlitePool, - config: Config, - worker: WorkerContext, -) -> impl Stream> + Send { - stream::unfold((), move |_| { - let register = keep_alive(pool.clone(), config.clone(), worker.clone()); - let interval = apalis_core::timer::Delay::new(*config.keep_alive()); - interval.then(move |_| register.map(|res| Some((res, ())))) - }) -} diff --git a/src/queries/list_queues.rs b/src/queries/list_queues.rs index 2a8d2fe..7ac9d70 100644 --- a/src/queries/list_queues.rs +++ b/src/queries/list_queues.rs @@ -1,37 +1,40 @@ -use apalis_core::backend::{BackendExt, ListQueues, QueueInfo}; -use ulid::Ulid; +use apalis_core::backend::{Backend, ListQueues, QueueInfo}; -use crate::{CompactType, SqliteContext, SqliteStorage}; +use crate::{SqliteStorage, error::Error}; struct QueueInfoRow { name: String, - stats: String, // JSON string - workers: String, // JSON string - activity: String, // JSON string + stats: Option, // JSON string + workers: Option, // JSON string + activity: Option, // JSON string } impl From for QueueInfo { fn from(row: QueueInfoRow) -> Self { Self { name: row.name, - stats: serde_json::from_str(&row.stats).unwrap(), - workers: serde_json::from_str(&row.workers).unwrap(), - activity: serde_json::from_str(&row.activity).unwrap(), + stats: row + .stats + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(), + workers: row + .workers + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(), + activity: row + .activity + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(), } } } -impl ListQueues for SqliteStorage +impl ListQueues for SqliteStorage where - Self: BackendExt< - Context = SqliteContext, - Compact = CompactType, - IdType = Ulid, - Error = sqlx::Error, - >, + Self: Backend, { fn list_queues(&self) -> impl Future, Self::Error>> + Send { - let pool = self.pool.clone(); + let pool = self.persistence.pool.clone(); async move { let queues = sqlx::query_file_as!(QueueInfoRow, "queries/backend/list_queues.sql") diff --git a/src/queries/list_tasks.rs b/src/queries/list_tasks.rs index a2c9a4d..95d0d1e 100644 --- a/src/queries/list_tasks.rs +++ b/src/queries/list_tasks.rs @@ -1,30 +1,22 @@ use apalis_core::{ - backend::{BackendExt, Filter, ListAllTasks, ListTasks, codec::Codec}, - task::{Task, status::Status}, + backend::{Backend, Filter, ListAllTasks, ListTasks}, + task::status::Status, }; -use apalis_sql::{TaskRow, from_row::FromRowError}; -use ulid::Ulid; -use crate::{CompactType, SqliteContext, SqliteStorage, SqliteTask, from_row::SqliteTaskRow}; +use crate::{SqliteStorage, SqliteTask}; +use crate::{error::Error, from_row::SqliteTaskRow}; -impl ListTasks for SqliteStorage +impl ListTasks for SqliteStorage where - Self: BackendExt< - Context = SqliteContext, - Compact = CompactType, - IdType = Ulid, - Error = sqlx::Error, - >, - D: Codec, - D::Error: std::error::Error + Send + Sync + 'static, + Self: Backend, Args: 'static, { fn list_tasks( &self, filter: &Filter, - ) -> impl Future>, Self::Error>> + Send { - let queue = self.config().queue().to_string(); - let pool = self.pool.clone(); + ) -> impl Future, Self::Error>> + Send { + let queue = self.persistence.config.queue.as_ref(); + let pool = self.persistence.pool.clone(); let limit = filter.limit() as i32; let offset = filter.offset() as i32; let status = filter @@ -44,42 +36,27 @@ where .fetch_all(&pool) .await? .into_iter() - .map(|r| { - let row: TaskRow = r - .try_into() - .map_err(|e: sqlx::Error| FromRowError::DecodeError(e.into()))?; - row.try_into_task_compact().and_then(|t| { - t.try_map(|a| D::decode(&a).map_err(|e| FromRowError::DecodeError(e.into()))) - }) - }) - .collect::, _>>() - .map_err(|e| sqlx::Error::Decode(e.into()))?; + .map(|r| r.try_into()) + .collect::, _>>()?; Ok(tasks) } } } -impl ListAllTasks for SqliteStorage +impl ListAllTasks for SqliteStorage where - Self: BackendExt< - Context = SqliteContext, - Compact = CompactType, - IdType = Ulid, - Error = sqlx::Error, - >, + Self: Backend, { fn list_all_tasks( &self, filter: &Filter, - ) -> impl Future< - Output = Result>, Self::Error>, - > + Send { + ) -> impl Future, Self::Error>> + Send { let status = filter .status .as_ref() .map(|s| s.to_string()) .unwrap_or(Status::Pending.to_string()); - let pool = self.pool.clone(); + let pool = self.persistence.pool.clone(); let limit = filter.limit() as i32; let offset = filter.offset() as i32; async move { @@ -93,11 +70,7 @@ where .fetch_all(&pool) .await? .into_iter() - .map(|r| { - let row: TaskRow = r.try_into()?; - row.try_into_task_compact() - .map_err(|e| sqlx::Error::Protocol(e.to_string())) - }) + .map(|r| r.try_into()) .collect::, _>>()?; Ok(tasks) } diff --git a/src/queries/list_workers.rs b/src/queries/list_workers.rs index ce956e7..9fce12c 100644 --- a/src/queries/list_workers.rs +++ b/src/queries/list_workers.rs @@ -1,8 +1,7 @@ -use apalis_core::backend::{BackendExt, ListWorkers, RunningWorker}; +use apalis_core::backend::{Backend, ListWorkers, RunningWorker}; use futures::TryFutureExt; -use ulid::Ulid; -use crate::{CompactType, SqliteContext, SqliteStorage}; +use crate::{SqliteStorage, error::Error}; struct Worker { id: String, @@ -13,18 +12,13 @@ struct Worker { started_at: Option, } -impl ListWorkers for SqliteStorage +impl ListWorkers for SqliteStorage where - Self: BackendExt< - Context = SqliteContext, - Compact = CompactType, - IdType = Ulid, - Error = sqlx::Error, - >, + Self: Backend, { fn list_workers(&self) -> impl Future, Self::Error>> + Send { - let queue = self.config().queue().to_string(); - let pool = self.pool.clone(); + let queue = self.persistence.config.queue.to_string(); + let pool = self.persistence.pool.clone(); let limit = 100; let offset = 0; async move { @@ -56,7 +50,7 @@ where fn list_all_workers( &self, ) -> impl Future, Self::Error>> + Send { - let pool = self.pool.clone(); + let pool = self.persistence.pool.clone(); let limit = 100; let offset = 0; async move { diff --git a/src/queries/lock_task.rs b/src/queries/lock_task.rs index 1fdd548..b8d7c9e 100644 --- a/src/queries/lock_task.rs +++ b/src/queries/lock_task.rs @@ -1,17 +1,19 @@ -use sqlx::{Executor, Sqlite}; +use sqlx::Executor; -/// Lock a task, given a worker -pub async fn lock_task Executor<'a, Database = Sqlite>>( - pool: E, - task_id: &str, +/// Lock multiple tasks, given a worker +pub async fn lock_tasks<'a, E>( + executor: E, + task_ids: &[String], worker_id: &str, -) -> Result<(), sqlx::Error> { - let res = sqlx::query_file!("queries/task/lock.sql", task_id, worker_id) - .execute(pool) +) -> Result +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ + let ids_json = serde_json::to_string(task_ids).map_err(|e| sqlx::Error::Decode(Box::new(e)))?; + + let res = sqlx::query_file!("queries/task/lock.sql", ids_json, worker_id) + .execute(executor) .await?; - if res.rows_affected() == 0 { - return Err(sqlx::Error::RowNotFound); - } - Ok(()) + Ok(res.rows_affected()) } diff --git a/src/queries/metrics.rs b/src/queries/metrics.rs index a21db25..cc1170e 100644 --- a/src/queries/metrics.rs +++ b/src/queries/metrics.rs @@ -1,7 +1,8 @@ -use apalis_core::backend::{BackendExt, Metrics, Statistic}; -use ulid::Ulid; +use std::str::FromStr; -use crate::{CompactType, SqliteContext, SqliteStorage}; +use apalis_core::backend::{Backend, Metrics, StatType, Statistic}; + +use crate::{SqliteStorage, error::Error}; struct StatisticRow { /// The priority of the statistic (lower number means higher priority) @@ -14,17 +15,12 @@ struct StatisticRow { pub value: Option, } -impl Metrics for SqliteStorage +impl Metrics for SqliteStorage where - Self: BackendExt< - Context = SqliteContext, - Compact = CompactType, - IdType = Ulid, - Error = sqlx::Error, - >, + Self: Backend, { fn global(&self) -> impl Future, Self::Error>> + Send { - let pool = self.pool.clone(); + let pool = self.persistence.pool.clone(); async move { let rec = sqlx::query_file_as!(StatisticRow, "queries/backend/overview.sql") .fetch_all(&pool) @@ -32,7 +28,7 @@ where .into_iter() .map(|r| Statistic { priority: Some(r.priority as u64), - stat_type: super::stat_type_from_string(&r.r#type), + stat_type: FromStr::from_str(&r.r#type).unwrap_or(StatType::Number), title: r.statistic, value: r.value.unwrap_or_default().to_string(), }) @@ -41,8 +37,8 @@ where } } fn fetch_by_queue(&self) -> impl Future, Self::Error>> + Send { - let pool = self.pool.clone(); - let queue_id = self.config().queue().to_string(); + let pool = self.persistence.pool.clone(); + let queue_id = self.persistence.config.queue.as_ref(); async move { let rec = sqlx::query_file_as!( StatisticRow, @@ -54,7 +50,7 @@ where .into_iter() .map(|r| Statistic { priority: Some(r.priority as u64), - stat_type: super::stat_type_from_string(&r.r#type), + stat_type: FromStr::from_str(&r.r#type).unwrap_or(StatType::Number), title: r.statistic, value: r.value.unwrap_or_default().to_string(), }) diff --git a/src/queries/mod.rs b/src/queries/mod.rs index 225ee79..66e542f 100644 --- a/src/queries/mod.rs +++ b/src/queries/mod.rs @@ -1,40 +1,36 @@ -//! Module for various query implementations for the SQLite backend. -//! -//! Each submodule contains specific query logic used by the backend. -use apalis_core::backend::StatType; - -/// ACK a task on success or failure -pub mod ack_task; +//! Queries needed for polling, updating and exposing tasks. +// /// ACK a task on success or failure +mod ack_task; /// Fetch tasks by their IDs -pub mod fetch_by_id; +mod fetch_by_id; + +/// Fetch the next stream of tasks +mod fetch_next; /// Keep workers alive by updating their heartbeat -pub mod keep_alive; +mod keep_alive; /// List available queues -pub mod list_queues; +mod list_queues; /// List tasks in a specific queue -pub mod list_tasks; +mod list_tasks; /// List workers -pub mod list_workers; +mod list_workers; /// Lock a task for processing -pub mod lock_task; +mod lock_task; /// Metrics related queries -pub mod metrics; +mod metrics; /// Re-enqueue orphaned tasks that were being processed by dead workers -pub mod reenqueue_orphaned; +mod reenqueue_orphaned; /// Register a new worker in the database -pub mod register_worker; +mod register_worker; /// Vacuum the database to optimize space -pub mod vacuum; +mod vacuum; /// Wait for tasks to complete and stream their results -pub mod wait_for; +mod wait_for; -/// Convert a string representation of a stat type to the corresponding `StatType` enum variant -fn stat_type_from_string(s: &str) -> StatType { - match s { - "Decimal" => StatType::Decimal, - "Percentage" => StatType::Percentage, - "Timestamp" => StatType::Timestamp, - _ => StatType::Number, // default fallback - } -} +pub use crate::queries::{ + ack_task::AckPayload, ack_task::ack_tasks, fetch_next::fetch_next, keep_alive::keep_alive, + lock_task::lock_tasks, reenqueue_orphaned::reenqueue_abandoned, + reenqueue_orphaned::reenqueue_orphaned, register_worker::register_worker, +}; +pub use crate::sink::push_tasks; diff --git a/src/queries/reenqueue_orphaned.rs b/src/queries/reenqueue_orphaned.rs index 9bea218..67d02b6 100644 --- a/src/queries/reenqueue_orphaned.rs +++ b/src/queries/reenqueue_orphaned.rs @@ -1,54 +1,62 @@ -use std::time::Duration; +use apalis_core::{backend::WorkerFilter, task::task_id::coalesce_ids}; +use sqlx::Executor; -use futures::{FutureExt, Stream, stream}; -use sqlx::SqlitePool; - -use crate::Config; - -/// Re-enqueue tasks that were being processed by workers that have not sent a keep-alive signal within the specified duration -pub fn reenqueue_orphaned( - pool: SqlitePool, - config: &Config, -) -> impl Future> + Send { - let dead_for = config.reenqueue_orphaned_after().as_secs() as i64; - let queue = config.queue().to_string(); - async move { - match sqlx::query_file!("queries/backend/reenqueue_orphaned.sql", dead_for, queue,) - .execute(&pool) - .await - { - Ok(res) => { - if res.rows_affected() > 0 { - log::info!( - "Re-enqueued {} orphaned tasks that were being processed by dead workers", - res.rows_affected() - ); - } - Ok(res.rows_affected()) - } - Err(e) => { - log::error!("Failed to re-enqueue orphaned tasks: {e}"); - Err(e) - } - } +/// Re-enqueue tasks that were being processed by dead workers +/// +/// A worker that has not sent a keep-alive signal within the heartbeat duration is considered dead +pub async fn reenqueue_orphaned<'a, E>( + executor: E, + dead_for: i64, + queue: &str, + filter: &WorkerFilter, +) -> Result +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ + let (exclude_id, only_id) = match filter { + WorkerFilter::AllExcept(id) => (Some(id), None), + WorkerFilter::Only(id) => (None, Some(id)), + WorkerFilter::None => (None, None), + _ => unreachable!(), + }; + match sqlx::query_file!( + "queries/backend/reenqueue_orphaned.sql", + dead_for, + queue, + exclude_id, + only_id, + ) + .execute(executor) + .await + { + Ok(res) => Ok(res.rows_affected()), + Err(e) => Err(e), } } -/// Create a stream that periodically re-enqueues orphaned tasks -pub fn reenqueue_orphaned_stream( - pool: SqlitePool, - config: Config, - interval: Duration, -) -> impl Stream> + Send { - let config = config; - stream::unfold((), move |_| { - let pool = pool.clone(); - let config = config.clone(); - let interval = apalis_core::timer::Delay::new(interval); - let fut = async move { - interval.await; - reenqueue_orphaned(pool, &config).await - }; - fut.map(|res| Some((res, ()))) - }) +/// Re-enqueue tasks that were being processed by dying a worker +/// +/// This will be invoked during `Backend::poll_close` +pub async fn reenqueue_abandoned<'a, E>( + executor: E, + queue: &str, + worker: &str, + task_ids: &Vec, +) -> Result +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ + let task_ids = coalesce_ids(task_ids); + match sqlx::query_file!( + "queries/backend/reenqueue_abandoned.sql", + queue, + worker, + task_ids + ) + .execute(executor) + .await + { + Ok(res) => Ok(res.rows_affected()), + Err(e) => Err(e), + } } diff --git a/src/queries/register_worker.rs b/src/queries/register_worker.rs index bb21bae..3bf4baf 100644 --- a/src/queries/register_worker.rs +++ b/src/queries/register_worker.rs @@ -1,19 +1,22 @@ use apalis_core::worker::context::WorkerContext; -use sqlx::SqlitePool; +use sqlx::Executor; -use crate::Config; +use crate::config::Config; /// Register a new worker in the database -pub async fn register_worker( - pool: SqlitePool, - config: Config, - worker: WorkerContext, +pub async fn register_worker<'a, E>( + executor: E, + config: &Config, + worker: &WorkerContext, storage_type: &str, -) -> Result<(), sqlx::Error> { +) -> Result<(), sqlx::Error> +where + E: Executor<'a, Database = sqlx::Sqlite>, +{ let worker_id = worker.name().to_owned(); - let queue = config.queue().to_string(); + let queue = config.queue.to_string(); let layers = worker.get_service().to_owned(); - let keep_alive = config.keep_alive().as_secs() as i64; + let keep_alive = config.heartbeat_interval.as_secs() as i64; let res = sqlx::query_file!( "queries/backend/register_worker.sql", worker_id, @@ -22,7 +25,7 @@ pub async fn register_worker( layers, keep_alive, ) - .execute(&pool) + .execute(executor) .await?; if res.rows_affected() == 0 { return Err(sqlx::Error::Io(std::io::Error::new( diff --git a/src/queries/vacuum.rs b/src/queries/vacuum.rs index 3071e11..23f6567 100644 --- a/src/queries/vacuum.rs +++ b/src/queries/vacuum.rs @@ -1,18 +1,15 @@ -use apalis_core::backend::{BackendExt, Vacuum}; -use ulid::Ulid; +use apalis_core::backend::{Backend, Vacuum}; -use crate::{CompactType, SqliteStorage}; +use crate::{Error, SqliteStorage}; -impl Vacuum for SqliteStorage +impl Vacuum for SqliteStorage where - Self: BackendExt, - F: Send, - Decode: Send, + Self: Backend, Args: Send, { async fn vacuum(&mut self) -> Result { let res = sqlx::query_file!("queries/backend/vacuum.sql") - .execute(&self.pool) + .execute(&self.persistence.pool) .await?; Ok(res.rows_affected() as usize) } diff --git a/src/queries/wait_for.rs b/src/queries/wait_for.rs index 44c70e9..be0b4d2 100644 --- a/src/queries/wait_for.rs +++ b/src/queries/wait_for.rs @@ -1,33 +1,32 @@ use std::{collections::HashSet, str::FromStr, vec}; use apalis_core::{ - backend::{BackendExt, TaskResult, WaitForCompletion}, - task::{status::Status, task_id::TaskId}, + backend::{Backend, TaskResult, WaitForCompletion}, + task::{ + status::Status, + task_id::{TaskId, coalesce_ids}, + }, }; use futures::{StreamExt, stream::BoxStream}; -use serde::de::DeserializeOwned; -use ulid::Ulid; -use crate::{CompactType, SqliteStorage}; +use crate::{Error, SqliteStorage}; #[derive(Debug)] struct ResultRow { pub id: Option, pub status: Option, pub result: Option, + pub attempt: Option, } -impl WaitForCompletion for SqliteStorage +impl WaitForCompletion for SqliteStorage where - Self: BackendExt, - Result: DeserializeOwned, + Self: Backend, + O: Send + 'static + serde::de::DeserializeOwned, { - type ResultStream = BoxStream<'static, Result, Self::Error>>; - fn wait_for( - &self, - task_ids: impl IntoIterator>, - ) -> Self::ResultStream { - let pool = self.pool.clone(); + type ResultStream = BoxStream<'static, Result, Self::Error>>; + fn wait_for(&self, task_ids: impl IntoIterator) -> Self::ResultStream { + let pool = self.persistence.pool.clone(); let ids: HashSet = task_ids.into_iter().map(|id| id.to_string()).collect(); let stream = futures::stream::unfold(ids, move |mut remaining_ids| { @@ -38,7 +37,7 @@ where } let ids_vec: Vec = remaining_ids.iter().cloned().collect(); - let ids_vec = serde_json::to_string(&ids_vec).unwrap(); + let ids_vec = coalesce_ids(ids_vec); let rows = sqlx::query_file_as!( ResultRow, "queries/backend/fetch_completed_tasks.sql", @@ -57,15 +56,14 @@ where for row in rows { let task_id = row.id.clone().unwrap(); remaining_ids.remove(&task_id); - // Here we would normally decode the output O from the row - // For simplicity, we assume O is String and the output is stored in row.output let result: Result = serde_json::from_str(&row.result.unwrap()).unwrap(); - results.push(Ok(TaskResult::new( - TaskId::from_str(&task_id).ok()?, - Status::from_str(&row.status.unwrap()).ok()?, + results.push(Ok(TaskResult { + task_id: TaskId::from_str(&task_id).ok()?, + status: Status::from_str(&row.status.unwrap()).ok()?, + attempt: row.attempt.unwrap_or_default() as usize, result, - ))); + })); } Some((futures::stream::iter(results), remaining_ids)) @@ -77,13 +75,12 @@ where // Implementation of check_status fn check_status( &self, - task_ids: impl IntoIterator> + Send, - ) -> impl Future>, Self::Error>> + Send { - let pool = self.pool.clone(); - let ids: Vec = task_ids.into_iter().map(|id| id.to_string()).collect(); + task_ids: impl IntoIterator + Send, + ) -> impl Future>, Self::Error>> + Send { + let pool = self.persistence.pool.clone(); async move { - let ids = serde_json::to_string(&ids).unwrap(); + let ids = coalesce_ids(task_ids); let rows = sqlx::query_file_as!(ResultRow, "queries/backend/fetch_completed_tasks.sql", ids) .fetch_all(&pool) @@ -91,20 +88,17 @@ where let mut results = Vec::new(); for row in rows { - let task_id = TaskId::from_str(&row.id.unwrap()) - .map_err(|_| sqlx::Error::Protocol("Invalid task ID".into()))?; + let task_id = TaskId::from_str(&row.id.unwrap()).map_err(Error::TaskIdError)?; - let result: Result = serde_json::from_str(&row.result.unwrap()) - .map_err(|_| sqlx::Error::Protocol("Failed to decode result".into()))?; + let result: Result = + serde_json::from_str(&row.result.unwrap()).map_err(Error::JsonError)?; - results.push(TaskResult::new( + results.push(TaskResult { task_id, - row.status - .unwrap() - .parse() - .map_err(|_| sqlx::Error::Protocol("Invalid status value".into()))?, + status: row.status.unwrap().parse().map_err(Error::StatusError)?, result, - )); + attempt: row.attempt.unwrap_or_default() as usize, + }); } Ok(results) diff --git a/src/shared.rs b/src/shared.rs index c99af53..15b122f 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -1,48 +1,147 @@ +//! Shared SQLite storage for multiple workers. +//! +//! This module provides [`SqliteStorageFactory`], a factory for creating +//! multiple [`SqliteStorage`] instances that share a single SQLite connection +//! pool and task-fetching loop. +//! +//! ## Why shared storage? +//! +//! A SQLite backend can be used by multiple workers, but having every worker +//! independently poll the database can result in unnecessary database queries +//! and contention. +//! +//! [`SqliteStorageFactory`] addresses this by maintaining a +//! single shared polling task for all storage instances created by the factory. +//! +//! When a task is inserted into the jobs table, SQLite's update hook notifies +//! the shared poller. +//! +//! The poller fetches available tasks for all registered +//! queues in a batch and routes each task to the [`SharedFetcher`] belonging to +//! the corresponding storage instance. +//! +//! The resulting architecture is roughly: +//! +//! ```text +//! SQLite +//! │ +//! update hook +//! │ +//! ▼ +//! ┌───────────────────┐ +//! │ Shared poller │ +//! │ │ +//! │ fetches tasks for │ +//! │ all registered │ +//! │ queues in batches │ +//! └─────────┬─────────┘ +//! │ +//! ┌─────────────┼─────────────┐ +//! │ │ │ +//! ▼ ▼ ▼ +//! queue A queue B queue C +//! │ │ │ +//! ▼ ▼ ▼ +//! Worker A Worker B Worker C +//! ``` +//! +//! ## Creating a factory +//! +//! The factory owns the underlying [`SqlitePool`] and can create multiple +//! backends from it: +//! +//! ```ignore +//! # use apalis_sqlite::shared::SqliteStorageFactory; +//! # use apalis_core::backend::factory::BackendFactory; +//! let mut factory = SqliteStorageFactory::new("sqlite://jobs.db"); +//! +//! let first = factory.create().unwrap(); +//! let second = factory.create().unwrap(); +//! ``` +//! +//! Each backend has its own queue registration and receiver, while the +//! database connection pool and polling task are shared. +//! +//! ## Queue registration +//! +//! Each backend created by the factory is associated with a queue derived from +//! its [`Config`]. +//! +//! A queue may only be registered once with a factory. +//! +//! Attempting to create another backend for an already-registered queue returns +//! [`SharedSqliteError::NamespaceExists`]. +//! +//! ## Task dispatch +//! +//! [`SharedFetcher`] implements [`Stream`] and receives tasks from the shared +//! polling loop through an asynchronous channel. +//! +//! The shared poller keeps running while at least one fetcher is being polled and dispatches each +//! fetched task to the channel associated with its queue. +//! +//! The resulting backend is an [`Interleave`] combining the normal +//! [`SqliteStorage`] implementation with [`SharedFetcher`]. +//! +//! This allows task insertion and other storage operations to continue using the normal SQLite +//! backend while task consumption is coordinated by the shared polling loop. +//! +//! ## Pool configuration +//! +//! [`SqliteStorageFactory::new`] creates a pool with unlimited connection +//! lifetime and idle timeout. For applications requiring more control over the +//! SQLite pool, [`SqliteStorageFactory::new_with_pool_options`] accepts custom +//! [`PoolOptions`]. +//! +//! [`SqlitePool`]: sqlx::SqlitePool +//! [`PoolOptions`]: sqlx::pool::PoolOptions +//! [`Config`]: crate::Config +//! [`Interleave`]: apalis_core::backend::ext::interleave::Interleave +//! [`SqliteStorage`]: crate::SqliteStorage +//! [`SqliteStorageFactory`]: crate::shared::SqliteStorageFactory +//! [`SharedFetcher`]: crate::shared::SharedFetcher +//! [`SharedSqliteError`]: crate::shared::SharedSqliteError +//! [`Stream`]: futures::Stream use std::{ cmp::max, collections::{HashMap, HashSet}, future::ready, - marker::PhantomData, pin::Pin, sync::Arc, task::{Context, Poll}, }; use crate::{ - CompactType, Config, JOBS_TABLE, SqliteContext, SqliteStorage, SqliteTask, - ack::{LockTaskLayer, SqliteAck}, + Config, JOBS_TABLE, SqliteStorage, SqliteTask, callback::{DbEvent, update_hook_callback}, - fetcher::SqlitePollFetcher, - initial_heartbeat, keep_alive, }; -use crate::{from_row::SqliteTaskRow, sink::SqliteSink}; -use apalis_codec::json::JsonCodec; -use apalis_core::{ - backend::{Backend, BackendExt, TaskStream, codec::Codec, queue::Queue, shared::MakeShared}, - layers::Stack, - worker::{context::WorkerContext, ext::ack::AcknowledgeLayer}, -}; -use apalis_sql::TaskRow; +use crate::{Error, from_row::SqliteTaskRow}; + +use apalis_core::backend::{BackendConfig, ext::interleave::Interleave, factory::BackendFactory}; use futures::{ FutureExt, SinkExt, Stream, StreamExt, TryStreamExt, channel::mpsc::{self, Receiver, Sender}, future::{BoxFuture, Shared}, lock::Mutex, - stream::{self, BoxStream, select}, + ready, }; +use serde::{Serialize, de::DeserializeOwned}; use sqlx::{Sqlite, SqlitePool, pool::PoolOptions, sqlite::SqliteOperation}; -use ulid::Ulid; + +/// An [`SqliteStorage`] interleaving with [`SharedFetcher`] +pub type SharedSqliteStorage = Interleave, SharedFetcher>; + +type Registry = Arc>>>>; /// Shared Sqlite storage backend that can be used across multiple workers #[derive(Clone, Debug)] -pub struct SharedSqliteStorage { +pub struct SqliteStorageFactory { pool: SqlitePool, - registry: Arc>>>>, + registry: Registry, drive: Shared>, - _marker: PhantomData, } -impl SharedSqliteStorage { +impl SqliteStorageFactory { /// Get a reference to the underlying Sqlite connection pool #[must_use] pub fn pool(&self) -> &SqlitePool { @@ -50,16 +149,10 @@ impl SharedSqliteStorage { } } -impl SharedSqliteStorage> { - /// Create a new shared Sqlite storage backend with the given database URL - #[must_use] - pub fn new(url: &str) -> Self { - Self::new_with_codec(url) - } - +impl SqliteStorageFactory { /// Create a new shared Sqlite storage backend with the given database URL and codec #[must_use] - pub fn new_with_codec(url: &str) -> SharedSqliteStorage { + pub fn new(url: &str) -> SqliteStorageFactory { Self::new_with_pool_options( url, PoolOptions::new().max_lifetime(None).idle_timeout(None), @@ -68,10 +161,7 @@ impl SharedSqliteStorage> { /// Create a new shared Sqlite storage backend with the given database URL and pool options #[must_use] - pub fn new_with_pool_options( - url: &str, - options: PoolOptions, - ) -> SharedSqliteStorage { + pub fn new_with_pool_options(url: &str, options: PoolOptions) -> SqliteStorageFactory { let (tx, rx) = mpsc::unbounded::(); let pool = options .after_connect(move |conn, _meta| { @@ -85,11 +175,11 @@ impl SharedSqliteStorage> { .connect_lazy(url) .expect("Failed to create Sqlite pool"); - let registry: Arc>>>> = - Arc::new(Mutex::new(HashMap::default())); + let registry: Registry = Registry::default(); + let p = pool.clone(); let instances = registry.clone(); - SharedSqliteStorage { + SqliteStorageFactory { pool, drive: async move { rx.filter(|a| { @@ -105,43 +195,36 @@ impl SharedSqliteStorage> { let job_types = serde_json::to_string( &instances.keys().cloned().collect::>(), ) - .unwrap(); - let row_ids = serde_json::to_string(&row_ids).unwrap(); + .map_err(Error::JsonError)?; + let row_ids = serde_json::to_string(&row_ids).map_err(Error::JsonError)?; let mut tx = pool.begin().await?; - let buffer_size = max(10, instances.len()) as i32; + let batch_size = max(10, instances.len()) as i32; let res: Vec<_> = sqlx::query_file_as!( SqliteTaskRow, "queries/backend/fetch_next_shared.sql", job_types, row_ids, - buffer_size, + batch_size, ) .fetch(&mut *tx) - .map(|r| { - let row: TaskRow = r?.try_into()?; - row.try_into_task_compact() - .map_err(|e| sqlx::Error::Protocol(e.to_string())) - }) + .map_ok(|r| r.try_into()) .try_collect() .await?; tx.commit().await?; - Ok::<_, sqlx::Error>(res) + Ok::<_, Error>(res) } }) - .for_each(|r| async { + .map_ok(futures::stream::iter) + .try_flatten() + .for_each(|r: Result| async { match r { - Ok(tasks) => { + Ok(task) => { let mut instances = instances.lock().await; - for task in tasks { - if let Some(tx) = instances.get_mut( - task.parts - .ctx - .queue() - .as_ref() - .expect("Namespace must be set"), - ) { - let _ = tx.send(task).await; - } + if let Some(tx) = instances + .get_mut(&task.queue().expect("Queue must be set").to_string()) + && let Err(e) = tx.send(Ok(task)).await + { + log::error!("Error pushing task: {e:?}"); } } Err(e) => { @@ -154,7 +237,6 @@ impl SharedSqliteStorage> { .boxed() .shared(), registry, - _marker: PhantomData, } } } @@ -170,170 +252,60 @@ pub enum SharedSqliteError { RegistryLocked, } -impl> MakeShared - for SharedSqliteStorage +impl BackendFactory for SqliteStorageFactory +where + Args: Send + Sync + Serialize + DeserializeOwned + 'static, { - type Backend = SqliteStorage>; - type Config = Config; - type MakeError = SharedSqliteError; - fn make_shared(&mut self) -> Result + type Backend = SharedSqliteStorage; + type Error = SharedSqliteError; + + fn create(&mut self) -> Result where - Self::Config: Default, + ::Config: Default, { - Self::make_shared_with_config(self, Config::new(std::any::type_name::())) + let config = Config::default().queue(std::any::type_name::()); + self.create_with_config(config) } - fn make_shared_with_config( + + fn create_with_config( &mut self, - config: Self::Config, - ) -> Result { - let (tx, rx) = mpsc::channel(config.buffer_size()); + config: ::Config, + ) -> Result { + let (tx, rx) = mpsc::channel(config.batch_size); let mut r = self .registry .try_lock() .ok_or(SharedSqliteError::RegistryLocked)?; - if r.insert(config.queue().to_string(), tx).is_some() { - return Err(SharedSqliteError::NamespaceExists( - config.queue().to_string(), - )); + if r.insert(config.queue.to_string(), tx).is_some() { + return Err(SharedSqliteError::NamespaceExists(config.queue.to_string())); } - let sink = SqliteSink::new(&self.pool, &config); - Ok(SqliteStorage { - config, - fetcher: SharedFetcher { + Ok(Interleave::new( + SqliteStorage::new(&self.pool).with_config(config), + SharedFetcher { poller: self.drive.clone(), - receiver: Arc::new(std::sync::Mutex::new(rx)), + receiver: Arc::new(Mutex::new(rx)), }, - pool: self.pool.clone(), - sink, - job_type: PhantomData, - codec: PhantomData, - }) + )) } } +/// A fetcher that uses a channel to receive jobs from a shared polling point #[derive(Clone, Debug)] -pub struct SharedFetcher { +pub struct SharedFetcher { poller: Shared>, - receiver: Arc>>>, + receiver: Arc>>>, } -impl Stream for SharedFetcher { - type Item = SqliteTask; +impl Stream for SharedFetcher { + type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); // Keep the poller alive by polling it, but ignoring the output let _ = this.poller.poll_unpin(cx); - // Delegate actual items to receiver - this.receiver.lock().unwrap().poll_next_unpin(cx) - } -} - -impl Backend for SqliteStorage> -where - Args: Send + 'static + Unpin + Sync, - Decode: Codec + 'static + Unpin + Send + Sync, - Decode::Error: std::error::Error + Send + Sync + 'static, -{ - type Args = Args; - - type IdType = Ulid; - - type Error = sqlx::Error; - - type Stream = TaskStream, sqlx::Error>; - - type Beat = BoxStream<'static, Result<(), sqlx::Error>>; - - type Context = SqliteContext; - - type Layer = Stack, LockTaskLayer>; - - fn heartbeat(&self, worker: &WorkerContext) -> Self::Beat { - let keep_alive_interval = *self.config.keep_alive(); - let pool = self.pool.clone(); - let worker = worker.clone(); - let config = self.config.clone(); - - stream::unfold((), move |()| async move { - apalis_core::timer::sleep(keep_alive_interval).await; - Some(((), ())) - }) - .then(move |_| keep_alive(pool.clone(), config.clone(), worker.clone())) - .boxed() - } - - fn middleware(&self) -> Self::Layer { - let lock = LockTaskLayer::new(self.pool.clone()); - let ack = AcknowledgeLayer::new(SqliteAck::new(self.pool.clone())); - Stack::new(ack, lock) - } - - fn poll(self, worker: &WorkerContext) -> Self::Stream { - self.poll_shared(worker) - .map(|a| match a { - Ok(Some(task)) => Ok(Some( - task.try_map(|t| Decode::decode(&t)) - .map_err(|e| sqlx::Error::Decode(e.into()))?, - )), - Ok(None) => Ok(None), - Err(e) => Err(e), - }) - .boxed() - } -} - -impl BackendExt - for SqliteStorage> -where - Self: Backend, - Decode: Codec + Send + 'static, - Decode::Error: std::error::Error + Send + Sync + 'static, - Args: Send + 'static + Unpin, -{ - type Codec = Decode; - type Compact = CompactType; - type CompactStream = TaskStream, sqlx::Error>; - - fn get_queue(&self) -> Queue { - self.config.queue().clone() - } - - fn poll_compact(self, worker: &WorkerContext) -> Self::CompactStream { - self.poll_shared(worker).boxed() - } -} - -impl SqliteStorage> { - fn poll_shared( - self, - worker: &WorkerContext, - ) -> impl Stream>, sqlx::Error>> + 'static { - let pool = self.pool.clone(); - let worker = worker.clone(); - // Initial registration heartbeat - // This ensures that the worker is registered before fetching any tasks - // This also ensures that the worker is marked as alive in case it crashes - // before fetching any tasks - // Subsequent heartbeats are handled in the heartbeat stream - let init = initial_heartbeat( - pool, - self.config.clone(), - worker.clone(), - "SharedSqliteStorage", - ); - let starter = stream::once(init) - .map_ok(|_| None) // Noop after initial heartbeat - .boxed(); - let lazy_fetcher = self.fetcher.map(|s| Ok(Some(s))).boxed(); - - let eager_fetcher = StreamExt::boxed(SqlitePollFetcher::::new( - &self.pool, - &self.config, - &worker, - )); - starter.chain(select(lazy_fetcher, eager_fetcher)).boxed() + let mut guard = ready!(this.receiver.lock().poll_unpin(cx)); + guard.poll_next_unpin(cx) } } @@ -342,20 +314,22 @@ mod tests { use std::time::Duration; use apalis_core::{ - backend::TaskSink, error::BoxDynError, task::task_id::TaskId, - worker::builder::WorkerBuilder, + backend::TaskSink, + error::BoxDynError, + task::task_id::TaskId, + worker::{builder::WorkerBuilder, context::WorkerContext}, }; use super::*; #[tokio::test] - async fn basic_worker() { - let mut store = SharedSqliteStorage::new(":memory:"); - SqliteStorage::setup(store.pool()).await.unwrap(); + async fn factory_worker() { + let mut factory = SqliteStorageFactory::new(":memory:"); + SqliteStorage::setup(factory.pool()).await.unwrap(); - let mut map_store = store.make_shared().unwrap(); + let mut map_store = factory.create().unwrap(); - let mut int_store = store.make_shared().unwrap(); + let mut int_store: SharedSqliteStorage = factory.create().unwrap(); map_store .push(HashMap::::from([("value".to_string(), 42)])) @@ -363,9 +337,9 @@ mod tests { .unwrap(); int_store.push(99).await.unwrap(); - async fn send_reminder( + async fn send_reminder( _: T, - _task_id: TaskId, + _task_id: TaskId, wrk: WorkerContext, ) -> Result<(), BoxDynError> { tokio::time::sleep(Duration::from_secs(2)).await; diff --git a/src/sink.rs b/src/sink.rs index 02c0fb4..2072962 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -1,155 +1,68 @@ use std::{ pin::Pin, - sync::Arc, task::{Context, Poll}, }; -use futures::{ - FutureExt, Sink, - future::{BoxFuture, Shared}, -}; -use sqlx::SqlitePool; +use futures::Sink; +use sqlx::Executor; use ulid::Ulid; -use crate::{CompactType, Config, SqliteStorage, SqliteTask}; - -type FlushFuture = BoxFuture<'static, Result<(), Arc>>; - -/// Sink for pushing tasks into the sqlite backend -#[pin_project::pin_project] -#[derive(Debug)] -pub struct SqliteSink { - pool: SqlitePool, - config: Config, - buffer: Vec>, - #[pin] - flush_future: Option>, - _marker: std::marker::PhantomData<(Args, Codec)>, -} - -impl Clone for SqliteSink { - fn clone(&self) -> Self { - Self { - pool: self.pool.clone(), - config: self.config.clone(), - buffer: Vec::new(), - flush_future: None, - _marker: std::marker::PhantomData, - } - } -} +use crate::{SqliteStorage, SqliteTask, error::Error}; /// Push a batch of tasks into the database -pub async fn push_tasks( - pool: SqlitePool, - cfg: Config, - buffer: Vec>, -) -> Result<(), Arc> { - let mut tx = pool.begin().await?; - for task in buffer { +pub async fn push_tasks<'a, E>( + executor: &'a mut E, + queue: &str, + tasks: &[SqliteTask], +) -> Result<(), Error> +where + for<'e> &'e mut E: Executor<'e, Database = sqlx::Sqlite>, +{ + for task in tasks { let id = task - .parts - .task_id + .task_id() + .as_ref() .map(|id| id.to_string()) - .unwrap_or(Ulid::new().to_string()); - let run_at = task.parts.run_at as i64; - let max_attempts = task.parts.ctx.max_attempts(); - let priority = task.parts.ctx.priority(); - let args = task.args; - // Use specified queue if specified, otherwise use default - let job_type = cfg.queue().to_string(); - let idempotency_key = task.parts.idempotency_key; - let meta = serde_json::to_string(&task.parts.ctx.meta()).unwrap_or_default(); + .unwrap_or(Ulid::generate().to_string()); + let run_at = task.run_at().unwrap_or(0) as i64; + let max_attempts = task.max_attempts().unwrap_or(25) as i64; + let priority = task.priority().unwrap_or_default() as i64; + let args = &task.args; + let idempotency_key = task.idempotency_key(); + let meta = serde_json::to_string(task.metadata()).unwrap_or_default(); sqlx::query_file!( "queries/task/sink.sql", args, id, - job_type, + queue, max_attempts, run_at, priority, meta, idempotency_key ) - .execute(&mut *tx) + .execute(&mut *executor) .await?; } - tx.commit().await?; - Ok(()) } -impl SqliteSink { - /// Create a new SqliteSink - #[must_use] - pub fn new(pool: &SqlitePool, config: &Config) -> Self { - Self { - pool: pool.clone(), - config: config.clone(), - buffer: Vec::new(), - _marker: std::marker::PhantomData, - flush_future: None, - } - } -} +impl Sink for SqliteStorage { + type Error = Error; -impl Sink> for SqliteStorage -where - Args: Send + Sync + 'static, -{ - type Error = sqlx::Error; - - fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } - fn start_send(self: Pin<&mut Self>, item: SqliteTask) -> Result<(), Self::Error> { - // Add the item to the buffer - self.project().sink.buffer.push(item); - Ok(()) + fn start_send(self: Pin<&mut Self>, item: SqliteTask) -> Result<(), Self::Error> { + self.project().persistence.start_send(item) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut this = self.project(); - - // If there's no existing future and buffer is empty, we're done - if this.sink.flush_future.is_none() && this.sink.buffer.is_empty() { - return Poll::Ready(Ok(())); - } - - // Create the future only if we don't have one and there's work to do - if this.sink.flush_future.is_none() && !this.sink.buffer.is_empty() { - let pool = this.pool.clone(); - let config = this.config.clone(); - let buffer = std::mem::take(&mut this.sink.buffer); - let sink_fut = push_tasks(pool, config, buffer); - this.sink.flush_future = Some((Box::pin(sink_fut) as FlushFuture).shared()); - } - - // Poll the existing future - if let Some(mut fut) = this.sink.flush_future.take() { - match fut.poll_unpin(cx) { - Poll::Ready(Ok(())) => { - // Future completed successfully, don't put it back - Poll::Ready(Ok(())) - } - Poll::Ready(Err(e)) => { - // Future completed with error, don't put it back - Poll::Ready(Err(Arc::into_inner(e).unwrap())) - } - Poll::Pending => { - // Future is still pending, put it back and return Pending - this.sink.flush_future = Some(fut); - Poll::Pending - } - } - } else { - // No future and no work to do - Poll::Ready(Ok(())) - } + Sink::poll_flush(self.project().persistence, cx) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.poll_flush(cx) + Sink::poll_close(self.project().persistence, cx) } } diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 8dd036d..fc3bd92 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -7,32 +7,24 @@ version = "0.10" [policy.apalis-sqlite] audit-as-crates-io = true +[[exemptions.aho-corasick]] +version = "1.1.5" +criteria = "safe-to-run" + [[exemptions.allocator-api2]] version = "0.2.21" criteria = "safe-to-deploy" -[[exemptions.android_system_properties]] -version = "0.1.5" -criteria = "safe-to-deploy" - -[[exemptions.anyhow]] -version = "1.0.102" -criteria = "safe-to-deploy" - [[exemptions.apalis]] -version = "1.0.0-rc.9" +version = "1.0.0-rc.10" criteria = "safe-to-run" [[exemptions.apalis-codec]] -version = "0.1.0-rc.9" +version = "0.1.0-rc.10" criteria = "safe-to-deploy" [[exemptions.apalis-core]] -version = "1.0.0-rc.9" -criteria = "safe-to-deploy" - -[[exemptions.apalis-sql]] -version = "1.0.0-rc.9" +version = "1.0.0-rc.10" criteria = "safe-to-deploy" [[exemptions.apalis-sqlite]] @@ -40,7 +32,7 @@ version = "1.0.0-rc.2" criteria = "safe-to-deploy" [[exemptions.apalis-workflow]] -version = "0.1.0-rc.9" +version = "0.1.0-rc.10" criteria = "safe-to-run" [[exemptions.async-channel]] @@ -55,22 +47,18 @@ criteria = "safe-to-deploy" version = "1.14.0" criteria = "safe-to-deploy" -[[exemptions.async-global-executor]] -version = "2.4.1" +[[exemptions.async-fs]] +version = "2.2.0" criteria = "safe-to-deploy" -[[exemptions.async-io]] -version = "1.13.0" +[[exemptions.async-global-executor]] +version = "2.4.1" criteria = "safe-to-deploy" [[exemptions.async-io]] version = "2.6.0" criteria = "safe-to-deploy" -[[exemptions.async-lock]] -version = "2.8.0" -criteria = "safe-to-deploy" - [[exemptions.async-lock]] version = "3.4.2" criteria = "safe-to-deploy" @@ -92,35 +80,39 @@ version = "1.1.2" criteria = "safe-to-deploy" [[exemptions.autocfg]] -version = "1.5.0" +version = "1.5.1" criteria = "safe-to-deploy" [[exemptions.base64]] version = "0.22.1" criteria = "safe-to-deploy" -[[exemptions.base64ct]] -version = "1.8.3" +[[exemptions.bincode]] +version = "2.0.1" criteria = "safe-to-deploy" -[[exemptions.bitflags]] -version = "1.3.2" +[[exemptions.bincode_derive]] +version = "2.0.1" criteria = "safe-to-deploy" [[exemptions.bitflags]] -version = "2.11.1" +version = "2.13.2" criteria = "safe-to-deploy" [[exemptions.block-buffer]] version = "0.10.4" criteria = "safe-to-deploy" +[[exemptions.block-buffer]] +version = "0.12.1" +criteria = "safe-to-deploy" + [[exemptions.blocking]] -version = "1.6.2" +version = "1.7.0" criteria = "safe-to-deploy" [[exemptions.bumpalo]] -version = "3.20.2" +version = "3.20.3" criteria = "safe-to-deploy" [[exemptions.byteorder]] @@ -128,27 +120,27 @@ version = "1.5.0" criteria = "safe-to-deploy" [[exemptions.bytes]] -version = "1.11.1" +version = "1.12.1" criteria = "safe-to-deploy" [[exemptions.cc]] -version = "1.2.61" +version = "1.4.6" criteria = "safe-to-deploy" [[exemptions.cfg-if]] version = "1.0.4" criteria = "safe-to-deploy" -[[exemptions.chrono]] -version = "0.4.44" +[[exemptions.chacha20]] +version = "0.10.2" criteria = "safe-to-deploy" -[[exemptions.concurrent-queue]] -version = "2.5.0" +[[exemptions.cmov]] +version = "0.5.4" criteria = "safe-to-deploy" -[[exemptions.const-oid]] -version = "0.9.6" +[[exemptions.concurrent-queue]] +version = "2.5.0" criteria = "safe-to-deploy" [[exemptions.core-foundation]] @@ -163,6 +155,10 @@ criteria = "safe-to-deploy" version = "0.2.17" criteria = "safe-to-deploy" +[[exemptions.cpufeatures]] +version = "0.3.1" +criteria = "safe-to-deploy" + [[exemptions.crc]] version = "3.4.0" criteria = "safe-to-deploy" @@ -172,31 +168,39 @@ version = "2.5.0" criteria = "safe-to-deploy" [[exemptions.crossbeam-queue]] -version = "0.3.12" +version = "0.3.14" criteria = "safe-to-deploy" [[exemptions.crossbeam-utils]] -version = "0.8.21" +version = "0.8.23" criteria = "safe-to-deploy" [[exemptions.crypto-common]] -version = "0.1.7" +version = "0.1.6" criteria = "safe-to-deploy" -[[exemptions.der]] -version = "0.7.10" +[[exemptions.crypto-common]] +version = "0.2.2" criteria = "safe-to-deploy" -[[exemptions.deranged]] -version = "0.5.8" +[[exemptions.ctutils]] +version = "0.4.2" +criteria = "safe-to-deploy" + +[[exemptions.dashmap]] +version = "6.2.1" criteria = "safe-to-deploy" [[exemptions.digest]] version = "0.10.7" criteria = "safe-to-deploy" +[[exemptions.digest]] +version = "0.11.3" +criteria = "safe-to-deploy" + [[exemptions.displaydoc]] -version = "0.2.5" +version = "0.2.7" criteria = "safe-to-deploy" [[exemptions.dotenvy]] @@ -204,7 +208,7 @@ version = "0.15.7" criteria = "safe-to-deploy" [[exemptions.either]] -version = "1.15.0" +version = "1.18.0" criteria = "safe-to-deploy" [[exemptions.equivalent]] @@ -216,7 +220,7 @@ version = "0.3.14" criteria = "safe-to-deploy" [[exemptions.etcetera]] -version = "0.8.0" +version = "0.11.0" criteria = "safe-to-deploy" [[exemptions.event-listener]] @@ -224,7 +228,7 @@ version = "2.5.3" criteria = "safe-to-deploy" [[exemptions.event-listener]] -version = "5.4.1" +version = "5.4.2" criteria = "safe-to-deploy" [[exemptions.event-listener-strategy]] @@ -232,15 +236,11 @@ version = "0.5.4" criteria = "safe-to-deploy" [[exemptions.fastrand]] -version = "1.9.0" -criteria = "safe-to-deploy" - -[[exemptions.fastrand]] -version = "2.4.1" +version = "2.5.0" criteria = "safe-to-deploy" [[exemptions.find-msvc-tools]] -version = "0.1.9" +version = "0.1.12" criteria = "safe-to-deploy" [[exemptions.fixedbitset]] @@ -248,11 +248,15 @@ version = "0.5.7" criteria = "safe-to-run" [[exemptions.flume]] -version = "0.11.1" +version = "0.12.0" criteria = "safe-to-deploy" [[exemptions.foldhash]] version = "0.1.5" +criteria = "safe-to-run" + +[[exemptions.foldhash]] +version = "0.2.0" criteria = "safe-to-deploy" [[exemptions.foreign-types]] @@ -268,19 +272,19 @@ version = "1.2.2" criteria = "safe-to-deploy" [[exemptions.futures]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-channel]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-core]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-executor]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-intrusive]] @@ -288,11 +292,7 @@ version = "0.5.0" criteria = "safe-to-deploy" [[exemptions.futures-io]] -version = "0.3.32" -criteria = "safe-to-deploy" - -[[exemptions.futures-lite]] -version = "1.13.0" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-lite]] @@ -300,27 +300,27 @@ version = "2.6.1" criteria = "safe-to-deploy" [[exemptions.futures-macro]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-sink]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-task]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.futures-timer]] -version = "3.0.3" +version = "3.0.4" criteria = "safe-to-deploy" [[exemptions.futures-util]] -version = "0.3.32" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.generic-array]] -version = "0.14.7" +version = "0.14.9" criteria = "safe-to-deploy" [[exemptions.getrandom]] @@ -328,27 +328,31 @@ version = "0.2.17" criteria = "safe-to-deploy" [[exemptions.getrandom]] -version = "0.3.4" -criteria = "safe-to-deploy" - -[[exemptions.getrandom]] -version = "0.4.2" +version = "0.4.3" criteria = "safe-to-deploy" [[exemptions.gloo-timers]] version = "0.3.0" criteria = "safe-to-deploy" +[[exemptions.hashbrown]] +version = "0.14.5" +criteria = "safe-to-deploy" + [[exemptions.hashbrown]] version = "0.15.5" +criteria = "safe-to-run" + +[[exemptions.hashbrown]] +version = "0.16.1" criteria = "safe-to-deploy" [[exemptions.hashbrown]] -version = "0.17.0" +version = "0.17.1" criteria = "safe-to-deploy" [[exemptions.hashlink]] -version = "0.10.0" +version = "0.11.1" criteria = "safe-to-deploy" [[exemptions.heck]] @@ -356,11 +360,7 @@ version = "0.5.0" criteria = "safe-to-deploy" [[exemptions.hermit-abi]] -version = "0.3.9" -criteria = "safe-to-deploy" - -[[exemptions.hermit-abi]] -version = "0.5.2" +version = "0.5.3" criteria = "safe-to-deploy" [[exemptions.hex]] @@ -368,55 +368,43 @@ version = "0.4.3" criteria = "safe-to-deploy" [[exemptions.hkdf]] -version = "0.12.4" +version = "0.13.0" criteria = "safe-to-deploy" [[exemptions.hmac]] -version = "0.12.1" -criteria = "safe-to-deploy" - -[[exemptions.home]] -version = "0.5.12" -criteria = "safe-to-deploy" - -[[exemptions.iana-time-zone]] -version = "0.1.65" +version = "0.13.0" criteria = "safe-to-deploy" -[[exemptions.iana-time-zone-haiku]] -version = "0.1.2" +[[exemptions.hybrid-array]] +version = "0.4.15" criteria = "safe-to-deploy" [[exemptions.icu_collections]] -version = "2.2.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.icu_locale_core]] -version = "2.2.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.icu_normalizer]] -version = "2.2.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.icu_normalizer_data]] -version = "2.2.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.icu_properties]] -version = "2.2.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.icu_properties_data]] -version = "2.2.0" +version = "2.3.0" criteria = "safe-to-deploy" [[exemptions.icu_provider]] -version = "2.2.0" -criteria = "safe-to-deploy" - -[[exemptions.id-arena]] -version = "2.3.0" +version = "2.3.1" criteria = "safe-to-deploy" [[exemptions.idna]] @@ -428,15 +416,7 @@ version = "1.2.2" criteria = "safe-to-deploy" [[exemptions.indexmap]] -version = "2.14.0" -criteria = "safe-to-deploy" - -[[exemptions.instant]] -version = "0.1.13" -criteria = "safe-to-deploy" - -[[exemptions.io-lifetimes]] -version = "1.0.11" +version = "2.14.2" criteria = "safe-to-deploy" [[exemptions.itoa]] @@ -444,7 +424,7 @@ version = "1.0.18" criteria = "safe-to-deploy" [[exemptions.js-sys]] -version = "0.3.98" +version = "0.3.105" criteria = "safe-to-deploy" [[exemptions.kv-log-macro]] @@ -453,30 +433,14 @@ criteria = "safe-to-deploy" [[exemptions.lazy_static]] version = "1.5.0" -criteria = "safe-to-deploy" - -[[exemptions.leb128fmt]] -version = "0.1.0" -criteria = "safe-to-deploy" +criteria = "safe-to-run" [[exemptions.libc]] -version = "0.2.186" -criteria = "safe-to-deploy" - -[[exemptions.libm]] -version = "0.2.16" -criteria = "safe-to-deploy" - -[[exemptions.libredox]] -version = "0.1.16" +version = "0.2.189" criteria = "safe-to-deploy" [[exemptions.libsqlite3-sys]] -version = "0.30.1" -criteria = "safe-to-deploy" - -[[exemptions.linux-raw-sys]] -version = "0.3.8" +version = "0.37.0" criteria = "safe-to-deploy" [[exemptions.linux-raw-sys]] @@ -484,7 +448,7 @@ version = "0.12.1" criteria = "safe-to-deploy" [[exemptions.litemap]] -version = "0.8.2" +version = "0.8.3" criteria = "safe-to-deploy" [[exemptions.lock_api]] @@ -492,40 +456,32 @@ version = "0.4.14" criteria = "safe-to-deploy" [[exemptions.log]] -version = "0.4.29" +version = "0.4.34" criteria = "safe-to-deploy" +[[exemptions.matchers]] +version = "0.2.0" +criteria = "safe-to-run" + [[exemptions.md-5]] -version = "0.10.6" +version = "0.11.0" criteria = "safe-to-deploy" [[exemptions.memchr]] -version = "2.8.0" +version = "2.8.3" criteria = "safe-to-deploy" [[exemptions.mio]] -version = "1.2.0" +version = "1.2.3" criteria = "safe-to-deploy" [[exemptions.native-tls]] version = "0.2.18" criteria = "safe-to-deploy" -[[exemptions.num-bigint-dig]] -version = "0.8.6" -criteria = "safe-to-deploy" - -[[exemptions.num-conv]] -version = "0.2.1" -criteria = "safe-to-deploy" - -[[exemptions.num-integer]] -version = "0.1.46" -criteria = "safe-to-deploy" - -[[exemptions.num-iter]] -version = "0.1.45" -criteria = "safe-to-deploy" +[[exemptions.nu-ansi-term]] +version = "0.50.3" +criteria = "safe-to-run" [[exemptions.num-traits]] version = "0.2.19" @@ -536,7 +492,7 @@ version = "1.21.4" criteria = "safe-to-deploy" [[exemptions.openssl]] -version = "0.10.79" +version = "0.10.81" criteria = "safe-to-deploy" [[exemptions.openssl-macros]] @@ -548,7 +504,7 @@ version = "0.2.1" criteria = "safe-to-deploy" [[exemptions.openssl-sys]] -version = "0.9.115" +version = "0.9.117" criteria = "safe-to-deploy" [[exemptions.parking]] @@ -563,10 +519,6 @@ criteria = "safe-to-deploy" version = "0.9.12" criteria = "safe-to-deploy" -[[exemptions.pem-rfc7468]] -version = "0.7.0" -criteria = "safe-to-deploy" - [[exemptions.percent-encoding]] version = "2.3.2" criteria = "safe-to-deploy" @@ -576,11 +528,11 @@ version = "0.8.3" criteria = "safe-to-run" [[exemptions.pin-project]] -version = "1.1.12" +version = "1.1.13" criteria = "safe-to-deploy" [[exemptions.pin-project-internal]] -version = "1.1.12" +version = "1.1.13" criteria = "safe-to-deploy" [[exemptions.pin-project-lite]] @@ -595,24 +547,8 @@ criteria = "safe-to-deploy" version = "0.2.5" criteria = "safe-to-deploy" -[[exemptions.pkcs1]] -version = "0.7.5" -criteria = "safe-to-deploy" - -[[exemptions.pkcs8]] -version = "0.10.2" -criteria = "safe-to-deploy" - [[exemptions.pkg-config]] -version = "0.3.33" -criteria = "safe-to-deploy" - -[[exemptions.plain]] -version = "0.2.3" -criteria = "safe-to-deploy" - -[[exemptions.polling]] -version = "2.8.0" +version = "0.3.34" criteria = "safe-to-deploy" [[exemptions.polling]] @@ -620,31 +556,15 @@ version = "3.11.0" criteria = "safe-to-deploy" [[exemptions.potential_utf]] -version = "0.1.5" -criteria = "safe-to-deploy" - -[[exemptions.powerfmt]] -version = "0.2.0" -criteria = "safe-to-deploy" - -[[exemptions.ppv-lite86]] -version = "0.2.21" -criteria = "safe-to-deploy" - -[[exemptions.prettyplease]] -version = "0.2.37" +version = "0.1.6" criteria = "safe-to-deploy" [[exemptions.proc-macro2]] -version = "1.0.106" +version = "1.0.107" criteria = "safe-to-deploy" [[exemptions.quote]] -version = "1.0.45" -criteria = "safe-to-deploy" - -[[exemptions.r-efi]] -version = "5.3.0" +version = "1.0.47" criteria = "safe-to-deploy" [[exemptions.r-efi]] @@ -652,36 +572,24 @@ version = "6.0.0" criteria = "safe-to-deploy" [[exemptions.rand]] -version = "0.8.6" -criteria = "safe-to-deploy" - -[[exemptions.rand]] -version = "0.9.4" -criteria = "safe-to-deploy" - -[[exemptions.rand_chacha]] -version = "0.3.1" -criteria = "safe-to-deploy" - -[[exemptions.rand_chacha]] -version = "0.9.0" -criteria = "safe-to-deploy" - -[[exemptions.rand_core]] -version = "0.6.4" +version = "0.10.2" criteria = "safe-to-deploy" [[exemptions.rand_core]] -version = "0.9.5" +version = "0.10.1" criteria = "safe-to-deploy" [[exemptions.redox_syscall]] version = "0.5.18" criteria = "safe-to-deploy" -[[exemptions.redox_syscall]] -version = "0.7.5" -criteria = "safe-to-deploy" +[[exemptions.regex-automata]] +version = "0.4.18" +criteria = "safe-to-run" + +[[exemptions.regex-syntax]] +version = "0.8.11" +criteria = "safe-to-run" [[exemptions.ring]] version = "0.17.14" @@ -695,35 +603,23 @@ criteria = "safe-to-deploy" version = "1.3.1" criteria = "safe-to-deploy" -[[exemptions.rsa]] -version = "0.9.10" -criteria = "safe-to-deploy" - -[[exemptions.rustix]] -version = "0.37.28" -criteria = "safe-to-deploy" - [[exemptions.rustix]] version = "1.1.4" criteria = "safe-to-deploy" [[exemptions.rustls]] -version = "0.23.40" +version = "0.23.45" criteria = "safe-to-deploy" [[exemptions.rustls-pki-types]] -version = "1.14.1" +version = "1.15.1" criteria = "safe-to-deploy" [[exemptions.rustls-webpki]] -version = "0.103.13" +version = "0.103.15" criteria = "safe-to-deploy" [[exemptions.rustversion]] -version = "1.0.22" -criteria = "safe-to-deploy" - -[[exemptions.ryu]] version = "1.0.23" criteria = "safe-to-deploy" @@ -743,44 +639,44 @@ criteria = "safe-to-deploy" version = "2.17.0" criteria = "safe-to-deploy" -[[exemptions.semver]] -version = "1.0.28" -criteria = "safe-to-deploy" - [[exemptions.serde]] -version = "1.0.228" +version = "1.0.229" criteria = "safe-to-deploy" [[exemptions.serde_core]] -version = "1.0.228" +version = "1.0.229" criteria = "safe-to-deploy" [[exemptions.serde_derive]] -version = "1.0.228" +version = "1.0.229" criteria = "safe-to-deploy" [[exemptions.serde_json]] -version = "1.0.149" +version = "1.0.151" criteria = "safe-to-deploy" -[[exemptions.serde_urlencoded]] -version = "0.7.1" +[[exemptions.sha1]] +version = "0.11.0" criteria = "safe-to-deploy" -[[exemptions.sha1]] -version = "0.10.6" +[[exemptions.sha1_smol]] +version = "1.0.1" criteria = "safe-to-deploy" [[exemptions.sha2]] version = "0.10.9" criteria = "safe-to-deploy" -[[exemptions.shlex]] -version = "1.3.0" +[[exemptions.sha2]] +version = "0.11.0" criteria = "safe-to-deploy" -[[exemptions.signature]] -version = "2.2.0" +[[exemptions.sharded-slab]] +version = "0.1.7" +criteria = "safe-to-run" + +[[exemptions.shlex]] +version = "2.0.1" criteria = "safe-to-deploy" [[exemptions.slab]] @@ -788,51 +684,43 @@ version = "0.4.12" criteria = "safe-to-deploy" [[exemptions.smallvec]] -version = "1.15.1" -criteria = "safe-to-deploy" - -[[exemptions.socket2]] -version = "0.4.10" +version = "1.16.1" criteria = "safe-to-deploy" [[exemptions.socket2]] -version = "0.6.3" +version = "0.6.5" criteria = "safe-to-deploy" [[exemptions.spin]] -version = "0.9.8" -criteria = "safe-to-deploy" - -[[exemptions.spki]] -version = "0.7.3" +version = "0.9.9" criteria = "safe-to-deploy" [[exemptions.sqlx]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.sqlx-core]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.sqlx-macros]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.sqlx-macros-core]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.sqlx-mysql]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.sqlx-postgres]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.sqlx-sqlite]] -version = "0.8.6" +version = "0.9.0" criteria = "safe-to-deploy" [[exemptions.stable_deref_trait]] @@ -848,7 +736,11 @@ version = "2.6.1" criteria = "safe-to-deploy" [[exemptions.syn]] -version = "2.0.117" +version = "2.0.119" +criteria = "safe-to-deploy" + +[[exemptions.syn]] +version = "3.0.5" criteria = "safe-to-deploy" [[exemptions.sync_wrapper]] @@ -856,7 +748,7 @@ version = "1.0.2" criteria = "safe-to-run" [[exemptions.synstructure]] -version = "0.13.2" +version = "0.14.0" criteria = "safe-to-deploy" [[exemptions.tempfile]] @@ -864,51 +756,39 @@ version = "3.27.0" criteria = "safe-to-deploy" [[exemptions.thiserror]] -version = "2.0.18" +version = "2.0.20" criteria = "safe-to-deploy" [[exemptions.thiserror-impl]] -version = "2.0.18" +version = "2.0.20" criteria = "safe-to-deploy" -[[exemptions.time]] -version = "0.3.47" -criteria = "safe-to-deploy" - -[[exemptions.time-core]] -version = "0.1.8" -criteria = "safe-to-deploy" - -[[exemptions.time-macros]] -version = "0.2.27" -criteria = "safe-to-deploy" +[[exemptions.thread_local]] +version = "1.1.10" +criteria = "safe-to-run" [[exemptions.tinystr]] -version = "0.8.3" +version = "0.8.4" criteria = "safe-to-deploy" [[exemptions.tinyvec]] -version = "1.11.0" -criteria = "safe-to-deploy" - -[[exemptions.tinyvec_macros]] -version = "0.1.1" +version = "1.13.3" criteria = "safe-to-deploy" [[exemptions.tokio]] -version = "1.52.2" +version = "1.53.1" criteria = "safe-to-deploy" [[exemptions.tokio-macros]] -version = "2.7.0" +version = "2.7.2" criteria = "safe-to-deploy" [[exemptions.tokio-stream]] -version = "0.1.18" +version = "0.1.19" criteria = "safe-to-deploy" [[exemptions.tokio-util]] -version = "0.7.18" +version = "0.7.19" criteria = "safe-to-run" [[exemptions.tower]] @@ -935,12 +815,20 @@ criteria = "safe-to-deploy" version = "0.1.36" criteria = "safe-to-deploy" +[[exemptions.tracing-log]] +version = "0.2.0" +criteria = "safe-to-run" + +[[exemptions.tracing-subscriber]] +version = "0.3.23" +criteria = "safe-to-run" + [[exemptions.typenum]] -version = "1.20.0" +version = "1.20.1" criteria = "safe-to-deploy" [[exemptions.ulid]] -version = "1.2.1" +version = "3.0.0" criteria = "safe-to-deploy" [[exemptions.unicode-bidi]] @@ -959,14 +847,14 @@ criteria = "safe-to-deploy" version = "0.1.4" criteria = "safe-to-deploy" -[[exemptions.unicode-xid]] -version = "0.2.6" -criteria = "safe-to-deploy" - [[exemptions.untrusted]] version = "0.9.0" criteria = "safe-to-deploy" +[[exemptions.unty]] +version = "0.0.4" +criteria = "safe-to-deploy" + [[exemptions.url]] version = "2.5.8" criteria = "safe-to-deploy" @@ -975,8 +863,16 @@ criteria = "safe-to-deploy" version = "1.0.4" criteria = "safe-to-deploy" +[[exemptions.uuid]] +version = "1.26.1" +criteria = "safe-to-deploy" + +[[exemptions.valuable]] +version = "0.1.1" +criteria = "safe-to-deploy" + [[exemptions.value-bag]] -version = "1.12.0" +version = "1.14.1" criteria = "safe-to-deploy" [[exemptions.vcpkg]] @@ -987,56 +883,32 @@ criteria = "safe-to-deploy" version = "0.9.5" criteria = "safe-to-deploy" -[[exemptions.waker-fn]] -version = "1.2.0" +[[exemptions.virtue]] +version = "0.0.18" criteria = "safe-to-deploy" [[exemptions.wasi]] version = "0.11.1+wasi-snapshot-preview1" criteria = "safe-to-deploy" -[[exemptions.wasip2]] -version = "1.0.3+wasi-0.2.9" -criteria = "safe-to-deploy" - -[[exemptions.wasip3]] -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -criteria = "safe-to-deploy" - -[[exemptions.wasite]] -version = "0.1.0" -criteria = "safe-to-deploy" - [[exemptions.wasm-bindgen]] -version = "0.2.121" +version = "0.2.128" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-futures]] -version = "0.4.71" +version = "0.4.78" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-macro]] -version = "0.2.121" +version = "0.2.128" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-macro-support]] -version = "0.2.121" +version = "0.2.128" criteria = "safe-to-deploy" [[exemptions.wasm-bindgen-shared]] -version = "0.2.121" -criteria = "safe-to-deploy" - -[[exemptions.wasm-encoder]] -version = "0.244.0" -criteria = "safe-to-deploy" - -[[exemptions.wasm-metadata]] -version = "0.244.0" -criteria = "safe-to-deploy" - -[[exemptions.wasmparser]] -version = "0.244.0" +version = "0.2.128" criteria = "safe-to-deploy" [[exemptions.web-time]] @@ -1044,57 +916,17 @@ version = "1.1.0" criteria = "safe-to-deploy" [[exemptions.webpki-roots]] -version = "0.26.11" -criteria = "safe-to-deploy" - -[[exemptions.webpki-roots]] -version = "1.0.7" +version = "1.0.9" criteria = "safe-to-deploy" [[exemptions.whoami]] -version = "1.6.1" -criteria = "safe-to-deploy" - -[[exemptions.winapi]] -version = "0.3.9" -criteria = "safe-to-deploy" - -[[exemptions.winapi-i686-pc-windows-gnu]] -version = "0.4.0" -criteria = "safe-to-deploy" - -[[exemptions.winapi-x86_64-pc-windows-gnu]] -version = "0.4.0" -criteria = "safe-to-deploy" - -[[exemptions.windows-core]] -version = "0.62.2" -criteria = "safe-to-deploy" - -[[exemptions.windows-implement]] -version = "0.60.2" -criteria = "safe-to-deploy" - -[[exemptions.windows-interface]] -version = "0.59.3" +version = "2.1.3" criteria = "safe-to-deploy" [[exemptions.windows-link]] version = "0.2.1" criteria = "safe-to-deploy" -[[exemptions.windows-result]] -version = "0.4.1" -criteria = "safe-to-deploy" - -[[exemptions.windows-strings]] -version = "0.5.1" -criteria = "safe-to-deploy" - -[[exemptions.windows-sys]] -version = "0.48.0" -criteria = "safe-to-deploy" - [[exemptions.windows-sys]] version = "0.52.0" criteria = "safe-to-deploy" @@ -1103,34 +935,18 @@ criteria = "safe-to-deploy" version = "0.61.2" criteria = "safe-to-deploy" -[[exemptions.windows-targets]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows-targets]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_aarch64_gnullvm]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_aarch64_gnullvm]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_aarch64_msvc]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_aarch64_msvc]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_i686_gnu]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_i686_gnu]] version = "0.52.6" criteria = "safe-to-deploy" @@ -1139,110 +955,58 @@ criteria = "safe-to-deploy" version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_i686_msvc]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_i686_msvc]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_x86_64_gnu]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_x86_64_gnu]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_x86_64_gnullvm]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_x86_64_gnullvm]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.windows_x86_64_msvc]] -version = "0.48.5" -criteria = "safe-to-deploy" - [[exemptions.windows_x86_64_msvc]] version = "0.52.6" criteria = "safe-to-deploy" -[[exemptions.wit-bindgen]] -version = "0.51.0" -criteria = "safe-to-deploy" - -[[exemptions.wit-bindgen]] -version = "0.57.1" -criteria = "safe-to-deploy" - -[[exemptions.wit-bindgen-core]] -version = "0.51.0" -criteria = "safe-to-deploy" - -[[exemptions.wit-bindgen-rust]] -version = "0.51.0" -criteria = "safe-to-deploy" - -[[exemptions.wit-bindgen-rust-macro]] -version = "0.51.0" -criteria = "safe-to-deploy" - -[[exemptions.wit-component]] -version = "0.244.0" -criteria = "safe-to-deploy" - -[[exemptions.wit-parser]] -version = "0.244.0" -criteria = "safe-to-deploy" - [[exemptions.writeable]] -version = "0.6.3" +version = "0.6.4" criteria = "safe-to-deploy" [[exemptions.yoke]] -version = "0.8.2" +version = "0.8.3" criteria = "safe-to-deploy" [[exemptions.yoke-derive]] -version = "0.8.2" -criteria = "safe-to-deploy" - -[[exemptions.zerocopy]] -version = "0.8.48" -criteria = "safe-to-deploy" - -[[exemptions.zerocopy-derive]] -version = "0.8.48" +version = "0.8.3" criteria = "safe-to-deploy" [[exemptions.zerofrom]] -version = "0.1.7" +version = "0.1.8" criteria = "safe-to-deploy" [[exemptions.zerofrom-derive]] -version = "0.1.7" +version = "0.1.8" criteria = "safe-to-deploy" [[exemptions.zeroize]] -version = "1.8.2" +version = "1.9.0" criteria = "safe-to-deploy" [[exemptions.zerotrie]] -version = "0.2.4" +version = "0.2.5" criteria = "safe-to-deploy" [[exemptions.zerovec]] -version = "0.11.6" +version = "0.11.8" criteria = "safe-to-deploy" [[exemptions.zerovec-derive]] -version = "0.11.3" +version = "0.11.6" criteria = "safe-to-deploy" [[exemptions.zmij]] -version = "1.0.21" +version = "1.0.23" criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 318a5f5..65e0997 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -20,3 +20,7 @@ audited_as = "1.0.0-rc.6" [[unpublished.apalis-sqlite]] version = "1.0.0-rc.8" audited_as = "1.0.0-rc.7" + +[[unpublished.apalis-sqlite]] +version = "1.0.0-rc.9" +audited_as = "1.0.0-rc.8"