@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.
@@ -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
+ }
@@ -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
- [id, input.code, input.type, now, input.metadata ? JSON.stringify(input.metadata) : null]
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 = ? WHERE id = ?`,
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
  }
@@ -1,2 +1,3 @@
1
1
  export { BatchService } from './batch.js';
2
2
  export { TaskService } from './task.js';
3
+ export { BatchInterruptionService } from './batch-interruption.js';
@@ -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 < MAX_RATE_LIMIT_TEST_REQUESTS) {
220
+ while (Date.now() - startTime < durationMs && totalRequests < maxRequests) {
218
221
  const batchStart = Date.now();
219
222
  const results = await sendBatch(options, safeConcurrency);
220
223