1295 lines
35 KiB
TypeScript
1295 lines
35 KiB
TypeScript
import { containerTest } from "@internal/testcontainers";
|
|
import { parsePacket } from "@trigger.dev/core/v3";
|
|
import { setTimeout } from "timers/promises";
|
|
import { describe } from "vitest";
|
|
import { PostgresRunStore } from "@internal/run-store";
|
|
import { UpdateMetadataService } from "~/services/metadata/updateMetadata.server";
|
|
import { MetadataTooLargeError } from "~/utils/packets";
|
|
|
|
vi.setConfig({ testTimeout: 30_000 }); // 30 seconds timeout
|
|
|
|
describe("UpdateMetadataService.call", () => {
|
|
containerTest(
|
|
"should apply operations to update the run metadata",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Now we insert a row into the table
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
const result = await service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "set",
|
|
key: "foo",
|
|
value: "bar",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "bar",
|
|
value: "baz",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
|
|
expect(result?.metadata).toEqual({
|
|
foo: "bar",
|
|
bar: ["baz"],
|
|
});
|
|
|
|
const updatedTaskRun = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: taskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: updatedTaskRun?.metadata ?? undefined,
|
|
dataType: updatedTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata).toEqual({
|
|
foo: "bar",
|
|
bar: ["baz"],
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should throw a MetadataTooLargeError when metadata is too large and not apply the operations",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024, // 1KB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Now we insert a row into the table
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
await service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage1",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage2",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage3",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage4",
|
|
},
|
|
],
|
|
});
|
|
|
|
const updatedTaskRun = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: taskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: updatedTaskRun?.metadata ?? undefined,
|
|
dataType: updatedTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata).toEqual({
|
|
logs: [
|
|
"thisisalargemessage1",
|
|
"thisisalargemessage2",
|
|
"thisisalargemessage3",
|
|
"thisisalargemessage4",
|
|
],
|
|
});
|
|
|
|
// Now lets try and add a very large log message
|
|
const callPromise = service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: Array.from({ length: 1000 }, () => "anotherlogmessage").join(""),
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(callPromise).rejects.toThrow(MetadataTooLargeError);
|
|
|
|
const updatedTaskRun2 = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: taskRun.id,
|
|
},
|
|
});
|
|
|
|
const parsedMetadata2 = JSON.parse(updatedTaskRun2?.metadata ?? "{}");
|
|
|
|
expect(parsedMetadata2).toEqual({
|
|
logs: [
|
|
"thisisalargemessage1",
|
|
"thisisalargemessage2",
|
|
"thisisalargemessage3",
|
|
"thisisalargemessage4",
|
|
],
|
|
});
|
|
|
|
// okay now lets try and add a smaller log message
|
|
await service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage5",
|
|
},
|
|
],
|
|
});
|
|
|
|
const updatedTaskRun3 = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: taskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata3 = await parsePacket({
|
|
data: updatedTaskRun3?.metadata ?? undefined,
|
|
dataType: updatedTaskRun3?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata3).toEqual({
|
|
logs: [
|
|
"thisisalargemessage1",
|
|
"thisisalargemessage2",
|
|
"thisisalargemessage3",
|
|
"thisisalargemessage4",
|
|
"thisisalargemessage5",
|
|
],
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should apply buffered operations to update the parent run metadata",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Now we insert a row into the table
|
|
const parentTaskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1235",
|
|
taskIdentifier: "my-child-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
parentTaskRunId: parentTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const result = await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "set",
|
|
key: "foo",
|
|
value: "bar",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "bar",
|
|
value: "baz",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.metadata).toEqual({});
|
|
|
|
// Wait for the buffered operations to be applied
|
|
await setTimeout(1000);
|
|
|
|
const updatedParentTaskRun = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: parentTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: updatedParentTaskRun?.metadata ?? undefined,
|
|
dataType: updatedParentTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata).toEqual({
|
|
foo: "bar",
|
|
bar: ["baz"],
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should handle applying buffered operations to update the root run metadata when the root run metadata is too large",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024, // 1KB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Create a root task run
|
|
const rootTaskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_root",
|
|
taskIdentifier: "root-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
// Create a child task run
|
|
const childTaskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_child",
|
|
taskIdentifier: "child-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
rootTaskRunId: rootTaskRun.id,
|
|
},
|
|
});
|
|
|
|
// Create a grandchild task run
|
|
const grandchildTaskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_grandchild",
|
|
taskIdentifier: "grandchild-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
parentTaskRunId: childTaskRun.id,
|
|
rootTaskRunId: rootTaskRun.id,
|
|
},
|
|
});
|
|
|
|
// Add some initial metadata to root
|
|
const result = await service.call(grandchildTaskRun.id, {
|
|
rootOperations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "initiallog1",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "initiallog2",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.metadata).toEqual({});
|
|
|
|
// Wait for the buffered operations to be applied
|
|
await setTimeout(1000);
|
|
|
|
const updatedRootTaskRun = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: rootTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: updatedRootTaskRun?.metadata ?? undefined,
|
|
dataType: updatedRootTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata).toEqual({
|
|
logs: ["initiallog1", "initiallog2"],
|
|
});
|
|
|
|
// Now try to add a very large log message to root
|
|
await service.call(grandchildTaskRun.id, {
|
|
rootOperations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: Array.from({ length: 1000 }, () => "anotherlogmessage").join(""),
|
|
},
|
|
],
|
|
});
|
|
|
|
await setTimeout(1000);
|
|
|
|
const updatedRootTaskRun2 = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: rootTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata2 = await parsePacket({
|
|
data: updatedRootTaskRun2?.metadata ?? undefined,
|
|
dataType: updatedRootTaskRun2?.metadataType ?? "application/json",
|
|
});
|
|
|
|
// The large operation should have been dropped
|
|
expect(metadata2).toEqual({
|
|
logs: ["initiallog1", "initiallog2"],
|
|
});
|
|
|
|
// Now add a smaller log message
|
|
await service.call(grandchildTaskRun.id, {
|
|
rootOperations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "smalllog3",
|
|
},
|
|
],
|
|
});
|
|
|
|
await setTimeout(1000);
|
|
|
|
const updatedRootTaskRun3 = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: rootTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata3 = await parsePacket({
|
|
data: updatedRootTaskRun3?.metadata ?? undefined,
|
|
dataType: updatedRootTaskRun3?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata3).toEqual({
|
|
logs: ["initiallog1", "initiallog2", "smalllog3"],
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should handle applying buffered operations to update the parent run metadata when the parent run metadata is too large",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024, // 1KB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Now we insert a row into the table
|
|
const parentTaskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1235",
|
|
taskIdentifier: "my-child-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
parentTaskRunId: parentTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const result = await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage1",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage2",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage3",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage4",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.metadata).toEqual({});
|
|
|
|
// Wait for the buffered operations to be applied
|
|
await setTimeout(1000);
|
|
|
|
const updatedParentTaskRun = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: parentTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: updatedParentTaskRun?.metadata ?? undefined,
|
|
dataType: updatedParentTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata).toEqual({
|
|
logs: [
|
|
"thisisalargemessage1",
|
|
"thisisalargemessage2",
|
|
"thisisalargemessage3",
|
|
"thisisalargemessage4",
|
|
],
|
|
});
|
|
|
|
// Now lets try and add a very large log message
|
|
await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: Array.from({ length: 1000 }, () => "anotherlogmessage").join(""),
|
|
},
|
|
],
|
|
});
|
|
|
|
await setTimeout(1000);
|
|
|
|
const updatedParentTaskRun2 = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: parentTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata2 = await parsePacket({
|
|
data: updatedParentTaskRun2?.metadata ?? undefined,
|
|
dataType: updatedParentTaskRun2?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata2).toEqual({
|
|
logs: [
|
|
"thisisalargemessage1",
|
|
"thisisalargemessage2",
|
|
"thisisalargemessage3",
|
|
"thisisalargemessage4",
|
|
],
|
|
});
|
|
|
|
// Now lets try and add a smaller log message
|
|
await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "thisisalargemessage5",
|
|
},
|
|
],
|
|
});
|
|
|
|
await setTimeout(1000);
|
|
|
|
const updatedParentTaskRun3 = await prisma.taskRun.findUnique({
|
|
where: {
|
|
id: parentTaskRun.id,
|
|
},
|
|
});
|
|
|
|
const metadata3 = await parsePacket({
|
|
data: updatedParentTaskRun3?.metadata ?? undefined,
|
|
dataType: updatedParentTaskRun3?.metadataType ?? "application/json",
|
|
});
|
|
|
|
expect(metadata3).toEqual({
|
|
logs: [
|
|
"thisisalargemessage1",
|
|
"thisisalargemessage2",
|
|
"thisisalargemessage3",
|
|
"thisisalargemessage4",
|
|
"thisisalargemessage5",
|
|
],
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should handle optimistic locking conflicts for buffered operations",
|
|
async ({ prisma, redisOptions }) => {
|
|
let onAfterReadCallCount = 0;
|
|
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100000, // Very long interval so we can control flushing
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
onAfterRead: async (runId, metadataVersion) => {
|
|
onAfterReadCallCount++;
|
|
// Simulate concurrent update after first read
|
|
if (onAfterReadCallCount === 1) {
|
|
await prisma.taskRun.updateMany({
|
|
where: { id: runId },
|
|
data: {
|
|
metadata: JSON.stringify({ concurrent: "update" }),
|
|
metadataVersion: { increment: 1 },
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
// Buffer some operations
|
|
await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "set",
|
|
key: "buffered",
|
|
value: "value1",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Manually trigger flush
|
|
await service.flushOperations();
|
|
|
|
// The first attempt should fail due to optimistic lock
|
|
// But it should retry and succeed
|
|
expect(onAfterReadCallCount).toBeGreaterThanOrEqual(2);
|
|
|
|
const finalTaskRun = await prisma.taskRun.findUnique({
|
|
where: { id: taskRun.id },
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: finalTaskRun?.metadata ?? undefined,
|
|
dataType: finalTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
// Should have both the concurrent update and the buffered operation
|
|
expect(metadata).toEqual({
|
|
concurrent: "update",
|
|
buffered: "value1",
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should handle optimistic locking conflicts for immediate operations",
|
|
async ({ prisma, redisOptions }) => {
|
|
let onAfterReadCallCount = 0;
|
|
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
onAfterRead: async (runId, metadataVersion) => {
|
|
onAfterReadCallCount++;
|
|
// Simulate concurrent update after each read (up to 3 times)
|
|
if (onAfterReadCallCount <= 3) {
|
|
await prisma.taskRun.updateMany({
|
|
where: { id: runId },
|
|
data: {
|
|
metadata: JSON.stringify({ concurrent: `update${onAfterReadCallCount}` }),
|
|
metadataVersion: { increment: 1 },
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
// Try immediate operations - should retry 3 times then buffer
|
|
const result = await service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "set",
|
|
key: "immediate",
|
|
value: "value1",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Should have retried 3 times
|
|
expect(onAfterReadCallCount).toBe(4); // Initial read + 3 retries
|
|
|
|
// The result should optimistically return the applied operations
|
|
expect(result?.metadata).toEqual({
|
|
concurrent: "update3",
|
|
immediate: "value1",
|
|
});
|
|
|
|
// Wait for buffered operations to be flushed
|
|
await setTimeout(1000);
|
|
|
|
const finalTaskRun = await prisma.taskRun.findUnique({
|
|
where: { id: taskRun.id },
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: finalTaskRun?.metadata ?? undefined,
|
|
dataType: finalTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
// Should eventually have the immediate operation applied
|
|
expect(metadata).toEqual({
|
|
concurrent: "update3",
|
|
immediate: "value1",
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should cull set operations keeping only the latest value for each key",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100000, // Very long interval so we can control flushing
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
},
|
|
});
|
|
|
|
// Buffer multiple set operations for the same key
|
|
await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "set",
|
|
key: "status",
|
|
value: "starting",
|
|
},
|
|
],
|
|
});
|
|
|
|
await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "set",
|
|
key: "status",
|
|
value: "running",
|
|
},
|
|
{
|
|
type: "set",
|
|
key: "progress",
|
|
value: 50,
|
|
},
|
|
],
|
|
});
|
|
|
|
await service.call(taskRun.id, {
|
|
parentOperations: [
|
|
{
|
|
type: "set",
|
|
key: "status",
|
|
value: "completed",
|
|
},
|
|
{
|
|
type: "set",
|
|
key: "progress",
|
|
value: 100,
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "Task started",
|
|
},
|
|
{
|
|
type: "append",
|
|
key: "logs",
|
|
value: "Task completed",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Manually trigger flush
|
|
await service.flushOperations();
|
|
|
|
const finalTaskRun = await prisma.taskRun.findUnique({
|
|
where: { id: taskRun.id },
|
|
});
|
|
|
|
const metadata = await parsePacket({
|
|
data: finalTaskRun?.metadata ?? undefined,
|
|
dataType: finalTaskRun?.metadataType ?? "application/json",
|
|
});
|
|
|
|
// Should only have the latest set values, but all append operations
|
|
expect(metadata).toEqual({
|
|
status: "completed",
|
|
progress: 100,
|
|
logs: ["Task started", "Task completed"],
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should not update run metadata if completed more than 1 hour ago",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Create a task run completed more than 1 hour ago
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
completedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
|
},
|
|
});
|
|
|
|
// Try to update metadata - should throw
|
|
await expect(
|
|
service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "set",
|
|
key: "status",
|
|
value: "updated",
|
|
},
|
|
],
|
|
})
|
|
).rejects.toThrow("Cannot update metadata for a completed run");
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"should allow updating run metadata if completed within 1 hour",
|
|
async ({ prisma, redisOptions }) => {
|
|
const service = new UpdateMetadataService({
|
|
prisma,
|
|
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
|
|
flushIntervalMs: 100,
|
|
flushEnabled: true,
|
|
flushLoggingEnabled: true,
|
|
maximumSize: 1024 * 1024 * 1, // 1MB
|
|
logLevel: "debug",
|
|
});
|
|
|
|
const organization = await prisma.organization.create({
|
|
data: {
|
|
title: "test",
|
|
slug: "test",
|
|
},
|
|
});
|
|
|
|
const project = await prisma.project.create({
|
|
data: {
|
|
name: "test",
|
|
slug: "test",
|
|
organizationId: organization.id,
|
|
externalRef: "test",
|
|
},
|
|
});
|
|
|
|
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: "test",
|
|
type: "DEVELOPMENT",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: "test",
|
|
pkApiKey: "test",
|
|
shortcode: "test",
|
|
},
|
|
});
|
|
|
|
// Create a task run completed 30 minutes ago
|
|
const taskRun = await prisma.taskRun.create({
|
|
data: {
|
|
friendlyId: "run_1234",
|
|
taskIdentifier: "my-task",
|
|
payload: JSON.stringify({ foo: "bar" }),
|
|
traceId: "1234",
|
|
spanId: "1234",
|
|
queue: "test",
|
|
runtimeEnvironmentId: runtimeEnvironment.id,
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
completedAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
|
|
},
|
|
});
|
|
|
|
// Should be able to update metadata
|
|
const result = await service.call(taskRun.id, {
|
|
operations: [
|
|
{
|
|
type: "set",
|
|
key: "status",
|
|
value: "updated",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result?.metadata).toEqual({
|
|
status: "updated",
|
|
});
|
|
|
|
service.stopFlushing();
|
|
}
|
|
);
|
|
});
|