@mevdragon/vidfarm-devcli 0.2.13 → 0.3.0
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/README.md +11 -4
- package/SKILL.director.md +97 -0
- package/SKILL.platform.md +66 -6
- package/dist/src/cli.js +137 -1524
- package/dist/src/dev-serve.js +169 -0
- package/package.json +5 -24
- package/.env.example +0 -60
- package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +0 -275
- package/auto-create-hyperframe-templates/extractor-system-prompt.md +0 -128
- package/auto-create-hyperframe-templates/input-manifest.schema.json +0 -56
- package/auto-create-hyperframe-templates/lambda-memory-estimator.md +0 -118
- package/auto-create-hyperframe-templates/production-graph.schema.json +0 -314
- package/auto-create-hyperframe-templates/runbook.md +0 -198
- package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +0 -368
- package/auto-create-hyperframe-templates/template-plan.schema.json +0 -311
- package/auto-create-hyperframe-templates/template-planner-prompt.md +0 -144
- package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +0 -599
- package/auto-create-templates/extractor-system-prompt.md +0 -119
- package/auto-create-templates/production-graph.schema.json +0 -276
- package/auto-create-templates/runbook.md +0 -140
- package/auto-create-templates/template-plan.schema.json +0 -301
- package/auto-create-templates/template-planner-prompt.md +0 -137
- package/dist/src/account-pages-legacy.js +0 -9336
- package/dist/src/account-pages-legacy.js.map +0 -1
- package/dist/src/account-pages.js +0 -61
- package/dist/src/account-pages.js.map +0 -1
- package/dist/src/app.js +0 -14023
- package/dist/src/app.js.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/composition-runtime.js +0 -581
- package/dist/src/composition-runtime.js.map +0 -1
- package/dist/src/config.js +0 -140
- package/dist/src/config.js.map +0 -1
- package/dist/src/context.js +0 -492
- package/dist/src/context.js.map +0 -1
- package/dist/src/dev-app-legacy.js +0 -694
- package/dist/src/dev-app-legacy.js.map +0 -1
- package/dist/src/dev-app.js +0 -6
- package/dist/src/dev-app.js.map +0 -1
- package/dist/src/domain.js +0 -2
- package/dist/src/domain.js.map +0 -1
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat-history.js.map +0 -1
- package/dist/src/editor-chat.js +0 -311
- package/dist/src/editor-chat.js.map +0 -1
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/flockposter-cache-store.js.map +0 -1
- package/dist/src/frontend/homepage-client.js +0 -117
- package/dist/src/frontend/homepage-client.js.map +0 -1
- package/dist/src/frontend/homepage-shared.js +0 -4
- package/dist/src/frontend/homepage-shared.js.map +0 -1
- package/dist/src/frontend/homepage-store.js +0 -28
- package/dist/src/frontend/homepage-store.js.map +0 -1
- package/dist/src/frontend/homepage-view.js +0 -260
- package/dist/src/frontend/homepage-view.js.map +0 -1
- package/dist/src/frontend/page-runtime-client.js +0 -103
- package/dist/src/frontend/page-runtime-client.js.map +0 -1
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/page-runtime-store.js.map +0 -1
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/sentry.js.map +0 -1
- package/dist/src/frontend/template-editor-chat.js +0 -3767
- package/dist/src/frontend/template-editor-chat.js.map +0 -1
- package/dist/src/help-page.js +0 -333
- package/dist/src/help-page.js.map +0 -1
- package/dist/src/homepage.js +0 -919
- package/dist/src/homepage.js.map +0 -1
- package/dist/src/hyperframes/composition.js +0 -180
- package/dist/src/hyperframes/composition.js.map +0 -1
- package/dist/src/index.js +0 -16
- package/dist/src/index.js.map +0 -1
- package/dist/src/instrument.js +0 -30
- package/dist/src/instrument.js.map +0 -1
- package/dist/src/lib/crypto.js +0 -45
- package/dist/src/lib/crypto.js.map +0 -1
- package/dist/src/lib/dev-log.js +0 -54
- package/dist/src/lib/dev-log.js.map +0 -1
- package/dist/src/lib/display-name.js +0 -11
- package/dist/src/lib/display-name.js.map +0 -1
- package/dist/src/lib/ids.js +0 -24
- package/dist/src/lib/ids.js.map +0 -1
- package/dist/src/lib/images.js +0 -19
- package/dist/src/lib/images.js.map +0 -1
- package/dist/src/lib/json.js +0 -15
- package/dist/src/lib/json.js.map +0 -1
- package/dist/src/lib/template-dna.js +0 -543
- package/dist/src/lib/template-dna.js.map +0 -1
- package/dist/src/lib/template-paths.js +0 -28
- package/dist/src/lib/template-paths.js.map +0 -1
- package/dist/src/lib/template-style-options.js +0 -203
- package/dist/src/lib/template-style-options.js.map +0 -1
- package/dist/src/lib/time.js +0 -7
- package/dist/src/lib/time.js.map +0 -1
- package/dist/src/lib/video-quality-harness.js +0 -60
- package/dist/src/lib/video-quality-harness.js.map +0 -1
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-runtime.js.map +0 -1
- package/dist/src/page-shell.js +0 -2393
- package/dist/src/page-shell.js.map +0 -1
- package/dist/src/primitive-context.js +0 -299
- package/dist/src/primitive-context.js.map +0 -1
- package/dist/src/primitive-registry.js +0 -2181
- package/dist/src/primitive-registry.js.map +0 -1
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitive-sdk.js.map +0 -1
- package/dist/src/primitives/remotion/html-image.js +0 -30
- package/dist/src/primitives/remotion/html-image.js.map +0 -1
- package/dist/src/primitives/remotion/media-slideshow.js +0 -60
- package/dist/src/primitives/remotion/media-slideshow.js.map +0 -1
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/react-page-shell.js.map +0 -1
- package/dist/src/ready-post-schedule-component.js +0 -1515
- package/dist/src/ready-post-schedule-component.js.map +0 -1
- package/dist/src/registry.js +0 -296
- package/dist/src/registry.js.map +0 -1
- package/dist/src/runtime.js +0 -31
- package/dist/src/runtime.js.map +0 -1
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/api-call-history.js.map +0 -1
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/auth.js.map +0 -1
- package/dist/src/services/billing.js +0 -231
- package/dist/src/services/billing.js.map +0 -1
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/chat-threads.js.map +0 -1
- package/dist/src/services/fork-access.js +0 -94
- package/dist/src/services/fork-access.js.map +0 -1
- package/dist/src/services/ghostcut.js +0 -132
- package/dist/src/services/ghostcut.js.map +0 -1
- package/dist/src/services/hyperframes.js +0 -1014
- package/dist/src/services/hyperframes.js.map +0 -1
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-capacity.js.map +0 -1
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/job-logs.js.map +0 -1
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/jobs.js.map +0 -1
- package/dist/src/services/media-processing.js +0 -743
- package/dist/src/services/media-processing.js.map +0 -1
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/primitive-media-lambda.js.map +0 -1
- package/dist/src/services/providers.js +0 -2870
- package/dist/src/services/providers.js.map +0 -1
- package/dist/src/services/rate-limits.js +0 -237
- package/dist/src/services/rate-limits.js.map +0 -1
- package/dist/src/services/remotion.js +0 -688
- package/dist/src/services/remotion.js.map +0 -1
- package/dist/src/services/serverless-auth.js +0 -374
- package/dist/src/services/serverless-auth.js.map +0 -1
- package/dist/src/services/serverless-jobs.js +0 -1074
- package/dist/src/services/serverless-jobs.js.map +0 -1
- package/dist/src/services/serverless-provider-keys.js +0 -401
- package/dist/src/services/serverless-provider-keys.js.map +0 -1
- package/dist/src/services/serverless-records.js +0 -1086
- package/dist/src/services/serverless-records.js.map +0 -1
- package/dist/src/services/serverless-template-configs.js +0 -67
- package/dist/src/services/serverless-template-configs.js.map +0 -1
- package/dist/src/services/storage.js +0 -382
- package/dist/src/services/storage.js.map +0 -1
- package/dist/src/services/template-certification.js +0 -475
- package/dist/src/services/template-certification.js.map +0 -1
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-loader.js.map +0 -1
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-runtime-bundles.js.map +0 -1
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/template-sources.js.map +0 -1
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/video-normalization.js.map +0 -1
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/services/webhooks.js.map +0 -1
- package/dist/src/template-editor-pages.js +0 -6624
- package/dist/src/template-editor-pages.js.map +0 -1
- package/dist/src/template-editor-shell.js +0 -2713
- package/dist/src/template-editor-shell.js.map +0 -1
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/template-sdk.js.map +0 -1
- package/dist/src/worker.js +0 -17
- package/dist/src/worker.js.map +0 -1
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/readme.secret.md +0 -89
- package/templates/vidfarm_template_0000/README.md +0 -106
- package/templates/vidfarm_template_0000/SKILL.md +0 -266
- package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/composition.json +0 -11
- package/templates/vidfarm_template_0000/src/lib/images.js +0 -221
- package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
- package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
- package/templates/vidfarm_template_0000/src/sdk.js +0 -3
- package/templates/vidfarm_template_0000/src/style-options.js +0 -200
- package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
- package/templates/vidfarm_template_0000/src/template.js +0 -1566
- package/templates/vidfarm_template_0000/template.config.json +0 -21
|
@@ -1,1074 +0,0 @@
|
|
|
1
|
-
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
2
|
-
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, ScanCommand, TransactWriteCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
|
|
3
|
-
import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
|
|
4
|
-
import { config } from "../config.js";
|
|
5
|
-
import { createId } from "../lib/ids.js";
|
|
6
|
-
import { devLog } from "../lib/dev-log.js";
|
|
7
|
-
import { nowIso } from "../lib/time.js";
|
|
8
|
-
import { ActiveJobLimitExceededError } from "./job-capacity.js";
|
|
9
|
-
const dynamodb = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
|
|
10
|
-
marshallOptions: {
|
|
11
|
-
removeUndefinedValues: true
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
const sfn = new SFNClient({});
|
|
15
|
-
const ACTIVE_ROOT_JOB_STATUSES = ["queued", "running", "waiting_for_provider", "waiting_for_child", "waiting_for_human"];
|
|
16
|
-
const ACTIVE_JOB_STATUSES = new Set(ACTIVE_ROOT_JOB_STATUSES);
|
|
17
|
-
export class ServerlessJobsService {
|
|
18
|
-
async createRootJob(input) {
|
|
19
|
-
const tableName = requireMainTableName();
|
|
20
|
-
const createdAt = nowIso();
|
|
21
|
-
const job = {
|
|
22
|
-
id: createId("job"),
|
|
23
|
-
templateId: input.templateId,
|
|
24
|
-
operationName: input.operationName,
|
|
25
|
-
workflowName: input.workflowName,
|
|
26
|
-
tracer: input.tracer,
|
|
27
|
-
status: "queued",
|
|
28
|
-
customerId: input.customer.id,
|
|
29
|
-
payload: {
|
|
30
|
-
...input.payload,
|
|
31
|
-
_providerHint: input.providerHint ?? null
|
|
32
|
-
},
|
|
33
|
-
result: null,
|
|
34
|
-
error: null,
|
|
35
|
-
progress: 0,
|
|
36
|
-
webhookUrl: normalizeWebhookUrl(input.webhookUrl) ?? normalizeWebhookUrl(input.customer.defaultWebhookUrl),
|
|
37
|
-
parentJobId: null,
|
|
38
|
-
priority: 100,
|
|
39
|
-
attemptCount: 0,
|
|
40
|
-
maxAttempts: 6,
|
|
41
|
-
runAfter: createdAt,
|
|
42
|
-
createdAt,
|
|
43
|
-
updatedAt: createdAt,
|
|
44
|
-
startedAt: null,
|
|
45
|
-
completedAt: null
|
|
46
|
-
};
|
|
47
|
-
await this.persistRootAndStart(tableName, job);
|
|
48
|
-
devLog("jobs.root.created", {
|
|
49
|
-
job_id: job.id,
|
|
50
|
-
customer_id: job.customerId,
|
|
51
|
-
template_id: job.templateId,
|
|
52
|
-
operation_name: job.operationName,
|
|
53
|
-
workflow_name: job.workflowName,
|
|
54
|
-
tracer: job.tracer
|
|
55
|
-
});
|
|
56
|
-
return job;
|
|
57
|
-
}
|
|
58
|
-
async createChildJob(input) {
|
|
59
|
-
const createdAt = nowIso();
|
|
60
|
-
const job = {
|
|
61
|
-
id: createId("job"),
|
|
62
|
-
templateId: input.templateId,
|
|
63
|
-
operationName: input.operationName,
|
|
64
|
-
workflowName: input.workflowName,
|
|
65
|
-
tracer: input.tracer,
|
|
66
|
-
status: "queued",
|
|
67
|
-
customerId: input.customerId,
|
|
68
|
-
payload: {
|
|
69
|
-
...input.payload,
|
|
70
|
-
_providerHint: input.providerHint ?? null
|
|
71
|
-
},
|
|
72
|
-
result: null,
|
|
73
|
-
error: null,
|
|
74
|
-
progress: 0,
|
|
75
|
-
webhookUrl: null,
|
|
76
|
-
parentJobId: input.parentJobId,
|
|
77
|
-
priority: 110,
|
|
78
|
-
attemptCount: 0,
|
|
79
|
-
maxAttempts: 6,
|
|
80
|
-
runAfter: createdAt,
|
|
81
|
-
createdAt,
|
|
82
|
-
updatedAt: createdAt,
|
|
83
|
-
startedAt: null,
|
|
84
|
-
completedAt: null
|
|
85
|
-
};
|
|
86
|
-
await dynamodb.send(new PutCommand({
|
|
87
|
-
TableName: requireMainTableName(),
|
|
88
|
-
Item: {
|
|
89
|
-
...jobToItem(job),
|
|
90
|
-
parent_job_id: input.parentJobId
|
|
91
|
-
},
|
|
92
|
-
ConditionExpression: "attribute_not_exists(pk)"
|
|
93
|
-
}));
|
|
94
|
-
await dynamodb.send(new PutCommand({
|
|
95
|
-
TableName: requireMainTableName(),
|
|
96
|
-
Item: {
|
|
97
|
-
pk: jobPk(input.parentJobId),
|
|
98
|
-
sk: `CHILD#${createdAt}#${job.id}`,
|
|
99
|
-
entity_type: "JobChildPointer",
|
|
100
|
-
child_job_id: job.id,
|
|
101
|
-
job_id: input.parentJobId,
|
|
102
|
-
customer_id: input.customerId,
|
|
103
|
-
created_at: createdAt
|
|
104
|
-
}
|
|
105
|
-
}));
|
|
106
|
-
try {
|
|
107
|
-
const executionArn = await this.startWorkflow(job);
|
|
108
|
-
if (executionArn) {
|
|
109
|
-
await dynamodb.send(new UpdateCommand({
|
|
110
|
-
TableName: requireMainTableName(),
|
|
111
|
-
Key: jobKey(job.id),
|
|
112
|
-
UpdateExpression: "set step_execution_arn = :arn, updated_at = :updated",
|
|
113
|
-
ExpressionAttributeValues: {
|
|
114
|
-
":arn": executionArn,
|
|
115
|
-
":updated": nowIso()
|
|
116
|
-
}
|
|
117
|
-
}));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
devLog("jobs.child.workflow_start_failed", {
|
|
122
|
-
job_id: job.id,
|
|
123
|
-
parent_job_id: input.parentJobId,
|
|
124
|
-
error: error instanceof Error ? error.message : String(error)
|
|
125
|
-
}, "error");
|
|
126
|
-
}
|
|
127
|
-
devLog("jobs.child.created", {
|
|
128
|
-
job_id: job.id,
|
|
129
|
-
parent_job_id: input.parentJobId,
|
|
130
|
-
customer_id: job.customerId,
|
|
131
|
-
template_id: job.templateId,
|
|
132
|
-
operation_name: job.operationName,
|
|
133
|
-
workflow_name: job.workflowName,
|
|
134
|
-
tracer: job.tracer
|
|
135
|
-
});
|
|
136
|
-
return job;
|
|
137
|
-
}
|
|
138
|
-
async getJob(jobId) {
|
|
139
|
-
const tableName = requireMainTableName();
|
|
140
|
-
const response = await dynamodb.send(new GetCommand({
|
|
141
|
-
TableName: tableName,
|
|
142
|
-
Key: jobKey(jobId)
|
|
143
|
-
}));
|
|
144
|
-
const job = response.Item ? itemToJob(response.Item) : null;
|
|
145
|
-
devLog("jobs.get", {
|
|
146
|
-
job_id: jobId,
|
|
147
|
-
found: Boolean(job),
|
|
148
|
-
customer_id: job?.customerId ?? null,
|
|
149
|
-
status: job?.status ?? null
|
|
150
|
-
});
|
|
151
|
-
return job;
|
|
152
|
-
}
|
|
153
|
-
async listChildJobs(parentJobId) {
|
|
154
|
-
const response = await dynamodb.send(new QueryCommand({
|
|
155
|
-
TableName: requireMainTableName(),
|
|
156
|
-
KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
|
|
157
|
-
ExpressionAttributeValues: {
|
|
158
|
-
":pk": jobPk(parentJobId),
|
|
159
|
-
":prefix": "CHILD#"
|
|
160
|
-
},
|
|
161
|
-
ScanIndexForward: false,
|
|
162
|
-
Limit: 500
|
|
163
|
-
}));
|
|
164
|
-
const childIds = (response.Items ?? [])
|
|
165
|
-
.map((item) => typeof item.child_job_id === "string" ? item.child_job_id : null)
|
|
166
|
-
.filter((id) => Boolean(id));
|
|
167
|
-
const jobs = [];
|
|
168
|
-
for (const childId of childIds) {
|
|
169
|
-
const child = await this.getJob(childId);
|
|
170
|
-
if (child) {
|
|
171
|
-
jobs.push(child);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return jobs;
|
|
175
|
-
}
|
|
176
|
-
async listJobs(input) {
|
|
177
|
-
const tableName = requireMainTableName();
|
|
178
|
-
const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
|
|
179
|
-
const index = input.templateId ? "gsi2" : input.tracer ? "gsi3" : "gsi1";
|
|
180
|
-
const partition = input.templateId
|
|
181
|
-
? customerTemplatePk(input.customerId, input.templateId)
|
|
182
|
-
: input.tracer
|
|
183
|
-
? customerTracerPk(input.customerId, input.tracer)
|
|
184
|
-
: customerPk(input.customerId);
|
|
185
|
-
const response = await dynamodb.send(new QueryCommand({
|
|
186
|
-
TableName: tableName,
|
|
187
|
-
IndexName: index,
|
|
188
|
-
KeyConditionExpression: `${index}pk = :pk`,
|
|
189
|
-
ExpressionAttributeValues: {
|
|
190
|
-
":pk": partition
|
|
191
|
-
},
|
|
192
|
-
ScanIndexForward: false,
|
|
193
|
-
Limit: limit,
|
|
194
|
-
ExclusiveStartKey: input.cursor ? cursorToStartKey(input, index) : undefined
|
|
195
|
-
}));
|
|
196
|
-
const jobs = (response.Items ?? []).map(itemToJob);
|
|
197
|
-
const filtered = jobs.filter((job) => {
|
|
198
|
-
if (input.tracer && job.tracer !== input.tracer) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
if (input.startTime && job.createdAt < input.startTime) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
if (input.endTime && job.createdAt > input.endTime) {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
return true;
|
|
208
|
-
});
|
|
209
|
-
devLog("jobs.list", {
|
|
210
|
-
customer_id: input.customerId,
|
|
211
|
-
template_id: input.templateId ?? null,
|
|
212
|
-
tracer: input.tracer ?? null,
|
|
213
|
-
count: filtered.length,
|
|
214
|
-
limit
|
|
215
|
-
});
|
|
216
|
-
return filtered;
|
|
217
|
-
}
|
|
218
|
-
async cancelJob(jobId) {
|
|
219
|
-
const now = nowIso();
|
|
220
|
-
await this.addLogEvent({
|
|
221
|
-
id: createId("evt"),
|
|
222
|
-
jobId,
|
|
223
|
-
level: "warn",
|
|
224
|
-
message: "Job cancelled",
|
|
225
|
-
metadata: { reason: "user_request" },
|
|
226
|
-
progress: null,
|
|
227
|
-
artifactKey: null,
|
|
228
|
-
createdAt: now
|
|
229
|
-
});
|
|
230
|
-
await this.updateJobStatus({
|
|
231
|
-
id: jobId,
|
|
232
|
-
status: "cancelled",
|
|
233
|
-
error: { message: "Cancelled by user request." },
|
|
234
|
-
completedAt: now
|
|
235
|
-
});
|
|
236
|
-
devLog("jobs.cancelled", {
|
|
237
|
-
job_id: jobId
|
|
238
|
-
}, "warn");
|
|
239
|
-
}
|
|
240
|
-
async updateJobStatus(input) {
|
|
241
|
-
const current = await this.getJob(input.id);
|
|
242
|
-
const timestamp = nowIso();
|
|
243
|
-
const isTerminalStatus = input.status === "succeeded" || input.status === "failed" || input.status === "cancelled";
|
|
244
|
-
const shouldReleaseRootCapacity = Boolean(isTerminalStatus
|
|
245
|
-
&& current
|
|
246
|
-
&& current.parentJobId === null
|
|
247
|
-
&& ACTIVE_ROOT_JOB_STATUSES.includes(current.status));
|
|
248
|
-
if (isTerminalStatus && current && !ACTIVE_ROOT_JOB_STATUSES.includes(current.status)) {
|
|
249
|
-
devLog("jobs.status.terminal_update_skipped", {
|
|
250
|
-
job_id: input.id,
|
|
251
|
-
current_status: current.status,
|
|
252
|
-
attempted_status: input.status
|
|
253
|
-
}, "warn");
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const nextCompletedAt = input.completedAt
|
|
257
|
-
?? (isTerminalStatus ? timestamp : current?.completedAt ?? null);
|
|
258
|
-
const nextStartedAt = input.startedAt !== undefined
|
|
259
|
-
? input.startedAt
|
|
260
|
-
: input.status === "running"
|
|
261
|
-
? current?.startedAt ?? timestamp
|
|
262
|
-
: input.status === "queued"
|
|
263
|
-
? null
|
|
264
|
-
: current?.startedAt ?? null;
|
|
265
|
-
const expressionAttributeValues = {
|
|
266
|
-
":status": input.status,
|
|
267
|
-
":progress": input.progress ?? current?.progress ?? 0,
|
|
268
|
-
":result": input.result === undefined ? current?.result ?? null : input.result,
|
|
269
|
-
":error": input.error === undefined ? current?.error ?? null : input.error,
|
|
270
|
-
":runAfter": input.runAfter ?? current?.runAfter ?? timestamp,
|
|
271
|
-
":startedAt": nextStartedAt,
|
|
272
|
-
":completedAt": nextCompletedAt,
|
|
273
|
-
":updatedAt": timestamp
|
|
274
|
-
};
|
|
275
|
-
const conditionExpression = isTerminalStatus
|
|
276
|
-
? [
|
|
277
|
-
"#status = :queuedStatus",
|
|
278
|
-
"#status = :runningStatus",
|
|
279
|
-
"#status = :waitingForProviderStatus",
|
|
280
|
-
"#status = :waitingForChildStatus",
|
|
281
|
-
"#status = :waitingForHumanStatus"
|
|
282
|
-
].join(" OR ")
|
|
283
|
-
: [
|
|
284
|
-
"attribute_not_exists(#status)",
|
|
285
|
-
"(#status <> :succeededStatus AND #status <> :failedStatus AND #status <> :cancelledStatus)"
|
|
286
|
-
].join(" OR ");
|
|
287
|
-
if (isTerminalStatus) {
|
|
288
|
-
expressionAttributeValues[":queuedStatus"] = "queued";
|
|
289
|
-
expressionAttributeValues[":runningStatus"] = "running";
|
|
290
|
-
expressionAttributeValues[":waitingForProviderStatus"] = "waiting_for_provider";
|
|
291
|
-
expressionAttributeValues[":waitingForChildStatus"] = "waiting_for_child";
|
|
292
|
-
expressionAttributeValues[":waitingForHumanStatus"] = "waiting_for_human";
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
expressionAttributeValues[":succeededStatus"] = "succeeded";
|
|
296
|
-
expressionAttributeValues[":failedStatus"] = "failed";
|
|
297
|
-
expressionAttributeValues[":cancelledStatus"] = "cancelled";
|
|
298
|
-
}
|
|
299
|
-
try {
|
|
300
|
-
await dynamodb.send(new UpdateCommand({
|
|
301
|
-
TableName: requireMainTableName(),
|
|
302
|
-
Key: jobKey(input.id),
|
|
303
|
-
UpdateExpression: [
|
|
304
|
-
"set #status = :status",
|
|
305
|
-
"progress = :progress",
|
|
306
|
-
"result_json = :result",
|
|
307
|
-
"error_json = :error",
|
|
308
|
-
"run_after = :runAfter",
|
|
309
|
-
"started_at = :startedAt",
|
|
310
|
-
"completed_at = :completedAt",
|
|
311
|
-
"updated_at = :updatedAt"
|
|
312
|
-
].join(", "),
|
|
313
|
-
ConditionExpression: conditionExpression,
|
|
314
|
-
ExpressionAttributeNames: {
|
|
315
|
-
"#status": "status"
|
|
316
|
-
},
|
|
317
|
-
ExpressionAttributeValues: expressionAttributeValues
|
|
318
|
-
}));
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
if (isConditionalCheckFailed(error)) {
|
|
322
|
-
if (isTerminalStatus) {
|
|
323
|
-
devLog("jobs.status.terminal_update_condition_failed", {
|
|
324
|
-
job_id: input.id,
|
|
325
|
-
attempted_status: input.status
|
|
326
|
-
}, "warn");
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
devLog("jobs.status.non_terminal_update_skipped", {
|
|
330
|
-
job_id: input.id,
|
|
331
|
-
attempted_status: input.status,
|
|
332
|
-
progress: input.progress ?? current?.progress ?? 0
|
|
333
|
-
}, "warn");
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
throw error;
|
|
337
|
-
}
|
|
338
|
-
if (shouldReleaseRootCapacity && current) {
|
|
339
|
-
await this.decrementRootCapacity(current.customerId, current.id);
|
|
340
|
-
}
|
|
341
|
-
devLog("jobs.status.updated", {
|
|
342
|
-
job_id: input.id,
|
|
343
|
-
status: input.status,
|
|
344
|
-
progress: input.progress ?? current?.progress ?? 0,
|
|
345
|
-
completed_at: nextCompletedAt
|
|
346
|
-
}, input.status === "failed" ? "error" : input.status === "cancelled" ? "warn" : "info");
|
|
347
|
-
}
|
|
348
|
-
async insertArtifact(record) {
|
|
349
|
-
const createdAt = nowIso();
|
|
350
|
-
await dynamodb.send(new PutCommand({
|
|
351
|
-
TableName: requireMainTableName(),
|
|
352
|
-
Item: artifactToItem({
|
|
353
|
-
id: record.id,
|
|
354
|
-
jobId: record.jobId,
|
|
355
|
-
customerId: record.customerId,
|
|
356
|
-
templateId: record.templateId,
|
|
357
|
-
kind: record.kind,
|
|
358
|
-
storageKey: record.storageKey,
|
|
359
|
-
publicUrl: record.publicUrl ?? null,
|
|
360
|
-
metadata: record.metadata ?? {},
|
|
361
|
-
createdAt
|
|
362
|
-
})
|
|
363
|
-
}));
|
|
364
|
-
devLog("jobs.artifact.inserted", {
|
|
365
|
-
artifact_id: record.id,
|
|
366
|
-
job_id: record.jobId,
|
|
367
|
-
customer_id: record.customerId,
|
|
368
|
-
template_id: record.templateId,
|
|
369
|
-
kind: record.kind,
|
|
370
|
-
storage_key: record.storageKey,
|
|
371
|
-
has_public_url: Boolean(record.publicUrl)
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
async sweepStaleActiveJobs(input = {}) {
|
|
375
|
-
const customerIds = input.customerId
|
|
376
|
-
? [input.customerId]
|
|
377
|
-
: await this.listCapacityCustomerIds(input.maxCustomers ?? 100);
|
|
378
|
-
let swept = 0;
|
|
379
|
-
let scanned = 0;
|
|
380
|
-
for (const customerId of customerIds) {
|
|
381
|
-
const result = await this.sweepStaleActiveJobsForCustomer(customerId, {
|
|
382
|
-
maxJobs: input.maxJobsPerCustomer ?? 1000
|
|
383
|
-
});
|
|
384
|
-
swept += result.swept;
|
|
385
|
-
scanned += result.scanned;
|
|
386
|
-
}
|
|
387
|
-
return {
|
|
388
|
-
customers: customerIds.length,
|
|
389
|
-
scanned,
|
|
390
|
-
swept
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
async listArtifactsForJob(input) {
|
|
394
|
-
const response = await dynamodb.send(new QueryCommand({
|
|
395
|
-
TableName: requireMainTableName(),
|
|
396
|
-
KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
|
|
397
|
-
ExpressionAttributeValues: {
|
|
398
|
-
":pk": jobPk(input.jobId),
|
|
399
|
-
":prefix": "ARTIFACT#"
|
|
400
|
-
},
|
|
401
|
-
ScanIndexForward: false,
|
|
402
|
-
Limit: Math.max(1, Math.min(input.limit ?? 100, 501))
|
|
403
|
-
}));
|
|
404
|
-
const artifacts = (response.Items ?? [])
|
|
405
|
-
.map(itemToArtifact)
|
|
406
|
-
.filter((artifact) => {
|
|
407
|
-
if (artifact.customerId !== input.customerId) {
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
if (input.templateId && artifact.templateId !== input.templateId) {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
if (input.cursor && (artifact.createdAt > input.cursor.createdAt || (artifact.createdAt === input.cursor.createdAt && artifact.id >= input.cursor.id))) {
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
return true;
|
|
417
|
-
});
|
|
418
|
-
devLog("jobs.artifacts.list", {
|
|
419
|
-
job_id: input.jobId,
|
|
420
|
-
customer_id: input.customerId,
|
|
421
|
-
template_id: input.templateId ?? null,
|
|
422
|
-
count: artifacts.length,
|
|
423
|
-
limit: input.limit ?? 100
|
|
424
|
-
});
|
|
425
|
-
return artifacts;
|
|
426
|
-
}
|
|
427
|
-
async listLogs(input) {
|
|
428
|
-
const response = await dynamodb.send(new QueryCommand({
|
|
429
|
-
TableName: requireMainTableName(),
|
|
430
|
-
KeyConditionExpression: "pk = :pk and begins_with(sk, :prefix)",
|
|
431
|
-
ExpressionAttributeValues: {
|
|
432
|
-
":pk": jobPk(input.jobId),
|
|
433
|
-
":prefix": "LOG#"
|
|
434
|
-
},
|
|
435
|
-
ScanIndexForward: false,
|
|
436
|
-
Limit: Math.max(1, Math.min(input.limit ?? 100, 500))
|
|
437
|
-
}));
|
|
438
|
-
return (response.Items ?? [])
|
|
439
|
-
.map(itemToLog)
|
|
440
|
-
.filter((event) => {
|
|
441
|
-
if (input.startTime && event.createdAt < input.startTime) {
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
if (input.endTime && event.createdAt > input.endTime) {
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
if (input.cursor && (event.createdAt > input.cursor.createdAt || (event.createdAt === input.cursor.createdAt && event.id >= input.cursor.id))) {
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
return true;
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
async listLogStorageKeys() {
|
|
454
|
-
return [];
|
|
455
|
-
}
|
|
456
|
-
async getRootQueueCapacityStatus(customer) {
|
|
457
|
-
await this.sweepStaleActiveJobsForCustomer(customer.id);
|
|
458
|
-
await this.ensureRootCapacityCounter(customer.id);
|
|
459
|
-
const response = await dynamodb.send(new GetCommand({
|
|
460
|
-
TableName: requireMainTableName(),
|
|
461
|
-
Key: customerJobCapacityKey(customer.id)
|
|
462
|
-
}));
|
|
463
|
-
const activeCount = typeof response.Item?.active_count === "number" ? response.Item.active_count : 0;
|
|
464
|
-
const limit = config.MAX_ACTIVE_JOBS_PER_CUSTOMER;
|
|
465
|
-
return {
|
|
466
|
-
plan_tier: customer.planTier,
|
|
467
|
-
scope: "root_jobs",
|
|
468
|
-
customer: {
|
|
469
|
-
limit,
|
|
470
|
-
pending_count: activeCount,
|
|
471
|
-
active_count: activeCount,
|
|
472
|
-
remaining: Math.max(0, limit - activeCount),
|
|
473
|
-
enabled: true
|
|
474
|
-
},
|
|
475
|
-
pending_statuses: ACTIVE_ROOT_JOB_STATUSES,
|
|
476
|
-
retry_after_seconds: 30
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
async persistRootAndStart(tableName, job) {
|
|
480
|
-
await this.sweepStaleActiveJobsForCustomer(job.customerId);
|
|
481
|
-
await this.ensureRootCapacityCounter(job.customerId);
|
|
482
|
-
const queuedLogId = createId("evt");
|
|
483
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
484
|
-
try {
|
|
485
|
-
await this.writeRootJobTransaction(tableName, job, queuedLogId);
|
|
486
|
-
break;
|
|
487
|
-
}
|
|
488
|
-
catch (error) {
|
|
489
|
-
if (!isTransactionCancelledForCapacity(error)) {
|
|
490
|
-
throw error;
|
|
491
|
-
}
|
|
492
|
-
const activeCount = await this.reconcileRootCapacityCounter(job.customerId);
|
|
493
|
-
if (attempt === 0 && activeCount < config.MAX_ACTIVE_JOBS_PER_CUSTOMER) {
|
|
494
|
-
devLog("jobs.capacity.reconciled_retry", {
|
|
495
|
-
job_id: job.id,
|
|
496
|
-
customer_id: job.customerId,
|
|
497
|
-
active_count: activeCount,
|
|
498
|
-
limit: config.MAX_ACTIVE_JOBS_PER_CUSTOMER
|
|
499
|
-
}, "warn");
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
throw new ActiveJobLimitExceededError(`Customer has reached the active job limit of ${config.MAX_ACTIVE_JOBS_PER_CUSTOMER}.`, config.MAX_ACTIVE_JOBS_PER_CUSTOMER, activeCount, ACTIVE_ROOT_JOB_STATUSES, 30);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
const executionArn = await this.startWorkflow(job);
|
|
507
|
-
if (executionArn) {
|
|
508
|
-
await dynamodb.send(new UpdateCommand({
|
|
509
|
-
TableName: tableName,
|
|
510
|
-
Key: jobKey(job.id),
|
|
511
|
-
UpdateExpression: "set step_execution_arn = :arn, updated_at = :updated",
|
|
512
|
-
ExpressionAttributeValues: {
|
|
513
|
-
":arn": executionArn,
|
|
514
|
-
":updated": nowIso()
|
|
515
|
-
}
|
|
516
|
-
}));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
catch (error) {
|
|
520
|
-
await this.updateJobStatus({
|
|
521
|
-
id: job.id,
|
|
522
|
-
status: "failed",
|
|
523
|
-
error: {
|
|
524
|
-
message: error instanceof Error ? error.message : "Unable to start job workflow.",
|
|
525
|
-
type: "job_workflow_start_failed"
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
throw error;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
async writeRootJobTransaction(tableName, job, queuedLogId) {
|
|
532
|
-
await dynamodb.send(new TransactWriteCommand({
|
|
533
|
-
TransactItems: [
|
|
534
|
-
{
|
|
535
|
-
Update: {
|
|
536
|
-
TableName: tableName,
|
|
537
|
-
Key: customerJobCapacityKey(job.customerId),
|
|
538
|
-
UpdateExpression: [
|
|
539
|
-
"set active_count = if_not_exists(active_count, :zero) + :one",
|
|
540
|
-
"updated_at = :updated",
|
|
541
|
-
"capacity_limit = :limit",
|
|
542
|
-
"entity_type = :entityType"
|
|
543
|
-
].join(", "),
|
|
544
|
-
ConditionExpression: "attribute_not_exists(active_count) OR active_count < :limit",
|
|
545
|
-
ExpressionAttributeValues: {
|
|
546
|
-
":zero": 0,
|
|
547
|
-
":one": 1,
|
|
548
|
-
":limit": config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
|
|
549
|
-
":updated": job.createdAt,
|
|
550
|
-
":entityType": "CustomerJobCapacity"
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
Put: {
|
|
556
|
-
TableName: tableName,
|
|
557
|
-
Item: jobToItem(job),
|
|
558
|
-
ConditionExpression: "attribute_not_exists(pk)"
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
Put: {
|
|
563
|
-
TableName: tableName,
|
|
564
|
-
Item: {
|
|
565
|
-
pk: jobPk(job.id),
|
|
566
|
-
sk: `LOG#${job.createdAt}#${queuedLogId}`,
|
|
567
|
-
entity_type: "JobLogEvent",
|
|
568
|
-
id: queuedLogId,
|
|
569
|
-
job_id: job.id,
|
|
570
|
-
level: "info",
|
|
571
|
-
message: "Job queued",
|
|
572
|
-
metadata_json: {
|
|
573
|
-
runtime: "serverless",
|
|
574
|
-
template_id: job.templateId,
|
|
575
|
-
operation_name: job.operationName
|
|
576
|
-
},
|
|
577
|
-
progress: 0,
|
|
578
|
-
artifact_key: null,
|
|
579
|
-
created_at: job.createdAt
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
]
|
|
584
|
-
}));
|
|
585
|
-
}
|
|
586
|
-
async ensureRootCapacityCounter(customerId) {
|
|
587
|
-
const tableName = requireMainTableName();
|
|
588
|
-
const response = await dynamodb.send(new GetCommand({
|
|
589
|
-
TableName: tableName,
|
|
590
|
-
Key: customerJobCapacityKey(customerId)
|
|
591
|
-
}));
|
|
592
|
-
if (response.Item) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
const activeCount = await this.countActiveRootJobs(customerId);
|
|
596
|
-
try {
|
|
597
|
-
await dynamodb.send(new PutCommand({
|
|
598
|
-
TableName: tableName,
|
|
599
|
-
Item: {
|
|
600
|
-
...customerJobCapacityKey(customerId),
|
|
601
|
-
entity_type: "CustomerJobCapacity",
|
|
602
|
-
customer_id: customerId,
|
|
603
|
-
active_count: activeCount,
|
|
604
|
-
capacity_limit: config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
|
|
605
|
-
updated_at: nowIso()
|
|
606
|
-
},
|
|
607
|
-
ConditionExpression: "attribute_not_exists(pk)"
|
|
608
|
-
}));
|
|
609
|
-
}
|
|
610
|
-
catch (error) {
|
|
611
|
-
if (isConditionalCheckFailed(error)) {
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
throw error;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
async reconcileRootCapacityCounter(customerId) {
|
|
618
|
-
await this.sweepStaleActiveJobsForCustomer(customerId);
|
|
619
|
-
const activeCount = await this.countActiveRootJobs(customerId);
|
|
620
|
-
await dynamodb.send(new UpdateCommand({
|
|
621
|
-
TableName: requireMainTableName(),
|
|
622
|
-
Key: customerJobCapacityKey(customerId),
|
|
623
|
-
UpdateExpression: "set active_count = :activeCount, capacity_limit = :limit, updated_at = :updated, entity_type = :entityType",
|
|
624
|
-
ExpressionAttributeValues: {
|
|
625
|
-
":activeCount": activeCount,
|
|
626
|
-
":limit": config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
|
|
627
|
-
":updated": nowIso(),
|
|
628
|
-
":entityType": "CustomerJobCapacity"
|
|
629
|
-
}
|
|
630
|
-
}));
|
|
631
|
-
return activeCount;
|
|
632
|
-
}
|
|
633
|
-
async getRootCapacityCount(customerId) {
|
|
634
|
-
const response = await dynamodb.send(new GetCommand({
|
|
635
|
-
TableName: requireMainTableName(),
|
|
636
|
-
Key: customerJobCapacityKey(customerId)
|
|
637
|
-
}));
|
|
638
|
-
return typeof response.Item?.active_count === "number" ? response.Item.active_count : null;
|
|
639
|
-
}
|
|
640
|
-
async countActiveRootJobs(customerId) {
|
|
641
|
-
let count = 0;
|
|
642
|
-
let exclusiveStartKey;
|
|
643
|
-
do {
|
|
644
|
-
const response = await dynamodb.send(new QueryCommand({
|
|
645
|
-
TableName: requireMainTableName(),
|
|
646
|
-
IndexName: "gsi1",
|
|
647
|
-
KeyConditionExpression: "gsi1pk = :pk",
|
|
648
|
-
ExpressionAttributeValues: {
|
|
649
|
-
":pk": customerPk(customerId)
|
|
650
|
-
},
|
|
651
|
-
ScanIndexForward: false,
|
|
652
|
-
Limit: 500,
|
|
653
|
-
ExclusiveStartKey: exclusiveStartKey
|
|
654
|
-
}));
|
|
655
|
-
for (const item of response.Items ?? []) {
|
|
656
|
-
const job = itemToJob(item);
|
|
657
|
-
if (job.parentJobId === null && ACTIVE_ROOT_JOB_STATUSES.includes(job.status)) {
|
|
658
|
-
count += 1;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
exclusiveStartKey = response.LastEvaluatedKey;
|
|
662
|
-
} while (exclusiveStartKey);
|
|
663
|
-
return count;
|
|
664
|
-
}
|
|
665
|
-
async listCapacityCustomerIds(limit) {
|
|
666
|
-
const customerIds = [];
|
|
667
|
-
let exclusiveStartKey;
|
|
668
|
-
do {
|
|
669
|
-
const response = await dynamodb.send(new ScanCommand({
|
|
670
|
-
TableName: requireMainTableName(),
|
|
671
|
-
FilterExpression: "entity_type = :entityType",
|
|
672
|
-
ExpressionAttributeValues: {
|
|
673
|
-
":entityType": "CustomerJobCapacity"
|
|
674
|
-
},
|
|
675
|
-
Limit: Math.max(1, Math.min(limit - customerIds.length, 100)),
|
|
676
|
-
ExclusiveStartKey: exclusiveStartKey
|
|
677
|
-
}));
|
|
678
|
-
for (const item of response.Items ?? []) {
|
|
679
|
-
const customerId = typeof item.customer_id === "string"
|
|
680
|
-
? item.customer_id
|
|
681
|
-
: parseCustomerIdFromCapacityPk(item.pk);
|
|
682
|
-
if (customerId) {
|
|
683
|
-
customerIds.push(customerId);
|
|
684
|
-
}
|
|
685
|
-
if (customerIds.length >= limit) {
|
|
686
|
-
break;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
exclusiveStartKey = customerIds.length >= limit
|
|
690
|
-
? undefined
|
|
691
|
-
: response.LastEvaluatedKey;
|
|
692
|
-
} while (exclusiveStartKey);
|
|
693
|
-
return customerIds;
|
|
694
|
-
}
|
|
695
|
-
async sweepStaleActiveJobsForCustomer(customerId, input = {}) {
|
|
696
|
-
const maxJobs = Math.max(1, input.maxJobs ?? 1000);
|
|
697
|
-
let scanned = 0;
|
|
698
|
-
let swept = 0;
|
|
699
|
-
let exclusiveStartKey;
|
|
700
|
-
do {
|
|
701
|
-
const response = await dynamodb.send(new QueryCommand({
|
|
702
|
-
TableName: requireMainTableName(),
|
|
703
|
-
IndexName: "gsi1",
|
|
704
|
-
KeyConditionExpression: "gsi1pk = :pk",
|
|
705
|
-
ExpressionAttributeValues: {
|
|
706
|
-
":pk": customerPk(customerId)
|
|
707
|
-
},
|
|
708
|
-
ScanIndexForward: false,
|
|
709
|
-
Limit: Math.min(500, maxJobs - scanned),
|
|
710
|
-
ExclusiveStartKey: exclusiveStartKey
|
|
711
|
-
}));
|
|
712
|
-
for (const item of response.Items ?? []) {
|
|
713
|
-
scanned += 1;
|
|
714
|
-
const job = itemToJob(item);
|
|
715
|
-
const stale = describeStaleActiveJob(job);
|
|
716
|
-
if (!stale) {
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
await this.failStaleActiveJob(job, stale);
|
|
720
|
-
swept += 1;
|
|
721
|
-
}
|
|
722
|
-
exclusiveStartKey = scanned >= maxJobs
|
|
723
|
-
? undefined
|
|
724
|
-
: response.LastEvaluatedKey;
|
|
725
|
-
} while (exclusiveStartKey);
|
|
726
|
-
if (swept > 0) {
|
|
727
|
-
await this.reconcileRootCapacityCounterAfterSweep(customerId);
|
|
728
|
-
}
|
|
729
|
-
return { scanned, swept };
|
|
730
|
-
}
|
|
731
|
-
async failStaleActiveJob(job, stale) {
|
|
732
|
-
const now = nowIso();
|
|
733
|
-
await this.addLogEvent({
|
|
734
|
-
id: createId("evt"),
|
|
735
|
-
jobId: job.id,
|
|
736
|
-
level: "error",
|
|
737
|
-
message: "Job marked failed by stale-job sweeper",
|
|
738
|
-
metadata: {
|
|
739
|
-
type: "stale_job_swept",
|
|
740
|
-
stale_reason: stale.reason,
|
|
741
|
-
stale_threshold_seconds: stale.thresholdSeconds,
|
|
742
|
-
stale_age_seconds: stale.ageSeconds,
|
|
743
|
-
previous_status: job.status,
|
|
744
|
-
started_at: job.startedAt,
|
|
745
|
-
updated_at: job.updatedAt,
|
|
746
|
-
created_at: job.createdAt
|
|
747
|
-
},
|
|
748
|
-
progress: job.progress,
|
|
749
|
-
artifactKey: null,
|
|
750
|
-
createdAt: now
|
|
751
|
-
});
|
|
752
|
-
await this.updateJobStatus({
|
|
753
|
-
id: job.id,
|
|
754
|
-
status: "failed",
|
|
755
|
-
error: {
|
|
756
|
-
message: `Job was marked failed because it became stale: ${stale.reason}.`,
|
|
757
|
-
type: "stale_job_swept",
|
|
758
|
-
stale_reason: stale.reason,
|
|
759
|
-
stale_threshold_seconds: stale.thresholdSeconds,
|
|
760
|
-
stale_age_seconds: stale.ageSeconds
|
|
761
|
-
},
|
|
762
|
-
completedAt: now
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
async reconcileRootCapacityCounterAfterSweep(customerId) {
|
|
766
|
-
const activeCount = await this.countActiveRootJobs(customerId);
|
|
767
|
-
await dynamodb.send(new UpdateCommand({
|
|
768
|
-
TableName: requireMainTableName(),
|
|
769
|
-
Key: customerJobCapacityKey(customerId),
|
|
770
|
-
UpdateExpression: "set active_count = :activeCount, capacity_limit = :limit, updated_at = :updated, entity_type = :entityType",
|
|
771
|
-
ExpressionAttributeValues: {
|
|
772
|
-
":activeCount": activeCount,
|
|
773
|
-
":limit": config.MAX_ACTIVE_JOBS_PER_CUSTOMER,
|
|
774
|
-
":updated": nowIso(),
|
|
775
|
-
":entityType": "CustomerJobCapacity"
|
|
776
|
-
}
|
|
777
|
-
}));
|
|
778
|
-
}
|
|
779
|
-
async decrementRootCapacity(customerId, jobId) {
|
|
780
|
-
try {
|
|
781
|
-
await dynamodb.send(new UpdateCommand({
|
|
782
|
-
TableName: requireMainTableName(),
|
|
783
|
-
Key: customerJobCapacityKey(customerId),
|
|
784
|
-
UpdateExpression: "set active_count = active_count - :one, updated_at = :updated",
|
|
785
|
-
ConditionExpression: "active_count >= :one",
|
|
786
|
-
ExpressionAttributeValues: {
|
|
787
|
-
":one": 1,
|
|
788
|
-
":updated": nowIso()
|
|
789
|
-
}
|
|
790
|
-
}));
|
|
791
|
-
devLog("jobs.capacity.decremented", {
|
|
792
|
-
job_id: jobId,
|
|
793
|
-
customer_id: customerId
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
catch (error) {
|
|
797
|
-
if (isConditionalCheckFailed(error)) {
|
|
798
|
-
devLog("jobs.capacity.decrement_skipped", {
|
|
799
|
-
job_id: jobId,
|
|
800
|
-
customer_id: customerId
|
|
801
|
-
}, "warn");
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
throw error;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
async startWorkflow(job) {
|
|
808
|
-
if (!config.VIDFARM_JOB_STATE_MACHINE_ARN) {
|
|
809
|
-
return null;
|
|
810
|
-
}
|
|
811
|
-
const response = await sfn.send(new StartExecutionCommand({
|
|
812
|
-
stateMachineArn: config.VIDFARM_JOB_STATE_MACHINE_ARN,
|
|
813
|
-
name: job.id.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
|
|
814
|
-
input: JSON.stringify({
|
|
815
|
-
job_id: job.id,
|
|
816
|
-
customer_id: job.customerId,
|
|
817
|
-
template_id: job.templateId,
|
|
818
|
-
operation_name: job.operationName,
|
|
819
|
-
workflow_name: job.workflowName,
|
|
820
|
-
stage: "run",
|
|
821
|
-
attempt: 1
|
|
822
|
-
})
|
|
823
|
-
}));
|
|
824
|
-
devLog("jobs.workflow.started", {
|
|
825
|
-
job_id: job.id,
|
|
826
|
-
execution_arn: response.executionArn ?? null,
|
|
827
|
-
state_machine_arn: config.VIDFARM_JOB_STATE_MACHINE_ARN
|
|
828
|
-
});
|
|
829
|
-
return response.executionArn ?? null;
|
|
830
|
-
}
|
|
831
|
-
async addLogEvent(event) {
|
|
832
|
-
await dynamodb.send(new PutCommand({
|
|
833
|
-
TableName: requireMainTableName(),
|
|
834
|
-
Item: {
|
|
835
|
-
pk: jobPk(event.jobId),
|
|
836
|
-
sk: `LOG#${event.createdAt}#${event.id}`,
|
|
837
|
-
entity_type: "JobLogEvent",
|
|
838
|
-
id: event.id,
|
|
839
|
-
job_id: event.jobId,
|
|
840
|
-
level: event.level,
|
|
841
|
-
message: event.message,
|
|
842
|
-
metadata_json: event.metadata,
|
|
843
|
-
progress: event.progress,
|
|
844
|
-
artifact_key: event.artifactKey,
|
|
845
|
-
created_at: event.createdAt
|
|
846
|
-
}
|
|
847
|
-
}));
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
function isConditionalCheckFailed(error) {
|
|
851
|
-
return Boolean(error
|
|
852
|
-
&& typeof error === "object"
|
|
853
|
-
&& "name" in error
|
|
854
|
-
&& error.name === "ConditionalCheckFailedException");
|
|
855
|
-
}
|
|
856
|
-
function isTransactionCancelledForCapacity(error) {
|
|
857
|
-
if (!error || typeof error !== "object" || !("name" in error) || error.name !== "TransactionCanceledException") {
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
const reasons = "CancellationReasons" in error ? error.CancellationReasons : null;
|
|
861
|
-
return Array.isArray(reasons) && reasons.some((reason) => {
|
|
862
|
-
return reason && typeof reason === "object" && "Code" in reason && reason.Code === "ConditionalCheckFailed";
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
function requireMainTableName() {
|
|
866
|
-
if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
|
|
867
|
-
throw new Error("Serverless job store requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
|
|
868
|
-
}
|
|
869
|
-
return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
|
|
870
|
-
}
|
|
871
|
-
function normalizeWebhookUrl(value) {
|
|
872
|
-
if (typeof value !== "string") {
|
|
873
|
-
return null;
|
|
874
|
-
}
|
|
875
|
-
const trimmed = value.trim();
|
|
876
|
-
return trimmed ? trimmed : null;
|
|
877
|
-
}
|
|
878
|
-
function jobPk(jobId) {
|
|
879
|
-
return `JOB#${jobId}`;
|
|
880
|
-
}
|
|
881
|
-
function jobKey(jobId) {
|
|
882
|
-
return {
|
|
883
|
-
pk: jobPk(jobId),
|
|
884
|
-
sk: "META"
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
function customerPk(customerId) {
|
|
888
|
-
return `CUSTOMER#${customerId}`;
|
|
889
|
-
}
|
|
890
|
-
function customerJobCapacityKey(customerId) {
|
|
891
|
-
return {
|
|
892
|
-
pk: customerPk(customerId),
|
|
893
|
-
sk: "JOB_CAPACITY"
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
function parseCustomerIdFromCapacityPk(value) {
|
|
897
|
-
if (typeof value !== "string" || !value.startsWith("CUSTOMER#")) {
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
return value.slice("CUSTOMER#".length) || null;
|
|
901
|
-
}
|
|
902
|
-
function describeStaleActiveJob(job) {
|
|
903
|
-
if (!ACTIVE_JOB_STATUSES.has(job.status)) {
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
const wallClockAgeSeconds = secondsSince(job.createdAt);
|
|
907
|
-
if (wallClockAgeSeconds >= config.MAX_JOB_WALL_CLOCK_SECONDS) {
|
|
908
|
-
return {
|
|
909
|
-
reason: "wall_clock_timeout",
|
|
910
|
-
thresholdSeconds: config.MAX_JOB_WALL_CLOCK_SECONDS,
|
|
911
|
-
ageSeconds: wallClockAgeSeconds
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
if (job.status === "running") {
|
|
915
|
-
const heartbeatAt = job.updatedAt || job.startedAt || job.createdAt;
|
|
916
|
-
const heartbeatAgeSeconds = secondsSince(heartbeatAt);
|
|
917
|
-
if (heartbeatAgeSeconds >= config.STALE_RUNNING_JOB_SECONDS) {
|
|
918
|
-
return {
|
|
919
|
-
reason: "running_heartbeat_timeout",
|
|
920
|
-
thresholdSeconds: config.STALE_RUNNING_JOB_SECONDS,
|
|
921
|
-
ageSeconds: heartbeatAgeSeconds
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
if (job.status === "queued") {
|
|
926
|
-
const runAfterAgeSeconds = secondsSince(job.runAfter);
|
|
927
|
-
if (runAfterAgeSeconds >= config.MAX_JOB_WALL_CLOCK_SECONDS) {
|
|
928
|
-
return {
|
|
929
|
-
reason: "queued_run_after_timeout",
|
|
930
|
-
thresholdSeconds: config.MAX_JOB_WALL_CLOCK_SECONDS,
|
|
931
|
-
ageSeconds: runAfterAgeSeconds
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
return null;
|
|
936
|
-
}
|
|
937
|
-
function secondsSince(value) {
|
|
938
|
-
const timestamp = value ? Date.parse(value) : Number.NaN;
|
|
939
|
-
const effectiveTimestamp = Number.isFinite(timestamp) ? timestamp : Date.now();
|
|
940
|
-
return Math.max(0, Math.floor((Date.now() - effectiveTimestamp) / 1000));
|
|
941
|
-
}
|
|
942
|
-
function customerTemplatePk(customerId, templateId) {
|
|
943
|
-
return `CUSTOMER#${customerId}#TEMPLATE#${templateId}`;
|
|
944
|
-
}
|
|
945
|
-
function customerTracerPk(customerId, tracer) {
|
|
946
|
-
return `CUSTOMER#${customerId}#TRACER#${tracer}`;
|
|
947
|
-
}
|
|
948
|
-
function jobSortKey(job) {
|
|
949
|
-
return `JOB#${job.createdAt}#${job.id}`;
|
|
950
|
-
}
|
|
951
|
-
function jobToItem(job) {
|
|
952
|
-
return {
|
|
953
|
-
pk: jobPk(job.id),
|
|
954
|
-
sk: "META",
|
|
955
|
-
entity_type: "Job",
|
|
956
|
-
gsi1pk: customerPk(job.customerId),
|
|
957
|
-
gsi1sk: jobSortKey(job),
|
|
958
|
-
gsi2pk: customerTemplatePk(job.customerId, job.templateId),
|
|
959
|
-
gsi2sk: jobSortKey(job),
|
|
960
|
-
gsi3pk: customerTracerPk(job.customerId, job.tracer),
|
|
961
|
-
gsi3sk: jobSortKey(job),
|
|
962
|
-
job_id: job.id,
|
|
963
|
-
template_id: job.templateId,
|
|
964
|
-
operation_name: job.operationName,
|
|
965
|
-
workflow_name: job.workflowName,
|
|
966
|
-
tracer: job.tracer,
|
|
967
|
-
status: job.status,
|
|
968
|
-
customer_id: job.customerId,
|
|
969
|
-
payload_json: job.payload,
|
|
970
|
-
result_json: job.result,
|
|
971
|
-
error_json: job.error,
|
|
972
|
-
progress: job.progress,
|
|
973
|
-
webhook_url: job.webhookUrl,
|
|
974
|
-
parent_job_id: job.parentJobId,
|
|
975
|
-
priority: job.priority,
|
|
976
|
-
attempt_count: job.attemptCount,
|
|
977
|
-
max_attempts: job.maxAttempts,
|
|
978
|
-
run_after: job.runAfter,
|
|
979
|
-
created_at: job.createdAt,
|
|
980
|
-
updated_at: job.updatedAt,
|
|
981
|
-
started_at: job.startedAt,
|
|
982
|
-
completed_at: job.completedAt
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
function itemToJob(item) {
|
|
986
|
-
return {
|
|
987
|
-
id: String(item.job_id),
|
|
988
|
-
templateId: String(item.template_id),
|
|
989
|
-
operationName: String(item.operation_name),
|
|
990
|
-
workflowName: String(item.workflow_name),
|
|
991
|
-
tracer: String(item.tracer),
|
|
992
|
-
status: item.status,
|
|
993
|
-
customerId: String(item.customer_id),
|
|
994
|
-
payload: asRecord(item.payload_json),
|
|
995
|
-
result: item.result_json == null ? null : asRecord(item.result_json),
|
|
996
|
-
error: item.error_json == null ? null : asRecord(item.error_json),
|
|
997
|
-
progress: typeof item.progress === "number" ? item.progress : 0,
|
|
998
|
-
webhookUrl: typeof item.webhook_url === "string" ? item.webhook_url : null,
|
|
999
|
-
parentJobId: typeof item.parent_job_id === "string" ? item.parent_job_id : null,
|
|
1000
|
-
priority: typeof item.priority === "number" ? item.priority : 100,
|
|
1001
|
-
attemptCount: typeof item.attempt_count === "number" ? item.attempt_count : 0,
|
|
1002
|
-
maxAttempts: typeof item.max_attempts === "number" ? item.max_attempts : 6,
|
|
1003
|
-
runAfter: String(item.run_after),
|
|
1004
|
-
createdAt: String(item.created_at),
|
|
1005
|
-
updatedAt: String(item.updated_at),
|
|
1006
|
-
startedAt: typeof item.started_at === "string" ? item.started_at : null,
|
|
1007
|
-
completedAt: typeof item.completed_at === "string" ? item.completed_at : null
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
function itemToLog(item) {
|
|
1011
|
-
return {
|
|
1012
|
-
id: String(item.id),
|
|
1013
|
-
jobId: String(item.job_id),
|
|
1014
|
-
level: item.level,
|
|
1015
|
-
message: String(item.message),
|
|
1016
|
-
metadata: asRecord(item.metadata_json),
|
|
1017
|
-
progress: typeof item.progress === "number" ? item.progress : null,
|
|
1018
|
-
artifactKey: typeof item.artifact_key === "string" ? item.artifact_key : null,
|
|
1019
|
-
createdAt: String(item.created_at)
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
function artifactToItem(artifact) {
|
|
1023
|
-
return {
|
|
1024
|
-
pk: jobPk(artifact.jobId),
|
|
1025
|
-
sk: `ARTIFACT#${artifact.createdAt}#${artifact.id}`,
|
|
1026
|
-
entity_type: "Artifact",
|
|
1027
|
-
id: artifact.id,
|
|
1028
|
-
artifact_id: artifact.id,
|
|
1029
|
-
job_id: artifact.jobId,
|
|
1030
|
-
customer_id: artifact.customerId,
|
|
1031
|
-
template_id: artifact.templateId,
|
|
1032
|
-
kind: artifact.kind,
|
|
1033
|
-
storage_key: artifact.storageKey,
|
|
1034
|
-
public_url: artifact.publicUrl,
|
|
1035
|
-
metadata_json: artifact.metadata,
|
|
1036
|
-
created_at: artifact.createdAt
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
function itemToArtifact(item) {
|
|
1040
|
-
return {
|
|
1041
|
-
id: String(item.artifact_id ?? item.id),
|
|
1042
|
-
jobId: String(item.job_id),
|
|
1043
|
-
customerId: String(item.customer_id),
|
|
1044
|
-
templateId: String(item.template_id),
|
|
1045
|
-
kind: String(item.kind),
|
|
1046
|
-
storageKey: String(item.storage_key),
|
|
1047
|
-
publicUrl: typeof item.public_url === "string" ? item.public_url : null,
|
|
1048
|
-
metadata: asRecord(item.metadata_json),
|
|
1049
|
-
createdAt: String(item.created_at)
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
function asRecord(value) {
|
|
1053
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
1054
|
-
? value
|
|
1055
|
-
: {};
|
|
1056
|
-
}
|
|
1057
|
-
function cursorToStartKey(input, index) {
|
|
1058
|
-
const cursor = input.cursor;
|
|
1059
|
-
if (!cursor) {
|
|
1060
|
-
return undefined;
|
|
1061
|
-
}
|
|
1062
|
-
const syntheticJob = { id: cursor.id, createdAt: cursor.createdAt };
|
|
1063
|
-
return {
|
|
1064
|
-
pk: jobPk(cursor.id),
|
|
1065
|
-
sk: "META",
|
|
1066
|
-
[`${index}pk`]: input.templateId
|
|
1067
|
-
? customerTemplatePk(input.customerId, input.templateId)
|
|
1068
|
-
: input.tracer
|
|
1069
|
-
? customerTracerPk(input.customerId, input.tracer)
|
|
1070
|
-
: customerPk(input.customerId),
|
|
1071
|
-
[`${index}sk`]: jobSortKey(syntheticJob)
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
//# sourceMappingURL=serverless-jobs.js.map
|