Free up concurrency when using triggerAndWait. Improved errors on runs (#1272)

* Add an error to the final attempt if there isn’t one

* Improved the checkpointResumer test task

* Using triggerAndWait or batchTriggerAndWait frees up concurrency

Normally it frees up just env and org concurrency. If it’s a recursive task then it will free up the run concurrency too (e.g. a task calling itself).

* Removed the filepath and export name from attempt spans

* Run inspector: only show an error if the run is in a finished state
This commit is contained in:
Matt Aitken
2024-08-25 20:15:45 +01:00
committed by GitHub
parent 0979a521bd
commit f7d32b83b1
7 changed files with 148 additions and 33 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Removed the folder/filepath from Attempt spans
@@ -135,20 +135,24 @@ export class SpanPresenter extends BasePresenter {
return;
}
const finishedAttempt = await this._replica.taskRunAttempt.findFirst({
select: {
output: true,
outputType: true,
error: true,
},
where: {
status: { in: FINAL_ATTEMPT_STATUSES },
taskRunId: run.id,
},
orderBy: {
createdAt: "desc",
},
});
const isFinished = isFinalRunStatus(run.status);
const finishedAttempt = isFinished
? await this._replica.taskRunAttempt.findFirst({
select: {
output: true,
outputType: true,
error: true,
},
where: {
status: { in: FINAL_ATTEMPT_STATUSES },
taskRunId: run.id,
},
orderBy: {
createdAt: "desc",
},
})
: null;
const output =
finishedAttempt === null
@@ -258,7 +262,7 @@ export class SpanPresenter extends BasePresenter {
costInCents: run.costInCents,
totalCostInCents: run.costInCents + run.baseCostInCents,
usageDurationMs: run.usageDurationMs,
isFinished: isFinalRunStatus(run.status),
isFinished,
isRunning: RUNNING_STATUSES.includes(run.status),
payload,
payloadType: run.payloadType,
+19 -1
View File
@@ -1,4 +1,4 @@
import { TaskRunFailedExecutionResult } from "@trigger.dev/core/v3";
import { sanitizeError, TaskRunFailedExecutionResult } from "@trigger.dev/core/v3";
import { logger } from "~/services/logger.server";
import { createExceptionPropertiesFromError, eventRepository } from "./eventRepository.server";
import { BaseService } from "./services/baseService.server";
@@ -44,6 +44,24 @@ export class FailedTaskRunService extends BaseService {
completedAt: new Date(),
});
// Get the final attempt and add the error to it, if it's not already set
const finalAttempt = await this._prisma.taskRunAttempt.findFirst({
where: {
taskRunId: taskRun.id,
},
orderBy: { id: "desc" },
});
if (finalAttempt && !finalAttempt.error) {
// Haven't set the status because the attempt might still be running
await this._prisma.taskRunAttempt.update({
where: { id: finalAttempt.id },
data: {
error: sanitizeError(completion.error),
},
});
}
// Now we need to "complete" the task run event/span
await eventRepository.completeEvent(taskRun.spanId, {
endTime: new Date(),
+80
View File
@@ -542,6 +542,60 @@ export class MarQS {
);
}
public async releaseConcurrency(messageId: string, releaseForRun: boolean = false) {
return this.#trace(
"releaseConcurrency",
async (span) => {
span.setAttributes({
[SemanticAttributes.MESSAGE_ID]: messageId,
});
const message = await this.readMessage(messageId);
if (!message) {
return;
}
span.setAttributes({
[SemanticAttributes.QUEUE]: message.queue,
[SemanticAttributes.MESSAGE_ID]: message.messageId,
[SemanticAttributes.CONCURRENCY_KEY]: message.concurrencyKey,
[SemanticAttributes.PARENT_QUEUE]: message.parentQueue,
});
const concurrencyKey = this.keys.currentConcurrencyKeyFromQueue(message.queue);
const envConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(message.queue);
const orgConcurrencyKey = this.keys.orgCurrentConcurrencyKeyFromQueue(message.queue);
logger.debug("Calling releaseConcurrency", {
messageId,
queue: message.queue,
concurrencyKey,
envConcurrencyKey,
orgConcurrencyKey,
service: this.name,
releaseForRun,
});
return this.redis.releaseConcurrency(
//don't release the for the run, it breaks concurrencyLimits
releaseForRun ? concurrencyKey : "",
envConcurrencyKey,
orgConcurrencyKey,
message.messageId
);
},
{
kind: SpanKind.CONSUMER,
attributes: {
[SEMATTRS_MESSAGING_OPERATION]: "releaseConcurrency",
[SEMATTRS_MESSAGE_ID]: messageId,
[SEMATTRS_MESSAGING_SYSTEM]: "marqs",
},
}
);
}
async #trace<T>(
name: string,
fn: (span: Span) => Promise<T>,
@@ -1488,6 +1542,24 @@ end
`,
});
this.redis.defineCommand("releaseConcurrency", {
numberOfKeys: 3,
lua: `
local concurrencyKey = KEYS[1]
local envCurrentConcurrencyKey = KEYS[2]
local orgCurrentConcurrencyKey = KEYS[3]
local messageId = ARGV[1]
-- Update the concurrency keys
if concurrencyKey ~= "" then
redis.call('SREM', concurrencyKey, messageId)
end
redis.call('SREM', envCurrentConcurrencyKey, messageId)
redis.call('SREM', orgCurrentConcurrencyKey, messageId)
`,
});
this.redis.defineCommand("heartbeatMessage", {
numberOfKeys: 1,
lua: `
@@ -1699,6 +1771,14 @@ declare module "ioredis" {
callback?: Callback<void>
): Result<void, Context>;
releaseConcurrency(
concurrencyKey: string,
envConcurrencyKey: string,
orgConcurrencyKey: string,
messageId: string,
callback?: Callback<void>
): Result<void, Context>;
heartbeatMessage(
visibilityQueue: string,
messageId: string,
@@ -107,6 +107,7 @@ export class TriggerTaskService extends BaseService {
select: {
id: true,
status: true,
taskIdentifier: true,
},
},
},
@@ -143,6 +144,7 @@ export class TriggerTaskService extends BaseService {
select: {
id: true,
status: true,
taskIdentifier: true,
},
},
},
@@ -391,6 +393,20 @@ export class TriggerTaskService extends BaseService {
this._prisma
);
//release the concurrency for the env and org, if part of a (batch)triggerAndWait
if (dependentAttempt) {
const isSameTask = dependentAttempt.taskRun.taskIdentifier === taskId;
await marqs?.releaseConcurrency(dependentAttempt.taskRun.id, isSameTask);
}
if (dependentBatchRun?.dependentTaskAttempt) {
const isSameTask =
dependentBatchRun.dependentTaskAttempt.taskRun.taskIdentifier === taskId;
await marqs?.releaseConcurrency(
dependentBatchRun.dependentTaskAttempt.taskRun.id,
isSameTask
);
}
if (!run) {
return;
}
@@ -202,17 +202,6 @@ export class TaskExecutor {
kind: SpanKind.CONSUMER,
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "attempt",
...accessoryAttributes({
items: [
{
text: ctx.task.filePath,
},
{
text: `${ctx.task.exportName}.run()`,
},
],
style: "codepath",
}),
},
},
this._tracer.extractContext(traceContext)
@@ -18,13 +18,16 @@ export const checkpointBatchResumer = task({
/** Test that checkpoints and resuming works if the checkpoint isn't created before the resume */
export const checkpointResumer = task({
id: "checkpoint-resume",
queue: {
concurrencyLimit: 1,
},
run: async ({ count = 1 }: Payload) => {
await noop.triggerAndWait();
logger.info(`Successfully 1/3 resumed`);
await noop.triggerAndWait();
logger.info(`Successfully 2/3 resumed`);
await noop.triggerAndWait();
logger.info(`Successfully 3/3 resumed`);
logger.info(`Starting ${count} runs`);
for (let i = 0; i < count; i++) {
await noop.triggerAndWait();
logger.info(`Successfully ${i + 1}/${count} resumed`);
}
},
});