pipework 0.252.0 → 0.252.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +854 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
// Step graphs over a jobs Queue. A graph is defined once; each request names the
|
|
2
|
+
// results it wants for an item, and only the steps whose result is not already
|
|
3
|
+
// recorded for their inputs run, each as an ordinary queued job.
|
|
4
|
+
//
|
|
5
|
+
// A result's identity is its content key: sha256 of (graph, step, version, the
|
|
6
|
+
// element, the hash of every need). A changed input changes the keys that depend
|
|
7
|
+
// on it and nothing else, so invalidation needs no bookkeeping.
|
|
8
|
+
//
|
|
9
|
+
// Run state changes only under a row lock on the run (`advance` and every writer
|
|
10
|
+
// take it before touching tasks), so steps finishing together serialize.
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from 'node:crypto'
|
|
13
|
+
import { sql } from '../../data/db/sql.js'
|
|
14
|
+
import type { DB } from '../../data/db/index.js'
|
|
15
|
+
import { scopedExecutor } from '../../auth/tenant/scoped-db.js'
|
|
16
|
+
import { schema } from '../../data/schema/namespace.js'
|
|
17
|
+
import { fitting } from '../../request/di/index.js'
|
|
18
|
+
import type { Handler } from '../../request/di/types.js'
|
|
19
|
+
import { resolveActorId } from '../../data/write/audit/actor.js'
|
|
20
|
+
import { getContext, runInContext } from '../../request/context/store.js'
|
|
21
|
+
import { createJobContext } from '../../request/context/create.js'
|
|
22
|
+
import { bindTransaction } from '../../request/context/guard.js'
|
|
23
|
+
import { propagateLocals } from '../../request/context/run-in-transaction.js'
|
|
24
|
+
import { isTracingEnabled } from '../../trace/entry/tracing-enabled.js'
|
|
25
|
+
import { openTrace } from '../../trace/entry/open-trace.js'
|
|
26
|
+
import { finalizeTrace } from '../../trace/entry/finalize-trace.js'
|
|
27
|
+
import { __flowStep } from '../../trace/runtime/flow-step.js'
|
|
28
|
+
import { parentTraceRef } from '../../trace/entry/parent-trace.js'
|
|
29
|
+
import type { ParentTraceRef } from '../../trace/entry/open-trace.js'
|
|
30
|
+
import { serializeForTrace } from '../../trace/capture/bounded-serializer.js'
|
|
31
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
32
|
+
import { recordFaultFor, faultMessage } from '../../infra/alerts/faults.js'
|
|
33
|
+
import type { DeadLetterInfo } from '../jobs/queue.js'
|
|
34
|
+
import type { Manifold } from '../../core/pipework.js'
|
|
35
|
+
import { projectStepError } from '../../trace/runtime/step-error.js'
|
|
36
|
+
import { StepItemRunningError, StepTimeoutError } from './errors.js'
|
|
37
|
+
import { canonicalJson, inputHash, sha256Hex, valueHash } from './hash.js'
|
|
38
|
+
import { SINGLE, registerStepTables } from './table.js'
|
|
39
|
+
import type {
|
|
40
|
+
DecideOptions, EndedRun, EndedRunsOptions, GraphConfig, GraphFailure, GraphSelection, ScopeOptions, GraphForEachContext, GraphHost, GraphRunStatus, GraphStepContext, GraphStepDefinition,
|
|
41
|
+
PlanOptions, PlanResult, RemoveItemOptions, RemoveItemResult, RequestOptions, RequestResult, RunStatus, RunTask, StepCounts, StepGraph, StepPlan, StepResultRecord, TaskState,
|
|
42
|
+
} from './types.js'
|
|
43
|
+
|
|
44
|
+
const RUN = 'pipework_step_run'
|
|
45
|
+
const TASK = 'pipework_step_task'
|
|
46
|
+
const RESULT = 'pipework_step_result'
|
|
47
|
+
const NAME_RE = /^[A-Za-z][A-Za-z0-9_.-]*$/
|
|
48
|
+
const CLASS_RE = /^[A-Za-z][A-Za-z0-9_-]*$/
|
|
49
|
+
const DEFAULT_CLASS = 'default'
|
|
50
|
+
const MS_PER_DAY = 86_400_000
|
|
51
|
+
|
|
52
|
+
type TaskStatus = 'blocked' | 'queued' | 'waiting' | 'done' | 'reused' | 'failed' | 'cancelled'
|
|
53
|
+
|
|
54
|
+
interface Task {
|
|
55
|
+
readonly id: string
|
|
56
|
+
readonly step: string
|
|
57
|
+
readonly instance: number
|
|
58
|
+
status: TaskStatus
|
|
59
|
+
jobId: string | null
|
|
60
|
+
inputKey: string | null
|
|
61
|
+
resultId: string | null
|
|
62
|
+
readonly element: unknown
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface RunRow {
|
|
66
|
+
readonly id: string
|
|
67
|
+
readonly tenant: string
|
|
68
|
+
readonly item: string
|
|
69
|
+
readonly status: GraphRunStatus
|
|
70
|
+
readonly priority: number
|
|
71
|
+
readonly stepPriority: Readonly<Record<string, number>> | null
|
|
72
|
+
readonly targets: readonly string[]
|
|
73
|
+
readonly inputs: Readonly<Record<string, unknown>>
|
|
74
|
+
readonly parent: ParentTraceRef | null
|
|
75
|
+
readonly actor: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ResultValue { readonly value: unknown; readonly hash: string }
|
|
79
|
+
|
|
80
|
+
interface Outcome { reused: number; queued: number; completed: boolean }
|
|
81
|
+
|
|
82
|
+
function fail(graph: string, message: string): never {
|
|
83
|
+
throw new Error(`[pipework] steps.define("${graph}"): ${message}\n`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toJson(value: unknown): unknown {
|
|
87
|
+
return value === undefined ? null : JSON.parse(JSON.stringify(value))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stepDefinitionChecks(config: GraphConfig): { order: string[]; inputs: ReadonlySet<string> } {
|
|
91
|
+
const graph = config.name
|
|
92
|
+
if (typeof graph !== 'string' || !NAME_RE.test(graph)) {
|
|
93
|
+
throw new Error(`[pipework] steps.define: graph name "${String(graph)}" must start with a letter and use letters, digits, "_", "." or "-".\n`)
|
|
94
|
+
}
|
|
95
|
+
const inputs = new Set(config.inputs ?? [])
|
|
96
|
+
const names = Object.keys(config.steps)
|
|
97
|
+
if (names.length === 0) fail(graph, 'no steps.')
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
const def = config.steps[name]!
|
|
100
|
+
if (!NAME_RE.test(name)) fail(graph, `step name "${name}" must start with a letter and use letters, digits, "_", "." or "-".`)
|
|
101
|
+
if (inputs.has(name)) fail(graph, `"${name}" is both a request input and a step.`)
|
|
102
|
+
if (!Number.isInteger(def.version)) fail(graph, `step "${name}" needs an integer version.`)
|
|
103
|
+
for (const need of def.needs ?? []) {
|
|
104
|
+
if (!inputs.has(need) && config.steps[need] === undefined) {
|
|
105
|
+
fail(graph, `step "${name}" needs "${need}", which is neither a step nor a declared input (steps: ${names.join(', ')}; inputs: ${[...inputs].join(', ') || '(none)'}).`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (def.workers !== undefined && (typeof def.workers !== 'string' || !CLASS_RE.test(def.workers))) {
|
|
109
|
+
fail(graph, `step "${name}" names worker class "${String(def.workers)}", which must start with a letter and use letters, digits, "_" or "-".`)
|
|
110
|
+
}
|
|
111
|
+
if (def.priority !== undefined && !Number.isInteger(def.priority)) {
|
|
112
|
+
fail(graph, `step "${name}" has priority ${String(def.priority)}, which must be an integer.`)
|
|
113
|
+
}
|
|
114
|
+
if (def.timeoutMs !== undefined) {
|
|
115
|
+
if (!Number.isInteger(def.timeoutMs) || def.timeoutMs <= 0) fail(graph, `step "${name}" has timeoutMs ${String(def.timeoutMs)}, which must be a positive integer (milliseconds).`)
|
|
116
|
+
if (def.person === true) fail(graph, `step "${name}" is a person step and cannot have timeoutMs: it holds no worker.`)
|
|
117
|
+
}
|
|
118
|
+
if (def.forEachOnly !== undefined) {
|
|
119
|
+
if (!Array.isArray(def.forEachOnly)) fail(graph, `step "${name}" has forEachOnly ${String(def.forEachOnly)}, which must be an array of need names.`)
|
|
120
|
+
for (const n of def.forEachOnly) {
|
|
121
|
+
if (!(def.needs ?? []).includes(n)) fail(graph, `step "${name}" lists "${String(n)}" in forEachOnly, which is not one of its needs (${(def.needs ?? []).join(', ') || '(none)'}).`)
|
|
122
|
+
}
|
|
123
|
+
if (def.forEachOnly.length > 0 && def.forEach === undefined) fail(graph, `step "${name}" has forEachOnly "${def.forEachOnly.join('", "')}" but no forEach.`)
|
|
124
|
+
}
|
|
125
|
+
if (def.byElement !== undefined) {
|
|
126
|
+
const by = def.byElement
|
|
127
|
+
if (!Array.isArray(by) || by.length === 0) fail(graph, `step "${name}" has byElement ${String(by)}, which must be a non-empty array of need names.`)
|
|
128
|
+
if (def.forEach !== undefined) fail(graph, `step "${name}" has both forEach and byElement; byElement takes its list from the steps it names.`)
|
|
129
|
+
if (def.forEachOnly !== undefined) fail(graph, `step "${name}" has forEachOnly but no forEach.`)
|
|
130
|
+
if (def.person === true) fail(graph, `step "${name}" is a person step and cannot have byElement.`)
|
|
131
|
+
for (const n of by) {
|
|
132
|
+
if (!(def.needs ?? []).includes(n)) fail(graph, `step "${name}" lists "${String(n)}" in byElement, which is not one of its needs (${(def.needs ?? []).join(', ') || '(none)'}).`)
|
|
133
|
+
if (inputs.has(n) || config.steps[n] === undefined) fail(graph, `step "${name}" lists "${n}" in byElement, but it is a request input, not a fan-out step.`)
|
|
134
|
+
if (config.steps[n]!.forEach === undefined && config.steps[n]!.byElement === undefined) {
|
|
135
|
+
fail(graph, `step "${name}" lists "${n}" in byElement, but "${n}" is not a fan-out step (it has neither forEach nor byElement).`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (def.person === true) {
|
|
140
|
+
if (def.forEach !== undefined) fail(graph, `step "${name}" is a person step and cannot have forEach.`)
|
|
141
|
+
if (def.run !== undefined) fail(graph, `step "${name}" is a person step and cannot have run: its value comes from decide() or autoAccept.`)
|
|
142
|
+
} else {
|
|
143
|
+
if (def.run === undefined) fail(graph, `step "${name}" has no run.`)
|
|
144
|
+
if (def.autoAccept !== undefined) fail(graph, `step "${name}" has autoAccept but is not a person step.`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Topological order; a back edge is a cycle.
|
|
148
|
+
const order: string[] = []
|
|
149
|
+
const state = new Map<string, 'visiting' | 'done'>()
|
|
150
|
+
const visit = (name: string, path: string[]): void => {
|
|
151
|
+
const seen = state.get(name)
|
|
152
|
+
if (seen === 'done') return
|
|
153
|
+
if (seen === 'visiting') fail(graph, `steps form a cycle: ${[...path.slice(path.indexOf(name)), name].join(' -> ')}.`)
|
|
154
|
+
state.set(name, 'visiting')
|
|
155
|
+
for (const need of config.steps[name]!.needs ?? []) {
|
|
156
|
+
if (config.steps[need] !== undefined) visit(need, [...path, name])
|
|
157
|
+
}
|
|
158
|
+
state.set(name, 'done')
|
|
159
|
+
order.push(name)
|
|
160
|
+
}
|
|
161
|
+
for (const name of names) visit(name, [])
|
|
162
|
+
// byElement is a subset of needs, so with no cycle every chain of byElement ends at a forEach step.
|
|
163
|
+
const rootOf = (name: string): string => {
|
|
164
|
+
const def = config.steps[name]!
|
|
165
|
+
return def.forEach !== undefined ? name : rootOf(def.byElement![0]!)
|
|
166
|
+
}
|
|
167
|
+
for (const name of names) {
|
|
168
|
+
const by = config.steps[name]!.byElement
|
|
169
|
+
if (by === undefined) continue
|
|
170
|
+
const roots = by.map(rootOf)
|
|
171
|
+
if (new Set(roots).size > 1) {
|
|
172
|
+
fail(graph, `step "${name}" lists byElement steps over different lists: ${by.map((n, i) => `"${n}" (list of "${roots[i]!}")`).join(', ')}.`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { order, inputs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function defineGraph(config: GraphConfig): StepGraph {
|
|
179
|
+
const { order, inputs: inputNames } = stepDefinitionChecks(config)
|
|
180
|
+
registerStepTables()
|
|
181
|
+
|
|
182
|
+
const graph = config.name
|
|
183
|
+
const jobType = `pipework.step.${graph}`
|
|
184
|
+
const steps = config.steps
|
|
185
|
+
const classOf = (name: string): string => steps[name]!.workers ?? DEFAULT_CLASS
|
|
186
|
+
const typeOfClass = (cls: string): string => cls === DEFAULT_CLASS ? jobType : `${jobType}:${cls}`
|
|
187
|
+
const classes = [...new Set(order.map(classOf))]
|
|
188
|
+
const jobTypes = classes.map(typeOfClass)
|
|
189
|
+
const queue = config.queue
|
|
190
|
+
const isFanOut = (name: string): boolean => steps[name]!.forEach !== undefined || steps[name]!.byElement !== undefined
|
|
191
|
+
const running = new Map<string, Set<AbortController>>()
|
|
192
|
+
|
|
193
|
+
/** Every step the targets need, transitively, in dependency order. */
|
|
194
|
+
function closureOf(targets: readonly string[]): string[] {
|
|
195
|
+
const wanted = new Set<string>()
|
|
196
|
+
const add = (name: string): void => {
|
|
197
|
+
if (wanted.has(name)) return
|
|
198
|
+
wanted.add(name)
|
|
199
|
+
for (const need of steps[name]!.needs ?? []) if (steps[need] !== undefined) add(need)
|
|
200
|
+
}
|
|
201
|
+
for (const t of targets) add(t)
|
|
202
|
+
return order.filter(n => wanted.has(n))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The steps that depend on `step`, directly or not. */
|
|
206
|
+
function dependentsOf(step: string): Set<string> {
|
|
207
|
+
const out = new Set<string>()
|
|
208
|
+
const grow = (name: string): void => {
|
|
209
|
+
for (const other of order) {
|
|
210
|
+
if (!out.has(other) && (steps[other]!.needs ?? []).includes(name)) { out.add(other); grow(other) }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
grow(step)
|
|
214
|
+
return out
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function checkRequest(opts: { item: string; targets: readonly string[]; inputs?: Readonly<Record<string, unknown>>; stepPriority?: Readonly<Record<string, number>> }, what: string): string[] {
|
|
218
|
+
if (typeof opts.item !== 'string' || opts.item === '') throw new Error(`[pipework] ${graph}.${what}: item must be a non-empty string.\n`)
|
|
219
|
+
if (!Array.isArray(opts.targets) || opts.targets.length === 0) throw new Error(`[pipework] ${graph}.${what}: targets must name at least one step.\n`)
|
|
220
|
+
for (const t of opts.targets) {
|
|
221
|
+
if (steps[t] === undefined) {
|
|
222
|
+
throw new Error(`[pipework] ${graph}.${what}: unknown target step "${t}" (steps: ${order.join(', ')}).\n`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const name of Object.keys(opts.inputs ?? {})) {
|
|
226
|
+
if (!inputNames.has(name)) {
|
|
227
|
+
throw new Error(`[pipework] ${graph}.${what}: unknown input "${name}" (inputs: ${[...inputNames].join(', ') || '(none)'}).\n`)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (opts.stepPriority !== undefined) {
|
|
231
|
+
for (const [name, p] of Object.entries(opts.stepPriority)) {
|
|
232
|
+
if (steps[name] === undefined) {
|
|
233
|
+
throw new Error(`[pipework] ${graph}.${what}: stepPriority names unknown step "${name}" (steps: ${order.join(', ')}).\n`)
|
|
234
|
+
}
|
|
235
|
+
if (!Number.isInteger(p)) throw new Error(`[pipework] ${graph}.${what}: stepPriority for step "${name}" is ${String(p)}, which must be an integer.\n`)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const closure = closureOf(opts.targets)
|
|
239
|
+
for (const name of closure) {
|
|
240
|
+
for (const need of steps[name]!.needs ?? []) {
|
|
241
|
+
if (inputNames.has(need) && (opts.inputs ?? {})[need] === undefined) {
|
|
242
|
+
throw new Error(`[pipework] ${graph}.${what}: step "${name}" needs input "${need}", which the request does not give.\n`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return closure
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function tenantOf(explicit: string | undefined, what: string): string {
|
|
250
|
+
const tenant = explicit ?? getContext()?.tenant ?? null
|
|
251
|
+
if (tenant === null || tenant === '') {
|
|
252
|
+
throw new Error(`[pipework] ${graph}.${what}: no tenant. Pass { tenant } or call inside a tenant Flow; results are recorded per tenant.\n`)
|
|
253
|
+
}
|
|
254
|
+
return tenant
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The tenant of a tenant-scoped handle, which stands in when a call names none. */
|
|
258
|
+
const handleTenant = (db: DB): string | undefined => scopedExecutor(db, null, graph).tenant ?? undefined
|
|
259
|
+
|
|
260
|
+
let attached: Manifold | undefined
|
|
261
|
+
|
|
262
|
+
/** The session variable the tenant policies read: the graph's own, else the active or attached Manifold's, else the default. */
|
|
263
|
+
function sessionVarOf(instance?: Manifold): string {
|
|
264
|
+
if (config.sessionVar !== undefined) return config.sessionVar
|
|
265
|
+
const m = instance ?? getContext()?.instance ?? attached
|
|
266
|
+
if (m === undefined) return 'pipework.tenant_id'
|
|
267
|
+
return m.config.database(m.config.defaultDatabaseName()).rls?.sessionVar ?? 'pipework.tenant_id'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Sets the tenant session variable for the rest of the transaction `tx` is in, and puts back what was there. */
|
|
271
|
+
async function asTenant<T>(tx: DB, tenant: string | null, fn: () => Promise<T>, instance?: Manifold): Promise<T> {
|
|
272
|
+
if (tenant === null) return fn()
|
|
273
|
+
const sessionVar = sessionVarOf(instance)
|
|
274
|
+
const before = await tx.execute(sql`SELECT current_setting(${sessionVar}, true) AS v`)
|
|
275
|
+
await tx.execute(sql`SELECT set_config(${sessionVar}, ${tenant}, true)`)
|
|
276
|
+
const result = await fn()
|
|
277
|
+
await tx.execute(sql`SELECT set_config(${sessionVar}, ${(before[0]?.['v'] as string | null) ?? ''}, true)`)
|
|
278
|
+
return result
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function inTenantTx<T>(handle: DB, scope: string | null, fn: (tx: DB) => Promise<T>): Promise<T> {
|
|
282
|
+
const { db, tenant } = scopedExecutor(handle, scope, `${graph} step graph`)
|
|
283
|
+
return db.transaction(async (tx) => asTenant(tx, tenant, () => fn(tx)))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The tenant a call acts for: the one given, else the Flow's, else none (an owner connection needs none). */
|
|
287
|
+
const scopeOf = (explicit?: string): string | null => explicit ?? getContext()?.tenant ?? null
|
|
288
|
+
|
|
289
|
+
/** Cancels the jobs that are still pending in one statement, skipping any row another transaction holds: never waits on a job while the run lock is held. */
|
|
290
|
+
async function cancelPending(tx: DB, jobIds: readonly string[], why: { reason: string; message: string }): Promise<void> {
|
|
291
|
+
if (jobIds.length === 0) return
|
|
292
|
+
const table = quoted(queueTable())
|
|
293
|
+
await tx.execute(sql`
|
|
294
|
+
UPDATE ${sql.raw(table)} SET status = 'cancelled', completed_at = now(), claim_token = NULL,
|
|
295
|
+
error_payload = jsonb_build_object('reason', ${why.reason}::text, 'message', ${why.message}::text)
|
|
296
|
+
WHERE id IN (SELECT id FROM ${sql.raw(table)}
|
|
297
|
+
WHERE id ${sql.inList(jobIds, 'uuid')} AND status = 'pending'
|
|
298
|
+
FOR UPDATE SKIP LOCKED)`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function lockRun(tx: DB, runId: string): Promise<RunRow | null> {
|
|
302
|
+
const rows = await tx.execute(sql`
|
|
303
|
+
SELECT id::text, tenant_id::text AS tenant, item, status, priority, step_priority, targets, inputs,
|
|
304
|
+
trace_id::text AS trace_id, trace_root_id::text AS trace_root_id, trace_step_id::text AS trace_step_id, actor
|
|
305
|
+
FROM ${sql.raw(RUN)} WHERE id = ${runId}::uuid AND graph = ${graph} FOR UPDATE`)
|
|
306
|
+
const row = rows[0]
|
|
307
|
+
if (row === undefined) return null
|
|
308
|
+
const traceId = row['trace_id'] as string | null
|
|
309
|
+
const rootId = row['trace_root_id'] as string | null
|
|
310
|
+
return {
|
|
311
|
+
id: row['id'] as string,
|
|
312
|
+
tenant: row['tenant'] as string,
|
|
313
|
+
item: row['item'] as string,
|
|
314
|
+
status: row['status'] as GraphRunStatus,
|
|
315
|
+
priority: row['priority'] as number,
|
|
316
|
+
stepPriority: row['step_priority'] as Record<string, number> | null,
|
|
317
|
+
targets: row['targets'] as string[],
|
|
318
|
+
inputs: row['inputs'] as Record<string, unknown>,
|
|
319
|
+
parent: traceId !== null && rootId !== null ? { id: traceId, rootId, stepId: row['trace_step_id'] as string | null } : null,
|
|
320
|
+
actor: row['actor'] as string | null,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function loadTasks(tx: DB, runId: string): Promise<Task[]> {
|
|
325
|
+
const rows = await tx.execute(sql`
|
|
326
|
+
SELECT id::text, step, instance, status, job_id::text AS job_id, input_key, result_id::text AS result_id, element
|
|
327
|
+
FROM ${sql.raw(TASK)} WHERE run_id = ${runId}::uuid ORDER BY step, instance`)
|
|
328
|
+
return rows.map(r => ({
|
|
329
|
+
id: r['id'] as string,
|
|
330
|
+
step: r['step'] as string,
|
|
331
|
+
instance: r['instance'] as number,
|
|
332
|
+
status: r['status'] as TaskStatus,
|
|
333
|
+
jobId: r['job_id'] as string | null,
|
|
334
|
+
inputKey: r['input_key'] as string | null,
|
|
335
|
+
resultId: r['result_id'] as string | null,
|
|
336
|
+
element: r['element'],
|
|
337
|
+
}))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function loadResults(tx: DB, ids: readonly string[]): Promise<Map<string, ResultValue>> {
|
|
341
|
+
const out = new Map<string, ResultValue>()
|
|
342
|
+
if (ids.length === 0) return out
|
|
343
|
+
const rows = await tx.execute(sql`
|
|
344
|
+
SELECT id::text, value, value_hash FROM ${sql.raw(RESULT)}
|
|
345
|
+
WHERE id ${sql.inList(ids, 'uuid')}`)
|
|
346
|
+
for (const r of rows) out.set(r['id'] as string, { value: r['value'], hash: r['value_hash'] as string })
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function findRecorded(
|
|
351
|
+
tx: DB, tenant: string, item: string, step: string, key: string, instance: number,
|
|
352
|
+
): Promise<{ id: string; value: unknown; hash: string } | null> {
|
|
353
|
+
const rows = await tx.execute(sql`
|
|
354
|
+
SELECT id::text, value, value_hash FROM ${sql.raw(RESULT)}
|
|
355
|
+
WHERE tenant_id = ${tenant}::uuid AND graph = ${graph} AND item = ${item} AND step = ${step}
|
|
356
|
+
AND version = ${steps[step]!.version} AND input_key = ${key}
|
|
357
|
+
ORDER BY (instance = ${instance}) DESC, created_at ASC LIMIT 1`)
|
|
358
|
+
const row = rows[0]
|
|
359
|
+
return row === undefined ? null : { id: row['id'] as string, value: row['value'], hash: row['value_hash'] as string }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Records a result; when one already holds the key (a concurrent run got there) that one is returned. */
|
|
363
|
+
async function recordResult(
|
|
364
|
+
tx: DB, run: RunRow, step: string, key: string, instance: number, value: unknown, replace: boolean,
|
|
365
|
+
): Promise<{ id: string; hash: string; value: unknown }> {
|
|
366
|
+
const json = JSON.stringify(value)
|
|
367
|
+
const hash = valueHash(value)
|
|
368
|
+
const conflict = replace
|
|
369
|
+
? sql`ON CONFLICT (tenant_id, graph, item, step, version, input_key, instance)
|
|
370
|
+
DO UPDATE SET value = EXCLUDED.value, value_hash = EXCLUDED.value_hash, created_at = now()`
|
|
371
|
+
: sql`ON CONFLICT (tenant_id, graph, item, step, version, input_key, instance) DO NOTHING`
|
|
372
|
+
const inserted = await tx.execute(sql`
|
|
373
|
+
INSERT INTO ${sql.raw(RESULT)} (tenant_id, graph, item, step, version, input_key, instance, value, value_hash)
|
|
374
|
+
VALUES (${run.tenant}::uuid, ${graph}, ${run.item}, ${step}, ${steps[step]!.version}, ${key}, ${instance}, ${json}::jsonb, ${hash})
|
|
375
|
+
${conflict} RETURNING id::text`)
|
|
376
|
+
if (inserted[0] !== undefined) return { id: inserted[0]['id'] as string, hash, value }
|
|
377
|
+
const rows = await tx.execute(sql`
|
|
378
|
+
SELECT id::text, value, value_hash FROM ${sql.raw(RESULT)}
|
|
379
|
+
WHERE tenant_id = ${run.tenant}::uuid AND graph = ${graph} AND item = ${run.item} AND step = ${step}
|
|
380
|
+
AND version = ${steps[step]!.version} AND input_key = ${key} AND instance = ${instance}`)
|
|
381
|
+
const row = rows[0]!
|
|
382
|
+
return { id: row['id'] as string, hash: row['value_hash'] as string, value: row['value'] }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function stageReused(step: string, instance: number, resultId: string): void {
|
|
386
|
+
const buffer = getContext()?.trace?.buffer
|
|
387
|
+
if (buffer === undefined) return
|
|
388
|
+
const now = new Date()
|
|
389
|
+
buffer.stageExternal({
|
|
390
|
+
id: randomUUID(),
|
|
391
|
+
parentStepId: null,
|
|
392
|
+
flowStepRef: 'external:pipework.step.reused',
|
|
393
|
+
status: 'ok',
|
|
394
|
+
startedAt: now,
|
|
395
|
+
completedAt: now,
|
|
396
|
+
durationMicros: 0n,
|
|
397
|
+
capturedInputs: [serializeForTrace({ step, instance: instance === SINGLE ? null : instance, resultId })],
|
|
398
|
+
error: null,
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stageTimeout(step: string, instance: number, timeoutMs: number, startedAt: Date, error: StepTimeoutError): void {
|
|
403
|
+
const buffer = getContext()?.trace?.buffer
|
|
404
|
+
if (buffer === undefined) return
|
|
405
|
+
const now = new Date()
|
|
406
|
+
buffer.stageExternal({
|
|
407
|
+
id: randomUUID(),
|
|
408
|
+
parentStepId: null,
|
|
409
|
+
flowStepRef: 'external:pipework.step.timeout',
|
|
410
|
+
status: 'error',
|
|
411
|
+
startedAt,
|
|
412
|
+
completedAt: now,
|
|
413
|
+
durationMicros: BigInt(Math.max(0, now.getTime() - startedAt.getTime())) * 1000n,
|
|
414
|
+
capturedInputs: [serializeForTrace({ step, instance: instance === SINGLE ? null : instance, timeoutMs })],
|
|
415
|
+
error: projectStepError(error),
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** The state of one run during an `advance`: its tasks and the results they hold. */
|
|
420
|
+
class RunState {
|
|
421
|
+
constructor(
|
|
422
|
+
readonly run: RunRow,
|
|
423
|
+
readonly tasks: Task[],
|
|
424
|
+
readonly results: Map<string, ResultValue>,
|
|
425
|
+
readonly inputHashes: Record<string, string>,
|
|
426
|
+
) {}
|
|
427
|
+
|
|
428
|
+
of(step: string): Task[] { return this.tasks.filter(t => t.step === step) }
|
|
429
|
+
single(step: string): Task | undefined { return this.tasks.find(t => t.step === step && t.instance === SINGLE) }
|
|
430
|
+
instances(step: string): Task[] {
|
|
431
|
+
return this.tasks.filter(t => t.step === step && t.instance >= 0).sort((a, b) => a.instance - b.instance)
|
|
432
|
+
}
|
|
433
|
+
instanceOf(step: string, i: number): Task { return this.instances(step).find(t => t.instance === i)! }
|
|
434
|
+
good(t: Task): boolean { return t.status === 'done' || t.status === 'reused' }
|
|
435
|
+
|
|
436
|
+
complete(step: string): boolean {
|
|
437
|
+
const single = this.single(step)
|
|
438
|
+
if (single === undefined) return false
|
|
439
|
+
if (!isFanOut(step)) return this.good(single)
|
|
440
|
+
return single.status === 'done' && this.instances(step).every(t => this.good(t))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
resultOf(t: Task): ResultValue {
|
|
444
|
+
const r = t.resultId === null ? undefined : this.results.get(t.resultId)
|
|
445
|
+
if (r === undefined) throw new Error(`[pipework] ${graph}: task ${t.id} (${t.step}) has no recorded result.\n`)
|
|
446
|
+
return r
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** What a step reads: the value and hash of each need, and the request inputs among them. */
|
|
450
|
+
gather(def: GraphStepDefinition, task?: Task): { values: Record<string, unknown>; hashes: Record<string, string | string[]>; inputs: Record<string, unknown> } {
|
|
451
|
+
const values: Record<string, unknown> = {}
|
|
452
|
+
const hashes: Record<string, string | string[]> = {}
|
|
453
|
+
const inputs: Record<string, unknown> = {}
|
|
454
|
+
for (const need of def.needs ?? []) {
|
|
455
|
+
if (inputNames.has(need)) {
|
|
456
|
+
values[need] = this.run.inputs[need]
|
|
457
|
+
inputs[need] = this.run.inputs[need]
|
|
458
|
+
hashes[need] = this.inputHashes[need]!
|
|
459
|
+
} else if (def.byElement?.includes(need) === true && task !== undefined) {
|
|
460
|
+
const r = this.resultOf(this.instanceOf(need, task.instance))
|
|
461
|
+
values[need] = r.value
|
|
462
|
+
hashes[need] = r.hash
|
|
463
|
+
} else if (isFanOut(need)) {
|
|
464
|
+
const rs = this.instances(need).map(t => this.resultOf(t))
|
|
465
|
+
values[need] = rs.map(r => r.value)
|
|
466
|
+
hashes[need] = rs.map(r => r.hash)
|
|
467
|
+
} else {
|
|
468
|
+
const r = this.resultOf(this.single(need)!)
|
|
469
|
+
values[need] = r.value
|
|
470
|
+
hashes[need] = r.hash
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return { values, hashes, inputs }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function inputHashesOf(inputs: Readonly<Record<string, unknown>>): Record<string, string> {
|
|
478
|
+
return Object.fromEntries(Object.entries(inputs).map(([k, v]) => [k, inputHash(v)]))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** A step's needs (values or hashes) without those that only choose its list: what its key and `run` cover. */
|
|
482
|
+
function without<T>(def: GraphStepDefinition, values: Record<string, T>): Record<string, T> {
|
|
483
|
+
const only = def.forEachOnly
|
|
484
|
+
if (only === undefined || only.length === 0) return values
|
|
485
|
+
return Object.fromEntries(Object.entries(values).filter(([n]) => !only.includes(n)))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function contentKey(step: string, element: unknown, fanOut: boolean, hashes: Record<string, string | string[]>): string {
|
|
489
|
+
return sha256Hex(canonicalJson({
|
|
490
|
+
graph, step, version: steps[step]!.version, element: fanOut ? valueHash(element) : null, needs: hashes,
|
|
491
|
+
}))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function loadState(tx: DB, run: RunRow): Promise<RunState> {
|
|
495
|
+
const tasks = await loadTasks(tx, run.id)
|
|
496
|
+
const results = await loadResults(tx, tasks.flatMap(t => t.resultId === null ? [] : [t.resultId]))
|
|
497
|
+
return new RunState(run, tasks, results, inputHashesOf(run.inputs))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** What a step's context carries besides its data: the database handles `run` is given. */
|
|
501
|
+
interface StepIo {
|
|
502
|
+
checkpoint<R>(fn: (tx: DB) => Promise<R>): Promise<R>
|
|
503
|
+
withResult(fn: (tx: DB) => Promise<void>): void
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function stepContext(state: RunState, def: GraphStepDefinition, task: Task, fanOut: boolean, signal: AbortSignal, io: StepIo): Omit<GraphStepContext, 'attempt' | 'maxAttempts'> {
|
|
507
|
+
const g = state.gather(def, task)
|
|
508
|
+
return {
|
|
509
|
+
needs: without(def, g.values),
|
|
510
|
+
element: fanOut ? task.element : undefined,
|
|
511
|
+
index: fanOut ? task.instance : null,
|
|
512
|
+
item: state.run.item,
|
|
513
|
+
inputs: without(def, g.inputs),
|
|
514
|
+
runId: state.run.id,
|
|
515
|
+
signal,
|
|
516
|
+
checkpoint: io.checkpoint,
|
|
517
|
+
withResult: io.withResult,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** The priority of a step's jobs: the request's `stepPriority` for it, else its definition's `priority`, else the run's. */
|
|
522
|
+
function stepJobPriority(run: RunRow, def: GraphStepDefinition, step: string): number {
|
|
523
|
+
return run.stepPriority?.[step] ?? def.priority ?? run.priority
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function setTask(tx: DB, t: Task, patch: { status: TaskStatus; jobId?: string; inputKey?: string; resultId?: string; error?: unknown }): Promise<void> {
|
|
527
|
+
await tx.execute(sql`
|
|
528
|
+
UPDATE ${sql.raw(TASK)} SET status = ${patch.status},
|
|
529
|
+
job_id = COALESCE(${patch.jobId ?? null}::uuid, job_id),
|
|
530
|
+
input_key = COALESCE(${patch.inputKey ?? null}, input_key),
|
|
531
|
+
result_id = COALESCE(${patch.resultId ?? null}::uuid, result_id),
|
|
532
|
+
error = COALESCE(${patch.error === undefined ? null : JSON.stringify(patch.error)}::jsonb, error)
|
|
533
|
+
WHERE id = ${t.id}::uuid`)
|
|
534
|
+
t.status = patch.status
|
|
535
|
+
if (patch.jobId !== undefined) t.jobId = patch.jobId
|
|
536
|
+
if (patch.inputKey !== undefined) t.inputKey = patch.inputKey
|
|
537
|
+
if (patch.resultId !== undefined) t.resultId = patch.resultId
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Moves a run as far as its recorded results allow, in one pass over the steps in
|
|
542
|
+
* dependency order: expands fan-out steps whose list is now known, reuses recorded
|
|
543
|
+
* results, queues the rest, and marks the run done when nothing is left. The run
|
|
544
|
+
* row is locked first, so concurrent finishes advance it one at a time.
|
|
545
|
+
*/
|
|
546
|
+
async function advance(tx: DB, runId: string): Promise<Outcome & { run: RunRow | null }> {
|
|
547
|
+
const outcome: Outcome = { reused: 0, queued: 0, completed: false }
|
|
548
|
+
const run = await lockRun(tx, runId)
|
|
549
|
+
if (run === null || run.status !== 'running') return { ...outcome, run }
|
|
550
|
+
const state = await loadState(tx, run)
|
|
551
|
+
const closure = closureOf(run.targets)
|
|
552
|
+
const signal = new AbortController().signal
|
|
553
|
+
|
|
554
|
+
const process = async (task: Task, def: GraphStepDefinition, fanOut: boolean): Promise<void> => {
|
|
555
|
+
const g = state.gather(def, task)
|
|
556
|
+
const key = contentKey(task.step, task.element, fanOut, without(def, g.hashes))
|
|
557
|
+
const recorded = await findRecorded(tx, run.tenant, run.item, task.step, key, task.instance)
|
|
558
|
+
if (recorded !== null) {
|
|
559
|
+
state.results.set(recorded.id, { value: recorded.value, hash: recorded.hash })
|
|
560
|
+
await setTask(tx, task, { status: 'reused', inputKey: key, resultId: recorded.id })
|
|
561
|
+
stageReused(task.step, task.instance, recorded.id)
|
|
562
|
+
outcome.reused++
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
if (def.person === true) {
|
|
566
|
+
const accepted = def.autoAccept === undefined ? undefined : await def.autoAccept(stepContext(state, def, task, fanOut, signal, {
|
|
567
|
+
checkpoint: (fn) => fn(tx),
|
|
568
|
+
withResult: () => {
|
|
569
|
+
throw new Error(`[pipework] ${graph}: step "${task.step}" is a person step; withResult is for steps that run (a person step records through decide).\n`)
|
|
570
|
+
},
|
|
571
|
+
}))
|
|
572
|
+
if (accepted !== undefined) {
|
|
573
|
+
const rec = await recordResult(tx, run, task.step, key, task.instance, toJson(accepted), false)
|
|
574
|
+
state.results.set(rec.id, { value: rec.value, hash: rec.hash })
|
|
575
|
+
await setTask(tx, task, { status: 'done', inputKey: key, resultId: rec.id })
|
|
576
|
+
} else {
|
|
577
|
+
await setTask(tx, task, { status: 'waiting', inputKey: key })
|
|
578
|
+
}
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
const jobId = await queue.enqueue(tx, {
|
|
582
|
+
type: typeOfClass(classOf(task.step)),
|
|
583
|
+
payload: { kind: 'step', runId: run.id, taskId: task.id },
|
|
584
|
+
priority: stepJobPriority(run, def, task.step),
|
|
585
|
+
...(def.maxAttempts !== undefined ? { maxAttempts: def.maxAttempts } : {}),
|
|
586
|
+
tenant: run.tenant,
|
|
587
|
+
parent: run.parent,
|
|
588
|
+
actor: run.actor,
|
|
589
|
+
})
|
|
590
|
+
await setTask(tx, task, { status: 'queued', inputKey: key, jobId })
|
|
591
|
+
outcome.queued++
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
for (const name of closure) {
|
|
595
|
+
if (state.complete(name)) continue
|
|
596
|
+
const def = steps[name]!
|
|
597
|
+
const by = def.byElement
|
|
598
|
+
const ready = (def.needs ?? []).every(n => steps[n] === undefined
|
|
599
|
+
|| (by?.includes(n) === true ? state.single(n)?.status === 'done' : state.complete(n)))
|
|
600
|
+
if (!ready) continue
|
|
601
|
+
const expander = state.single(name)
|
|
602
|
+
if (expander === undefined) continue
|
|
603
|
+
|
|
604
|
+
if (!isFanOut(name)) {
|
|
605
|
+
if (expander.status === 'blocked') await process(expander, def, false)
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const expand = async (elements: readonly unknown[]): Promise<void> => {
|
|
610
|
+
if (elements.length > 0) {
|
|
611
|
+
const rows = await tx.execute(sql`
|
|
612
|
+
INSERT INTO ${sql.raw(TASK)} (tenant_id, run_id, step, instance, element, status)
|
|
613
|
+
VALUES ${sql.join(elements.map((el, i) =>
|
|
614
|
+
sql`(${run.tenant}::uuid, ${run.id}::uuid, ${name}, ${i}, ${JSON.stringify(el)}::jsonb, 'blocked')`), sql`, `)}
|
|
615
|
+
RETURNING id::text, instance`)
|
|
616
|
+
for (const r of rows) {
|
|
617
|
+
const i = r['instance'] as number
|
|
618
|
+
state.tasks.push({
|
|
619
|
+
id: r['id'] as string, step: name, instance: i, status: 'blocked', jobId: null, inputKey: null, resultId: null, element: elements[i],
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
await setTask(tx, expander, { status: 'done' })
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (expander.status === 'blocked') {
|
|
627
|
+
if (by !== undefined) {
|
|
628
|
+
await expand(state.instances(by[0]!).map(t => t.element))
|
|
629
|
+
} else {
|
|
630
|
+
const g = state.gather(def)
|
|
631
|
+
const forEachCtx: GraphForEachContext = { needs: g.values, item: run.item, inputs: g.inputs, runId: run.id }
|
|
632
|
+
const list = await def.forEach!(forEachCtx)
|
|
633
|
+
if (!Array.isArray(list)) {
|
|
634
|
+
throw new Error(`[pipework] ${graph}: forEach of step "${name}" must return an array, got ${typeof list}.\n`)
|
|
635
|
+
}
|
|
636
|
+
await expand(list.map(toJson))
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const t of state.instances(name)) {
|
|
640
|
+
if (t.status !== 'blocked') continue
|
|
641
|
+
if (by !== undefined && !by.every(n => state.good(state.instanceOf(n, t.instance)))) continue
|
|
642
|
+
await process(t, def, true)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (closure.every(n => state.complete(n))) {
|
|
647
|
+
const done = await tx.execute(sql`
|
|
648
|
+
UPDATE ${sql.raw(RUN)} SET status = 'done', finished_at = now(), notified = ${config.onDone === undefined}
|
|
649
|
+
WHERE id = ${run.id}::uuid AND status = 'running' RETURNING id`)
|
|
650
|
+
if (done.length > 0) {
|
|
651
|
+
outcome.completed = true
|
|
652
|
+
await tx.execute(sql`SELECT pg_notify('pipework_step_run', ${run.id})`)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return { ...outcome, run }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Marks the run's done callback delivered and calls it; false when it already was. */
|
|
659
|
+
async function deliverDone(tx: DB, runId: string): Promise<void> {
|
|
660
|
+
const rows = await tx.execute(sql`
|
|
661
|
+
UPDATE ${sql.raw(RUN)} SET notified = true
|
|
662
|
+
WHERE id = ${runId}::uuid AND status = 'done' AND notified = false
|
|
663
|
+
RETURNING item, tenant_id::text AS tenant`)
|
|
664
|
+
const row = rows[0]
|
|
665
|
+
if (row === undefined || config.onDone === undefined) return
|
|
666
|
+
await config.onDone({ runId, item: row['item'] as string, tenant: row['tenant'] as string })
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** After an advance outside a step job: a finished run's callback is delivered by a job, which has a Flow to run it in. */
|
|
670
|
+
async function scheduleDone(tx: DB, run: RunRow | null, outcome: Outcome): Promise<void> {
|
|
671
|
+
if (run === null || !outcome.completed || config.onDone === undefined) return
|
|
672
|
+
await queue.enqueue(tx, {
|
|
673
|
+
type: typeOfClass(classOf(run.targets[0]!)), payload: { kind: 'finish', runId: run.id, taskId: run.id }, priority: run.priority, tenant: run.tenant, parent: run.parent, actor: run.actor,
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function request(db: DB, opts: RequestOptions): Promise<RequestResult> {
|
|
678
|
+
const closure = checkRequest(opts, 'request')
|
|
679
|
+
const tenant = tenantOf(opts.tenant ?? handleTenant(db), 'request')
|
|
680
|
+
const priority = opts.priority ?? config.priority ?? 0
|
|
681
|
+
const stepPriority = opts.stepPriority === undefined || Object.keys(opts.stepPriority).length === 0 ? null : opts.stepPriority
|
|
682
|
+
const parent = parentTraceRef()
|
|
683
|
+
const flow = getContext()
|
|
684
|
+
const actor = opts.carryActor === false || flow === undefined ? null : resolveActorId(flow)
|
|
685
|
+
const inputs = toJson(opts.inputs ?? {})
|
|
686
|
+
|
|
687
|
+
return inTenantTx(db, tenant, async (tx) => {
|
|
688
|
+
const created = await tx.execute(sql`
|
|
689
|
+
INSERT INTO ${sql.raw(RUN)} (tenant_id, graph, item, targets, priority, step_priority, inputs, trace_id, trace_root_id, trace_step_id, actor)
|
|
690
|
+
VALUES (${tenant}::uuid, ${graph}, ${opts.item}, ${JSON.stringify(opts.targets)}::jsonb, ${priority}, ${stepPriority === null ? null : JSON.stringify(stepPriority)}::jsonb, ${JSON.stringify(inputs)}::jsonb,
|
|
691
|
+
${parent?.id ?? null}::uuid, ${parent?.rootId ?? null}::uuid, ${parent?.stepId ?? null}::uuid, ${actor}::text)
|
|
692
|
+
RETURNING id::text`)
|
|
693
|
+
const runId = created[0]!['id'] as string
|
|
694
|
+
await tx.execute(sql`
|
|
695
|
+
INSERT INTO ${sql.raw(TASK)} (tenant_id, run_id, step, instance, status)
|
|
696
|
+
VALUES ${sql.join(closure.map(step => sql`(${tenant}::uuid, ${runId}::uuid, ${step}, ${SINGLE}, 'blocked')`), sql`, `)}`)
|
|
697
|
+
const outcome = await advance(tx, runId)
|
|
698
|
+
await scheduleDone(tx, outcome.run, outcome)
|
|
699
|
+
return { runId, reused: outcome.reused, queued: outcome.queued }
|
|
700
|
+
})
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function plan(db: DB, opts: PlanOptions): Promise<PlanResult> {
|
|
704
|
+
const closure = checkRequest(opts, 'plan')
|
|
705
|
+
const tenant = tenantOf(opts.tenant ?? handleTenant(db), 'plan')
|
|
706
|
+
const inputs = toJson(opts.inputs ?? {}) as Record<string, unknown>
|
|
707
|
+
const hashes = inputHashesOf(inputs)
|
|
708
|
+
|
|
709
|
+
return inTenantTx(db, tenant, async (tx) => {
|
|
710
|
+
// What each step would hold if the run were made now: its list and, per instance, the recorded hash and value (null where none is).
|
|
711
|
+
interface Known { elements: unknown[] | null; hashes: Array<string | null>; values: unknown[] }
|
|
712
|
+
const known = new Map<string, Known>()
|
|
713
|
+
const full = (k: Known | undefined): k is Known & { elements: unknown[] } =>
|
|
714
|
+
k !== undefined && k.elements !== null && k.hashes.every(h => h !== null)
|
|
715
|
+
const result: Record<string, StepPlan> = {}
|
|
716
|
+
for (const name of closure) {
|
|
717
|
+
const def = steps[name]!
|
|
718
|
+
const fanOut = isFanOut(name)
|
|
719
|
+
const needs = def.needs ?? []
|
|
720
|
+
const by = def.byElement
|
|
721
|
+
const unknownList = (): void => { result[name] = { reusable: 0, toRun: fanOut ? null : 1 } }
|
|
722
|
+
const stepNeeds = needs.filter(n => steps[n] !== undefined && by?.includes(n) !== true)
|
|
723
|
+
if (!stepNeeds.every(n => full(known.get(n)))) { unknownList(); continue }
|
|
724
|
+
if (by !== undefined && known.get(by[0]!)?.elements == null) { unknownList(); continue }
|
|
725
|
+
|
|
726
|
+
const needValues: Record<string, unknown> = {}
|
|
727
|
+
const needHashes: Record<string, string | string[]> = {}
|
|
728
|
+
const given: Record<string, unknown> = {}
|
|
729
|
+
for (const n of needs) {
|
|
730
|
+
if (inputNames.has(n)) { needValues[n] = inputs[n]; given[n] = inputs[n]; needHashes[n] = hashes[n]! } else if (by?.includes(n) !== true) {
|
|
731
|
+
const k = known.get(n)!
|
|
732
|
+
needValues[n] = isFanOut(n) ? k.values : k.values[0]
|
|
733
|
+
needHashes[n] = isFanOut(n) ? k.hashes as string[] : k.hashes[0]!
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const elements: unknown[] = by !== undefined
|
|
737
|
+
? known.get(by[0]!)!.elements!
|
|
738
|
+
: fanOut ? (await def.forEach!({ needs: needValues, item: opts.item, inputs: given, runId: '' })).map(toJson) : [undefined]
|
|
739
|
+
let reusable = 0
|
|
740
|
+
const values: unknown[] = []
|
|
741
|
+
const valueHashes: Array<string | null> = []
|
|
742
|
+
for (const [i, element] of elements.entries()) {
|
|
743
|
+
let own = needHashes
|
|
744
|
+
let checkable = true
|
|
745
|
+
if (by !== undefined) {
|
|
746
|
+
own = { ...needHashes }
|
|
747
|
+
for (const n of by) {
|
|
748
|
+
const h = known.get(n)?.hashes[i] ?? null
|
|
749
|
+
if (h === null) checkable = false
|
|
750
|
+
else own[n] = h
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const rec = checkable
|
|
754
|
+
? await findRecorded(tx, tenant, opts.item, name, contentKey(name, element, fanOut, without(def, own)), fanOut ? i : SINGLE)
|
|
755
|
+
: null
|
|
756
|
+
if (rec !== null) reusable++
|
|
757
|
+
values.push(rec?.value)
|
|
758
|
+
valueHashes.push(rec?.hash ?? null)
|
|
759
|
+
}
|
|
760
|
+
result[name] = { reusable, toRun: elements.length - reusable }
|
|
761
|
+
known.set(name, { elements, hashes: valueHashes, values })
|
|
762
|
+
}
|
|
763
|
+
return { steps: result }
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function status(db: DB, runId: string, opts?: ScopeOptions): Promise<RunStatus> {
|
|
768
|
+
return inTenantTx(db, scopeOf(opts?.tenant), tx => statusIn(tx, runId))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function stateOf(status: TaskStatus, jobStatus: string | null): TaskState {
|
|
772
|
+
if (status === 'done' || status === 'reused' || status === 'failed' || status === 'cancelled') return status
|
|
773
|
+
return status === 'queued' && (jobStatus === 'claimed' || jobStatus === 'running') ? 'running' : 'waiting'
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function statusIn(db: DB, runId: string): Promise<RunStatus> {
|
|
777
|
+
const runs = await db.execute(sql`SELECT status, inputs, finished_at FROM ${sql.raw(RUN)} WHERE id = ${runId}::uuid AND graph = ${graph}`)
|
|
778
|
+
if (runs[0] === undefined) throw new Error(`[pipework] ${graph}.status: no run "${runId}".\n`)
|
|
779
|
+
const tasks = await db.execute(sql`
|
|
780
|
+
SELECT t.step, t.instance, t.status, t.element, j.status AS job_status
|
|
781
|
+
FROM ${sql.raw(TASK)} t LEFT JOIN ${sql.raw(quoted(queueTable()))} j ON j.id = t.job_id
|
|
782
|
+
WHERE t.run_id = ${runId}::uuid`)
|
|
783
|
+
const zero = (): { total: number; done: number; reused: number; running: number; waiting: number; failed: number; cancelled: number } =>
|
|
784
|
+
({ total: 0, done: 0, reused: 0, running: 0, waiting: 0, failed: 0, cancelled: 0 })
|
|
785
|
+
const counts = new Map<string, ReturnType<typeof zero>>()
|
|
786
|
+
for (const t of tasks) {
|
|
787
|
+
const step = t['step'] as string
|
|
788
|
+
const fanOut = steps[step] !== undefined && isFanOut(step)
|
|
789
|
+
const c = counts.get(step) ?? zero()
|
|
790
|
+
counts.set(step, c)
|
|
791
|
+
if (fanOut && (t['instance'] as number) < 0) continue
|
|
792
|
+
c.total++
|
|
793
|
+
c[stateOf(t['status'] as TaskStatus, t['job_status'] as string | null)]++
|
|
794
|
+
}
|
|
795
|
+
const listDone = new Set<string>()
|
|
796
|
+
for (const t of tasks) if ((t['instance'] as number) < 0 && t['status'] === 'done') listDone.add(t['step'] as string)
|
|
797
|
+
const out: Record<string, StepCounts> = {}
|
|
798
|
+
const list: RunTask[] = []
|
|
799
|
+
for (const name of order) {
|
|
800
|
+
const c = counts.get(name)
|
|
801
|
+
if (c === undefined) continue
|
|
802
|
+
out[name] = steps[name] !== undefined && isFanOut(name) ? { ...c, listKnown: listDone.has(name) } : c
|
|
803
|
+
const mine = tasks.filter(t => t['step'] === name).sort((a, b) => (a['instance'] as number) - (b['instance'] as number))
|
|
804
|
+
for (const t of mine) {
|
|
805
|
+
list.push({
|
|
806
|
+
step: name, instance: t['instance'] as number, element: t['element'] ?? null,
|
|
807
|
+
state: stateOf(t['status'] as TaskStatus, t['job_status'] as string | null),
|
|
808
|
+
})
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const finished = runs[0]['finished_at'] as string | Date | null
|
|
812
|
+
const raw = runs[0]['inputs']
|
|
813
|
+
const inputs = (typeof raw === 'string' ? JSON.parse(raw) : raw ?? {}) as Record<string, unknown>
|
|
814
|
+
return {
|
|
815
|
+
status: runs[0]['status'] as GraphRunStatus, steps: out, tasks: list, inputs,
|
|
816
|
+
finishedAt: finished === null ? null : new Date(finished),
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function ended(db: DB, opts?: EndedRunsOptions): Promise<readonly EndedRun[]> {
|
|
821
|
+
const tenant = scopedExecutor(db, scopeOf(opts?.tenant), `${graph}.ended`).tenant
|
|
822
|
+
const limit = opts?.limit ?? 1000
|
|
823
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error(`[pipework] ${graph}.ended: limit must be a positive integer.\n`)
|
|
824
|
+
const only = tenant === null ? sql`` : sql` AND tenant_id = ${tenant}::uuid`
|
|
825
|
+
const byIds = opts?.ids === undefined ? sql`` : opts.ids.length === 0 ? sql` AND false`
|
|
826
|
+
: sql` AND id ${sql.inList(opts.ids, 'uuid')}`
|
|
827
|
+
const before = opts?.finishedBefore === undefined ? sql`` : sql` AND finished_at < ${opts.finishedBefore.toISOString()}::timestamptz`
|
|
828
|
+
const rows = await inTenantTx(db, tenant, tx => tx.execute(sql`
|
|
829
|
+
SELECT id::text, tenant_id::text, item, status, finished_at, notified FROM ${sql.raw(RUN)}
|
|
830
|
+
WHERE graph = ${graph} AND status <> 'running' AND finished_at IS NOT NULL${only}${byIds}${before}
|
|
831
|
+
ORDER BY finished_at, id LIMIT ${limit}`))
|
|
832
|
+
return rows.map(r => ({
|
|
833
|
+
id: r['id'] as string,
|
|
834
|
+
tenant: r['tenant_id'] as string,
|
|
835
|
+
item: r['item'] as string,
|
|
836
|
+
status: r['status'] as EndedRun['status'],
|
|
837
|
+
finishedAt: new Date(r['finished_at'] as string | Date),
|
|
838
|
+
notified: r['notified'] as boolean,
|
|
839
|
+
}))
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** The queue's table name, read from its definition. */
|
|
843
|
+
function queueTable(): string {
|
|
844
|
+
return (queue.definition as unknown as { $name: string }).$name
|
|
845
|
+
}
|
|
846
|
+
const quoted = (name: string): string => `"${name.replace(/"/g, '""')}"`
|
|
847
|
+
|
|
848
|
+
async function cancelRun(tx: DB, runId: string): Promise<void> {
|
|
849
|
+
const run = await lockRun(tx, runId)
|
|
850
|
+
if (run === null || run.status !== 'running') return
|
|
851
|
+
await tx.execute(sql`UPDATE ${sql.raw(RUN)} SET status = 'cancelled', finished_at = now() WHERE id = ${runId}::uuid`)
|
|
852
|
+
const live = await tx.execute(sql`
|
|
853
|
+
UPDATE ${sql.raw(TASK)} SET status = 'cancelled'
|
|
854
|
+
WHERE run_id = ${runId}::uuid AND status IN ('blocked', 'queued', 'waiting') RETURNING job_id::text`)
|
|
855
|
+
await cancelPending(tx, live.flatMap(r => r['job_id'] === null ? [] : [r['job_id'] as string]),
|
|
856
|
+
{ reason: 'run-cancelled', message: `${graph} run ${runId} was cancelled` })
|
|
857
|
+
await tx.execute(sql`SELECT pg_notify('pipework_step_run', ${runId})`)
|
|
858
|
+
for (const c of running.get(runId) ?? []) c.abort(new Error(`[pipework] ${graph} run ${runId} was cancelled.`))
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function cancel(db: DB, runId: string, opts?: ScopeOptions): Promise<void> {
|
|
862
|
+
await inTenantTx(db, scopeOf(opts?.tenant), async (tx) => { await cancelRun(tx, runId) })
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function decide(db: DB, opts: DecideOptions): Promise<void> {
|
|
866
|
+
const def = steps[opts.step]
|
|
867
|
+
if (def === undefined || def.person !== true) {
|
|
868
|
+
throw new Error(`[pipework] ${graph}.decide: "${opts.step}" is not a person step.\n`)
|
|
869
|
+
}
|
|
870
|
+
const instance = opts.index ?? SINGLE
|
|
871
|
+
const value = toJson(opts.value)
|
|
872
|
+
await inTenantTx(db, scopeOf(opts.tenant), async (tx) => {
|
|
873
|
+
const run = await lockRun(tx, opts.runId)
|
|
874
|
+
if (run === null) throw new Error(`[pipework] ${graph}.decide: no run "${opts.runId}".\n`)
|
|
875
|
+
const rows = await tx.execute(sql`
|
|
876
|
+
SELECT id::text, status, input_key, result_id::text AS result_id FROM ${sql.raw(TASK)}
|
|
877
|
+
WHERE run_id = ${run.id}::uuid AND step = ${opts.step} AND instance = ${instance} FOR UPDATE`)
|
|
878
|
+
const task = rows[0]
|
|
879
|
+
if (task === undefined) {
|
|
880
|
+
throw new Error(`[pipework] ${graph}.decide: run ${opts.runId} has no task for step "${opts.step}"${opts.index === undefined ? '' : ` instance ${String(opts.index)}`}.\n`)
|
|
881
|
+
}
|
|
882
|
+
const taskStatus = task['status'] as TaskStatus
|
|
883
|
+
if (taskStatus === 'waiting') {
|
|
884
|
+
const rec = await recordResult(tx, run, opts.step, task['input_key'] as string, instance, value, true)
|
|
885
|
+
await tx.execute(sql`UPDATE ${sql.raw(TASK)} SET status = 'done', result_id = ${rec.id}::uuid WHERE id = ${task['id'] as string}::uuid`)
|
|
886
|
+
const outcome = await advance(tx, run.id)
|
|
887
|
+
await scheduleDone(tx, outcome.run, outcome)
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
if (taskStatus === 'done' || taskStatus === 'reused') {
|
|
891
|
+
await tx.execute(sql`
|
|
892
|
+
UPDATE ${sql.raw(RESULT)} SET value = ${JSON.stringify(value)}::jsonb, value_hash = ${valueHash(value)}, created_at = now()
|
|
893
|
+
WHERE id = ${task['result_id'] as string}::uuid`)
|
|
894
|
+
return
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`[pipework] ${graph}.decide: step "${opts.step}" of run ${opts.runId} is ${taskStatus}, not waiting for a decision.\n`)
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function wait(db: DB, runId: string, opts: { readonly timeoutMs: number; readonly tenant?: string }): Promise<GraphRunStatus | 'timeout'> {
|
|
901
|
+
const deadline = Date.now() + opts.timeoutMs
|
|
902
|
+
for (;;) {
|
|
903
|
+
const rows = await inTenantTx(db, scopeOf(opts.tenant), tx =>
|
|
904
|
+
tx.execute(sql`SELECT status FROM ${sql.raw(RUN)} WHERE id = ${runId}::uuid AND graph = ${graph}`))
|
|
905
|
+
if (rows[0] === undefined) throw new Error(`[pipework] ${graph}.wait: no run "${runId}".\n`)
|
|
906
|
+
const s = rows[0]['status'] as GraphRunStatus
|
|
907
|
+
if (s !== 'running') return s
|
|
908
|
+
if (Date.now() >= deadline) return 'timeout'
|
|
909
|
+
await new Promise(r => setTimeout(r, Math.min(50, Math.max(1, deadline - Date.now()))))
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function sweep(db: DB, opts?: ScopeOptions): Promise<number> {
|
|
914
|
+
if (config.resultRetentionDays === undefined) return 0
|
|
915
|
+
const cutoff = new Date(Date.now() - config.resultRetentionDays * MS_PER_DAY).toISOString()
|
|
916
|
+
const tenant = scopedExecutor(db, scopeOf(opts?.tenant), `${graph}.sweep`).tenant
|
|
917
|
+
const only = tenant === null ? sql`` : sql` AND tenant_id = ${tenant}::uuid`
|
|
918
|
+
return inTenantTx(db, tenant, async (tx) => {
|
|
919
|
+
const old = await tx.execute(sql`
|
|
920
|
+
SELECT id::text FROM ${sql.raw(RUN)}
|
|
921
|
+
WHERE graph = ${graph} AND status <> 'running' AND finished_at < ${cutoff}::timestamptz${only}`)
|
|
922
|
+
if (old.length > 0) {
|
|
923
|
+
const ids = sql.inList(old.map(r => r['id'] as string), 'uuid')
|
|
924
|
+
await tx.execute(sql`DELETE FROM ${sql.raw(TASK)} WHERE run_id ${ids}`)
|
|
925
|
+
await tx.execute(sql`DELETE FROM ${sql.raw(RUN)} WHERE id ${ids}`)
|
|
926
|
+
}
|
|
927
|
+
const rows = await tx.execute(sql`
|
|
928
|
+
DELETE FROM ${sql.raw(RESULT)} WHERE graph = ${graph} AND created_at < ${cutoff}::timestamptz${only} RETURNING id`)
|
|
929
|
+
return rows.length
|
|
930
|
+
})
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function removeTenant(db: DB, tenantId: string): Promise<void> {
|
|
934
|
+
if (typeof tenantId !== 'string' || tenantId.trim() === '') {
|
|
935
|
+
throw new Error(`[pipework] ${graph}.removeTenant: tenantId must be a non-empty string.\n`)
|
|
936
|
+
}
|
|
937
|
+
await inTenantTx(db, tenantId, async (tx) => {
|
|
938
|
+
await tx.execute(sql`DELETE FROM ${sql.raw(TASK)} WHERE tenant_id = ${tenantId}::uuid`)
|
|
939
|
+
await tx.execute(sql`DELETE FROM ${sql.raw(RUN)} WHERE tenant_id = ${tenantId}::uuid`)
|
|
940
|
+
await tx.execute(sql`DELETE FROM ${sql.raw(RESULT)} WHERE tenant_id = ${tenantId}::uuid`)
|
|
941
|
+
})
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function itemScope(method: string, item: string, explicit?: string): string {
|
|
945
|
+
if (typeof item !== 'string' || item.trim() === '') {
|
|
946
|
+
throw new Error(`[pipework] ${graph}.${method}: item must be a non-empty string.\n`)
|
|
947
|
+
}
|
|
948
|
+
const tenant = scopeOf(explicit)
|
|
949
|
+
if (tenant === null) {
|
|
950
|
+
throw new Error(`[pipework] ${graph}.${method}: no tenant — pass options.tenant or call inside a Flow with one.\n`)
|
|
951
|
+
}
|
|
952
|
+
return tenant
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function results(db: DB, item: string, opts?: ScopeOptions): Promise<readonly StepResultRecord[]> {
|
|
956
|
+
const tenant = itemScope('results', item, opts?.tenant ?? handleTenant(db))
|
|
957
|
+
return inTenantTx(db, tenant, async (tx) => {
|
|
958
|
+
const rows = await tx.execute(sql`
|
|
959
|
+
SELECT id::text, step, version, instance, value, created_at FROM ${sql.raw(RESULT)}
|
|
960
|
+
WHERE graph = ${graph} AND item = ${item} AND tenant_id = ${tenant}::uuid
|
|
961
|
+
ORDER BY step, instance, created_at`)
|
|
962
|
+
return rows.map(r => ({
|
|
963
|
+
id: r['id'] as string,
|
|
964
|
+
step: r['step'] as string,
|
|
965
|
+
version: Number(r['version']),
|
|
966
|
+
instance: Number(r['instance']),
|
|
967
|
+
value: r['value'],
|
|
968
|
+
createdAt: new Date(r['created_at'] as string | Date),
|
|
969
|
+
}))
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function removeItem(db: DB, item: string, opts?: RemoveItemOptions): Promise<RemoveItemResult> {
|
|
974
|
+
const tenant = itemScope('removeItem', item, opts?.tenant ?? handleTenant(db))
|
|
975
|
+
return inTenantTx(db, tenant, async (tx) => {
|
|
976
|
+
const runs = await tx.execute(sql`
|
|
977
|
+
SELECT id::text, status FROM ${sql.raw(RUN)}
|
|
978
|
+
WHERE graph = ${graph} AND item = ${item} AND tenant_id = ${tenant}::uuid ORDER BY id FOR UPDATE`)
|
|
979
|
+
const live = runs.filter(r => r['status'] === 'running').map(r => r['id'] as string)
|
|
980
|
+
if (live.length > 0) {
|
|
981
|
+
if (opts?.running !== 'cancel') throw new StepItemRunningError(graph, item, live)
|
|
982
|
+
for (const id of live) await cancelRun(tx, id)
|
|
983
|
+
}
|
|
984
|
+
let tasks = 0
|
|
985
|
+
if (runs.length > 0) {
|
|
986
|
+
const ids = sql.inList(runs.map(r => r['id'] as string), 'uuid')
|
|
987
|
+
tasks = (await tx.execute(sql`DELETE FROM ${sql.raw(TASK)} WHERE run_id ${ids} RETURNING id`)).length
|
|
988
|
+
await tx.execute(sql`DELETE FROM ${sql.raw(RUN)} WHERE id ${ids}`)
|
|
989
|
+
}
|
|
990
|
+
const gone = await tx.execute(sql`
|
|
991
|
+
DELETE FROM ${sql.raw(RESULT)} WHERE graph = ${graph} AND item = ${item} AND tenant_id = ${tenant}::uuid RETURNING id`)
|
|
992
|
+
return { runs: runs.length, tasks, results: gone.length }
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
type Checkpoint = <R>(fn: (tx: DB) => Promise<R>) => Promise<R>
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* The step job's attempt and limit, from the Flow the worker surface built; a job run
|
|
1000
|
+
* outside it has neither, so they are read from the job's row.
|
|
1001
|
+
*/
|
|
1002
|
+
async function triesOf(checkpoint: Checkpoint, taskId: string): Promise<{ attempt: number; maxAttempts: number }> {
|
|
1003
|
+
const flow = getContext()
|
|
1004
|
+
if (flow?.jobAttempt != null && flow.jobMaxAttempts != null) return { attempt: flow.jobAttempt, maxAttempts: flow.jobMaxAttempts }
|
|
1005
|
+
return checkpoint(async (tx) => {
|
|
1006
|
+
const rows = await tx.execute(sql`
|
|
1007
|
+
SELECT j.attempt_count, j.max_attempts FROM ${sql.raw(TASK)} t JOIN ${sql.raw(quoted(queueTable()))} j ON j.id = t.job_id
|
|
1008
|
+
WHERE t.id = ${taskId}::uuid`)
|
|
1009
|
+
const row = rows[0]
|
|
1010
|
+
return { attempt: ((row?.['attempt_count'] as number | undefined) ?? 0) + 1, maxAttempts: (row?.['max_attempts'] as number | undefined) ?? 1 }
|
|
1011
|
+
})
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* The step job. The job holds no transaction while `run` waits: one short checkpoint
|
|
1016
|
+
* checks the task and reads the run's state, `run` executes outside any transaction,
|
|
1017
|
+
* and a second checkpoint records the result (with whatever `withResult` callbacks
|
|
1018
|
+
* asked to write) and advances the run.
|
|
1019
|
+
*/
|
|
1020
|
+
async function handleJob(checkpoint: Checkpoint, payload: { kind: string; runId: string; taskId: string }): Promise<unknown> {
|
|
1021
|
+
if (payload.kind === 'finish') {
|
|
1022
|
+
await checkpoint((tx) => deliverDone(tx, payload.runId))
|
|
1023
|
+
return { finished: payload.runId }
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const loaded = await checkpoint(async (tx) => {
|
|
1027
|
+
const live = await tx.execute(sql`
|
|
1028
|
+
SELECT 1 FROM ${sql.raw(TASK)} t JOIN ${sql.raw(RUN)} r ON r.id = t.run_id
|
|
1029
|
+
WHERE t.id = ${payload.taskId}::uuid AND t.run_id = ${payload.runId}::uuid AND t.status = 'queued' AND r.status = 'running'`)
|
|
1030
|
+
if (live.length === 0) return null
|
|
1031
|
+
|
|
1032
|
+
// Read the run's state without the lock: the step may take a long time, and other steps of the run must proceed meanwhile.
|
|
1033
|
+
const runRows = await tx.execute(sql`
|
|
1034
|
+
SELECT id::text, tenant_id::text AS tenant, item, status, priority, step_priority, targets, inputs, actor FROM ${sql.raw(RUN)} WHERE id = ${payload.runId}::uuid`)
|
|
1035
|
+
const r = runRows[0]!
|
|
1036
|
+
const run: RunRow = {
|
|
1037
|
+
id: r['id'] as string, tenant: r['tenant'] as string, item: r['item'] as string, status: r['status'] as GraphRunStatus,
|
|
1038
|
+
priority: r['priority'] as number, stepPriority: r['step_priority'] as Record<string, number> | null, targets: r['targets'] as string[], inputs: r['inputs'] as Record<string, unknown>, parent: null, actor: r['actor'] as string | null,
|
|
1039
|
+
}
|
|
1040
|
+
return { run, state: await loadState(tx, run) }
|
|
1041
|
+
})
|
|
1042
|
+
if (loaded === null) return { skipped: payload.taskId }
|
|
1043
|
+
const { run, state } = loaded
|
|
1044
|
+
const task = state.tasks.find(t => t.id === payload.taskId)!
|
|
1045
|
+
const def = steps[task.step]!
|
|
1046
|
+
const fanOut = isFanOut(task.step)
|
|
1047
|
+
|
|
1048
|
+
const tries = await triesOf(checkpoint, payload.taskId)
|
|
1049
|
+
const resultWrites: Array<(tx: DB) => Promise<void>> = []
|
|
1050
|
+
const controller = new AbortController()
|
|
1051
|
+
let controllers = running.get(run.id)
|
|
1052
|
+
if (controllers === undefined) { controllers = new Set(); running.set(run.id, controllers) }
|
|
1053
|
+
controllers.add(controller)
|
|
1054
|
+
let value: unknown
|
|
1055
|
+
let timer: NodeJS.Timeout | undefined
|
|
1056
|
+
const startedAt = new Date()
|
|
1057
|
+
try {
|
|
1058
|
+
const work = Promise.resolve(def.run!({
|
|
1059
|
+
...stepContext(state, def, task, fanOut, controller.signal, {
|
|
1060
|
+
checkpoint,
|
|
1061
|
+
withResult: (fn) => { resultWrites.push(fn) },
|
|
1062
|
+
}),
|
|
1063
|
+
...tries,
|
|
1064
|
+
}))
|
|
1065
|
+
if (def.timeoutMs === undefined) {
|
|
1066
|
+
value = toJson(await work)
|
|
1067
|
+
} else {
|
|
1068
|
+
const limit = def.timeoutMs
|
|
1069
|
+
const expired = new Promise<never>((_, reject) => {
|
|
1070
|
+
timer = setTimeout(() => {
|
|
1071
|
+
const timeout = new StepTimeoutError(graph, task.step, limit)
|
|
1072
|
+
controller.abort(timeout)
|
|
1073
|
+
reject(timeout)
|
|
1074
|
+
}, limit)
|
|
1075
|
+
timer.unref()
|
|
1076
|
+
})
|
|
1077
|
+
work.catch(() => {})
|
|
1078
|
+
try {
|
|
1079
|
+
value = toJson(await Promise.race([work, expired]))
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
if (err instanceof StepTimeoutError) stageTimeout(task.step, task.instance, limit, startedAt, err)
|
|
1082
|
+
throw err
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} finally {
|
|
1086
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
1087
|
+
controllers.delete(controller)
|
|
1088
|
+
if (controllers.size === 0) running.delete(run.id)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return checkpoint(async (tx) => {
|
|
1092
|
+
const locked = await lockRun(tx, run.id)
|
|
1093
|
+
if (locked === null) return { skipped: payload.taskId }
|
|
1094
|
+
const current = await tx.execute(sql`SELECT status FROM ${sql.raw(TASK)} WHERE id = ${task.id}::uuid FOR UPDATE`)
|
|
1095
|
+
for (const write of resultWrites) await write(tx)
|
|
1096
|
+
const rec = await recordResult(tx, locked, task.step, task.inputKey!, task.instance, value, false)
|
|
1097
|
+
if (current[0]?.['status'] !== 'queued' || locked.status !== 'running') {
|
|
1098
|
+
// Cancelled or failed while it ran: the result is kept for later requests, the run does not move.
|
|
1099
|
+
return { recorded: rec.id }
|
|
1100
|
+
}
|
|
1101
|
+
await tx.execute(sql`UPDATE ${sql.raw(TASK)} SET status = 'done', result_id = ${rec.id}::uuid WHERE id = ${task.id}::uuid`)
|
|
1102
|
+
const outcome = await advance(tx, run.id)
|
|
1103
|
+
if (outcome.completed) await deliverDone(tx, run.id)
|
|
1104
|
+
return { step: task.step, resultId: rec.id }
|
|
1105
|
+
})
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function handlerFor(type: string, database: string | undefined): Handler<Record<string, unknown>, unknown> {
|
|
1109
|
+
const builder = database === undefined ? fitting.use() : fitting.use(database)
|
|
1110
|
+
const key = database ?? 'db'
|
|
1111
|
+
return builder.job(type)
|
|
1112
|
+
.transaction('manual')
|
|
1113
|
+
.input(schema.check.object({ kind: schema.check.string(), runId: schema.check.string(), taskId: schema.check.string() }))
|
|
1114
|
+
.fit(async ({ input, checkpoint }: {
|
|
1115
|
+
input: { kind: string; runId: string; taskId: string }
|
|
1116
|
+
checkpoint: <R>(fn: (dbs: Record<string, DB>) => Promise<R>) => Promise<R>
|
|
1117
|
+
}) => {
|
|
1118
|
+
const tenant = getContext()?.tenant ?? null
|
|
1119
|
+
const atTenant: Checkpoint = (fn) => checkpoint((dbs) => {
|
|
1120
|
+
const tx = dbs[key]!
|
|
1121
|
+
return asTenant(tx, tenant, () => fn(tx))
|
|
1122
|
+
})
|
|
1123
|
+
return handleJob(atTenant, input)
|
|
1124
|
+
}) as unknown as Handler<Record<string, unknown>, unknown>
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function handlers(database: string | undefined, selected?: readonly string[]): Handler<Record<string, unknown>, unknown>[] {
|
|
1128
|
+
return (selected ?? classes).map(cls => handlerFor(typeOfClass(cls), database))
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function workers(...wanted: string[]): GraphSelection {
|
|
1132
|
+
if (wanted.length === 0) fail(graph, 'workers() needs at least one class.')
|
|
1133
|
+
for (const cls of wanted) {
|
|
1134
|
+
if (typeof cls !== 'string' || !CLASS_RE.test(cls)) {
|
|
1135
|
+
fail(graph, `worker class "${String(cls)}" must start with a letter and use letters, digits, "_" or "-".`)
|
|
1136
|
+
}
|
|
1137
|
+
if (!classes.includes(cls)) fail(graph, `no step uses worker class "${cls}" (classes: ${classes.join(', ')}).`)
|
|
1138
|
+
}
|
|
1139
|
+
return { graph: self, classes: [...new Set(wanted)] }
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** A step job of this graph dead-lettered: fail the task, what depends on it and the run, cancel the rest, tell the app once. */
|
|
1143
|
+
async function onDeadLetter(tx: DB, job: DeadLetterInfo, host: GraphHost): Promise<void> {
|
|
1144
|
+
if (!jobTypes.includes(job.type)) return
|
|
1145
|
+
const payload = job.payload as { kind?: string; runId?: string; taskId?: string }
|
|
1146
|
+
if (payload.kind !== 'step' || payload.runId === undefined || payload.taskId === undefined) return
|
|
1147
|
+
attached = host.instance
|
|
1148
|
+
await asTenant(tx, job.tenant, () => failRun(tx, job, payload.runId!, payload.taskId!, host), host.instance)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** The graph's `onFailed` inside its trace step; the step captures the failure as its input. */
|
|
1152
|
+
const onFailedStep = __flowStep({ ref: 'pipework/steps:on-failed', capture: [(failure) => failure] }, async (failure: GraphFailure) => { await config.onFailed!(failure) })
|
|
1153
|
+
|
|
1154
|
+
async function failRun(tx: DB, job: DeadLetterInfo, runId: string, taskId: string, host: GraphHost): Promise<void> {
|
|
1155
|
+
const run = await lockRun(tx, runId)
|
|
1156
|
+
if (run === null || run.status !== 'running') return
|
|
1157
|
+
const tasks = await loadTasks(tx, run.id)
|
|
1158
|
+
const dead = tasks.find(t => t.id === taskId)
|
|
1159
|
+
if (dead === undefined || dead.status !== 'queued') return
|
|
1160
|
+
|
|
1161
|
+
const dependents = dependentsOf(dead.step)
|
|
1162
|
+
await setTask(tx, dead, { status: 'failed', error: job.error })
|
|
1163
|
+
const cancelJobs: string[] = []
|
|
1164
|
+
for (const t of tasks) {
|
|
1165
|
+
if (t === dead || (t.status !== 'blocked' && t.status !== 'queued' && t.status !== 'waiting')) continue
|
|
1166
|
+
if (t.jobId !== null) cancelJobs.push(t.jobId)
|
|
1167
|
+
await setTask(tx, t, { status: dependents.has(t.step) ? 'failed' : 'cancelled' })
|
|
1168
|
+
}
|
|
1169
|
+
await cancelPending(tx, cancelJobs, { reason: 'run-failed', message: `${graph} run ${runId} failed at step ${dead.step}` })
|
|
1170
|
+
await tx.execute(sql`
|
|
1171
|
+
UPDATE ${sql.raw(RUN)} SET status = 'failed', finished_at = now(), notified = true,
|
|
1172
|
+
error = ${JSON.stringify({ step: dead.step, error: job.error })}::jsonb
|
|
1173
|
+
WHERE id = ${run.id}::uuid`)
|
|
1174
|
+
await tx.execute(sql`SELECT pg_notify('pipework_step_run', ${run.id})`)
|
|
1175
|
+
|
|
1176
|
+
if (config.onFailed === undefined) return
|
|
1177
|
+
const failure = { runId: run.id, item: run.item, tenant: run.tenant, step: dead.step, error: job.error }
|
|
1178
|
+
try {
|
|
1179
|
+
await tx.transaction(async (savepoint) => {
|
|
1180
|
+
const dbName = host.database ?? host.instance.config.defaultDatabaseName()
|
|
1181
|
+
const trace = isTracingEnabled(host.instance) ? openTrace({
|
|
1182
|
+
instance: host.instance,
|
|
1183
|
+
entryKind: 'job',
|
|
1184
|
+
parent: job.flow.traceId !== null && job.flow.rootId !== null
|
|
1185
|
+
? { id: job.flow.traceId, rootId: job.flow.rootId, stepId: job.flow.stepId }
|
|
1186
|
+
: null,
|
|
1187
|
+
tenant: run.tenant,
|
|
1188
|
+
auth: run.actor !== null ? { userId: run.actor } : null,
|
|
1189
|
+
requestId: job.id,
|
|
1190
|
+
sessionId: null,
|
|
1191
|
+
}) : null
|
|
1192
|
+
const base = createJobContext({
|
|
1193
|
+
instance: host.instance, jobId: job.id, jobType: job.type, tenant: run.tenant, auth: run.actor !== null ? { userId: run.actor } : null, ...(trace !== null ? { trace } : {}),
|
|
1194
|
+
})
|
|
1195
|
+
const ctx = { ...base, transactions: new Map([[dbName, bindTransaction(base, dbName, savepoint)]]) }
|
|
1196
|
+
await propagateLocals(savepoint, ctx, null)
|
|
1197
|
+
await finalizeTrace(host.instance, trace, runInContext(ctx, async () => { await onFailedStep(failure) }))
|
|
1198
|
+
})
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
getBaseLogger().error({ err, runId: run.id, graph }, 'Step graph onFailed callback failed; the run is already marked failed')
|
|
1201
|
+
void recordFaultFor(host.instance, 'step-graph-on-failed', faultMessage(err), { runId: run.id, graph })
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const self: StepGraph = {
|
|
1206
|
+
name: graph,
|
|
1207
|
+
jobType,
|
|
1208
|
+
jobTypes,
|
|
1209
|
+
queue,
|
|
1210
|
+
request, plan, status, ended, cancel, decide, wait, sweep, removeTenant, results, removeItem, workers,
|
|
1211
|
+
worker: { handlers, onDeadLetter },
|
|
1212
|
+
}
|
|
1213
|
+
return self
|
|
1214
|
+
}
|