Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 6 additions & 55 deletions src/gateway/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import type { TimerScanner } from "../services/timer-scanner.js";
import type { PipelineWorker } from "../services/pipeline-worker.js";
import type { StatefulPipelineManager } from "../utils/stateful-pipeline-manager.js";
import type { PipelineLogger } from "../utils/pipeline-factory.js";
import { createLocalTimerTask } from "./timer-routing.js";

const TAG = "[tdai-gateway]";
const VERSION = "0.1.0";
Expand Down Expand Up @@ -1134,63 +1135,13 @@ export class TdaiGateway {
type: backendType,
local: backendType === "local" ? {
onTimerExpired: (entry) => {
// Parse timer member by prefix: "offload-{type}:{instanceId}:{sessionId}[:{extra}]"
// or legacy "session:L2_schedule"
const member = entry.member;
let taskType: string;
let instanceId: string;
let sessionId: string;

const firstColon = member.indexOf(":");
const prefix = firstColon > 0 ? member.slice(0, firstColon) : member;

if (prefix === "offload-l1" || prefix === "offload-l15" || prefix === "offload-l2") {
taskType = prefix;
// Format: "offload-{type}:{instanceId}:{sessionId}[:{mmdFile}]"
// instanceId is the segment right after the prefix
const rest = member.slice(firstColon + 1);
const instanceEnd = rest.indexOf(":");
if (instanceEnd > 0) {
instanceId = rest.slice(0, instanceEnd);
sessionId = rest.slice(instanceEnd + 1);
} else {
instanceId = this.config.instanceId ?? "default";
sessionId = rest;
}
// For offload-l2: strip trailing ":{mmdFile}" from sessionId
// (mmdFile is extracted separately from timerMember in the executor)
if (prefix === "offload-l2" && sessionId.endsWith(".mmd")) {
const lastColon = sessionId.lastIndexOf(":");
if (lastColon > 0) {
sessionId = sessionId.slice(0, lastColon);
}
}
} else {
// Legacy format: "sessionId:L2_schedule" or "sessionId:L1_idle"
const lastColon = member.lastIndexOf(":");
const suffix = lastColon >= 0 ? member.slice(lastColon + 1) : "";
sessionId = lastColon >= 0 ? member.slice(0, lastColon) : member;
taskType = suffix === "L2_schedule" ? "offload-l2" : suffix === "L1_idle" ? "offload-l1" : "L3";
instanceId = this.config.instanceId ?? "default";
}
const now = Date.now();
// Extract targetMmdFile from member for offload-l2 (needed by pipeline-worker lockKey)
let targetMmdFile: string | undefined;
if (taskType === "offload-l2") {
const mmdMatch = member.match(/(\d+-[^:]+\.mmd)$/);
if (mmdMatch) targetMmdFile = mmdMatch[1];
}
const task = {
id: `${taskType}-${sessionId}-${now}`,
type: taskType as any,
instanceId,
sessionId,
priority: 0,
createdAt: now,
data: { triggeredBy: "timer_scanner", timerMember: member, instanceId, targetMmdFile },
};
const task = createLocalTimerTask({
entry,
defaultInstanceId: this.config.instanceId ?? "default",
});
this.stateBackend!.enqueueTask(task).then(() => {
this.logger.info(`[local-timer] Timer fired: ${member} → enqueued ${taskType} task`);
this.logger.info(`[local-timer] Timer fired: ${member} → enqueued ${task.type} task`);
}).catch((err) => {
this.logger.error(`[local-timer] Failed to enqueue task for ${member}: ${err instanceof Error ? err.message : String(err)}`);
});
Expand Down
62 changes: 62 additions & 0 deletions src/gateway/timer-routing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { createLocalTimerTask } from "./timer-routing.js";

describe("createLocalTimerTask", () => {
it("routes legacy L2_schedule timers to the scene extraction L2 worker", () => {
const task = createLocalTimerTask({
entry: { member: "session-a:L2_schedule", fireAtMs: 123 },
defaultInstanceId: "default-inst",
now: 456,
});

expect(task).toMatchObject({
id: "L2-session-a-456",
type: "L2",
instanceId: "default-inst",
sessionId: "session-a",
priority: 0,
createdAt: 456,
data: {
triggeredBy: "timer_scanner",
timerMember: "session-a:L2_schedule",
instanceId: "default-inst",
targetMmdFile: undefined,
},
});
});

it("routes legacy L1_idle timers to the normal L1 worker", () => {
const task = createLocalTimerTask({
entry: { member: "session-a:L1_idle", fireAtMs: 123 },
defaultInstanceId: "default-inst",
now: 456,
});

expect(task.type).toBe("L1");
expect(task.id).toBe("L1-session-a-456");
expect(task.sessionId).toBe("session-a");
});

it("keeps explicit offload-l2 timers on the offload worker", () => {
const task = createLocalTimerTask({
entry: {
member: "offload-l2:tenant-1:session-a:123456-topic.mmd",
fireAtMs: 123,
},
defaultInstanceId: "default-inst",
now: 456,
});

expect(task).toMatchObject({
id: "offload-l2-session-a-456",
type: "offload-l2",
instanceId: "tenant-1",
sessionId: "session-a",
data: {
timerMember: "offload-l2:tenant-1:session-a:123456-topic.mmd",
instanceId: "tenant-1",
targetMmdFile: "123456-topic.mmd",
},
});
});
});
59 changes: 59 additions & 0 deletions src/gateway/timer-routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { TaskPayload, TimerEntry } from "../core/state/types.js";

export interface LocalTimerTaskOptions {
entry: TimerEntry;
defaultInstanceId: string;
now?: number;
}

export function createLocalTimerTask(options: LocalTimerTaskOptions): TaskPayload {
const member = options.entry.member;
const now = options.now ?? Date.now();
let taskType: TaskPayload["type"];
let instanceId: string;
let sessionId: string;

const firstColon = member.indexOf(":");
const prefix = firstColon > 0 ? member.slice(0, firstColon) : member;

if (prefix === "offload-l1" || prefix === "offload-l15" || prefix === "offload-l2") {
taskType = prefix;
const rest = member.slice(firstColon + 1);
const instanceEnd = rest.indexOf(":");
if (instanceEnd > 0) {
instanceId = rest.slice(0, instanceEnd);
sessionId = rest.slice(instanceEnd + 1);
} else {
instanceId = options.defaultInstanceId;
sessionId = rest;
}
if (prefix === "offload-l2" && sessionId.endsWith(".mmd")) {
const lastColon = sessionId.lastIndexOf(":");
if (lastColon > 0) {
sessionId = sessionId.slice(0, lastColon);
}
}
} else {
const lastColon = member.lastIndexOf(":");
const suffix = lastColon >= 0 ? member.slice(lastColon + 1) : "";
sessionId = lastColon >= 0 ? member.slice(0, lastColon) : member;
taskType = suffix === "L2_schedule" ? "L2" : suffix === "L1_idle" ? "L1" : "L3";
instanceId = options.defaultInstanceId;
}

let targetMmdFile: string | undefined;
if (taskType === "offload-l2") {
const mmdMatch = member.match(/(\d+-[^:]+\.mmd)$/);
if (mmdMatch) targetMmdFile = mmdMatch[1];
}

return {
id: `${taskType}-${sessionId}-${now}`,
type: taskType,
instanceId,
sessionId,
priority: 0,
createdAt: now,
data: { triggeredBy: "timer_scanner", timerMember: member, instanceId, targetMmdFile },
};
}