chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
+374
View File
@@ -0,0 +1,374 @@
#!/usr/bin/env tsx
/**
* Recovery script for runs stuck in currentConcurrency with QUEUED execution status
*
* PROBLEM:
* During high database load, runs can get dequeued from Redis (added to currentConcurrency)
* but fail to update their execution status in the database. This leaves them stuck in an
* inconsistent state where they won't be re-dequeued because they're marked as "in progress"
* in Redis, but their database state still shows QUEUED.
*
* SOLUTION:
* This script identifies and recovers these stuck runs by:
* 1. Reading from the environment currentConcurrency Redis set
* 2. Checking which runs have QUEUED execution status (inconsistent state)
* 3. Re-adding them to their specific queue sorted sets
* 4. Removing them from the queue-specific currentConcurrency sets
* 5. Removing them from the environment-level currentConcurrency set
*
* SAFETY:
* - Dry-run mode when no write Redis URL is provided (read-only, no writes)
* - Uses separate Redis connections for reads and writes
* - Write connection only created when redisWriteUrl is provided
*
* ARGUMENTS:
* <environmentId> The Trigger.dev environment ID (e.g., env_abc123)
* <postgresUrl> PostgreSQL connection string
* <redisReadUrl> Redis connection string for reads (redis:// or rediss://)
* [redisWriteUrl] Optional Redis connection string for writes (omit for dry-run)
*
* USAGE:
* tsx scripts/recover-stuck-runs.ts <environmentId> <postgresUrl> <redisReadUrl> [redisWriteUrl]
*
* EXAMPLES:
*
* Dry-run mode (safe, no writes):
* tsx scripts/recover-stuck-runs.ts env_1234567890 \
* "postgresql://user:pass@localhost:5432/triggerdev" \
* "redis://readonly.example.com:6379"
*
* Execute mode (makes actual changes):
* tsx scripts/recover-stuck-runs.ts env_1234567890 \
* "postgresql://user:pass@localhost:5432/triggerdev" \
* "redis://readonly.example.com:6379" \
* "redis://writeonly.example.com:6379"
*/
import type { TaskRunExecutionStatus } from "@trigger.dev/database";
import { PrismaClient } from "@trigger.dev/database";
import { createRedisClient } from "@internal/redis";
interface StuckRun {
runId: string;
orgId: string;
projectId: string;
environmentId: string;
queue: string;
concurrencyKey: string | null;
executionStatus: TaskRunExecutionStatus;
snapshotCreatedAt: Date;
taskIdentifier: string;
}
interface RedisOperation {
type: "ZADD" | "SREM";
key: string;
args: (string | number)[];
description: string;
}
async function main() {
const [environmentId, postgresUrl, redisReadUrl, redisWriteUrl] = process.argv.slice(2);
if (!environmentId || !postgresUrl || !redisReadUrl) {
console.error(
"Usage: tsx scripts/recover-stuck-runs.ts <environmentId> <postgresUrl> <redisReadUrl> [redisWriteUrl]"
);
console.error("");
console.error("Dry-run mode when no redisWriteUrl is provided (read-only).");
console.error("Execute mode when redisWriteUrl is provided (makes actual changes).");
console.error("");
console.error("Example (dry-run):");
console.error(" tsx scripts/recover-stuck-runs.ts env_1234567890 \\");
console.error(' "postgresql://user:pass@localhost:5432/triggerdev" \\');
console.error(' "redis://readonly.example.com:6379"');
console.error("");
console.error("Example (execute):");
console.error(" tsx scripts/recover-stuck-runs.ts env_1234567890 \\");
console.error(' "postgresql://user:pass@localhost:5432/triggerdev" \\');
console.error(' "redis://readonly.example.com:6379" \\');
console.error(' "redis://writeonly.example.com:6379"');
process.exit(1);
}
const executeMode = !!redisWriteUrl;
if (executeMode) {
console.log("⚠️ EXECUTE MODE - Changes will be made to Redis\n");
} else {
console.log("🔍 DRY RUN MODE - No changes will be made to Redis\n");
}
console.log(`🔍 Scanning for stuck runs in environment: ${environmentId}`);
// Create Prisma client with the provided connection URL
const prisma = new PrismaClient({
datasources: {
db: {
url: postgresUrl,
},
},
});
try {
// Get environment details
const environment = await prisma.runtimeEnvironment.findUnique({
where: { id: environmentId },
include: {
organization: true,
project: true,
},
});
if (!environment) {
console.error(`❌ Environment not found: ${environmentId}`);
process.exit(1);
}
console.log(`📍 Environment: ${environment.slug} (${environment.type})`);
console.log(`📍 Organization: ${environment.organization.slug}`);
console.log(`📍 Project: ${environment.project.slug}`);
// Parse Redis read URL
const redisReadUrlObj = new URL(redisReadUrl);
const redisReadOptions = {
host: redisReadUrlObj.hostname,
port: parseInt(redisReadUrlObj.port || "6379"),
username: redisReadUrlObj.username || undefined,
password: redisReadUrlObj.password || undefined,
enableAutoPipelining: false,
...(redisReadUrlObj.protocol === "rediss:"
? {
tls: {
// If connecting via localhost tunnel to a remote Redis, disable cert verification
rejectUnauthorized: redisReadUrlObj.hostname === "localhost" ? false : true,
},
}
: {}),
};
// Create Redis read client
const redisRead = createRedisClient(redisReadOptions);
// Create Redis write client if redisWriteUrl is provided
let redisWrite = null;
if (redisWriteUrl) {
const redisWriteUrlObj = new URL(redisWriteUrl);
const redisWriteOptions = {
host: redisWriteUrlObj.hostname,
port: parseInt(redisWriteUrlObj.port || "6379"),
username: redisWriteUrlObj.username || undefined,
password: redisWriteUrlObj.password || undefined,
enableAutoPipelining: false,
...(redisWriteUrlObj.protocol === "rediss:"
? {
tls: {
// If connecting via localhost tunnel to a remote Redis, disable cert verification
rejectUnauthorized: redisWriteUrlObj.hostname === "localhost" ? false : true,
},
}
: {}),
};
redisWrite = createRedisClient(redisWriteOptions);
}
try {
// Build the Redis key for environment-level currentConcurrency set
// Format: engine:runqueue:{org:X}:proj:Y:env:Z:currentConcurrency
const envConcurrencyKey = `engine:runqueue:{org:${environment.organizationId}}:proj:${environment.projectId}:env:${environmentId}:currentConcurrency`;
console.log(`\n🔑 Checking Redis key: ${envConcurrencyKey}`);
// Get all run IDs in the environment's currentConcurrency set
const runIds = await redisRead.smembers(envConcurrencyKey);
if (runIds.length === 0) {
console.log(`✅ No runs in currentConcurrency set`);
return;
}
console.log(`📊 Found ${runIds.length} runs in currentConcurrency set`);
// Query database for latest snapshots and queue info of these runs.
// NOTE: raw join of TaskRunExecutionSnapshot to TaskRun, the one TaskRun read not behind
// RunStore (a join, not a by-id read, in an ops script). Revisit at table cutover.
const runInfo = await prisma.$queryRaw<
Array<{
runId: string;
executionStatus: TaskRunExecutionStatus;
snapshotCreatedAt: Date;
organizationId: string;
projectId: string;
environmentId: string;
taskIdentifier: string;
queue: string;
concurrencyKey: string | null;
}>
>`
SELECT DISTINCT ON (s."runId")
s."runId",
s."executionStatus",
s."createdAt" as "snapshotCreatedAt",
r."organizationId",
r."projectId",
r."runtimeEnvironmentId" as "environmentId",
r."taskIdentifier",
r."queue",
r."concurrencyKey"
FROM "TaskRunExecutionSnapshot" s
INNER JOIN "TaskRun" r ON r.id = s."runId"
WHERE s."runId" = ANY(${runIds})
AND s."isValid" = true
ORDER BY s."runId", s."createdAt" DESC
`;
const stuckRuns: StuckRun[] = [];
// Find runs with QUEUED execution status (inconsistent state)
for (const info of runInfo) {
if (info.executionStatus === "QUEUED") {
stuckRuns.push({
runId: info.runId,
orgId: info.organizationId,
projectId: info.projectId,
environmentId: info.environmentId,
queue: info.queue,
concurrencyKey: info.concurrencyKey,
executionStatus: info.executionStatus,
snapshotCreatedAt: info.snapshotCreatedAt,
taskIdentifier: info.taskIdentifier,
});
}
}
if (stuckRuns.length === 0) {
console.log(`✅ No stuck runs found (all runs have progressed beyond QUEUED state)`);
return;
}
console.log(`\n⚠️ Found ${stuckRuns.length} stuck runs in QUEUED state:`);
console.log(`════════════════════════════════════════════════════════════════`);
for (const run of stuckRuns) {
const age = Date.now() - run.snapshotCreatedAt.getTime();
const ageMinutes = Math.floor(age / 1000 / 60);
console.log(` • Run: ${run.runId}`);
console.log(` Task: ${run.taskIdentifier}`);
console.log(` Queue: ${run.queue}`);
console.log(` Concurrency Key: ${run.concurrencyKey || "(none)"}`);
console.log(` Status: ${run.executionStatus}`);
console.log(` Stuck for: ${ageMinutes} minutes`);
console.log(` Snapshot created: ${run.snapshotCreatedAt.toISOString()}`);
console.log();
}
// Prepare recovery operations
console.log(
`\n⚡ ${executeMode ? "Executing" : "Planning"} recovery for ${stuckRuns.length} stuck runs`
);
console.log(`This will:`);
console.log(` 1. Add each run back to its specific queue sorted set`);
console.log(` 2. Remove each run from the queue-specific currentConcurrency set`);
console.log(` 3. Remove each run from the env-level currentConcurrency set`);
console.log();
let successCount = 0;
let failureCount = 0;
const currentTimestamp = Date.now();
for (const run of stuckRuns) {
try {
// Build queue key: engine:runqueue:{org:X}:proj:Y:env:Z:queue:QUEUENAME
// Build queue currentConcurrency key: engine:runqueue:{org:X}:proj:Y:env:Z:queue:QUEUENAME:currentConcurrency
const queueKey = run.concurrencyKey
? `engine:runqueue:{org:${run.orgId}}:proj:${run.projectId}:env:${run.environmentId}:queue:${run.queue}:ck:${run.concurrencyKey}`
: `engine:runqueue:{org:${run.orgId}}:proj:${run.projectId}:env:${run.environmentId}:queue:${run.queue}`;
const queueConcurrencyKey = `${queueKey}:currentConcurrency`;
const operations: RedisOperation[] = [
{
type: "ZADD",
key: queueKey,
args: [currentTimestamp, run.runId],
description: `Add run to queue sorted set with score ${currentTimestamp}`,
},
{
type: "SREM",
key: queueConcurrencyKey,
args: [run.runId],
description: `Remove run from queue currentConcurrency set`,
},
{
type: "SREM",
key: envConcurrencyKey,
args: [run.runId],
description: `Remove run from env currentConcurrency set`,
},
];
if (executeMode && redisWrite) {
// Execute operations using the write client
await redisWrite.zadd(queueKey, currentTimestamp, run.runId);
const removedFromQueue = await redisWrite.srem(queueConcurrencyKey, run.runId);
const removedFromEnv = await redisWrite.srem(envConcurrencyKey, run.runId);
console.log(` ✓ Recovered run ${run.runId} (${run.taskIdentifier})`);
if (removedFromQueue === 0) {
console.log(` ⚠ Run was not in queue currentConcurrency set`);
}
if (removedFromEnv === 0) {
console.log(` ⚠ Run was not in env currentConcurrency set`);
}
successCount++;
} else {
// Dry run - just show what would be done
console.log(` 📝 Would recover run ${run.runId} (${run.taskIdentifier}):`);
for (const op of operations) {
console.log(` ${op.type} ${op.key}`);
console.log(` Args: ${JSON.stringify(op.args)}`);
console.log(` (${op.description})`);
}
successCount++;
}
} catch (error) {
console.error(` ✗ Failed to recover run ${run.runId}:`, error);
failureCount++;
}
}
console.log(`\n═══════════════════════════════════════════════════════════════`);
if (executeMode) {
console.log(`✅ Recovery complete!`);
console.log(` Recovered: ${successCount}`);
console.log(` Failed: ${failureCount}`);
console.log();
console.log(`️ Note: The recovered runs should be automatically dequeued`);
console.log(` by the master queue consumers within a few seconds.`);
} else {
console.log(`📋 Dry run complete - no changes were made`);
console.log(` Would recover: ${successCount}`);
console.log(` Would fail: ${failureCount}`);
console.log();
console.log(`💡 To execute these changes, run again with a redisWriteUrl argument`);
}
} finally {
await redisRead.quit();
if (redisWrite) {
await redisWrite.quit();
}
}
} catch (error) {
console.error("❌ Error during recovery:", error);
throw error;
} finally {
await prisma.$disconnect();
}
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});