Files
Matt Aitken 630e9556b0 Bulk actions 2.0 (and switch all run listing to ClickHouse) (#2264)
* useSearchParams has

* useSearchParams has

* useSearchParams has

* Consistent way to get the run filters

* Consistent way to get the run filters

* Consistent way to get the run filters

* Initial work on the new bulk actions

* Initial work on the new bulk actions

* Initial work on the new bulk actions

* WIP actions and filtering

* WIP actions and filtering

* WIP actions and filtering

* Empty filter arrays are set to undefined

* Empty filter arrays are set to undefined

* Empty filter arrays are set to undefined

* WIP prisma schema

Removed extra runtimeEnvironmentId

* WIP prisma schema

Removed extra runtimeEnvironmentId

* WIP prisma schema

Removed extra runtimeEnvironmentId

* Migrations

* Migrations

* Migrations

* BulkActionGroup changed some columns around

* BulkActionGroup changed some columns around

* BulkActionGroup changed some columns around

* New badge variant, removed unused ones

* New badge variant, removed unused ones

* New badge variant, removed unused ones

* Bulk action button

* Bulk action button

* Bulk action button

* Make the next runs page the default now

* Make the next runs page the default now

* Make the next runs page the default now

* Improved the RadioButton style

* Improved the RadioButton style

* Improved the RadioButton style

* Remove the old bulk action bar

* Remove the old bulk action bar

* Remove the old bulk action bar

* More UI progress

* More UI progress

* More UI progress

* Lots of UI changes to the Runs page

* Lots of UI changes to the Runs page

* Lots of UI changes to the Runs page

* Fixed period filter resetting everything

* Fixed period filter resetting everything

* Fixed period filter resetting everything

* Improved the Switch secondary style

* Improved the Switch secondary style

* Improved the Switch secondary style

* Buggy filter fixes

* Buggy filter fixes

* Buggy filter fixes

* Improved the filter display and fixed a bug with search param from object

* Improved the filter display and fixed a bug with search param from object

* Improved the filter display and fixed a bug with search param from object

* Clear button is minimal

* Clear button is minimal

* Clear button is minimal

* Using a presenter now

* Using a presenter now

* Using a presenter now

* Bulk actions are created, but not actually processed (yet)

* Bulk actions are created, but not actually processed (yet)

* Bulk actions are created, but not actually processed (yet)

* Bulk replay/cancel is working

* Bulk replay/cancel is working

* Bulk replay/cancel is working

* Multiple fixes, added bulk column to PG

* Multiple fixes, added bulk column to PG

* Multiple fixes, added bulk column to PG

* Bulk action run filtering working using CH

* Bulk action run filtering working using CH

* Bulk action run filtering working using CH

* Replay setting the bulk id on the runs

* Replay setting the bulk id on the runs

* Replay setting the bulk id on the runs

* Properly cap the time when doing a bulk action

* Properly cap the time when doing a bulk action

* Properly cap the time when doing a bulk action

* If the bulk action isn't recent, add it to the dropdown anyway

* If the bulk action isn't recent, add it to the dropdown anyway

* If the bulk action isn't recent, add it to the dropdown anyway

* Blank version of the bulk actions page

* Blank version of the bulk actions page

* Blank version of the bulk actions page

* Individually selected runs working

* Individually selected runs working

* Individually selected runs working

* Use selected mode if runs are checked

* Use selected mode if runs are checked

* Use selected mode if runs are checked

* Added the modal

* Added the modal

* Added the modal

* Marked the old bulk actions stuff as deprecated

* Marked the old bulk actions stuff as deprecated

* Marked the old bulk actions stuff as deprecated

* Renamed bulk action file

* Renamed bulk action file

* Renamed bulk action file

* Bulk run filter with the name and a default

* Bulk run filter with the name and a default

* Bulk run filter with the name and a default

* WIP on bulk actions page

* WIP on bulk actions page

* WIP on bulk actions page

* Updated panel, added new truncated id component

* Updated panel, added new truncated id component

* Updated panel, added new truncated id component

* Style improvements to the radio buttons

* Style improvements to the radio buttons

* Style improvements to the radio buttons

* Added an option action completion email

* Added an option action completion email

* Added an option action completion email

* Adds a blank state for the bulk actions page

* Adds a blank state for the bulk actions page

* Adds a blank state for the bulk actions page

* Nicer completed email

* Nicer completed email

* Nicer completed email

* Don't open the bulk action panel if there are no runs

* Don't open the bulk action panel if there are no runs

* Don't open the bulk action panel if there are no runs

* Runs blank state and bulk action accordion

* Runs blank state and bulk action accordion

* Runs blank state and bulk action accordion

* Updates secondary/small switch style

* Updates secondary/small switch style

* Updates secondary/small switch style

* Pagination buttons no longer split in twain (WIP)

* Pagination buttons no longer split in twain (WIP)

* Pagination buttons no longer split in twain (WIP)

* Aborting working

* Aborting working

* Aborting working

* Bulk action live reloading

* Bulk action live reloading

* Bulk action live reloading

* ListPagination works correctly in all states

* ListPagination works correctly in all states

* ListPagination works correctly in all states

* Run page, show friendlyId instead of number

* Run page, show friendlyId instead of number

* Run page, show friendlyId instead of number

* Bulk action help open by default if you have none

* Bulk action help open by default if you have none

* Bulk action help open by default if you have none

* Extra status filtering step because of replication delay

* Extra status filtering step because of replication delay

* Extra status filtering step because of replication delay

* Wider bulk action onboarding

* Wider bulk action onboarding

* Wider bulk action onboarding

* More sensible widths on the bulk action side panel

* More sensible widths on the bulk action side panel

* More sensible widths on the bulk action side panel

* Border color tweak to the RadioButton

* Border color tweak to the RadioButton

* Border color tweak to the RadioButton

* Improved the accordion component hover states

* Improved the accordion component hover states

* Improved the accordion component hover states

* Updates the bulk action blank state images to the latest UI

* Updates the bulk action blank state images to the latest UI

* Updates the bulk action blank state images to the latest UI

* Added R and C shortcuts back in

* Added R and C shortcuts back in

* Added R and C shortcuts back in

* Fix for selecting a single run

* Fix for selecting a single run

* Fix for selecting a single run

* Improved exit icon, added shortcut to modal

* Improved exit icon, added shortcut to modal

* Improved exit icon, added shortcut to modal

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Tidy imports

* Fix for grid layout when 1 page of bulk actions visible

* Fix for grid layout when 1 page of bulk actions visible

* Fix for grid layout when 1 page of bulk actions visible

* Removed the ... on the abort button

* Removed the ... on the abort button

* Removed the ... on the abort button

* Removed the ... on the abort button

* Animate the progress bar

* Set TZ="UTC" in the env example

* Filter summary in the bulk inspector

* Improves the pagination styling

* Improves the pagination styling

* Delete old bulk action routes

* Removed old Postgres RunListPresenter

* Retry any replication error where the message contains "timeout"

* Increase wait to make test less flaky

* The test was using run id instead of friendly id

* Safer array access

* Remove error log if there's a bad status

* Nicer frontend type safety with the bulk action and mode

* Switched a log to a debug log

* Retry replication unless the error is a known non-retry error

Flip the strategy to retry by default

* Make ClickHouse required

* Backfill run replication admin API endpoint

* Set a CLICKHOUSE_URL for unit tests

---------

Co-authored-by: James Ritchie <james@trigger.dev>
2025-07-14 15:50:01 +01:00

346 lines
12 KiB
TypeScript

import { ZodWorker } from "@internal/zod-worker";
import { DeliverEmailSchema } from "emails";
import { z } from "zod";
import { $replica, prisma } from "~/db.server";
import { env } from "~/env.server";
import {
BatchProcessingOptions as RunEngineBatchProcessingOptions,
RunEngineBatchTriggerService,
} from "~/runEngine/services/batchTrigger.server";
import { MarqsConcurrencyMonitor } from "~/v3/marqs/concurrencyMonitor.server";
import { scheduleEngine } from "~/v3/scheduleEngine.server";
import { DeliverAlertService } from "~/v3/services/alerts/deliverAlert.server";
import { PerformDeploymentAlertsService } from "~/v3/services/alerts/performDeploymentAlerts.server";
import { PerformTaskRunAlertsService } from "~/v3/services/alerts/performTaskRunAlerts.server";
import { BatchProcessingOptions, BatchTriggerV3Service } from "~/v3/services/batchTriggerV3.server";
import { PerformBulkActionService } from "~/v3/services/bulk/performBulkAction.server";
import {
CancelDevSessionRunsService,
CancelDevSessionRunsServiceOptions,
} from "~/v3/services/cancelDevSessionRuns.server";
import { CancelTaskAttemptDependenciesService } from "~/v3/services/cancelTaskAttemptDependencies.server";
import { EnqueueDelayedRunService } from "~/v3/services/enqueueDelayedRun.server";
import { ExecuteTasksWaitingForDeployService } from "~/v3/services/executeTasksWaitingForDeploy";
import { ExpireEnqueuedRunService } from "~/v3/services/expireEnqueuedRun.server";
import { ResumeBatchRunService } from "~/v3/services/resumeBatchRun.server";
import { ResumeTaskDependencyService } from "~/v3/services/resumeTaskDependency.server";
import { RetryAttemptService } from "~/v3/services/retryAttempt.server";
import { TimeoutDeploymentService } from "~/v3/services/timeoutDeployment.server";
import { GraphileMigrationHelperService } from "./db/graphileMigrationHelper.server";
import { sendEmail } from "./email.server";
import { logger } from "./logger.server";
const workerCatalog = {
// @deprecated, moved to commonWorker.server.ts
scheduleEmail: DeliverEmailSchema,
// @deprecated, but still used when resuming batch runs in a transaction
"v3.resumeBatchRun": z.object({
batchRunId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.resumeTaskDependency": z.object({
dependencyId: z.string(),
sourceTaskAttemptId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.timeoutDeployment": z.object({
deploymentId: z.string(),
fromStatus: z.string(),
errorMessage: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.executeTasksWaitingForDeploy": z.object({
backgroundWorkerId: z.string(),
}),
// @deprecated, moved to ScheduleEngine
"v3.triggerScheduledTask": z.object({
instanceId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.performTaskRunAlerts": z.object({
runId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.deliverAlert": z.object({
alertId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.performDeploymentAlerts": z.object({
deploymentId: z.string(),
}),
"v3.performBulkAction": z.object({
bulkActionGroupId: z.string(),
}),
"v3.performBulkActionItem": z.object({
bulkActionItemId: z.string(),
}),
// @deprecated, moved to legacyRunEngineWorker.server.ts
"v3.requeueTaskRun": z.object({
runId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.retryAttempt": z.object({
runId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.enqueueDelayedRun": z.object({
runId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.expireRun": z.object({
runId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.cancelTaskAttemptDependencies": z.object({
attemptId: z.string(),
}),
// @deprecated, moved to commonWorker.server.ts
"v3.cancelDevSessionRuns": CancelDevSessionRunsServiceOptions,
// @deprecated, moved to commonWorker.server.ts
"v3.processBatchTaskRun": BatchProcessingOptions,
// @deprecated, moved to commonWorker.server.ts
"runengine.processBatchTaskRun": RunEngineBatchProcessingOptions,
};
let workerQueue: ZodWorker<typeof workerCatalog>;
declare global {
var __worker__: ZodWorker<typeof workerCatalog>;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (env.NODE_ENV === "production") {
workerQueue = getWorkerQueue();
} else {
if (!global.__worker__) {
global.__worker__ = getWorkerQueue();
}
workerQueue = global.__worker__;
}
export async function init() {
const migrationHelper = new GraphileMigrationHelperService();
await migrationHelper.call();
if (env.WORKER_ENABLED === "true") {
await workerQueue.initialize();
}
}
function getWorkerQueue() {
return new ZodWorker({
name: "workerQueue",
prisma,
replica: $replica,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.WORKER_CONCURRENCY,
pollInterval: env.WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.WORKER_CONCURRENCY + 1,
},
logger: logger,
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: workerCatalog,
recurringTasks: {
"marqs.v3.queueConcurrencyMonitor": {
// run every 5 minutes
match: "*/5 * * * *",
handler: async (payload, job, helpers) => {
await MarqsConcurrencyMonitor.initiateV3Monitoring(helpers.abortSignal);
},
},
},
tasks: {
// @deprecated, moved to commonWorker.server.ts
scheduleEmail: {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
await sendEmail(payload);
},
},
// @deprecated, moved to commonWorker.server.ts but still used when resuming batch runs in a transaction
"v3.resumeBatchRun": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ResumeBatchRunService();
await service.call(payload.batchRunId);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.resumeTaskDependency": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ResumeTaskDependencyService();
return await service.call(payload.dependencyId, payload.sourceTaskAttemptId);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.timeoutDeployment": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new TimeoutDeploymentService();
return await service.call(payload.deploymentId, payload.fromStatus, payload.errorMessage);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.executeTasksWaitingForDeploy": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new ExecuteTasksWaitingForDeployService();
return await service.call(payload.backgroundWorkerId);
},
},
// @deprecated, moved to ScheduleEngine
"v3.triggerScheduledTask": {
priority: 0,
maxAttempts: 3, // total delay of 30 seconds
handler: async (payload, job) => {
await scheduleEngine.triggerScheduledTask({
instanceId: payload.instanceId,
finalAttempt: job.attempts === job.max_attempts,
});
},
},
// @deprecated, moved to alertsWorker.server.ts
"v3.performTaskRunAlerts": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskRunAlertsService();
return await service.call(payload.runId);
},
},
// @deprecated, moved to alertsWorker.server.ts
"v3.deliverAlert": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new DeliverAlertService();
return await service.call(payload.alertId);
},
},
// @deprecated, moved to alertsWorker.server.ts
"v3.performDeploymentAlerts": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformDeploymentAlertsService();
return await service.call(payload.deploymentId);
},
},
// @deprecated, new bulk actions use the new bulk actions worker
"v3.performBulkAction": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformBulkActionService();
return await service.call(payload.bulkActionGroupId);
},
},
// @deprecated, new bulk actions use the new bulk actions worker
"v3.performBulkActionItem": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformBulkActionService();
await service.performBulkActionItem(payload.bulkActionItemId);
},
},
"v3.requeueTaskRun": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {}, // This is now handled by redisWorker
},
// @deprecated, moved to commonWorker.server.ts
"v3.retryAttempt": {
priority: 0,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new RetryAttemptService();
return await service.call(payload.runId);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.enqueueDelayedRun": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new EnqueueDelayedRunService();
return await service.call(payload.runId);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.expireRun": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new ExpireEnqueuedRunService();
return await service.call(payload.runId);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.cancelTaskAttemptDependencies": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new CancelTaskAttemptDependenciesService();
return await service.call(payload.attemptId);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.cancelDevSessionRuns": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new CancelDevSessionRunsService();
return await service.call(payload);
},
},
// @deprecated, moved to commonWorker.server.ts
"v3.processBatchTaskRun": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new BatchTriggerV3Service(payload.strategy);
await service.processBatchTaskRun(payload);
},
},
// @deprecated, moved to commonWorker.server.ts
"runengine.processBatchTaskRun": {
priority: 0,
maxAttempts: 5,
handler: async (payload, job) => {
const service = new RunEngineBatchTriggerService(payload.strategy);
await service.processBatchTaskRun(payload);
},
},
},
});
}
export { workerQueue };