Job Status
When using asynchronous preview generation, FileLens creates jobs that run in the background. This guide covers how to monitor job status, track progress, and manage running jobs effectively.
Job Lifecycle
Job States
Asynchronous jobs progress through several states:
- Name
pending- Type
- info
- Description
Job has been created and is waiting to be processed
- Name
processing- Type
- warning
- Description
Job is currently being processed by the server
- Name
completed- Type
- success
- Description
Job has finished successfully with results available
- Name
failed- Type
- error
- Description
Job has failed due to an error during processing
Typical Job Flow
1. Job Creation
- Submit request to
/preview/async - Receive job ID
- Job enters "pending" state
2. Processing
- Job moves to "processing" state
- Progress updates available
- Server generates previews
3. Completion
- Job moves to "completed" state
- Result URLs become available
- Files ready for download
4. Cleanup
- Files available for limited time
- Job data eventually cleaned up
- Download before expiration
Job Response Format
{
"success": true,
"message": "Job created successfully",
"preview_urls": null,
"total_pages": null,
"job_id": "550e8400-e29b-41d4-a716-446655440000"
}
Status Monitoring
Basic Status Check
Check job status using the job ID:
curl http://localhost:3000/preview/status/550e8400-e29b-41d4-a716-446655440000
Progress Tracking
Polling for Updates
Continuously monitor job progress until completion:
class JobMonitor {
constructor(baseUrl = 'http://localhost:3000', pollInterval = 2000) {
this.baseUrl = baseUrl;
this.pollInterval = pollInterval;
}
async monitorJob(jobId, onProgress = null, maxWaitTime = 300000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
try {
const status = await this.getJobStatus(jobId);
// Call progress callback if provided
if (onProgress) {
onProgress(status);
}
// Log progress
this.logProgress(status);
// Check if job is complete
if (status.status === 'completed') {
console.log('✓ Job completed successfully!');
return status;
} else if (status.status === 'failed') {
throw new Error(`Job failed: ${status.message}`);
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, this.pollInterval));
} catch (error) {
console.error('Error checking job status:', error.message);
throw error;
}
}
throw new Error(`Job timeout after ${maxWaitTime}ms`);
}
async getJobStatus(jobId) {
const response = await fetch(`${this.baseUrl}/preview/status/${jobId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
}
logProgress(status) {
const progress = status.progress || 0;
const progressBar = this.createProgressBar(progress);
console.log(`Job ${status.job_id}: ${status.status}`);
console.log(`Progress: ${progressBar} ${progress}%`);
console.log(`Message: ${status.message}`);
console.log('---');
}
createProgressBar(progress, width = 20) {
const filled = Math.round((progress / 100) * width);
const empty = width - filled;
return '█'.repeat(filled) + '░'.repeat(empty);
}
}
// Usage with progress callback
const monitor = new JobMonitor();
const result = await monitor.monitorJob(
'550e8400-e29b-41d4-a716-446655440000',
(status) => {
// Custom progress handling
if (status.status === 'processing') {
console.log(`Processing page ${status.message}`);
}
}
);
console.log(`Job completed! Generated ${result.total_pages} previews.`);
Real-time Updates
For web applications, implement real-time progress updates:
class WebJobMonitor {
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl;
}
async startMonitoring(jobId, containerId) {
const container = document.getElementById(containerId);
const updateDisplay = (status) => {
container.innerHTML = this.renderProgress(status);
};
try {
const result = await this.pollUntilComplete(jobId, updateDisplay);
container.innerHTML = this.renderComplete(result);
return result;
} catch (error) {
container.innerHTML = this.renderError(error.message);
throw error;
}
}
async pollUntilComplete(jobId, onUpdate) {
while (true) {
const status = await this.getJobStatus(jobId);
onUpdate(status);
if (status.status === 'completed') {
return status;
} else if (status.status === 'failed') {
throw new Error(status.message);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
async getJobStatus(jobId) {
const response = await fetch(`${this.baseUrl}/preview/status/${jobId}`);
return await response.json();
}
renderProgress(status) {
const progress = status.progress || 0;
return `
<div class="job-monitor">
<h3>Job ${status.job_id}</h3>
<div class="status">Status: ${status.status}</div>
<div class="progress-container">
<div class="progress-bar" style="width: ${progress}%"></div>
</div>
<div class="progress-text">${progress}%</div>
<div class="message">${status.message}</div>
</div>
`;
}
renderComplete(result) {
return `
<div class="job-complete">
<h3>✓ Job Completed!</h3>
<p>Generated ${result.total_pages} preview files</p>
<div class="download-links">
${result.result_urls.map((url, index) =>
`<a href="${this.baseUrl}${url}" download>Download Page ${index + 1}</a>`
).join('')}
</div>
</div>
`;
}
renderError(message) {
return `
<div class="job-error">
<h3>✗ Job Failed</h3>
<p>Error: ${message}</p>
</div>
`;
}
}
// CSS for styling
const style = `
.job-monitor {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
font-family: Arial, sans-serif;
}
.progress-container {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-bar {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s ease;
}
.download-links a {
display: inline-block;
margin: 5px;
padding: 8px 16px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
}
`;
// Usage
const monitor = new WebJobMonitor();
const result = await monitor.startMonitoring(
'550e8400-e29b-41d4-a716-446655440000',
'progress-container'
);
Job Management
Multiple Job Tracking
Track multiple jobs simultaneously:
class MultiJobManager {
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl;
this.jobs = new Map();
}
addJob(jobId, metadata = {}) {
this.jobs.set(jobId, {
id: jobId,
status: 'pending',
progress: 0,
startTime: Date.now(),
...metadata
});
}
async checkAllJobs() {
const updates = [];
for (const [jobId, job] of this.jobs) {
if (job.status === 'completed' || job.status === 'failed') {
continue; // Skip finished jobs
}
try {
const status = await this.getJobStatus(jobId);
// Update job info
this.jobs.set(jobId, {
...job,
status: status.status,
progress: status.progress || 0,
message: status.message,
lastUpdate: Date.now()
});
updates.push({ jobId, status });
} catch (error) {
console.error(`Failed to check job ${jobId}:`, error.message);
}
}
return updates;
}
async getJobStatus(jobId) {
const response = await fetch(`${this.baseUrl}/preview/status/${jobId}`);
return await response.json();
}
getJobSummary() {
const summary = {
total: this.jobs.size,
pending: 0,
processing: 0,
completed: 0,
failed: 0
};
for (const job of this.jobs.values()) {
summary[job.status]++;
}
return summary;
}
getRunningJobs() {
return Array.from(this.jobs.values())
.filter(job => job.status === 'pending' || job.status === 'processing');
}
getCompletedJobs() {
return Array.from(this.jobs.values())
.filter(job => job.status === 'completed');
}
async waitForAllJobs(pollInterval = 5000) {
while (this.getRunningJobs().length > 0) {
await this.checkAllJobs();
const summary = this.getJobSummary();
console.log(`Jobs status: ${summary.completed} completed, ${summary.processing} processing, ${summary.pending} pending, ${summary.failed} failed`);
if (this.getRunningJobs().length > 0) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
const summary = this.getJobSummary();
console.log(`All jobs finished: ${summary.completed} completed, ${summary.failed} failed`);
return this.getCompletedJobs();
}
}
// Usage
const manager = new MultiJobManager();
// Add multiple jobs
const jobIds = [
'550e8400-e29b-41d4-a716-446655440000',
'550e8400-e29b-41d4-a716-446655440001',
'550e8400-e29b-41d4-a716-446655440002'
];
jobIds.forEach((jobId, index) => {
manager.addJob(jobId, {
name: `Document ${index + 1}`,
priority: index === 0 ? 'high' : 'normal'
});
});
// Wait for all jobs to complete
const completedJobs = await manager.waitForAllJobs();
console.log(`All ${completedJobs.length} jobs completed!`);
Job Timeout Handling
Implement proper timeout handling for long-running jobs:
class JobTimeoutManager {
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl;
this.defaultTimeout = 300000; // 5 minutes
}
async monitorWithTimeout(jobId, options = {}) {
const {
timeout = this.defaultTimeout,
onProgress = null,
onTimeout = null,
pollInterval = 2000
} = options;
return new Promise((resolve, reject) => {
let isResolved = false;
// Set timeout
const timeoutHandle = setTimeout(() => {
if (!isResolved) {
isResolved = true;
if (onTimeout) {
onTimeout(jobId);
}
reject(new Error(`Job ${jobId} timed out after ${timeout}ms`));
}
}, timeout);
// Start polling
this.pollJob(jobId, pollInterval, onProgress)
.then(result => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeoutHandle);
resolve(result);
}
})
.catch(error => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeoutHandle);
reject(error);
}
});
});
}
async pollJob(jobId, pollInterval, onProgress) {
while (true) {
const status = await this.getJobStatus(jobId);
if (onProgress) {
onProgress(status);
}
if (status.status === 'completed') {
return status;
} else if (status.status === 'failed') {
throw new Error(`Job failed: ${status.message}`);
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
async getJobStatus(jobId) {
const response = await fetch(`${this.baseUrl}/preview/status/${jobId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
}
}
// Usage
const timeoutManager = new JobTimeoutManager();
try {
const result = await timeoutManager.monitorWithTimeout(
'550e8400-e29b-41d4-a716-446655440000',
{
timeout: 600000, // 10 minutes
onProgress: (status) => {
console.log(`Progress: ${status.progress}%`);
},
onTimeout: (jobId) => {
console.log(`Job ${jobId} is taking longer than expected...`);
// Could implement notification system here
}
}
);
console.log('Job completed successfully!');
} catch (error) {
if (error.message.includes('timed out')) {
console.log('Job timed out - you can check status later');
// Implement retry logic or user notification
} else {
console.error('Job failed:', error.message);
}
}
Error Recovery
Handle job failures and implement recovery strategies:
class JobRecoveryManager {
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl;
this.retryAttempts = 3;
this.retryDelay = 5000;
}
async processWithRecovery(input, outputFormat, options) {
let lastError;
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
try {
console.log(`Attempt ${attempt}/${this.retryAttempts}`);
// Submit job
const jobId = await this.submitJob(input, outputFormat, options);
console.log(`Job submitted: ${jobId}`);
// Monitor with timeout
const result = await this.monitorJob(jobId);
console.log(`Job completed successfully on attempt ${attempt}`);
return result;
} catch (error) {
lastError = error;
console.error(`Attempt ${attempt} failed: ${error.message}`);
if (attempt < this.retryAttempts) {
console.log(`Retrying in ${this.retryDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
// Exponential backoff
this.retryDelay *= 2;
}
}
}
throw new Error(`All ${this.retryAttempts} attempts failed. Last error: ${lastError.message}`);
}
async submitJob(input, outputFormat, options) {
const response = await fetch(`${this.baseUrl}/preview/async`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input, output_format: outputFormat, options })
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message);
}
return result.job_id;
}
async monitorJob(jobId, maxWaitTime = 600000) {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
const status = await this.getJobStatus(jobId);
if (status.status === 'completed') {
return status;
} else if (status.status === 'failed') {
throw new Error(`Job failed: ${status.message}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Job timeout');
}
async getJobStatus(jobId) {
const response = await fetch(`${this.baseUrl}/preview/status/${jobId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
}
}
// Usage
const recovery = new JobRecoveryManager();
try {
const result = await recovery.processWithRecovery(
'https://example.com/complex-document.pdf',
'png',
{ width: 1920, height: 1080, all_pages: true }
);
console.log(`Successfully processed ${result.total_pages} pages`);
} catch (error) {
console.error('Failed to process document after all retries:', error.message);
// Implement fallback strategy or user notification
}