Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
# Cron Jobs & Task Scheduling in Node.js
|
||||
|
||||
**Meta-Description:** Task Scheduling für Node.js. Cron Syntax, node-cron, BullMQ Repeatable Jobs und Serverless Scheduling.
|
||||
|
||||
**Keywords:** Cron Jobs, Task Scheduling, Node.js, node-cron, BullMQ, Scheduled Tasks, Background Jobs
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
Automatisierte Aufgaben sind das Rückgrat jeder Anwendung: **Backups, Reports, Cleanup, Sync-Jobs**. Mit Cron Jobs laufen sie zuverlässig zu definierten Zeiten – ohne manuellen Eingriff.
|
||||
|
||||
---
|
||||
|
||||
## Cron Syntax
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CRON EXPRESSION │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────── Minute (0-59) │
|
||||
│ │ ┌───────────── Stunde (0-23) │
|
||||
│ │ │ ┌───────────── Tag des Monats (1-31) │
|
||||
│ │ │ │ ┌───────────── Monat (1-12) │
|
||||
│ │ │ │ │ ┌───────────── Wochentag (0-7, 0 und 7 = Sonntag)│
|
||||
│ │ │ │ │ │ │
|
||||
│ * * * * * │
|
||||
│ │
|
||||
│ Beispiele: │
|
||||
│ ├── * * * * * → Jede Minute │
|
||||
│ ├── 0 * * * * → Jede volle Stunde │
|
||||
│ ├── 0 0 * * * → Täglich um Mitternacht │
|
||||
│ ├── 0 8 * * 1-5 → Mo-Fr um 8:00 │
|
||||
│ ├── 0 0 1 * * → Am 1. jeden Monats │
|
||||
│ ├── */15 * * * * → Alle 15 Minuten │
|
||||
│ └── 0 9,18 * * * → Um 9:00 und 18:00 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## node-cron Setup
|
||||
|
||||
```bash
|
||||
npm install node-cron
|
||||
npm install -D @types/node-cron
|
||||
```
|
||||
|
||||
```typescript
|
||||
import cron from 'node-cron';
|
||||
|
||||
// Basic Scheduled Task
|
||||
cron.schedule('* * * * *', () => {
|
||||
console.log('Runs every minute');
|
||||
});
|
||||
|
||||
// Mit Timezone
|
||||
cron.schedule('0 8 * * *', () => {
|
||||
console.log('Daily at 8:00 AM Berlin time');
|
||||
}, {
|
||||
timezone: 'Europe/Berlin'
|
||||
});
|
||||
|
||||
// Task mit Start/Stop
|
||||
const task = cron.schedule('*/5 * * * *', () => {
|
||||
console.log('Every 5 minutes');
|
||||
}, {
|
||||
scheduled: false // Nicht automatisch starten
|
||||
});
|
||||
|
||||
// Manuell starten/stoppen
|
||||
task.start();
|
||||
// task.stop();
|
||||
|
||||
// Validation
|
||||
const isValid = cron.validate('* * * * *'); // true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Praktische Scheduler-Klasse
|
||||
|
||||
```typescript
|
||||
import cron, { ScheduledTask } from 'node-cron';
|
||||
|
||||
interface Job {
|
||||
name: string;
|
||||
schedule: string;
|
||||
handler: () => Promise<void>;
|
||||
enabled: boolean;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
class TaskScheduler {
|
||||
private tasks: Map<string, ScheduledTask> = new Map();
|
||||
private jobs: Map<string, Job> = new Map();
|
||||
|
||||
register(job: Job) {
|
||||
this.jobs.set(job.name, job);
|
||||
|
||||
if (job.enabled) {
|
||||
this.startJob(job.name);
|
||||
}
|
||||
}
|
||||
|
||||
startJob(name: string) {
|
||||
const job = this.jobs.get(name);
|
||||
if (!job) throw new Error(`Job ${name} not found`);
|
||||
|
||||
if (this.tasks.has(name)) {
|
||||
console.log(`Job ${name} already running`);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = cron.schedule(job.schedule, async () => {
|
||||
const startTime = Date.now();
|
||||
console.log(`[${name}] Starting...`);
|
||||
|
||||
try {
|
||||
await job.handler();
|
||||
console.log(`[${name}] Completed in ${Date.now() - startTime}ms`);
|
||||
} catch (error) {
|
||||
console.error(`[${name}] Failed:`, error);
|
||||
// Alert senden
|
||||
}
|
||||
}, {
|
||||
timezone: job.timezone || 'Europe/Berlin'
|
||||
});
|
||||
|
||||
this.tasks.set(name, task);
|
||||
console.log(`[${name}] Scheduled: ${job.schedule}`);
|
||||
}
|
||||
|
||||
stopJob(name: string) {
|
||||
const task = this.tasks.get(name);
|
||||
if (task) {
|
||||
task.stop();
|
||||
this.tasks.delete(name);
|
||||
console.log(`[${name}] Stopped`);
|
||||
}
|
||||
}
|
||||
|
||||
stopAll() {
|
||||
for (const [name, task] of this.tasks) {
|
||||
task.stop();
|
||||
console.log(`[${name}] Stopped`);
|
||||
}
|
||||
this.tasks.clear();
|
||||
}
|
||||
|
||||
getStatus(): Array<{ name: string; schedule: string; running: boolean }> {
|
||||
return Array.from(this.jobs.values()).map(job => ({
|
||||
name: job.name,
|
||||
schedule: job.schedule,
|
||||
running: this.tasks.has(job.name)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
const scheduler = new TaskScheduler();
|
||||
|
||||
scheduler.register({
|
||||
name: 'daily-backup',
|
||||
schedule: '0 2 * * *',
|
||||
handler: async () => {
|
||||
await performDatabaseBackup();
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
|
||||
scheduler.register({
|
||||
name: 'hourly-sync',
|
||||
schedule: '0 * * * *',
|
||||
handler: async () => {
|
||||
await syncExternalData();
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
|
||||
scheduler.register({
|
||||
name: 'weekly-report',
|
||||
schedule: '0 9 * * 1', // Montag 9:00
|
||||
handler: async () => {
|
||||
await generateWeeklyReport();
|
||||
await sendReportEmail();
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// Graceful Shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
scheduler.stopAll();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BullMQ Repeatable Jobs
|
||||
|
||||
```typescript
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import IORedis from 'ioredis';
|
||||
|
||||
const connection = new IORedis(process.env.REDIS_URL!);
|
||||
|
||||
// Queue für Scheduled Jobs
|
||||
const scheduledQueue = new Queue('scheduled-tasks', { connection });
|
||||
|
||||
// Repeatable Jobs registrieren
|
||||
async function setupScheduledJobs() {
|
||||
// Täglich um 2:00 UTC
|
||||
await scheduledQueue.add('database-backup', {}, {
|
||||
repeat: {
|
||||
pattern: '0 2 * * *',
|
||||
tz: 'Europe/Berlin'
|
||||
},
|
||||
jobId: 'daily-backup' // Verhindert Duplikate
|
||||
});
|
||||
|
||||
// Alle 5 Minuten
|
||||
await scheduledQueue.add('health-check', {}, {
|
||||
repeat: {
|
||||
every: 5 * 60 * 1000 // Millisekunden
|
||||
},
|
||||
jobId: 'health-check'
|
||||
});
|
||||
|
||||
// Wöchentlich Montag 9:00
|
||||
await scheduledQueue.add('weekly-report', { type: 'weekly' }, {
|
||||
repeat: {
|
||||
pattern: '0 9 * * 1',
|
||||
tz: 'Europe/Berlin'
|
||||
},
|
||||
jobId: 'weekly-report'
|
||||
});
|
||||
|
||||
// Monatlich am 1.
|
||||
await scheduledQueue.add('monthly-cleanup', {}, {
|
||||
repeat: {
|
||||
pattern: '0 3 1 * *',
|
||||
tz: 'Europe/Berlin'
|
||||
},
|
||||
jobId: 'monthly-cleanup'
|
||||
});
|
||||
}
|
||||
|
||||
// Worker für Scheduled Jobs
|
||||
const worker = new Worker('scheduled-tasks', async (job) => {
|
||||
console.log(`Processing ${job.name}`);
|
||||
|
||||
switch (job.name) {
|
||||
case 'database-backup':
|
||||
await performDatabaseBackup();
|
||||
break;
|
||||
|
||||
case 'health-check':
|
||||
await runHealthChecks();
|
||||
break;
|
||||
|
||||
case 'weekly-report':
|
||||
await generateAndSendReport('weekly');
|
||||
break;
|
||||
|
||||
case 'monthly-cleanup':
|
||||
await cleanupOldData();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown job: ${job.name}`);
|
||||
}
|
||||
|
||||
return { completed: true };
|
||||
}, { connection });
|
||||
|
||||
worker.on('completed', (job, result) => {
|
||||
console.log(`Job ${job.name} completed`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, error) => {
|
||||
console.error(`Job ${job?.name} failed:`, error);
|
||||
// Alert senden
|
||||
});
|
||||
|
||||
// Repeatable Jobs verwalten
|
||||
async function listScheduledJobs() {
|
||||
const jobs = await scheduledQueue.getRepeatableJobs();
|
||||
return jobs.map(job => ({
|
||||
name: job.name,
|
||||
pattern: job.pattern || `every ${job.every}ms`,
|
||||
next: new Date(job.next).toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
async function removeScheduledJob(jobId: string) {
|
||||
const jobs = await scheduledQueue.getRepeatableJobs();
|
||||
const job = jobs.find(j => j.id === jobId);
|
||||
|
||||
if (job) {
|
||||
await scheduledQueue.removeRepeatableByKey(job.key);
|
||||
console.log(`Removed job: ${jobId}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Serverless Cron (Vercel/AWS)
|
||||
|
||||
```typescript
|
||||
// Vercel Cron Jobs (vercel.json)
|
||||
{
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/cron/daily-backup",
|
||||
"schedule": "0 2 * * *"
|
||||
},
|
||||
{
|
||||
"path": "/api/cron/hourly-sync",
|
||||
"schedule": "0 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// app/api/cron/daily-backup/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
export const maxDuration = 60; // 60 Sekunden max
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Verify Cron Secret (Security)
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
await performBackup();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Backup failed:', error);
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// AWS EventBridge + Lambda
|
||||
// serverless.yml
|
||||
// functions:
|
||||
// dailyBackup:
|
||||
// handler: src/handlers/backup.handler
|
||||
// events:
|
||||
// - schedule: cron(0 2 * * ? *)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Job Locking (Prevent Duplicates)
|
||||
|
||||
```typescript
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
const redis = new Redis(process.env.REDIS_URL!);
|
||||
|
||||
async function withJobLock<T>(
|
||||
jobName: string,
|
||||
ttlSeconds: number,
|
||||
handler: () => Promise<T>
|
||||
): Promise<T | null> {
|
||||
const lockKey = `job-lock:${jobName}`;
|
||||
|
||||
// Versuche Lock zu erwerben
|
||||
const acquired = await redis.set(lockKey, Date.now().toString(), 'EX', ttlSeconds, 'NX');
|
||||
|
||||
if (!acquired) {
|
||||
console.log(`Job ${jobName} already running, skipping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await handler();
|
||||
} finally {
|
||||
// Lock freigeben
|
||||
await redis.del(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
cron.schedule('*/5 * * * *', async () => {
|
||||
await withJobLock('sync-products', 300, async () => {
|
||||
// Dieser Code läuft garantiert nur einmal
|
||||
await syncProducts();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Alerting
|
||||
|
||||
```typescript
|
||||
interface JobExecution {
|
||||
jobName: string;
|
||||
startedAt: Date;
|
||||
completedAt?: Date;
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
error?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
class JobMonitor {
|
||||
private executions: Map<string, JobExecution> = new Map();
|
||||
|
||||
async trackExecution<T>(
|
||||
jobName: string,
|
||||
handler: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const executionId = `${jobName}-${Date.now()}`;
|
||||
|
||||
this.executions.set(executionId, {
|
||||
jobName,
|
||||
startedAt: new Date(),
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await handler();
|
||||
|
||||
const execution = this.executions.get(executionId)!;
|
||||
execution.completedAt = new Date();
|
||||
execution.status = 'completed';
|
||||
execution.duration = execution.completedAt.getTime() - execution.startedAt.getTime();
|
||||
|
||||
// Log to database
|
||||
await this.saveExecution(execution);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const execution = this.executions.get(executionId)!;
|
||||
execution.completedAt = new Date();
|
||||
execution.status = 'failed';
|
||||
execution.error = (error as Error).message;
|
||||
execution.duration = execution.completedAt.getTime() - execution.startedAt.getTime();
|
||||
|
||||
await this.saveExecution(execution);
|
||||
await this.sendAlert(execution);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// Cleanup alte Executions
|
||||
setTimeout(() => this.executions.delete(executionId), 60000);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveExecution(execution: JobExecution) {
|
||||
await db.jobExecution.create({ data: execution });
|
||||
}
|
||||
|
||||
private async sendAlert(execution: JobExecution) {
|
||||
// Slack, Email, PagerDuty, etc.
|
||||
await slack.send({
|
||||
text: `❌ Job Failed: ${execution.jobName}`,
|
||||
attachments: [{
|
||||
color: 'danger',
|
||||
fields: [
|
||||
{ title: 'Error', value: execution.error || 'Unknown' },
|
||||
{ title: 'Duration', value: `${execution.duration}ms` }
|
||||
]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Task Scheduling in Node.js:
|
||||
|
||||
1. **node-cron**: Einfach für Single-Server
|
||||
2. **BullMQ**: Distributed, Reliable, Scalable
|
||||
3. **Serverless Cron**: Vercel, AWS EventBridge
|
||||
4. **Job Locking**: Verhindert Duplikate
|
||||
|
||||
Zuverlässige Automation für jede Anwendung.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Clock with automated tasks running at specific times, scheduling concept"
|
||||
2. "Multiple servers coordinating scheduled jobs, distributed cron"
|
||||
3. "Calendar with recurring events and checkmarks, automated workflow"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [node-cron Documentation](https://www.npmjs.com/package/node-cron)
|
||||
- [BullMQ Repeatable Jobs](https://docs.bullmq.io/guide/jobs/repeatable)
|
||||
- [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs)
|
||||
- [Cron Expression Generator](https://crontab.guru/)
|
||||
Reference in New Issue
Block a user