forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathocr-queue.service.ts
More file actions
299 lines (256 loc) · 8.14 KB
/
Copy pathocr-queue.service.ts
File metadata and controls
299 lines (256 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { InjectQueue } from "@nestjs/bull";
import { InjectRepository } from "@nestjs/typeorm";
import { Queue, Job } from "bull";
import { Repository } from "typeorm";
import { OcrJob, OcrJobStatus } from "./entities/ocr-job.entity";
import { OcrService } from "./ocr.service";
import { CreateOcrJobDto } from "./dto/ocr-job.dto";
import { OcrJobData } from "./ocr.processor";
/**
* OCR Queue Service
* Handles OCR job creation, status tracking, and queue management
*/
@Injectable()
export class OcrQueueService {
private readonly logger = new Logger(OcrQueueService.name);
constructor(
@InjectQueue('ocr')
private readonly ocrQueue: Queue<OcrJobData>,
@InjectRepository(OcrJob)
private readonly ocrJobRepository: Repository<OcrJob>,
private readonly ocrService: OcrService,
) {}
/**
* Create a new OCR job and add it to the queue
*/
async createJob(dto: CreateOcrJobDto, imageBuffer?: Buffer): Promise<OcrJob> {
// Create the job record in the database
const ocrJob = this.ocrJobRepository.create({
itemId: dto.itemId,
splitId: dto.splitId,
uploadedBy: dto.uploadedBy,
originalFilename: dto.originalFilename,
imageUrl: dto.imageUrl,
status: OcrJobStatus.PENDING,
progress: 0,
retryCount: 0,
maxRetries: 3,
needsManualReview: false,
});
const savedJob = await this.ocrJobRepository.save(ocrJob);
this.logger.log(`Created OCR job ${savedJob.id}`);
// Prepare job data
const jobData: OcrJobData = {
jobId: savedJob.id,
imageBuffer: imageBuffer ? imageBuffer.toString('base64') : undefined,
imageUrl: dto.imageUrl,
itemId: dto.itemId,
splitId: dto.splitId,
uploadedBy: dto.uploadedBy,
originalFilename: dto.originalFilename,
};
// Add job to queue with priority
const job = await this.ocrQueue.add(jobData, {
priority: dto.priority ?? 1,
attempts: 3, // Bull retry attempts
backoff: {
type: 'exponential',
delay: 2000, // Start with 2 seconds delay
},
removeOnComplete: false, // Keep completed jobs for history
removeOnFail: false, // Keep failed jobs for debugging
});
// Update queue job ID (convert to string)
savedJob.queueJobId = String(job.id);
await this.ocrJobRepository.save(savedJob);
this.logger.log(`Added OCR job ${savedJob.id} to queue with priority ${dto.priority ?? 1}`);
return savedJob;
}
/**
* Get OCR job status and details
*/
async getJob(jobId: string): Promise<OcrJob> {
const job = await this.ocrJobRepository.findOne({
where: { id: jobId },
});
if (!job) {
throw new NotFoundException(`OCR job ${jobId} not found`);
}
return job;
}
/**
* Get all OCR jobs for a specific item
*/
async getJobsByItem(itemId: string): Promise<OcrJob[]> {
return this.ocrJobRepository.find({
where: { itemId },
order: { createdAt: 'DESC' },
});
}
/**
* Get all OCR jobs that need manual review
*/
async getJobsNeedingReview(): Promise<OcrJob[]> {
return this.ocrJobRepository.find({
where: { needsManualReview: true },
order: { createdAt: 'DESC' },
});
}
/**
* Get failed jobs that might be retried
*/
async getFailedJobs(): Promise<OcrJob[]> {
return this.ocrJobRepository.find({
where: { status: OcrJobStatus.FAILED },
order: { createdAt: 'DESC' },
});
}
/**
* Retry a failed OCR job
*/
async retryJob(jobId: string): Promise<OcrJob> {
const job = await this.ocrJobRepository.findOne({
where: { id: jobId },
});
if (!job) {
throw new NotFoundException(`OCR job ${jobId} not found`);
}
if (job.status !== OcrJobStatus.FAILED) {
throw new Error(`Cannot retry job in status: ${job.status}`);
}
// Reset job status
job.status = OcrJobStatus.PENDING;
job.progress = 0;
job.errorMessage = undefined;
job.retryCount = 0;
await this.ocrJobRepository.save(job);
// Re-add to queue
const jobData: OcrJobData = {
jobId: job.id,
imageUrl: job.imageUrl,
itemId: job.itemId,
splitId: job.splitId,
uploadedBy: job.uploadedBy,
originalFilename: job.originalFilename,
};
await this.ocrQueue.add(jobData, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
this.logger.log(`Retrying OCR job ${jobId}`);
return job;
}
/**
* Cancel a pending OCR job
*/
async cancelJob(jobId: string): Promise<void> {
const job = await this.ocrJobRepository.findOne({
where: { id: jobId },
});
if (!job) {
throw new NotFoundException(`OCR job ${jobId} not found`);
}
if (job.status !== OcrJobStatus.PENDING && job.status !== OcrJobStatus.PROCESSING) {
throw new Error(`Cannot cancel job in status: ${job.status}`);
}
// Try to remove from queue if it hasn't been processed yet
if (job.queueJobId) {
try {
const queueJob = await this.ocrQueue.getJob(job.queueJobId);
if (queueJob) {
await queueJob.remove();
}
} catch (error) {
this.logger.warn(`Failed to remove job ${jobId} from queue:`, error);
}
}
// Update status
job.status = OcrJobStatus.FAILED;
job.errorMessage = 'Job cancelled by user';
await this.ocrJobRepository.save(job);
this.logger.log(`Cancelled OCR job ${jobId}`);
}
/**
* Get queue statistics
*/
async getQueueStats(): Promise<{
pending: number;
processing: number;
completed: number;
failed: number;
needsReview: number;
}> {
const [pending, processing, completed, failed, needsReview] = await Promise.all([
this.ocrJobRepository.count({ where: { status: OcrJobStatus.PENDING } }),
this.ocrJobRepository.count({ where: { status: OcrJobStatus.PROCESSING } }),
this.ocrJobRepository.count({ where: { status: OcrJobStatus.COMPLETED } }),
this.ocrJobRepository.count({ where: { status: OcrJobStatus.FAILED } }),
this.ocrJobRepository.count({ where: { status: OcrJobStatus.NEEDS_REVIEW } }),
]);
return { pending, processing, completed, failed, needsReview };
}
/**
* Get OCR job progress from the queue
*/
async getJobProgress(jobId: string): Promise<{ progress: number; status: string }> {
const job = await this.ocrJobRepository.findOne({
where: { id: jobId },
});
if (!job) {
throw new NotFoundException(`OCR job ${jobId} not found`);
}
// If job is still in queue, check queue status
if (job.queueJobId && (job.status === OcrJobStatus.PENDING || job.status === OcrJobStatus.PROCESSING)) {
try {
const queueJob = await this.ocrQueue.getJob(job.queueJobId);
if (queueJob) {
const progress = queueJob.progress();
return {
progress: Math.round(progress * 100),
status: job.status,
};
}
} catch {
// Ignore queue lookup errors
}
}
return {
progress: job.progress,
status: job.status,
};
}
/**
* Manually trigger OCR processing for a job (bypass queue for testing)
*/
async processJobManually(jobId: string): Promise<OcrJob> {
const job = await this.ocrJobRepository.findOne({
where: { id: jobId },
});
if (!job) {
throw new NotFoundException(`OCR job ${jobId} not found`);
}
if (job.status !== OcrJobStatus.PENDING) {
throw new Error(`Cannot process job in status: ${job.status}`);
}
// For manual processing, we would need to get the image data
// This is a simplified version that marks it as needing the queue
job.status = OcrJobStatus.PROCESSING;
await this.ocrJobRepository.save(job);
// Add to queue for processing
const jobData: OcrJobData = {
jobId: job.id,
imageUrl: job.imageUrl,
itemId: job.itemId,
splitId: job.splitId,
uploadedBy: job.uploadedBy,
originalFilename: job.originalFilename,
};
await this.ocrQueue.add(jobData);
this.logger.log(`Manually triggered processing for OCR job ${jobId}`);
return job;
}
}