Skip to content

Commit f2f7d5e

Browse files
committed
feat: 添加 FSC Gateway Daemon(中央调度器)
1 parent a708c26 commit f2f7d5e

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

fsc/fsc-gateway-daemon.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* FSC Gateway Daemon(中央调度器)
4+
*
5+
* 功能:
6+
* - 接收任务(来自 API/CLI)
7+
* - 分发任务到 Redis 队列
8+
* - 收集结果
9+
* - Session 管理
10+
*/
11+
12+
import { createClient } from 'redis';
13+
import winston from 'winston';
14+
15+
// ============ 配置 ============
16+
const REDIS_HOST = process.env.REDIS_HOST || '127.0.0.1';
17+
const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379');
18+
const TASK_QUEUE = 'fsc:task_queue';
19+
const RESULT_QUEUE = 'fsc:result_queue';
20+
const FAILED_QUEUE = 'fsc:failed_tasks';
21+
22+
// ============ Logger ============
23+
const logger = winston.createLogger({
24+
level: 'info',
25+
format: winston.format.combine(
26+
winston.format.timestamp(),
27+
winston.format.json()
28+
),
29+
transports: [
30+
new winston.transports.Console({
31+
format: winston.format.combine(
32+
winston.format.colorize(),
33+
winston.format.simple()
34+
)
35+
}),
36+
new winston.transports.File({ filename: 'fsc-gateway.log' })
37+
]
38+
});
39+
40+
// ============ Redis Client ============
41+
const redis = createClient({
42+
socket: {
43+
host: REDIS_HOST,
44+
port: REDIS_PORT,
45+
reconnectStrategy: (retries) => {
46+
if (retries > 10) {
47+
logger.error('Redis reconnect failed after 10 attempts');
48+
return new Error('Max reconnect attempts reached');
49+
}
50+
return Math.min(retries * 100, 3000);
51+
}
52+
}
53+
});
54+
55+
redis.on('error', (err) => logger.error('Redis error:', err));
56+
redis.on('connect', () => logger.info('Redis connected'));
57+
redis.on('reconnecting', () => logger.warn('Redis reconnecting...'));
58+
59+
// ============ 任务类型 ============
60+
interface Task {
61+
id: string;
62+
image: string;
63+
commands: string[];
64+
timeoutSeconds?: number;
65+
}
66+
67+
interface TaskResult {
68+
taskId: string;
69+
status: 'success' | 'failure' | 'timeout';
70+
output?: string;
71+
error?: string;
72+
timestamp: number;
73+
}
74+
75+
// ============ 提交任务 ============
76+
async function submitTask(task: Task): Promise<string> {
77+
logger.info(`[Gateway] Submitting task ${task.id}`);
78+
await redis.rPush(TASK_QUEUE, JSON.stringify(task));
79+
return task.id;
80+
}
81+
82+
// ============ 结果收集循环 ============
83+
let isShuttingDown = false;
84+
85+
async function resultCollectorLoop() {
86+
logger.info('Result collector starting...');
87+
88+
while (!isShuttingDown) {
89+
try {
90+
const result = await redis.blPop(RESULT_QUEUE, 5);
91+
92+
if (!result) {
93+
continue;
94+
}
95+
96+
const taskResult = JSON.parse(result.element) as TaskResult;
97+
logger.info(`[Gateway] Received result for task ${taskResult.taskId}: ${taskResult.status}`);
98+
99+
// TODO: 持久化结果、通知订阅者、更新 Session
100+
// 暂时先打日志
101+
} catch (error) {
102+
logger.error('Result collector error:', error);
103+
await new Promise(resolve => setTimeout(resolve, 1000));
104+
}
105+
}
106+
107+
logger.info('Result collector exited');
108+
}
109+
110+
// ============ 健康检查 ============
111+
setInterval(async () => {
112+
try {
113+
const queueLen = await redis.lLen(TASK_QUEUE);
114+
const resultLen = await redis.lLen(RESULT_QUEUE);
115+
const failedLen = await redis.lLen(FAILED_QUEUE);
116+
117+
await redis.set('fsc:gateway:health', JSON.stringify({
118+
timestamp: Date.now(),
119+
queues: {
120+
task: queueLen,
121+
result: resultLen,
122+
failed: failedLen
123+
}
124+
}), { EX: 60 });
125+
} catch (error) {
126+
logger.error('Health check failed:', error);
127+
}
128+
}, 30000);
129+
130+
// ============ 优雅退出 ============
131+
async function shutdown(signal: string) {
132+
logger.info(`Received ${signal}, shutting down gracefully...`);
133+
isShuttingDown = true;
134+
await redis.quit();
135+
logger.info('Shutdown complete');
136+
process.exit(0);
137+
}
138+
139+
process.on('SIGTERM', () => shutdown('SIGTERM'));
140+
process.on('SIGINT', () => shutdown('SIGINT'));
141+
142+
// ============ 启动 ============
143+
async function main() {
144+
logger.info('FSC Gateway Daemon starting...');
145+
await redis.connect();
146+
resultCollectorLoop().catch((error) => {
147+
logger.error('Fatal error in result collector:', error);
148+
process.exit(1);
149+
});
150+
}
151+
152+
main().catch((error) => {
153+
logger.error('Fatal error:', error);
154+
process.exit(1);
155+
});

0 commit comments

Comments
 (0)