@qianxude/tem 0.2.0 → 0.4.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/package.json +6 -1
- package/src/cli/commands/list.ts +141 -0
- package/src/cli/commands/report.ts +453 -0
- package/src/cli/commands/watch.ts +561 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/utils/format.ts +77 -0
- package/src/cli/utils/table.ts +116 -0
- package/src/core/tem.ts +29 -1
- package/src/core/worker.ts +78 -6
- package/src/database/index.ts +47 -7
- package/src/index.ts +1 -1
- package/src/interfaces/index.ts +60 -0
- package/src/services/batch-interruption.ts +192 -0
- package/src/services/batch.ts +32 -4
- package/src/services/index.ts +1 -0
- package/src/utils/auto-detect.ts +5 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as i from '../interfaces/index.js';
|
|
2
|
+
import type { Database } from '../database/index.js';
|
|
3
|
+
import type { BatchService } from './batch.js';
|
|
4
|
+
|
|
5
|
+
export interface BatchInterruptionRow {
|
|
6
|
+
batch_id: string;
|
|
7
|
+
reason: i.BatchInterruptionReason;
|
|
8
|
+
message: string;
|
|
9
|
+
stats_snapshot: string;
|
|
10
|
+
created_at: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class BatchInterruptionService implements i.BatchInterruptionService {
|
|
14
|
+
constructor(
|
|
15
|
+
private db: Database,
|
|
16
|
+
private batchService: BatchService
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if batch should be interrupted based on current stats.
|
|
21
|
+
* Called after each task failure or periodically.
|
|
22
|
+
*/
|
|
23
|
+
async checkAndInterruptIfNeeded(
|
|
24
|
+
batchId: string,
|
|
25
|
+
context: {
|
|
26
|
+
consecutiveFailures?: number;
|
|
27
|
+
rateLimitHits?: number;
|
|
28
|
+
concurrencyErrors?: number;
|
|
29
|
+
currentTaskRuntimeMs?: number;
|
|
30
|
+
}
|
|
31
|
+
): Promise<boolean> {
|
|
32
|
+
// Fetch batch with its interruption criteria
|
|
33
|
+
const { batch, criteria } = await this.batchService.getWithCriteria(batchId);
|
|
34
|
+
|
|
35
|
+
// If already interrupted or completed, no need to check
|
|
36
|
+
if (batch.status !== 'active') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If no criteria set, never interrupt
|
|
41
|
+
if (!criteria) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get current stats
|
|
46
|
+
const stats = await this.batchService.getStats(batchId);
|
|
47
|
+
|
|
48
|
+
// Check each criterion in order of severity
|
|
49
|
+
|
|
50
|
+
// 1. Check maxBatchRuntimeMs - total batch runtime
|
|
51
|
+
if (criteria.maxBatchRuntimeMs) {
|
|
52
|
+
const batchRuntimeMs = Date.now() - batch.createdAt.getTime();
|
|
53
|
+
if (batchRuntimeMs > criteria.maxBatchRuntimeMs) {
|
|
54
|
+
await this.interrupt(
|
|
55
|
+
batchId,
|
|
56
|
+
'batch_runtime_exceeded',
|
|
57
|
+
`Batch runtime (${batchRuntimeMs}ms) exceeded maximum (${criteria.maxBatchRuntimeMs}ms)`
|
|
58
|
+
);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Check taskTimeoutMs - single task runtime
|
|
64
|
+
if (criteria.taskTimeoutMs && context.currentTaskRuntimeMs) {
|
|
65
|
+
if (context.currentTaskRuntimeMs > criteria.taskTimeoutMs) {
|
|
66
|
+
await this.interrupt(
|
|
67
|
+
batchId,
|
|
68
|
+
'task_timeout',
|
|
69
|
+
`Task runtime (${context.currentTaskRuntimeMs}ms) exceeded maximum (${criteria.taskTimeoutMs}ms)`
|
|
70
|
+
);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. Check maxConsecutiveFailures
|
|
76
|
+
if (criteria.maxConsecutiveFailures && context.consecutiveFailures) {
|
|
77
|
+
if (context.consecutiveFailures >= criteria.maxConsecutiveFailures) {
|
|
78
|
+
await this.interrupt(
|
|
79
|
+
batchId,
|
|
80
|
+
'consecutive_failures_exceeded',
|
|
81
|
+
`Consecutive failures (${context.consecutiveFailures}) exceeded maximum (${criteria.maxConsecutiveFailures})`
|
|
82
|
+
);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. Check maxRateLimitHits
|
|
88
|
+
if (criteria.maxRateLimitHits && context.rateLimitHits) {
|
|
89
|
+
if (context.rateLimitHits >= criteria.maxRateLimitHits) {
|
|
90
|
+
await this.interrupt(
|
|
91
|
+
batchId,
|
|
92
|
+
'rate_limit_hits_exceeded',
|
|
93
|
+
`Rate limit hits (${context.rateLimitHits}) exceeded maximum (${criteria.maxRateLimitHits})`
|
|
94
|
+
);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 5. Check maxConcurrencyErrors (502/503 errors)
|
|
100
|
+
if (criteria.maxConcurrencyErrors && context.concurrencyErrors) {
|
|
101
|
+
if (context.concurrencyErrors >= criteria.maxConcurrencyErrors) {
|
|
102
|
+
await this.interrupt(
|
|
103
|
+
batchId,
|
|
104
|
+
'concurrency_errors_exceeded',
|
|
105
|
+
`Concurrency errors (${context.concurrencyErrors}) exceeded maximum (${criteria.maxConcurrencyErrors})`
|
|
106
|
+
);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 6. Check maxFailedTasks (absolute count)
|
|
112
|
+
if (criteria.maxFailedTasks) {
|
|
113
|
+
if (stats.failed >= criteria.maxFailedTasks) {
|
|
114
|
+
await this.interrupt(
|
|
115
|
+
batchId,
|
|
116
|
+
'failed_tasks_exceeded',
|
|
117
|
+
`Failed tasks (${stats.failed}) exceeded maximum (${criteria.maxFailedTasks})`
|
|
118
|
+
);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 7. Check maxErrorRate (percentage)
|
|
124
|
+
if (criteria.maxErrorRate && stats.total > 0) {
|
|
125
|
+
const errorRate = stats.failed / stats.total;
|
|
126
|
+
if (errorRate > criteria.maxErrorRate) {
|
|
127
|
+
await this.interrupt(
|
|
128
|
+
batchId,
|
|
129
|
+
'error_rate_exceeded',
|
|
130
|
+
`Error rate (${(errorRate * 100).toFixed(1)}%) exceeded maximum (${(criteria.maxErrorRate * 100).toFixed(1)}%)`
|
|
131
|
+
);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Interrupt a batch atomically.
|
|
141
|
+
*/
|
|
142
|
+
async interrupt(
|
|
143
|
+
batchId: string,
|
|
144
|
+
reason: i.BatchInterruptionReason,
|
|
145
|
+
message: string
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
// Get current stats for the log
|
|
148
|
+
const stats = await this.batchService.getStats(batchId);
|
|
149
|
+
|
|
150
|
+
// Update batch status to 'interrupted'
|
|
151
|
+
await this.batchService.updateStatus(batchId, 'interrupted');
|
|
152
|
+
|
|
153
|
+
// Log the interruption event
|
|
154
|
+
const id = crypto.randomUUID();
|
|
155
|
+
const now = new Date().toISOString();
|
|
156
|
+
|
|
157
|
+
this.db.run(
|
|
158
|
+
`INSERT INTO batch_interrupt_log (id, batch_id, reason, message, stats_snapshot, created_at)
|
|
159
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
160
|
+
[id, batchId, reason, message, JSON.stringify(stats), now]
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if a batch is active (can claim tasks from it).
|
|
166
|
+
*/
|
|
167
|
+
async isBatchActive(batchId: string): Promise<boolean> {
|
|
168
|
+
const batch = await this.batchService.getById(batchId);
|
|
169
|
+
return batch?.status === 'active';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get interruption history for a batch.
|
|
174
|
+
*/
|
|
175
|
+
async getInterruptionLog(batchId: string): Promise<i.BatchInterruption[]> {
|
|
176
|
+
const rows = this.db.query<BatchInterruptionRow>(
|
|
177
|
+
`SELECT batch_id, reason, message, stats_snapshot, created_at
|
|
178
|
+
FROM batch_interrupt_log
|
|
179
|
+
WHERE batch_id = ?
|
|
180
|
+
ORDER BY created_at DESC`,
|
|
181
|
+
[batchId]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return rows.map((row) => ({
|
|
185
|
+
batchId: row.batch_id,
|
|
186
|
+
reason: row.reason,
|
|
187
|
+
message: row.message,
|
|
188
|
+
statsAtInterruption: JSON.parse(row.stats_snapshot) as i.BatchStats,
|
|
189
|
+
createdAt: new Date(row.created_at),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/services/batch.ts
CHANGED
|
@@ -5,9 +5,11 @@ export interface BatchRow {
|
|
|
5
5
|
id: string;
|
|
6
6
|
code: string;
|
|
7
7
|
type: string;
|
|
8
|
+
status: i.BatchStatus;
|
|
8
9
|
created_at: string;
|
|
9
10
|
completed_at: string | null;
|
|
10
11
|
metadata: string | null;
|
|
12
|
+
interruption_criteria: string | null;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
function rowToBatch(row: BatchRow): i.Batch {
|
|
@@ -15,9 +17,13 @@ function rowToBatch(row: BatchRow): i.Batch {
|
|
|
15
17
|
id: row.id,
|
|
16
18
|
code: row.code,
|
|
17
19
|
type: row.type,
|
|
20
|
+
status: row.status ?? 'active',
|
|
18
21
|
createdAt: new Date(row.created_at),
|
|
19
22
|
completedAt: row.completed_at ? new Date(row.completed_at) : null,
|
|
20
23
|
metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, unknown>) : null,
|
|
24
|
+
interruptionCriteria: row.interruption_criteria
|
|
25
|
+
? (JSON.parse(row.interruption_criteria) as i.BatchInterruptionCriteria)
|
|
26
|
+
: null,
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
29
|
|
|
@@ -29,9 +35,16 @@ export class BatchService implements i.BatchService {
|
|
|
29
35
|
const now = new Date().toISOString();
|
|
30
36
|
|
|
31
37
|
this.db.run(
|
|
32
|
-
`INSERT INTO batch (id, code, type, created_at, metadata)
|
|
33
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
34
|
-
[
|
|
38
|
+
`INSERT INTO batch (id, code, type, created_at, metadata, interruption_criteria)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
40
|
+
[
|
|
41
|
+
id,
|
|
42
|
+
input.code,
|
|
43
|
+
input.type,
|
|
44
|
+
now,
|
|
45
|
+
input.metadata ? JSON.stringify(input.metadata) : null,
|
|
46
|
+
input.interruptionCriteria ? JSON.stringify(input.interruptionCriteria) : null,
|
|
47
|
+
]
|
|
35
48
|
);
|
|
36
49
|
|
|
37
50
|
const rows = this.db.query<BatchRow>('SELECT * FROM batch WHERE id = ?', [id]);
|
|
@@ -60,7 +73,7 @@ export class BatchService implements i.BatchService {
|
|
|
60
73
|
async complete(id: string): Promise<void> {
|
|
61
74
|
const now = new Date().toISOString();
|
|
62
75
|
this.db.run(
|
|
63
|
-
`UPDATE batch SET completed_at =
|
|
76
|
+
`UPDATE batch SET completed_at = ?, status = 'completed' WHERE id = ?`,
|
|
64
77
|
[now, id]
|
|
65
78
|
);
|
|
66
79
|
}
|
|
@@ -118,4 +131,19 @@ export class BatchService implements i.BatchService {
|
|
|
118
131
|
);
|
|
119
132
|
return result.changes ?? 0;
|
|
120
133
|
}
|
|
134
|
+
|
|
135
|
+
async updateStatus(id: string, status: i.BatchStatus): Promise<void> {
|
|
136
|
+
this.db.run(
|
|
137
|
+
`UPDATE batch SET status = ? WHERE id = ?`,
|
|
138
|
+
[status, id]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async getWithCriteria(id: string): Promise<{ batch: i.Batch; criteria: i.BatchInterruptionCriteria | null }> {
|
|
143
|
+
const batch = await this.getById(id);
|
|
144
|
+
if (!batch) {
|
|
145
|
+
throw new Error(`Batch not found: ${id}`);
|
|
146
|
+
}
|
|
147
|
+
return { batch, criteria: batch.interruptionCriteria };
|
|
148
|
+
}
|
|
121
149
|
}
|
package/src/services/index.ts
CHANGED
package/src/utils/auto-detect.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface DetectOptions {
|
|
|
6
6
|
timeoutMs?: number;
|
|
7
7
|
maxConcurrencyToTest?: number;
|
|
8
8
|
rateLimitTestDurationMs?: number;
|
|
9
|
+
maxRateLimitTestRequests?: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface DetectedConfig {
|
|
@@ -48,6 +49,7 @@ export async function detectConstraints(options: DetectOptions): Promise<Detecte
|
|
|
48
49
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
49
50
|
const maxConcurrency = options.maxConcurrencyToTest ?? DEFAULT_MAX_CONCURRENCY;
|
|
50
51
|
const rateLimitDurationMs = options.rateLimitTestDurationMs ?? DEFAULT_RATE_LIMIT_TEST_DURATION_MS;
|
|
52
|
+
const maxRateLimitTestRequests = options.maxRateLimitTestRequests ?? MAX_RATE_LIMIT_TEST_REQUESTS;
|
|
51
53
|
|
|
52
54
|
const requestOptions: RequestOptions = {
|
|
53
55
|
url: options.url,
|
|
@@ -64,7 +66,7 @@ export async function detectConstraints(options: DetectOptions): Promise<Detecte
|
|
|
64
66
|
|
|
65
67
|
// Phase 2: Detect rate limit using safe concurrency (80% of detected)
|
|
66
68
|
const safeConcurrency = Math.max(1, Math.floor(detectedConcurrency * 0.8));
|
|
67
|
-
const rateLimitResult = await detectRateLimit(requestOptions, safeConcurrency, rateLimitDurationMs, notes);
|
|
69
|
+
const rateLimitResult = await detectRateLimit(requestOptions, safeConcurrency, rateLimitDurationMs, maxRateLimitTestRequests, notes);
|
|
68
70
|
|
|
69
71
|
// Calculate confidence
|
|
70
72
|
const confidence = calculateConfidence(detectedConcurrency, rateLimitResult, notes);
|
|
@@ -206,6 +208,7 @@ async function detectRateLimit(
|
|
|
206
208
|
options: RequestOptions,
|
|
207
209
|
safeConcurrency: number,
|
|
208
210
|
durationMs: number,
|
|
211
|
+
maxRequests: number,
|
|
209
212
|
notes: string[]
|
|
210
213
|
): Promise<{ requests: number; windowMs: number }> {
|
|
211
214
|
const startTime = Date.now();
|
|
@@ -214,7 +217,7 @@ async function detectRateLimit(
|
|
|
214
217
|
let totalRequests = 0;
|
|
215
218
|
|
|
216
219
|
// Send requests as fast as possible at safe concurrency
|
|
217
|
-
while (Date.now() - startTime < durationMs && totalRequests <
|
|
220
|
+
while (Date.now() - startTime < durationMs && totalRequests < maxRequests) {
|
|
218
221
|
const batchStart = Date.now();
|
|
219
222
|
const results = await sendBatch(options, safeConcurrency);
|
|
220
223
|
|