@sturmfrei/litequu
Version:
A simple same-thread queuing system for Node.js using SQLite with retry mechanism and exponential backoff
422 lines (313 loc) • 10.4 kB
Markdown
A simple, persistent task queue for Node.js using SQLite as storage. Tasks are processed in the main thread with configurable concurrency, automatic retries with exponential backoff, and comprehensive event handling.
- ✅ **Persistent Storage**: Uses SQLite for reliable task persistence
- ⚡ **Same-Thread Processing**: Runs in the main Node.js thread (perfect for I/O-bound tasks)
- 🔄 **Automatic Retries**: Exponential backoff with configurable retry limits
- 🚦 **Concurrency Control**: Configurable maximum concurrent task processing
- 📊 **Event-Driven**: Comprehensive event system for monitoring
- 🔍 **Task Management**: Query task status, statistics, and cleanup utilities
- 🕐 **Auto-Processing**: Optional automatic task processing with polling
- 📦 **Zero Config**: Works out of the box with sensible defaults
## Installation
```bash
npm i @sturmfrei/litequu
```
## Quick Start
```javascript
import Queue from '@sturmfrei/litequu';
// Create a queue
const queue = new Queue({
dbPath: './my-queue.db',
maxConcurrent: 5,
maxRetries: 3,
baseRetryDelay: 1000,
});
// Add tasks
await queue.add({
type: 'send_email',
to: 'user@example.com',
subject: 'Welcome!',
});
// Process tasks
queue.process(async (taskData) => {
console.log('Processing:', taskData);
if (taskData.type === 'send_email') {
// Simulate email sending
await sendEmail(taskData.to, taskData.subject);
return `Email sent to ${taskData.to}`;
}
throw new Error(`Unknown task type: ${taskData.type}`);
});
// Listen to events
queue.on('completed', (info) => {
console.log(`Task ${info.taskId} completed:`, info.result);
});
queue.on('failed', (info) => {
console.log(`Task ${info.taskId} failed:`, info.error);
});
```
```javascript
const queue = new Queue({
// Database file path (default: './queue.db')
dbPath: './my-app-queue.db',
// Maximum concurrent tasks (default: 5)
maxConcurrent: 3,
// Maximum retry attempts (default: 15)
maxRetries: 5,
// Base retry delay in milliseconds (default: 15_000ms)
baseRetryDelay: 2000,
// Polling interval for auto-processing (default: 5000ms)
pollingInterval: 1000,
// Enable automatic processing (default: true)
autoProcess: true,
// Add jitter to retry delays (default: true)
jitter: true,
});
```
Add a task to the queue.
```javascript
const taskId = await queue.add({
action: 'process_image',
imageUrl: 'https://example.com/image.jpg',
userId: 123,
});
```
Start processing tasks with auto-polling enabled.
```javascript
queue.process(async (taskData) => {
// Your task processing logic
return result;
});
```
Process available tasks once without auto-polling.
```javascript
await queue.processOnce(async (taskData) => {
// Process single batch of tasks
return result;
});
```
Get queue statistics.
```javascript
const stats = await queue.getStats();
// Returns: [{ status: 'pending', count: 5 }, { status: 'completed', count: 10 }]
```
Get a specific task by ID.
```javascript
const task = await queue.getTask(123);
console.log(task.status, task.retry_count);
```
Remove completed tasks older than specified hours.
```javascript
await queue.cleanup(24); // Remove completed tasks older than 24 hours
```
Close the queue and database connection.
```javascript
await queue.close();
```
Get current queue status.
```javascript
const status = queue.status;
console.log(status.currentRunning); // Currently processing tasks
console.log(status.maxConcurrent); // Maximum concurrent tasks
console.log(status.isProcessing); // Whether queue is actively processing
```
The queue emits the following events:
Emitted when a task is added to the queue.
```javascript
queue.on('added', (info) => {
console.log(`Task ${info.taskId} added:`, info.taskData);
});
```
Emitted when a task completes successfully.
```javascript
queue.on('completed', (info) => {
console.log(`Task ${info.taskId} completed:`, info.result);
});
```
Emitted when a task fails and is scheduled for retry.
```javascript
queue.on('retried', (info) => {
console.log(
`Task ${info.taskId} retry ${info.retryCount} in ${info.delay}ms`
);
console.log(`Error: ${info.error}`);
});
```
Emitted when a task permanently fails (exceeds max retries).
```javascript
queue.on('failed', (info) => {
console.log(`Task ${info.taskId} permanently failed:`, info.error);
console.log(`Total attempts: ${info.retryCount}`);
});
```
Emitted when queue operations encounter errors.
```javascript
queue.on('error', (info) => {
console.error(`Queue error in ${info.operation}:`, info.error);
});
```
Tasks that fail are automatically retried with exponential backoff. The delay is roughly calculated as follows:
| Attempt | Next backoff | Total wait |
| ------- | ---------------------------- | --------------------------------- |
| 1 | 0d 0h 0m 7.5s – 0d 0h 0m 15s | 0d 0h 0m 7.5s – 0d 0h 0m 15s |
| 2 | 0d 0h 0m 15s – 0d 0h 0m 30s | 0d 0h 0m 22.5s – 0d 0h 0m 45s |
| 3 | 0d 0h 0m 30s – 0d 0h 1m 0s | 0d 0h 0m 52.5s – 0d 0h 1m 45s |
| 4 | 0d 0h 1m 0s – 0d 0h 2m 0s | 0d 0h 1m 52.5s – 0d 0h 3m 45s |
| 5 | 0d 0h 2m 0s – 0d 0h 4m 0s | 0d 0h 3m 52.5s – 0d 0h 7m 45s |
| 6 | 0d 0h 4m 0s – 0d 0h 8m 0s | 0d 0h 7m 52.5s – 0d 0h 15m 45s |
| 7 | 0d 0h 8m 0s – 0d 0h 16m 0s | 0d 0h 15m 52.5s – 0d 0h 31m 45s |
| 8 | 0d 0h 16m 0s – 0d 0h 32m 0s | 0d 0h 31m 52.5s – 0d 1h 3m 45s |
| 9 | 0d 0h 32m 0s – 0d 1h 4m 0s | 0d 1h 3m 52.5s – 0d 2h 7m 45s |
| 10 | 0d 1h 4m 0s – 0d 2h 8m 0s | 0d 2h 7m 52.5s – 0d 4h 15m 45s |
| 11 | 0d 2h 8m 0s – 0d 4h 16m 0s | 0d 4h 15m 52.5s – 0d 8h 31m 45s |
| 12 | 0d 4h 16m 0s – 0d 8h 32m 0s | 0d 8h 31m 52.5s – 0d 17h 3m 45s |
| 13 | 0d 8h 32m 0s – 0d 17h 4m 0s | 0d 17h 3m 52.5s – 1d 10h 7m 45s |
| 14 | 0d 17h 4m 0s – 1d 10h 8m 0s | 1d 10h 7m 52.5s – 2d 20h 15m 45s |
| 15 | 1d 10h 8m 0s – 2d 20h 16m 0s | 2d 20h 15m 52.5s – 5d 16h 31m 45s |
The formula for the delay is:
`floor(baseRetryDelay * 2^(retryCount - 1) * (jitter ? (0.5 + Math.random() * 0.5) : 1))`.
With jitter enabled (default), actual delays will vary by ±50% to prevent thundering herd effects.
## Examples
### Basic Usage
```javascript
import Queue from '@sturmfrei/litequu';
const queue = new Queue();
// Add some tasks
await queue.add({ type: 'backup', table: 'users' });
await queue.add({ type: 'backup', table: 'orders' });
// Process tasks
await queue.processOnce(async (task) => {
console.log(`Backing up ${task.table}...`);
// Simulate backup work
await new Promise((resolve) => setTimeout(resolve, 1000));
return `${task.table} backed up successfully`;
});
```
```javascript
const queue = new Queue({
autoProcess: true,
pollingInterval: 2000, // Check every 2 seconds
});
// Start processing (runs continuously)
queue.process(async (task) => {
return await handleTask(task);
});
// Tasks will be processed automatically as they're added
await queue.add({ work: 'to_do' });
```
```javascript
const queue = new Queue({
maxRetries: 3,
baseRetryDelay: 1000,
});
queue.on('retried', (info) => {
console.log(`Retry ${info.retryCount} for task ${info.taskId}`);
});
queue.on('failed', (info) => {
console.log(`Task ${info.taskId} gave up after ${info.retryCount} attempts`);
// Handle permanent failures (e.g., dead letter queue, alerting)
});
queue.process(async (task) => {
// This might fail and trigger retries
if (Math.random() < 0.5) {
throw new Error('Simulated failure');
}
return 'success';
});
```
Since tasks run in the main thread, avoid CPU-intensive operations:
```javascript
// ✅ Good - I/O bound tasks
queue.process(async (task) => {
await sendEmail(task.email);
await uploadFile(task.filePath);
await callWebhook(task.url);
});
// ❌ Avoid - CPU intensive tasks
queue.process(async (task) => {
// This will block the event loop
return heavyComputation(task.data);
});
```
```javascript
queue.process(async (task) => {
try {
return await processTask(task);
} catch (error) {
// Add context to errors for better debugging
throw new Error(`Failed to process ${task.type}: ${error.message}`);
}
});
```
```javascript
queue.process(async (task) => {
switch (task.type) {
case 'email':
return await sendEmail(task);
case 'webhook':
return await callWebhook(task);
case 'file_upload':
return await uploadFile(task);
default:
throw new Error(`Unknown task type: ${task.type}`);
}
});
```
```javascript
// Set up monitoring
setInterval(async () => {
const stats = await queue.getStats();
const pending = stats.find((s) => s.status === 'pending')?.count || 0;
const failed = stats.find((s) => s.status === 'failed')?.count || 0;
if (pending > 1000) {
console.warn('Queue backlog is growing:', pending);
}
if (failed > 100) {
console.error('High failure rate detected:', failed);
}
}, 60000); // Check every minute
```
```javascript
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
await queue.close(); // Wait for current tasks to finish
process.exit(0);
});
```
- **Single Process**: Designed for single-process applications
- **Main Thread**: Not suitable for CPU-intensive tasks
- **SQLite Concurrency**: Write operations are serialized by SQLite
- **Memory Usage**: Large task payloads are stored in the database
Contributions are welcome! Please read our contributing guidelines and submit pull requests for any improvements.
MIT License - see LICENSE file for details.