mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-06 07:10:41 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12d2ea9cb | ||
|
|
7c7d8ab06b |
@@ -133,7 +133,7 @@ jobs:
|
|||||||
docker run --rm --network none --entrypoint node \
|
docker run --rm --network none --entrypoint node \
|
||||||
qinglong-readiness:${{ matrix.image }} \
|
qinglong-readiness:${{ matrix.image }} \
|
||||||
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.4.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.4.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
||||||
- name: Load ARMv6 image for SQLite and Node regression checks
|
- name: Export ARMv6 image layers for Node regression checks
|
||||||
if: startsWith(matrix.image, 'alpine')
|
if: startsWith(matrix.image, 'alpine')
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
@@ -144,23 +144,14 @@ jobs:
|
|||||||
QL_URL=${{ steps.source.outputs.url }}
|
QL_URL=${{ steps.source.outputs.url }}
|
||||||
QL_BRANCH=${{ steps.source.outputs.branch }}
|
QL_BRANCH=${{ steps.source.outputs.branch }}
|
||||||
SOURCE_COMMIT=${{ steps.source.outputs.sha }}
|
SOURCE_COMMIT=${{ steps.source.outputs.sha }}
|
||||||
tags: qinglong-readiness:${{ matrix.image }}-armv6
|
outputs: type=docker,dest=${{ runner.temp }}/node-layers-arm.tar
|
||||||
load: true
|
|
||||||
push: false
|
push: false
|
||||||
provenance: false
|
provenance: false
|
||||||
sbom: false
|
sbom: false
|
||||||
- name: Verify ARMv6 SQLite on an ARM1176 CPU without networking
|
|
||||||
if: startsWith(matrix.image, 'alpine')
|
|
||||||
run: |
|
|
||||||
docker run --rm --network none --platform linux/arm/v6 \
|
|
||||||
-e QEMU_CPU=arm1176 --entrypoint node \
|
|
||||||
qinglong-readiness:${{ matrix.image }}-armv6 --jitless \
|
|
||||||
-e 'const assert=require("node:assert/strict"); const sqlite3=require("sqlite3"); const db=new sqlite3.Database(":memory:"); db.get("SELECT 42 AS n",(error,row)=>{assert.ifError(error); assert.equal(row.n,42); db.close()});'
|
|
||||||
- name: Check Node binaries in every serialized image layer
|
- name: Check Node binaries in every serialized image layer
|
||||||
if: startsWith(matrix.image, 'alpine')
|
if: startsWith(matrix.image, 'alpine')
|
||||||
run: |
|
run: |
|
||||||
docker save qinglong-readiness:${{ matrix.image }} -o "$RUNNER_TEMP/node-layers-amd64.tar"
|
docker save qinglong-readiness:${{ matrix.image }} -o "$RUNNER_TEMP/node-layers-amd64.tar"
|
||||||
docker save qinglong-readiness:${{ matrix.image }}-armv6 -o "$RUNNER_TEMP/node-layers-arm.tar"
|
|
||||||
python3 scripts/verify-image-node-layers.py "$RUNNER_TEMP/node-layers-amd64.tar" amd64
|
python3 scripts/verify-image-node-layers.py "$RUNNER_TEMP/node-layers-amd64.tar" amd64
|
||||||
python3 scripts/verify-image-node-layers.py "$RUNNER_TEMP/node-layers-arm.tar" arm
|
python3 scripts/verify-image-node-layers.py "$RUNNER_TEMP/node-layers-arm.tar" arm
|
||||||
rm "$RUNNER_TEMP/node-layers-amd64.tar" "$RUNNER_TEMP/node-layers-arm.tar"
|
rm "$RUNNER_TEMP/node-layers-amd64.tar" "$RUNNER_TEMP/node-layers-arm.tar"
|
||||||
|
|||||||
+28
-85
@@ -4,6 +4,7 @@ import taskLimit from './pLimit';
|
|||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
import { ICron } from '../protos/cron';
|
import { ICron } from '../protos/cron';
|
||||||
import { CrontabModel, CrontabStatus } from '../data/cron';
|
import { CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
|
import { killTask } from '../config/util';
|
||||||
import { RunningInstanceModel, InstanceStatus } from '../data/runningInstance';
|
import { RunningInstanceModel, InstanceStatus } from '../data/runningInstance';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { observeChildProcess, asError } from './childProcess';
|
import { observeChildProcess, asError } from './childProcess';
|
||||||
@@ -16,7 +17,7 @@ export function runCron(
|
|||||||
const execute = async () => {
|
const execute = async () => {
|
||||||
try {
|
try {
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
// A single-instance trigger must not interrupt work already in progress.
|
// Check if the cron is already running and stop it (only if multiple instances are not allowed)
|
||||||
try {
|
try {
|
||||||
const existingCron = await CrontabModel.findOne({
|
const existingCron = await CrontabModel.findOne({
|
||||||
where: { id: Number(cron.id) },
|
where: { id: Number(cron.id) },
|
||||||
@@ -29,93 +30,35 @@ export function runCron(
|
|||||||
const allowSingleInstances =
|
const allowSingleInstances =
|
||||||
existingCron?.allow_multiple_instances === 0;
|
existingCron?.allow_multiple_instances === 0;
|
||||||
|
|
||||||
if (allowSingleInstances) {
|
if (
|
||||||
// A queued manual run is an active reservation, even if its stored PID
|
allowSingleInstances &&
|
||||||
// belongs to a previous execution.
|
existingCron &&
|
||||||
if (
|
existingCron.pid &&
|
||||||
existingCron.status === CrontabStatus.queued &&
|
(existingCron.status === CrontabStatus.running ||
|
||||||
existingCron.pid
|
existingCron.status === CrontabStatus.queued)
|
||||||
) {
|
) {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][Skip queued single-instance task] Task ID: %s, PID: %s',
|
t('[schedule][停止已运行任务] 任务ID: %s, PID: %s'),
|
||||||
cron.id,
|
cron.id,
|
||||||
existingCron.pid,
|
existingCron.pid,
|
||||||
);
|
);
|
||||||
return;
|
await killTask(existingCron.pid, true);
|
||||||
}
|
// Mark old running instances as stopped
|
||||||
if (
|
const stoppedAt = dayjs().unix();
|
||||||
existingCron.status === CrontabStatus.running &&
|
await RunningInstanceModel.update(
|
||||||
existingCron.pid
|
{ status: InstanceStatus.stopped, finished_at: stoppedAt },
|
||||||
) {
|
{
|
||||||
let missing = false;
|
|
||||||
try {
|
|
||||||
process.kill(existingCron.pid, 0);
|
|
||||||
} catch (error) {
|
|
||||||
const code = (error as NodeJS.ErrnoException).code;
|
|
||||||
if (code === 'ESRCH') missing = true;
|
|
||||||
else if (code !== 'EPERM') throw error;
|
|
||||||
}
|
|
||||||
if (!missing) {
|
|
||||||
Logger.info(
|
|
||||||
'[schedule][Skip active single-instance task] Task ID: %s, PID: %s',
|
|
||||||
cron.id,
|
|
||||||
existingCron.pid,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture old instance IDs before releasing the stale cron state so
|
|
||||||
// a concurrent replacement cannot have its instance marked stopped.
|
|
||||||
const staleInstances = await RunningInstanceModel.findAll({
|
|
||||||
attributes: ['id'],
|
|
||||||
where: {
|
where: {
|
||||||
cron_id: Number(cron.id),
|
cron_id: Number(cron.id),
|
||||||
pid: existingCron.pid,
|
|
||||||
status: InstanceStatus.running,
|
status: InstanceStatus.running,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
if (!isCurrent()) return;
|
);
|
||||||
const [recovered] = await CrontabModel.update(
|
// Update the status to idle after killing
|
||||||
{
|
await CrontabModel.update(
|
||||||
status: CrontabStatus.idle,
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
pid: null,
|
{ where: { id: Number(cron.id) } },
|
||||||
queued_token: null,
|
);
|
||||||
} as any,
|
|
||||||
{
|
|
||||||
where: {
|
|
||||||
id: Number(cron.id),
|
|
||||||
pid: existingCron.pid,
|
|
||||||
status: CrontabStatus.running,
|
|
||||||
...(existingCron.log_path !== undefined
|
|
||||||
? { log_path: existingCron.log_path }
|
|
||||||
: {}),
|
|
||||||
...(existingCron.queued_token !== undefined
|
|
||||||
? { queued_token: existingCron.queued_token }
|
|
||||||
: {}),
|
|
||||||
...(existingCron.last_execution_time !== undefined
|
|
||||||
? { last_execution_time: existingCron.last_execution_time }
|
|
||||||
: {}),
|
|
||||||
...(existingCron.isDisabled !== undefined
|
|
||||||
? { isDisabled: existingCron.isDisabled }
|
|
||||||
: {}),
|
|
||||||
allow_multiple_instances: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (recovered !== 1) return;
|
|
||||||
const staleIds = staleInstances.map((instance) => instance.id!);
|
|
||||||
if (staleIds.length) {
|
|
||||||
await RunningInstanceModel.update(
|
|
||||||
{ status: InstanceStatus.stopped, finished_at: dayjs().unix() },
|
|
||||||
{ where: { id: staleIds, status: InstanceStatus.running } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Logger.info(
|
|
||||||
'[schedule][Recovered stale single-instance task] Task ID: %s, PID: %s',
|
|
||||||
cron.id,
|
|
||||||
existingCron.pid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
@@ -126,7 +69,7 @@ export function runCron(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidation can happen during the asynchronous database check.
|
// In particular, invalidation can happen while stopping a previous run.
|
||||||
if (!isCurrent()) return;
|
if (!isCurrent()) return;
|
||||||
Logger.info(
|
Logger.info(
|
||||||
t('[schedule][开始执行任务] 参数 %s'),
|
t('[schedule][开始执行任务] 参数 %s'),
|
||||||
|
|||||||
+2
-9
@@ -3,7 +3,6 @@
|
|||||||
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
|
||||||
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
||||||
ENV PATH=/opt/node-global/bin:${PATH}
|
ENV PATH=/opt/node-global/bin:${PATH}
|
||||||
|
|
||||||
@@ -18,16 +17,10 @@ RUN set -x \
|
|||||||
ppc64le) NODE_ARCH=ppc64 ;; \
|
ppc64le) NODE_ARCH=ppc64 ;; \
|
||||||
*) NODE_ARCH="${TARGETARCH}" ;; \
|
*) NODE_ARCH="${TARGETARCH}" ;; \
|
||||||
esac \
|
esac \
|
||||||
&& case "${TARGETARCH}/${TARGETVARIANT:-}" in \
|
|
||||||
arm/v6) NODE_LIBC=musl-armv6 ;; \
|
|
||||||
arm/v7) NODE_LIBC=musl-armv7 ;; \
|
|
||||||
arm/*) echo "Unsupported ARM variant: ${TARGETVARIANT:-unknown}" >&2; exit 1 ;; \
|
|
||||||
*) NODE_LIBC=musl ;; \
|
|
||||||
esac \
|
|
||||||
&& npm_config_target_platform=linux \
|
&& npm_config_target_platform=linux \
|
||||||
npm_config_target_arch="${NODE_ARCH}" \
|
npm_config_target_arch="${NODE_ARCH}" \
|
||||||
npm_config_target_libc="${NODE_LIBC}" \
|
npm_config_target_libc=musl \
|
||||||
pnpm install --prod --frozen-lockfile --side-effects-cache=false
|
pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
# Pin amd64 to upstream's musl runtime after native URL parser crashes with APK Node.
|
# Pin amd64 to upstream's musl runtime after native URL parser crashes with APK Node.
|
||||||
FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64
|
FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
|
||||||
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
||||||
ENV PATH=/opt/node-global/bin:${PATH}
|
ENV PATH=/opt/node-global/bin:${PATH}
|
||||||
|
|
||||||
@@ -18,16 +17,10 @@ RUN set -x \
|
|||||||
ppc64le) NODE_ARCH=ppc64 ;; \
|
ppc64le) NODE_ARCH=ppc64 ;; \
|
||||||
*) NODE_ARCH="${TARGETARCH}" ;; \
|
*) NODE_ARCH="${TARGETARCH}" ;; \
|
||||||
esac \
|
esac \
|
||||||
&& case "${TARGETARCH}/${TARGETVARIANT:-}" in \
|
|
||||||
arm/v6) NODE_LIBC=musl-armv6 ;; \
|
|
||||||
arm/v7) NODE_LIBC=musl-armv7 ;; \
|
|
||||||
arm/*) echo "Unsupported ARM variant: ${TARGETVARIANT:-unknown}" >&2; exit 1 ;; \
|
|
||||||
*) NODE_LIBC=musl ;; \
|
|
||||||
esac \
|
|
||||||
&& npm_config_target_platform=linux \
|
&& npm_config_target_platform=linux \
|
||||||
npm_config_target_arch="${NODE_ARCH}" \
|
npm_config_target_arch="${NODE_ARCH}" \
|
||||||
npm_config_target_libc="${NODE_LIBC}" \
|
npm_config_target_libc=musl \
|
||||||
pnpm install --prod --frozen-lockfile --side-effects-cache=false
|
pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
# Pin amd64 to upstream's musl runtime after native URL parser crashes with APK Node.
|
# Pin amd64 to upstream's musl runtime after native URL parser crashes with APK Node.
|
||||||
FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64
|
FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64
|
||||||
|
|||||||
@@ -6,11 +6,25 @@ ENV QL_BUNDLE_BASE_IMAGE=${BASE_IMAGE}
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
USER root
|
USER root
|
||||||
RUN ulimit -c 0; \
|
# The generic "arm" sqlite release is ARMv7. ARMv6 must compile its own addon.
|
||||||
if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
RUN ulimit -c 0; if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
||||||
|
apk add --no-cache --virtual .ql-bundle-build build-base binutils python3 nodejs-dev && \
|
||||||
|
cd "$(readlink -f /ql/node_modules/sqlite3)" && \
|
||||||
|
CFLAGS='-march=armv6 -mfpu=vfp -mfloat-abi=hard' \
|
||||||
|
CXXFLAGS='-march=armv6 -mfpu=vfp -mfloat-abi=hard' \
|
||||||
|
npm_config_nodedir=/usr npm_config_build_from_source=true QEMU_CPU=arm1176 \
|
||||||
|
node -e 'require("node:child_process").execFileSync(process.execPath,[require.resolve("@mapbox/node-pre-gyp/bin/node-pre-gyp"),"rebuild","--napi_build_version=6"],{stdio:"inherit"})' && \
|
||||||
|
readelf -A lib/binding/napi-v6-linux-arm/node_sqlite3.node | grep -E 'Tag_CPU_arch: v6($|K)' && \
|
||||||
|
rm -rf build && \
|
||||||
|
apk del .ql-bundle-build; \
|
||||||
|
fi
|
||||||
|
RUN ulimit -c 0; if [ "$TARGETARCH/$TARGETVARIANT" = arm/v6 ]; then \
|
||||||
export QEMU_CPU=arm1176; \
|
export QEMU_CPU=arm1176; \
|
||||||
set -- --jitless; \
|
node --jitless /ql/scripts/build-upgrade-bundle.cjs /ql /out && \
|
||||||
fi; \
|
for archive in /out/*.tar.gz; do \
|
||||||
node "$@" /ql/scripts/build-upgrade-bundle.cjs /ql /out
|
node /ql/shell/install-bundle.cjs --root /tmp/ql-bundle-offline-smoke --file "$archive" --install && \
|
||||||
|
rm -rf /tmp/ql-bundle-offline-smoke || exit 1; \
|
||||||
|
done; \
|
||||||
|
else node /ql/scripts/build-upgrade-bundle.cjs /ql /out; fi
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=bundle /out/ /
|
COPY --from=bundle /out/ /
|
||||||
|
|||||||
+2
-2
@@ -111,7 +111,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sqlite3": "npm:@whyour/sqlite3@1.1.4",
|
"sqlite3": "npm:@whyour/sqlite3@1.1.2",
|
||||||
"@codemirror/state": "6.5.4",
|
"@codemirror/state": "6.5.4",
|
||||||
"@codemirror/view": "6.39.16",
|
"@codemirror/view": "6.39.16",
|
||||||
"proxy-addr": "2.0.8",
|
"proxy-addr": "2.0.8",
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
"request-ip": "3.3.0",
|
"request-ip": "3.3.0",
|
||||||
"sequelize": "6.37.8",
|
"sequelize": "6.37.8",
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "npm:@whyour/sqlite3@1.1.4",
|
"sqlite3": "npm:@whyour/sqlite3@1.1.2",
|
||||||
"toad-scheduler": "^3.0.1",
|
"toad-scheduler": "^3.0.1",
|
||||||
"typedi": "^0.10.0",
|
"typedi": "^0.10.0",
|
||||||
"undici": "^7.9.0",
|
"undici": "^7.9.0",
|
||||||
|
|||||||
Generated
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
sqlite3: npm:@whyour/sqlite3@1.1.4
|
sqlite3: npm:@whyour/sqlite3@1.1.2
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.39.16
|
'@codemirror/view': 6.39.16
|
||||||
proxy-addr: 2.0.8
|
proxy-addr: 2.0.8
|
||||||
@@ -120,13 +120,13 @@ dependencies:
|
|||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
sequelize:
|
sequelize:
|
||||||
specifier: 6.37.8
|
specifier: 6.37.8
|
||||||
version: 6.37.8(@whyour/sqlite3@1.1.4)
|
version: 6.37.8(@whyour/sqlite3@1.1.2)
|
||||||
sockjs:
|
sockjs:
|
||||||
specifier: ^0.3.24
|
specifier: ^0.3.24
|
||||||
version: 0.3.24
|
version: 0.3.24
|
||||||
sqlite3:
|
sqlite3:
|
||||||
specifier: npm:@whyour/sqlite3@1.1.4
|
specifier: npm:@whyour/sqlite3@1.1.2
|
||||||
version: /@whyour/sqlite3@1.1.4
|
version: /@whyour/sqlite3@1.1.2
|
||||||
tar:
|
tar:
|
||||||
specifier: 7.5.20
|
specifier: 7.5.20
|
||||||
version: 7.5.20
|
version: 7.5.20
|
||||||
@@ -3193,7 +3193,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ngs9jhElXN7efS9WvvCC/p6rXMbihna8eNLVBc421Zf+VcFd+pR4DOcS6yA9V22EKtAy4DEj7LtvEEIm0bb80A==}
|
resolution: {integrity: sha512-Ngs9jhElXN7efS9WvvCC/p6rXMbihna8eNLVBc421Zf+VcFd+pR4DOcS6yA9V22EKtAy4DEj7LtvEEIm0bb80A==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
dependencies:
|
dependencies:
|
||||||
sqlite3: /@whyour/sqlite3@1.1.4
|
sqlite3: /@whyour/sqlite3@1.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5611,15 +5611,14 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@whyour/sqlite3@1.1.4:
|
/@whyour/sqlite3@1.1.2:
|
||||||
resolution: {integrity: sha512-MlWzqOdpZ0KPuBx/FYRrHZ8p3S0eCbaf2d8duDzWmfko/aky25Xoss67qDjFgbRjnwj9z0i0KNdNtQI02dnWKA==}
|
resolution: {integrity: sha512-OY24/OJMqLHIGolo4GCFW4lrJKAoOB+ZqERXb7u7DKZQz7QR3f0GALE99BZWQBSZEVVZ/StVeAb2fCkMfTtuDQ==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
node-gyp:
|
node-gyp:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mapbox/node-pre-gyp': 1.0.11
|
'@mapbox/node-pre-gyp': 1.0.11
|
||||||
detect-libc: 1.0.3
|
|
||||||
node-addon-api: 4.3.0
|
node-addon-api: 4.3.0
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -7602,6 +7601,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/detect-libc@2.1.2:
|
/detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
@@ -13903,7 +13903,7 @@ packages:
|
|||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/sequelize@6.37.8(@whyour/sqlite3@1.1.4):
|
/sequelize@6.37.8(@whyour/sqlite3@1.1.2):
|
||||||
resolution: {integrity: sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==}
|
resolution: {integrity: sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -13948,7 +13948,7 @@ packages:
|
|||||||
retry-as-promised: 7.0.4
|
retry-as-promised: 7.0.4
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
sequelize-pool: 7.1.0
|
sequelize-pool: 7.1.0
|
||||||
sqlite3: /@whyour/sqlite3@1.1.4
|
sqlite3: /@whyour/sqlite3@1.1.2
|
||||||
toposort-class: 1.0.1
|
toposort-class: 1.0.1
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
validator: 13.12.0
|
validator: 13.12.0
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
const { spawnSync } = require('node:child_process');
|
|
||||||
const load = require('../helpers/load-security-module.cjs');
|
|
||||||
|
|
||||||
function fixture(row, { claim = true, onSpawn = () => {} } = {}) {
|
|
||||||
const spawned = [],
|
|
||||||
cronWrites = [],
|
|
||||||
instanceWrites = [],
|
|
||||||
instanceReads = [];
|
|
||||||
const { runCron } = load('back/shared/runCron.ts', {
|
|
||||||
'./i18n': { t: (s) => s },
|
|
||||||
'cross-spawn': {
|
|
||||||
spawn: (cmd) => {
|
|
||||||
spawned.push(cmd);
|
|
||||||
onSpawn(row);
|
|
||||||
return { pid: process.pid };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'./pLimit': {
|
|
||||||
runWithCronLimit: (_cron, execute) => execute(),
|
|
||||||
removeQueuedCron() {},
|
|
||||||
},
|
|
||||||
'../loaders/logger': { info() {}, error() {} },
|
|
||||||
'../data/cron': {
|
|
||||||
CrontabModel: {
|
|
||||||
findOne: async () => row,
|
|
||||||
update: async (...args) => {
|
|
||||||
cronWrites.push(args);
|
|
||||||
if (!claim) return [0];
|
|
||||||
Object.assign(row, args[0]);
|
|
||||||
return [1];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CrontabStatus: { running: 0, idle: 1, queued: 3 },
|
|
||||||
},
|
|
||||||
'../data/runningInstance': {
|
|
||||||
RunningInstanceModel: {
|
|
||||||
findAll: async (options) => {
|
|
||||||
instanceReads.push(options);
|
|
||||||
return [{ id: 7 }];
|
|
||||||
},
|
|
||||||
update: async (...args) => instanceWrites.push(args),
|
|
||||||
},
|
|
||||||
InstanceStatus: { running: 0, stopped: 2 },
|
|
||||||
},
|
|
||||||
dayjs: () => ({ unix: () => 123 }),
|
|
||||||
'./childProcess': {
|
|
||||||
observeChildProcess: () => ({ completed: Promise.resolve({ code: 0 }) }),
|
|
||||||
asError: (e) => e,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
run: () => runCron('task example.py', { id: '1' }),
|
|
||||||
spawned,
|
|
||||||
cronWrites,
|
|
||||||
instanceWrites,
|
|
||||||
instanceReads,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitedPid() {
|
|
||||||
const child = spawnSync(process.execPath, ['-e', '']);
|
|
||||||
assert.equal(child.status, 0);
|
|
||||||
assert.throws(() => process.kill(child.pid, 0), { code: 'ESRCH' });
|
|
||||||
return child.pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
test('running single-instance task stays untouched while its process is alive', async () => {
|
|
||||||
const f = fixture({
|
|
||||||
allow_multiple_instances: 0,
|
|
||||||
pid: process.pid,
|
|
||||||
status: 0,
|
|
||||||
});
|
|
||||||
await f.run();
|
|
||||||
assert.deepEqual(f.spawned, []);
|
|
||||||
assert.deepEqual(f.cronWrites, []);
|
|
||||||
assert.deepEqual(f.instanceWrites, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('queued single-instance task keeps its reservation even with an exited PID', async () => {
|
|
||||||
const f = fixture({
|
|
||||||
allow_multiple_instances: 0,
|
|
||||||
pid: exitedPid(),
|
|
||||||
status: 3,
|
|
||||||
});
|
|
||||||
await f.run();
|
|
||||||
assert.deepEqual(f.spawned, []);
|
|
||||||
assert.deepEqual(f.cronWrites, []);
|
|
||||||
assert.deepEqual(f.instanceWrites, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exited running task is recovered and the next trigger sees the new active run', async () => {
|
|
||||||
const oldPid = exitedPid();
|
|
||||||
const f = fixture(
|
|
||||||
{
|
|
||||||
allow_multiple_instances: 0,
|
|
||||||
pid: oldPid,
|
|
||||||
status: 0,
|
|
||||||
log_path: 'old.log',
|
|
||||||
queued_token: 'old-token',
|
|
||||||
last_execution_time: 10,
|
|
||||||
isDisabled: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSpawn: (row) => {
|
|
||||||
row.status = 0;
|
|
||||||
row.pid = process.pid;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await f.run();
|
|
||||||
await f.run();
|
|
||||||
assert.equal(f.spawned.length, 1);
|
|
||||||
assert.deepEqual(f.instanceReads, [
|
|
||||||
{
|
|
||||||
attributes: ['id'],
|
|
||||||
where: { cron_id: 1, pid: oldPid, status: 0 },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
assert.deepEqual(f.cronWrites, [
|
|
||||||
[
|
|
||||||
{ status: 1, pid: null, queued_token: null },
|
|
||||||
{
|
|
||||||
where: {
|
|
||||||
id: 1,
|
|
||||||
pid: oldPid,
|
|
||||||
status: 0,
|
|
||||||
log_path: 'old.log',
|
|
||||||
queued_token: 'old-token',
|
|
||||||
last_execution_time: 10,
|
|
||||||
isDisabled: 0,
|
|
||||||
allow_multiple_instances: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
assert.deepEqual(f.instanceWrites, [
|
|
||||||
[{ status: 2, finished_at: 123 }, { where: { id: [7], status: 0 } }],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a lost stale-state claim does not spawn or change running instances', async () => {
|
|
||||||
const f = fixture(
|
|
||||||
{ allow_multiple_instances: 0, pid: exitedPid(), status: 0 },
|
|
||||||
{ claim: false },
|
|
||||||
);
|
|
||||||
await f.run();
|
|
||||||
assert.equal(f.cronWrites.length, 1);
|
|
||||||
assert.deepEqual(f.spawned, []);
|
|
||||||
assert.deepEqual(f.instanceWrites, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('EPERM means an existing process must still block a new run', async () => {
|
|
||||||
const pid = exitedPid();
|
|
||||||
const originalKill = process.kill;
|
|
||||||
process.kill = (target, signal) => {
|
|
||||||
if (target === pid && signal === 0)
|
|
||||||
throw Object.assign(new Error('denied'), { code: 'EPERM' });
|
|
||||||
return originalKill(target, signal);
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const f = fixture({ allow_multiple_instances: 0, pid, status: 0 });
|
|
||||||
await f.run();
|
|
||||||
assert.deepEqual(f.spawned, []);
|
|
||||||
assert.deepEqual(f.cronWrites, []);
|
|
||||||
} finally {
|
|
||||||
process.kill = originalKill;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('single-instance still starts idle tasks with a stale stored PID', async () => {
|
|
||||||
const f = fixture({
|
|
||||||
allow_multiple_instances: 0,
|
|
||||||
pid: exitedPid(),
|
|
||||||
status: 1,
|
|
||||||
});
|
|
||||||
await f.run();
|
|
||||||
assert.equal(f.spawned.length, 1);
|
|
||||||
assert.deepEqual(f.cronWrites, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('multi-instance keeps its existing concurrent execution behavior', async () => {
|
|
||||||
const f = fixture({
|
|
||||||
allow_multiple_instances: 1,
|
|
||||||
pid: process.pid,
|
|
||||||
status: 0,
|
|
||||||
});
|
|
||||||
await f.run();
|
|
||||||
assert.equal(f.spawned.length, 1);
|
|
||||||
assert.deepEqual(f.cronWrites, []);
|
|
||||||
});
|
|
||||||
@@ -4,6 +4,75 @@ const load = require('../helpers/load-security-module.cjs');
|
|||||||
|
|
||||||
const logger = { info() {}, error() {} };
|
const logger = { info() {}, error() {} };
|
||||||
|
|
||||||
|
function cronFixture(killTask) {
|
||||||
|
const events = [];
|
||||||
|
const { runCron } = load('back/shared/runCron.ts', {
|
||||||
|
'cross-spawn': {
|
||||||
|
spawn: () => {
|
||||||
|
events.push('spawn');
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'./childProcess': {
|
||||||
|
observeChildProcess: () => ({ completed: Promise.resolve({ code: 0 }) }),
|
||||||
|
asError: (e) => e,
|
||||||
|
},
|
||||||
|
'./pLimit': {
|
||||||
|
runWithCronLimit: async (_, fn) => fn(),
|
||||||
|
removeQueuedCron: () => events.push('release'),
|
||||||
|
},
|
||||||
|
'../loaders/logger': logger,
|
||||||
|
'../config/util': { killTask },
|
||||||
|
'../data/cron': {
|
||||||
|
CrontabModel: {
|
||||||
|
findOne: async () => ({
|
||||||
|
pid: 100,
|
||||||
|
status: 0,
|
||||||
|
allow_multiple_instances: 0,
|
||||||
|
}),
|
||||||
|
update: async () => events.push('idle'),
|
||||||
|
},
|
||||||
|
CrontabStatus: { running: 0, idle: 1, queued: 3 },
|
||||||
|
},
|
||||||
|
'../data/runningInstance': {
|
||||||
|
RunningInstanceModel: { update: async () => events.push('stopped') },
|
||||||
|
InstanceStatus: { running: 0, stopped: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { run: () => runCron('ignored', { id: '1' }), events };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('single-instance replacement does not spawn or finalize when termination fails', async () => {
|
||||||
|
const f = cronFixture(async () => {
|
||||||
|
throw Error('EPERM');
|
||||||
|
});
|
||||||
|
await f.run();
|
||||||
|
assert.deepEqual(f.events, ['release']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('single-instance replacement waits for verified exit before spawning', async () => {
|
||||||
|
let finish, entered, verified;
|
||||||
|
const pending = new Promise((resolve) => {
|
||||||
|
entered = resolve;
|
||||||
|
});
|
||||||
|
const gate = new Promise((resolve) => {
|
||||||
|
finish = resolve;
|
||||||
|
});
|
||||||
|
const f = cronFixture(async (pid, wait) => {
|
||||||
|
assert.equal(pid, 100);
|
||||||
|
verified = wait;
|
||||||
|
entered();
|
||||||
|
await gate;
|
||||||
|
});
|
||||||
|
const running = f.run();
|
||||||
|
await pending;
|
||||||
|
assert.deepEqual(f.events, []);
|
||||||
|
finish();
|
||||||
|
await running;
|
||||||
|
assert.equal(verified, true);
|
||||||
|
assert.deepEqual(f.events, ['stopped', 'idle', 'spawn', 'release']);
|
||||||
|
});
|
||||||
|
|
||||||
function subscriptionFixture(killTask) {
|
function subscriptionFixture(killTask) {
|
||||||
const updates = [];
|
const updates = [];
|
||||||
const Service = load('back/services/subscription.ts', {
|
const Service = load('back/services/subscription.ts', {
|
||||||
|
|||||||
Reference in New Issue
Block a user