Background Services
Background services are long-running processes that perform automated tasks, data synchronization, and monitoring without user interaction. This guide covers creating robust background services for the Qirvo platform.
Table of Contents
- Service Fundamentals
- Service Lifecycle
- Scheduled Tasks
- Event-Driven Processing
- Error Handling and Recovery
- Performance and Monitoring
Service Fundamentals
What are Background Services?
Background services are plugins that:
- Run continuously in the background
- Perform scheduled or event-driven tasks
- Synchronize data with external systems
- Monitor system health and performance
- Process queues and batch operations
Basic Service Structure
import { BasePlugin, PluginRuntimeContext } from '@qirvo/plugin-sdk';
export default class MyBackgroundService extends BasePlugin {
private isRunning = false;
private intervalId?: NodeJS.Timeout;
async onEnable(context: PluginRuntimeContext): Promise<void> {
this.log('info', 'Background service starting');
await this.startService();
}
async onDisable(): Promise<void> {
this.log('info', 'Background service stopping');
await this.stopService();
}
private async startService(): Promise<void> {
this.isRunning = true;
// Start periodic tasks
this.intervalId = setInterval(async () => {
if (this.isRunning) {
await this.performPeriodicTask();
}
}, 60000); // Every minute
// Start event listeners
this.setupEventListeners();
}
private async stopService(): Promise<void> {
this.isRunning = false;
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
await this.cleanup();
}
private async performPeriodicTask(): Promise<void> {
try {
this.log('debug', 'Performing periodic task');
// Task implementation
} catch (error) {
this.log('error', 'Periodic task failed:', error);
}
}
}
Service Lifecycle
Manifest Configuration
{
"type": "service",
"background": "dist/background.js",
"hooks": {
"onInstall": "setupService",
"onEnable": "startService",
"onDisable": "stopService",
"onUninstall": "cleanupService"
},
"permissions": [
"network-access",
"storage-read",
"storage-write",
"notifications"
]
}
Advanced Service Class
export default class DataSyncService extends BasePlugin {
private syncInterval: NodeJS.Timeout | null = null;
private isHealthy = true;
private lastSyncTime: Date | null = null;
private syncQueue: Array<SyncTask> = [];
private processing = false;
async onInstall(context: PluginRuntimeContext): Promise<void> {
this.log('info', 'Installing data sync service');
// Initialize service data
await this.setStorage('installDate', new Date().toISOString());
await this.setStorage('syncHistory', []);
await this.setStorage('errorCount', 0);
}
async onEnable(context: PluginRuntimeContext): Promise<void> {
this.log('info', 'Enabling data sync service');
const config = context.config as ServiceConfig;
// Validate configuration
if (!this.validateConfig(config)) {
throw new Error('Invalid service configuration');
}
// Start service components
await this.startSyncScheduler(config.syncInterval);
await this.startHealthMonitor();
await this.startQueueProcessor();
// Initial sync
await this.performInitialSync();
}
async onDisable(): Promise<void> {
this.log('info', 'Disabling data sync service');
// Stop all timers and processes
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
// Wait for current operations to complete
await this.gracefulShutdown();
}
private async startSyncScheduler(intervalMs: number): Promise<void> {
this.syncInterval = setInterval(async () => {
await this.scheduledSync();
}, intervalMs);
}
private async scheduledSync(): Promise<void> {
if (this.processing) {
this.log('debug', 'Sync already in progress, skipping');
return;
}
try {
this.processing = true;
await this.performDataSync();
this.lastSyncTime = new Date();
this.isHealthy = true;
} catch (error) {
this.log('error', 'Scheduled sync failed:', error);
await this.handleSyncError(error);
} finally {
this.processing = false;
}
}
}
interface ServiceConfig {
syncInterval: number;
apiEndpoint: string;
retryAttempts: number;
batchSize: number;
}
interface SyncTask {
id: string;
type: string;
data: any;
priority: number;
createdAt: Date;
}
Scheduled Tasks
Cron-like Scheduling
import { CronJob } from 'cron';
export default class ScheduledTaskService extends BasePlugin {
private jobs: CronJob[] = [];
async onEnable(context: PluginRuntimeContext): Promise<void> {
const config = context.config as ScheduleConfig;
// Daily backup at 2 AM
const backupJob = new CronJob('0 2 * * *', async () => {
await this.performBackup();
});
// Hourly cleanup
const cleanupJob = new CronJob('0 * * * *', async () => {
await this.performCleanup();
});
// Weekly report on Sundays at 9 AM
const reportJob = new CronJob('0 9 * * 0', async () => {
await this.generateWeeklyReport();
});
this.jobs = [backupJob, cleanupJob, reportJob];
this.jobs.forEach(job => job.start());
}
async onDisable(): Promise<void> {
this.jobs.forEach(job => job.stop());
this.jobs = [];
}
private async performBackup(): Promise<void> {
try {
this.log('info', 'Starting scheduled backup');
const data = await this.collectBackupData();
const compressed = await this.compressData(data);
await this.uploadBackup(compressed);
await this.notify('Backup Complete', 'Daily backup completed successfully', 'success');
} catch (error) {
this.log('error', 'Backup failed:', error);
await this.notify('Backup Failed', 'Daily backup failed', 'error');
}
}
}
interface ScheduleConfig {
backupEnabled: boolean;
cleanupEnabled: boolean;
reportEnabled: boolean;
backupRetention: number;
}
Custom Scheduling
export class TaskScheduler {
private tasks: Map<string, ScheduledTask> = new Map();
private timer: NodeJS.Timeout | null = null;
start(): void {
this.timer = setInterval(() => {
this.processTasks();
}, 1000); // Check every second
}
stop(): void {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
schedule(id: string, task: ScheduledTask): void {
this.tasks.set(id, task);
}
unschedule(id: string): void {
this.tasks.delete(id);
}
private async processTasks(): Promise<void> {
const now = new Date();
for (const [id, task] of this.tasks) {
if (this.shouldExecute(task, now)) {
try {
await task.execute();
this.updateLastRun(task, now);
} catch (error) {
console.error(`Task ${id} failed:`, error);
}
}
}
}
private shouldExecute(task: ScheduledTask, now: Date): boolean {
if (!task.lastRun) return true;
const timeSinceLastRun = now.getTime() - task.lastRun.getTime();
return timeSinceLastRun >= task.intervalMs;
}
private updateLastRun(task: ScheduledTask, time: Date): void {
task.lastRun = time;
}
}
interface ScheduledTask {
intervalMs: number;
lastRun?: Date;
execute: () => Promise<void>;
}
Event-Driven Processing
Event Listeners
export default class EventDrivenService extends BasePlugin {
private eventHandlers: Map<string, Function> = new Map();
async onEnable(context: PluginRuntimeContext): Promise<void> {
this.setupEventHandlers(context);
}
private setupEventHandlers(context: PluginRuntimeContext): void {
// User activity events
const userActivityHandler = this.handleUserActivity.bind(this);
context.bus.on('user.activity', userActivityHandler);
this.eventHandlers.set('user.activity', userActivityHandler);
// Data change events
const dataChangeHandler = this.handleDataChange.bind(this);
context.bus.on('data.changed', dataChangeHandler);
this.eventHandlers.set('data.changed', dataChangeHandler);
// System events
const systemEventHandler = this.handleSystemEvent.bind(this);
context.bus.on('system.event', systemEventHandler);
this.eventHandlers.set('system.event', systemEventHandler);
}
async onDisable(): Promise<void> {
// Clean up event listeners
for (const [event, handler] of this.eventHandlers) {
this.context.bus.off(event, handler);
}
this.eventHandlers.clear();
}
private async handleUserActivity(data: UserActivityEvent): Promise<void> {
try {
this.log('debug', 'Processing user activity:', data);
// Update user statistics
await this.updateUserStats(data.userId, data.activity);
// Trigger related workflows
if (data.activity === 'login') {
await this.processUserLogin(data);
}
} catch (error) {
this.log('error', 'Failed to process user activity:', error);
}
}
private async handleDataChange(data: DataChangeEvent): Promise<void> {
try {
this.log('debug', 'Processing data change:', data);
// Validate change
if (!this.validateDataChange(data)) {
this.log('warn', 'Invalid data change detected:', data);
return;
}
// Sync to external systems
await this.syncDataChange(data);
// Update search indexes
await this.updateSearchIndex(data);
} catch (error) {
this.log('error', 'Failed to process data change:', error);
}
}
}
interface UserActivityEvent {
userId: string;
activity: string;
timestamp: Date;
metadata?: any;
}
interface DataChangeEvent {
type: string;
id: string;
changes: any;
timestamp: Date;
}
Queue Processing
export class TaskQueue {
private queue: QueueItem[] = [];
private processing = false;
private maxConcurrent = 3;
private activeJobs = 0;
async enqueue(item: QueueItem): Promise<void> {
this.queue.push(item);
if (!this.processing) {
this.startProcessing();
}
}
private async startProcessing(): Promise<void> {
this.processing = true;
while (this.queue.length > 0 || this.activeJobs > 0) {
if (this.activeJobs < this.maxConcurrent && this.queue.length > 0) {
const item = this.queue.shift()!;
this.processItem(item);
} else {
// Wait a bit before checking again
await new Promise(resolve => setTimeout(resolve, 100));
}
}
this.processing = false;
}
private async processItem(item: QueueItem): Promise<void> {
this.activeJobs++;
try {
await item.process();
console.log(`Processed item ${item.id}`);
} catch (error) {
console.error(`Failed to process item ${item.id}:`, error);
if (item.retries < item.maxRetries) {
item.retries++;
this.queue.push(item); // Re-queue for retry
}
} finally {
this.activeJobs--;
}
}
}
interface QueueItem {
id: string;
process: () => Promise<void>;
retries: number;
maxRetries: number;
priority: number;
}
Error Handling and Recovery
Robust Error Handling
export default class ResilientService extends BasePlugin {
private errorCount = 0;
private maxErrors = 10;
private backoffMultiplier = 2;
private baseDelay = 1000;
private async performTaskWithRetry<T>(
task: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await task();
this.errorCount = 0; // Reset error count on success
return result;
} catch (error) {
lastError = error as Error;
this.errorCount++;
this.log('warn', `Task failed (attempt ${attempt}/${maxRetries}):`, error);
if (attempt < maxRetries) {
const delay = this.calculateBackoffDelay(attempt);
await this.sleep(delay);
}
}
}
// All retries failed
await this.handleCriticalError(lastError!);
throw lastError!;
}
private calculateBackoffDelay(attempt: number): number {
return this.baseDelay * Math.pow(this.backoffMultiplier, attempt - 1);
}
private async handleCriticalError(error: Error): Promise<void> {
this.log('error', 'Critical error occurred:', error);
if (this.errorCount >= this.maxErrors) {
this.log('error', 'Too many errors, disabling service');
await this.notify('Service Error', 'Service disabled due to repeated failures', 'error');
// Disable the service
await this.onDisable();
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Circuit Breaker Pattern
export class CircuitBreaker {
private failures = 0;
private lastFailureTime?: Date;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private failureThreshold = 5,
private recoveryTimeout = 60000
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = new Date();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
}
}
private shouldAttemptReset(): boolean {
return this.lastFailureTime &&
(Date.now() - this.lastFailureTime.getTime()) >= this.recoveryTimeout;
}
}
Performance and Monitoring
Health Checks
export default class MonitoredService extends BasePlugin {
private healthStatus: HealthStatus = {
status: 'healthy',
lastCheck: new Date(),
metrics: {}
};
async onEnable(context: PluginRuntimeContext): Promise<void> {
// Start health monitoring
setInterval(async () => {
await this.performHealthCheck();
}, 30000); // Every 30 seconds
}
private async performHealthCheck(): Promise<void> {
try {
const metrics = await this.collectMetrics();
this.healthStatus = {
status: this.determineHealthStatus(metrics),
lastCheck: new Date(),
metrics
};
// Store health status
await this.setStorage('healthStatus', this.healthStatus);
// Alert if unhealthy
if (this.healthStatus.status !== 'healthy') {
await this.notify('Service Health Alert',
`Service is ${this.healthStatus.status}`, 'warning');
}
} catch (error) {
this.log('error', 'Health check failed:', error);
this.healthStatus.status = 'unhealthy';
}
}
private async collectMetrics(): Promise<ServiceMetrics> {
return {
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
errorRate: await this.getStorage('errorRate') || 0,
lastSyncTime: await this.getStorage('lastSyncTime'),
queueSize: await this.getStorage('queueSize') || 0
};
}
private determineHealthStatus(metrics: ServiceMetrics): HealthStatusType {
if (metrics.errorRate > 0.1) return 'unhealthy';
if (metrics.memoryUsage.heapUsed > 500 * 1024 * 1024) return 'degraded';
if (metrics.queueSize > 1000) return 'degraded';
return 'healthy';
}
}
interface HealthStatus {
status: HealthStatusType;
lastCheck: Date;
metrics: ServiceMetrics;
}
type HealthStatusType = 'healthy' | 'degraded' | 'unhealthy';
interface ServiceMetrics {
memoryUsage: NodeJS.MemoryUsage;
uptime: number;
errorRate: number;
lastSyncTime?: string;
queueSize: number;
}
Performance Monitoring
export class PerformanceMonitor {
private metrics: Map<string, PerformanceMetric> = new Map();
startTimer(operation: string): () => void {
const startTime = Date.now();
return () => {
const duration = Date.now() - startTime;
this.recordMetric(operation, duration);
};
}
private recordMetric(operation: string, duration: number): void {
const existing = this.metrics.get(operation) || {
count: 0,
totalTime: 0,
minTime: Infinity,
maxTime: 0
};
existing.count++;
existing.totalTime += duration;
existing.minTime = Math.min(existing.minTime, duration);
existing.maxTime = Math.max(existing.maxTime, duration);
this.metrics.set(operation, existing);
}
getMetrics(): Record<string, PerformanceMetric & { avgTime: number }> {
const result: Record<string, PerformanceMetric & { avgTime: number }> = {};
for (const [operation, metric] of this.metrics) {
result[operation] = {
...metric,
avgTime: metric.totalTime / metric.count
};
}
return result;
}
}
interface PerformanceMetric {
count: number;
totalTime: number;
minTime: number;
maxTime: number;
}
Best Practices
Service Design
- Graceful Shutdown: Always implement proper cleanup
- Error Recovery: Use retry logic and circuit breakers
- Resource Management: Monitor memory and CPU usage
- Health Monitoring: Implement comprehensive health checks
Performance
- Batch Processing: Process items in batches when possible
- Queue Management: Use queues for async processing
- Connection Pooling: Reuse database and HTTP connections
- Memory Management: Clean up resources and avoid memory leaks
Reliability
- Idempotency: Ensure operations can be safely retried
- State Persistence: Save important state to survive restarts
- Monitoring: Log important events and metrics
- Alerting: Notify administrators of critical issues
Next: Hybrid Plugins for multi-type plugin development.