@objectstack/service-job
v4.0.5
Published
Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling
Downloads
567
Maintainers
Readme
@objectstack/service-job
Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling.
Features
- Cron Scheduling: Schedule jobs with cron expressions
- Interval Scheduling: Run jobs at fixed intervals
- Job Queue: Manage job execution queue
- Retry Logic: Automatic retry on failure with exponential backoff
- Job History: Track execution history and status
- Concurrency Control: Limit concurrent job execution
- Timezone Support: Schedule jobs in specific timezones
- Type-Safe: Full TypeScript support
Installation
pnpm add @objectstack/service-jobBasic Usage
import { defineStack } from '@objectstack/spec';
import { ServiceJob } from '@objectstack/service-job';
const stack = defineStack({
services: [
ServiceJob.configure({
timezone: 'America/New_York',
maxConcurrent: 5,
}),
],
});Configuration
interface JobServiceConfig {
/** Default timezone for cron jobs (default: 'UTC') */
timezone?: string;
/** Maximum concurrent job executions (default: 10) */
maxConcurrent?: number;
/** Enable job history tracking (default: true) */
enableHistory?: boolean;
/** Maximum history entries per job (default: 100) */
maxHistorySize?: number;
}Service API
// Get job service
const jobs = kernel.getService<IJobService>('job');Cron Jobs
// Schedule a job with cron expression
const job = await jobs.schedule({
name: 'daily_report',
schedule: '0 9 * * *', // Every day at 9 AM
handler: async (context) => {
console.log('Generating daily report...');
// Your job logic here
},
timezone: 'America/New_York',
});
// Common cron patterns:
// '*/5 * * * *' - Every 5 minutes
// '0 */2 * * *' - Every 2 hours
// '0 9 * * 1-5' - Weekdays at 9 AM
// '0 0 1 * *' - First day of every month at midnight
// '0 0 * * 0' - Every Sunday at midnightInterval Jobs
// Run every 30 seconds
const job = await jobs.scheduleInterval({
name: 'health_check',
interval: 30000, // milliseconds
handler: async (context) => {
console.log('Running health check...');
},
});
// Run every 5 minutes
const job = await jobs.scheduleInterval({
name: 'sync_data',
interval: 5 * 60 * 1000, // 5 minutes
handler: async (context) => {
// Sync data
},
});One-Time Jobs
// Schedule a one-time job
const job = await jobs.scheduleOnce({
name: 'send_reminder',
runAt: new Date('2024-12-25T09:00:00Z'),
handler: async (context) => {
console.log('Sending holiday reminder...');
},
});
// Schedule to run after a delay
const job = await jobs.scheduleOnce({
name: 'delayed_task',
delay: 3600000, // 1 hour from now
handler: async (context) => {
console.log('Executing delayed task...');
},
});Job Management
// List all jobs
const allJobs = await jobs.listJobs();
// Get job details
const job = await jobs.getJob('daily_report');
// Stop a job
await jobs.stopJob('daily_report');
// Resume a stopped job
await jobs.resumeJob('daily_report');
// Delete a job
await jobs.deleteJob('daily_report');
// Run a job immediately (ignoring schedule)
await jobs.runNow('daily_report');Advanced Features
Job Context
const job = await jobs.schedule({
name: 'process_orders',
schedule: '*/10 * * * *',
handler: async (context) => {
console.log('Job name:', context.jobName);
console.log('Execution ID:', context.executionId);
console.log('Scheduled time:', context.scheduledTime);
console.log('Execution count:', context.executionCount);
// Access services
const db = context.kernel.getService('database');
const orders = await db.find({ object: 'order', status: 'pending' });
// Process orders...
},
});Retry Configuration
const job = await jobs.schedule({
name: 'api_sync',
schedule: '0 * * * *', // Every hour
retry: {
maxAttempts: 3,
backoff: 'exponential', // 'linear' or 'exponential'
initialDelay: 1000, // 1 second
maxDelay: 60000, // 1 minute
},
handler: async (context) => {
// May fail and retry
await syncWithExternalAPI();
},
});Concurrency Control
const job = await jobs.schedule({
name: 'heavy_processing',
schedule: '*/5 * * * *',
concurrency: 1, // Only one instance can run at a time
handler: async (context) => {
// Long-running process
},
});Job History
// Get execution history for a job
const history = await jobs.getJobHistory('daily_report', {
limit: 50,
status: 'success', // 'success', 'failed', 'running'
});
// Example history entry:
// {
// executionId: 'exec:abc123',
// jobName: 'daily_report',
// status: 'success',
// startedAt: '2024-01-15T09:00:00Z',
// completedAt: '2024-01-15T09:05:23Z',
// duration: 323000, // milliseconds
// error: null,
// result: { records: 1250 }
// }
// Clear history for a job
await jobs.clearHistory('daily_report');Job Data & Results
const job = await jobs.schedule({
name: 'data_export',
schedule: '0 0 * * *',
handler: async (context) => {
const records = await exportData();
// Return result data
return {
recordCount: records.length,
fileSize: calculateSize(records),
exportedAt: new Date(),
};
},
});
// Get last execution result
const lastRun = await jobs.getLastExecution('data_export');
console.log('Last export:', lastRun.result);Common Patterns
Database Cleanup Job
jobs.schedule({
name: 'cleanup_old_records',
schedule: '0 2 * * *', // 2 AM daily
handler: async (context) => {
const db = context.kernel.getService('database');
// Delete records older than 90 days
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 90);
await db.delete({
object: 'audit_log',
filters: [{ field: 'created_at', operator: 'lt', value: cutoff }],
});
},
});Report Generation Job
jobs.schedule({
name: 'weekly_sales_report',
schedule: '0 8 * * 1', // Mondays at 8 AM
handler: async (context) => {
const analytics = context.kernel.getService('analytics');
const data = await analytics.query({
object: 'order',
aggregations: [{ function: 'sum', field: 'amount' }],
groupBy: ['sales_rep'],
filters: [{ field: 'created_at', operator: 'last_week' }],
});
// Generate and email report
await sendReport(data);
},
});Cache Warming Job
jobs.scheduleInterval({
name: 'warm_cache',
interval: 15 * 60 * 1000, // Every 15 minutes
handler: async (context) => {
const cache = context.kernel.getService('cache');
// Pre-load frequently accessed data
const popularProducts = await getPopularProducts();
await cache.set('popular_products', popularProducts, { ttl: 900 });
},
});REST API Endpoints
GET /api/v1/jobs # List all jobs
GET /api/v1/jobs/:name # Get job details
POST /api/v1/jobs/:name/run # Run job immediately
POST /api/v1/jobs/:name/stop # Stop job
POST /api/v1/jobs/:name/resume # Resume job
DELETE /api/v1/jobs/:name # Delete job
GET /api/v1/jobs/:name/history # Get execution historyBest Practices
- Idempotent Handlers: Job handlers should be idempotent (safe to run multiple times)
- Error Handling: Always handle errors gracefully and log failures
- Timeout Limits: Set reasonable timeout limits for long-running jobs
- Resource Limits: Limit concurrent executions to avoid overloading the system
- Monitoring: Monitor job execution times and failure rates
- Timezone Awareness: Always specify timezone for cron jobs to avoid ambiguity
- Cleanup: Periodically delete old job history to save storage
Performance Considerations
- Concurrency: Limit concurrent jobs based on system resources
- Job Duration: Keep job execution time reasonable (< 5 minutes ideal)
- History Size: Limit history entries to prevent memory bloat
- Batch Processing: Process records in batches for large datasets
Contract Implementation
Implements IJobService from @objectstack/spec/contracts:
interface IJobService {
schedule(options: ScheduleOptions): Promise<Job>;
scheduleInterval(options: IntervalOptions): Promise<Job>;
scheduleOnce(options: OnceOptions): Promise<Job>;
getJob(name: string): Promise<Job>;
listJobs(filter?: JobFilter): Promise<Job[]>;
stopJob(name: string): Promise<void>;
resumeJob(name: string): Promise<void>;
deleteJob(name: string): Promise<void>;
runNow(name: string): Promise<JobExecution>;
getJobHistory(name: string, options?: HistoryOptions): Promise<JobExecution[]>;
}License
Apache-2.0
