Μετάβαση στο περιεχόμενο

Επιλογές εργασιών

Το τελευταίο όρισμα των every(), cron() και at() είναι ένα αντικείμενο επιλογών (options object). Κάθε επιλογή έχει μια προεπιλεγμένη τιμή, επομένως ορίζετε μόνο ό,τι χρειάζεστε.

scheduler.every('report', '35m', buildReport, {
timeoutMs: 20 * 60_000,
maxAttempts: 2,
});
Επιλογή Προεπιλογή Απαιτεί lease backend Τι ελέγχει
jitterMs του scheduler (2000), μειωμένο για μικρά διαστήματα όχι Την τυχαία καθυστέρηση πριν από τη διεκδίκηση (claim)
maxLateMs του scheduler (60000) όχι Πόσο αργά επιτρέπεται σε ένα instance να ξεκινήσει μια εκτέλεση
timeoutMs κανένα όχι Τον μέγιστο χρόνο που μπορεί να διαρκέσει μια εκτέλεση
overlap 'skip' ναι Αν μια εκτέλεση μπορεί να ξεκινήσει ενώ η προηγούμενη βρίσκεται σε εξέλιξη
maxAttempts 3 ναι Πόσες φορές μπορεί να ξεκινήσει μια εκτέλεση όταν καταρρέουν οι runners
catchUpMs 300000 (5 λεπτά) ναι Πόσο παλιά μπορεί να είναι μια χαμένη εκτέλεση και παρ’ όλα αυτά να εκτελεστεί κατά την εκκίνηση
historySize του scheduler (1000) όχι Πόσες εκτελέσεις διατηρούνται στο ιστορικό
historyTtlMs του scheduler (κανένα) όχι, αλλά όχι σε RabbitMQ ή NATS Για πόσο χρονικό διάστημα διατηρούνται
offsetMs 0 όχι Μόνο για every(): μετατοπίζει το πλέγμα
timezone του scheduler όχι Μόνο για cron(): τη ζώνη ώρας
data κανένα όχι Δεδομένα για τις εκτελέσεις της εργασίας, που διαβάζονται στον handler ως ctx.data

«Του scheduler» σημαίνει την επιλογή με το ίδιο όνομα στο new Scheduler(). Δείτε τις Επιλογές scheduler.

Μια επιλογή που απαιτεί lease backend δεν προσφέρεται σε άλλο backend. Σε scheduler με backend RabbitMQ, NATS, ή Redis/Valkey χωρίς τη στρατηγική lease, τα every(), cron() και at() δεν δέχονται overlap, maxAttempts ή catchUpMs: ο κώδικας δεν μεταγλωττίζεται. Οι decorators και η απλή JavaScript δεν ελέγχονται έτσι, οπότε η εργασία απορρίπτεται όταν προστίθεται, με σφάλμα που ονομάζει την επιλογή και το backend. Το at() δεν δέχεται ποτέ overlap, αφού μια εφάπαξ εργασία δεν έχει προηγούμενη εκτέλεση.

const scheduler = new Scheduler({ backend: new RabbitMQBackend({ url }) });
scheduler.every('sync', '5m', sync, { timeoutMs: 60_000 }); // εντάξει
scheduler.every('sync', '5m', sync, { overlap: 'allow' }); // σφάλμα τύπου: το RabbitMQ δεν έχει μισθώσεις

Κάθε instance περιμένει ένα τυχαίο χρονικό διάστημα μεταξύ 0 και jitterMs μετά το slot προτού διεκδικήσει (claim) την εκτέλεση.

flowchart LR
  slot["slot<br/>12:00:00.000"] --> window["κάθε instance επιλέγει μια τυχαία στιγμή<br/>μέσα στα επόμενα jitterMs"] --> claim["το πρώτο που θα το ζητήσει κερδίζει"]
  • Μεγαλύτερο jitter κατανέμει το φορτίο πιο ομοιόμορφα και καθιστά την ώρα έναρξης λιγότερο ακριβή.
  • 0 σημαίνει καθόλου κατανομή: όλα τα instances κάνουν claim την ίδια ακριβώς στιγμή, και αυτό με το ταχύτερο ρολόι και σύνδεση συνήθως κερδίζει.

Πρέπει να είναι μικρότερο από το μισό του χρόνου μεταξύ των εκτελέσεων. Αν δεν το ορίσετε, η προεπιλογή είναι το jitterMs του scheduler (2 δευτερόλεπτα), ή το ένα τέταρτο του μεσοδιαστήματος όταν αυτό είναι μικρότερο. Έτσι, μια εργασία που εκτελείται κάθε δευτερόλεπτο παίρνει 250 ms.

scheduler.every('tick', '1s', tick); // jitter 250 ms, αυτόματα
scheduler.cron('nightly', '0 2 * * *', run, { jitterMs: 30_000 }); // ξεκινά μεταξύ 02:00:00 και 02:00:30
scheduler.every('precise', '1m', run, { jitterMs: 0 }); // ξεκινά ακριβώς στην ώρα του, πάντα από το ταχύτερο instance

Λειτουργεί σε κάθε backend. Χρησιμοποιήστε το όταν:

  • Μια βαριά εργασία πρέπει να πέφτει σε τυχαίο instance: μεγαλύτερο jitter μοιράζει τις εκτελέσεις, ώστε να μην κάνει ένα instance κάθε νυχτερινή ανακατασκευή.
  • Η ώρα έναρξης μετρά στο δευτερόλεπτο: jitterMs: 0. Τότε κερδίζει πάντα το ταχύτερο instance· αυτό είναι το τίμημα.
  • Πολλά instances καλούν μία εξάρτηση την ίδια στιγμή: το jitter μοιράζει τα πρώτα αιτήματα σε λίγα δευτερόλεπτα.
scheduler.cron('rebuild-search-index', '0 3 * * *', rebuild, { jitterMs: 5 * 60_000 }); // μοιρασμένο σε 5 λεπτά
scheduler.cron('market-open', '30 9 * * 1-5', open, { timezone: 'America/New_York', jitterMs: 0 }); // στο δευτερόλεπτο
scheduler.every('poll-partner-api', '1m', poll, { jitterMs: 20_000 }); // ήπια με το rate limit τους

Όταν ένα instance φτάσει σε μια εκτέλεση με καθυστέρηση μεγαλύτερη από maxLateMs μετά το slot της, την παραλείπει και εκπέμπει ένα event skip με αιτία 'late'.

Αυτό σας προστατεύει από παρωχημένες εκτελέσεις (stale runs). Μια διεργασία μπορεί να παγώσει για λεπτά: ένας φορητός υπολογιστής σε αναστολή λειτουργίας, μια παρατεταμένη παύση garbage collection, μια εικονική μηχανή που έχει κολλήσει. Όταν ξυπνήσει, τα timers της ενεργοποιούνται για εκτελέσεις που έπρεπε να είχαν γίνει πολύ νωρίτερα. Χωρίς αυτό το όριο, θα τις ξεκινούσε.

// Μια εκτέλεση που δεν μπορεί να ξεκινήσει εντός 10 δευτερολέπτων από την ώρα της δεν αξίζει να ξεκινήσει
scheduler.every('quote-refresh', '30s', refresh, { maxLateMs: 10_000 });

Πρέπει να είναι μεγαλύτερο από το jitterMs.

Λειτουργεί σε κάθε backend. Σε RabbitMQ, NATS και Redis/Valkey χωρίς lease, το jitterMs + maxLateMs πρέπει επίσης να είναι μικρότερο από όσο το backend θυμάται μια διεκδικημένη εκτέλεση, αλλιώς ένα καθυστερημένο instance θα μπορούσε να κερδίσει ξανά την ίδια εκτέλεση· το anarchon το ελέγχει όταν προστίθεται η εργασία. Χρησιμοποιήστε το όταν:

  • Μια μπαγιάτικη εκτέλεση είναι χειρότερη από καμία: τιμές, παρουσία, ό,τι παλιώνει σε δευτερόλεπτα. Κρατήστε το μικρό.
  • Μια καθυστερημένη εκτέλεση είναι ακόμα χρήσιμη: ένας καθαρισμός ή μια αναφορά μπορεί να τρέξει μισή ώρα αργότερα. Αυξήστε το.
scheduler.every('presence-sweep', '10s', sweep, { maxLateMs: 5_000 }); // 5 s αργά σημαίνει μπαγιάτικη παρουσία: παραλείπεται
scheduler.cron('cleanup-temp', '0 4 * * *', cleanup, { maxLateMs: 30 * 60_000 }); // εντάξει και μισή ώρα αργά
// Στο RabbitMQ, αυξήστε πρώτα το slotTtlMs (5 λεπτά από προεπιλογή), ώστε ο broker να θυμάται αρκετά
new Scheduler({ backend: new RabbitMQBackend({ url, slotTtlMs: 20 * 60_000 }), maxLateMs: 15 * 60_000 });

Μετά από αυτόν τον αριθμό χιλιοστών του δευτερολέπτου, το ctx.signal ακυρώνεται (aborts) και η εκτέλεση καταγράφεται ως αποτυχημένη, με το σφάλμα TimeoutError: Timed out after N ms.

scheduler.every('export', '5m', exportAll, { timeoutMs: 4 * 60_000 });

Ορίστε ένα timeout σε κάθε εργασία που μπορεί να «κολλήσει». Με overlap: 'skip', μια κολλημένη εκτέλεση συνεχίζει να ανανεώνει τη μίσθωσή (lease) της και μπλοκάρει κάθε μεταγενέστερη εκτέλεση αυτής της εργασίας.

Λειτουργεί σε κάθε backend. Χρησιμοποιήστε το όταν:

  • Η εργασία καλεί οτιδήποτε μέσω δικτύου: μια κολλημένη σύνδεση δεν πρέπει να κρατά την εργασία για πάντα. Περάστε το ctx.signal.
  • Η εργασία πρέπει να τελειώνει πριν από την επόμενη εκτέλεσή της: ορίστε το κάτω από το διάστημα, ώστε μια αργή εκτέλεση να αποτυγχάνει και η επόμενη να ξεκινά στην ώρα της.
scheduler.every('sync-crm', '15m', (ctx) => crm.sync({ signal: ctx.signal }), { timeoutMs: 10 * 60_000 });
scheduler.cron('send-digest', '0 8 * * *', sendDigest, { timeoutMs: 30 * 60_000 }); // ένας κολλημένος SMTP server δεν μπλοκάρει την αυριανή

Απαιτεί backend μίσθωσης.

Τι συμβαίνει όταν μια εκτέλεση πρέπει να ξεκινήσει ενώ η προηγούμενη εκτέλεση της ίδιας εργασίας βρίσκεται ακόμη σε εξέλιξη:

  • 'skip' (προεπιλογή): η νέα εκτέλεση παραλείπεται, με ένα event skip με αιτία 'overlap'.
  • 'allow': εκτελούνται και οι δύο.
Μια εργασία διάρκειας 7 δευτ. κάθε 3 δευτ., με overlap 'skip'
χρόνος :00 :03 :06 :09 :12 :15 :18
εκτέλεση [======= 7 s =======]
skip skip
[======= 7 s =======]
skip skip

Οι εκτελέσεις που παραλείπονται δεν μπαίνουν σε ουρά. Η εκτέλεση στο :03 δεν πραγματοποιείται ποτέ. Η επόμενη εκτέλεση που θα ξεκινήσει είναι η πρώτη που είναι προγραμματισμένη μετά τη λήξη της τρέχουσας εκτέλεσης.

// Ανεξάρτητη εργασία ανά εκτέλεση: επιτρέψτε την αλληλεπικάλυψή τους
scheduler.every('sample-metrics', '5s', sample, { overlap: 'allow' });

Λειτουργεί σε backends μίσθωσης: PostgreSQL, SQLite, Redis ή Valkey με lease. Στα υπόλοιπα οι εκτελέσεις μπορούν πάντα να επικαλύπτονται· αν δύο δεν πρέπει, προστατέψτε το μέσα στην εργασία (για παράδειγμα με ένα κλείδωμα βάσης). Χρησιμοποιήστε το όταν:

  • 'skip': οι εκτελέσεις της εργασίας γράφουν τα ίδια δεδομένα: συγχρονισμός, εξαγωγή, επαναδημιουργία ευρετηρίου. Δύο μαζί θα συγκρούονταν.
  • 'allow': κάθε εκτέλεση είναι αυτοτελής: ένα ping, ένα δείγμα μετρικών, ένας έλεγχος που απλώς καταγράφεται.
scheduler.every('reindex', '5m', reindex); // 'skip': δύο reindex θα συγκρούονταν
scheduler.every('ping-endpoints', '30s', ping, { overlap: 'allow' }); // κάθε ping είναι αυτοτελές

Απαιτεί backend μίσθωσης.

Πόσες φορές μπορεί να ξεκινήσει μία εκτέλεση όταν ο runner της πεθάνει (καταρρεύσει), συμπεριλαμβανομένης της πρώτης προσπάθειας. Η προεπιλογή είναι 3.

  • Μια εκτέλεση επαναλαμβάνεται μόνο όταν ο runner της σταμάτησε να ανανεώνει τη μίσθωση: κατέρρευσε, πάγωσε ή έχασε τη σύνδεση δικτύου.
  • Μια εκτέλεση που αποτυγχάνει (ο handler πετάει exception) δεν επαναλαμβάνεται. Οι επαναλήψεις προορίζονται για νεκρούς runners, όχι για σφάλματα. Αν θέλετε να κάνετε retry σε σφάλματα, υλοποιήστε το μέσα στον handler.
  • Μια παράδοση (hand-over) κατά το stop() δεν προσμετράται στο όριο.
scheduler.cron('charge-cards', '0 6 * * *', charge, { maxAttempts: 1 }); // δεν ξεκινά ποτέ δεύτερη φορά

Όταν το όριο εξαντληθεί, η εκτέλεση επισημαίνεται ως interrupted και εγκαταλείπεται.

Λειτουργεί σε backends μίσθωσης. Χρησιμοποιήστε το όταν:

  • 1: το αποτέλεσμα δεν πρέπει ποτέ να γίνει δύο φορές και δεν είναι idempotent: πληρωμές, μηνύματα χωρίς κλειδί αποφυγής διπλότυπων. Μια εκτέλεση που σταμάτησε στη μέση θέλει άνθρωπο, όχι επανάληψη.
  • 3 (η προεπιλογή): η εργασία μπορεί να ξαναγίνει με ασφάλεια, ή συνεχίζει από checkpoint.
  • Περισσότερες: μια μεγάλη, idempotent εργασία σε υποδομή όπου τα instances έρχονται και φεύγουν.
scheduler.cron('charge-cards', '0 6 * * *', charge, { maxAttempts: 1 }); // μια μισή χρέωση θέλει άνθρωπο
scheduler.every('rebuild-cache', '1h', rebuild, { maxAttempts: 5 }); // ασφαλές να ξαναγίνει: συνεχίζει αλλού

Απαιτεί backend μίσθωσης.

Κατά το start(), ένα instance ελέγχει το πιο πρόσφατο slot κάθε εργασίας. Αν κανείς δεν το εκτέλεσε και η προγραμματισμένη ώρα του ήταν πριν από λιγότερο από catchUpMs, το instance το εκτελεί τώρα, με ctx.reason === 'catch-up'.

Αυτό καλύπτει μια εκτέλεση που έπρεπε να τρέξει ενώ όλα τα instances ήταν εκτός λειτουργίας, για παράδειγμα κατά τη διάρκεια μιας πλήρους επανεκκίνησης.

scheduler.cron('daily-report', '0 6 * * *', report, { catchUpMs: 6 * 60 * 60_000 }); // έως και 6 ώρες καθυστέρηση
scheduler.every('refresh-cache', '5s', refresh, { catchUpMs: 0 }); // ποτέ ανάκτηση

Μόνο η πιο πρόσφατη εκτέλεση ανακτάται, όχι κάθε εκτέλεση που χάθηκε.

Λειτουργεί σε backends μίσθωσης. Χρησιμοποιήστε το όταν:

  • Μια ημερήσια ή μηνιαία εργασία πρέπει να γίνει και μετά από μεγάλη διακοπή: ώρες, ή μια ημέρα.
  • Μια ωριαία εργασία: έως περίπου το διάστημα, ώστε μια εκτέλεση catch-up να μην πέφτει ακριβώς πριν από την επόμενη.
  • Μια συχνή εργασία: 0, αφού η επόμενη εκτέλεση είναι σε λίγο.
scheduler.cron('monthly-invoices', '0 6 1 * *', invoice, { catchUpMs: 24 * 60 * 60_000 }); // μια ολόκληρη ημέρα για ανάκτηση
scheduler.every('hourly-rollup', '1h', rollup, { catchUpMs: 50 * 60_000 }); // ως λίγο πριν από την επόμενη
scheduler.every('heartbeat', '10s', beat, { catchUpMs: 0 }); // ποτέ

Το historySize αφορά το πόσες εκτελέσεις διατηρούνται ανά εργασία, ενώ το historyTtlMs απορρίπτει επιπλέον εγγραφές παλαιότερες από μια συγκεκριμένη διάρκεια.

scheduler.every('heartbeat', '5s', ping, { historySize: 100 });
scheduler.cron('audit', '0 * * * *', audit, { historyTtlMs: 30 * 24 * 60 * 60_000 }); // 30 ημέρες

Τα RabbitMQ και NATS περικόπτουν το ιστορικό μόνο βάσει πλήθους, οπότε απορρίπτουν το historyTtlMs. Το NATS κρατά ένα μέγεθος ιστορικού για όλες τις εργασίες του: το μεγαλύτερο ανάμεσα στο historySize του backend και σε εκείνο κάθε εργασίας.

Στα backends μίσθωσης ένα μικρό historySize είναι πάντα ασφαλές: μια εκτέλεση δεν ξεχνιέται ποτέ όσο ένα καθυστερημένο claim θα μπορούσε ακόμα να τη ζητήσει. Στα backends μόνο διεκδίκησης (claim-only backends), η μνήμη του store για τις διεκδικημένες εκτελέσεις πρέπει επίσης να καλύπτει τα καθυστερημένα instances. Το anarchon το ελέγχει αυτό όταν προσθέτετε την εργασία και αποτυγχάνει με σφάλμα (throws) αν οι ρυθμίσεις δεν είναι ασφαλείς. Δείτε το Χρονισμός και ρολόγια.

Λειτουργεί σε κάθε backend· το historyTtlMs όχι σε RabbitMQ ή NATS. Χρησιμοποιήστε το όταν:

  • Μια συχνή εργασία: κρατήστε το πολύ μερικές εκατοντάδες εκτελέσεις· κανείς δεν διαβάζει περισσότερες.
  • Έλεγχοι ή κανονιστική συμμόρφωση: κρατήστε έναν χρόνο βάσει ηλικίας, με πλήθος αρκετό για να τον χωρά.
scheduler.every('heartbeat', '10s', beat, { historySize: 60 }); // τα τελευταία 10 λεπτά
scheduler.cron('payouts', '0 7 * * *', payout, { historySize: 400, historyTtlMs: 365 * 24 * 60 * 60_000 }); // ένας χρόνος

Ένα σημείο εκκίνησης για συνηθισμένες εργασίες. «προεπιλογή» σημαίνει αφήστε το χωρίς τιμή.

Είδος εργασίας jitterMs maxLateMs timeoutMs overlap maxAttempts catchUpMs Εκτελέσιμο παράδειγμα
Πληρωμές, χρεώσεις προεπιλογή προεπιλογή ναι προεπιλογή 1 ώρες billing.ts
Ημερήσια αναφορά ή σύνοψη λεπτά 30 λεπτά ναι προεπιλογή προεπιλογή ώρες
Εξαγωγή ή ETL μεγαλύτερη από το διάστημά της προεπιλογή προεπιλογή κάτω από το διάστημα προεπιλογή προεπιλογή έως το διάστημα nightly-export.ts
Ανανέωση cache κάθε λίγα δευτερόλεπτα περίπου το ένα τρίτο του διαστήματος μικρό μικρό προεπιλογή προεπιλογή 0 cache-refresh.ts
Δείγμα μετρικών, ping 0 μικρό μικρό 'allow' 1 0
Αποστολή ό,τι κρατά ένα outbox προεπιλογή κάτω από τη μνήμη του backend ναι broker-notifications.ts
Εφάπαξ μετάπτωση (at()) 0 μεγάλο μεγάλο 1 μεγάλο

Ένα κενό κελί σημαίνει ότι η εργασία δεν χρειάζεται την επιλογή. Η εργασία του outbox δεν χρειάζεται καμία επιλογή μίσθωσης, οπότε τρέχει σε κάθε backend, και σε RabbitMQ και NATS· το overlap δεν αφορά το at().

// Μια βαριά νυχτερινή εργασία: το πολύ 2 ώρες, ξεκινά το πολύ δύο φορές, ποτέ δύο ταυτόχρονα
scheduler.cron('rebuild-index', '0 2 * * *', rebuild, {
timeoutMs: 2 * 60 * 60_000,
maxAttempts: 2,
});
// Μια ελαφριά, συχνή εργασία όπου μια χαμένη εκτέλεση δεν έχει σημασία
scheduler.every('refresh-cache', '5s', refresh, { catchUpMs: 0, historySize: 50 });
// Μια εργασία που δεν πρέπει να ξεκινήσει δύο φορές για το ίδιο slot
scheduler.cron('send-invoices', '0 9 1 * *', send, { maxAttempts: 1, catchUpMs: 12 * 60 * 60_000 });