Επιλογές εργασιών
Το τελευταίο όρισμα των 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 δεν έχει μισθώσειςjitterMs
Ενότητα με τίτλο «jitterMs»Κάθε 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:30scheduler.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 τουςmaxLateMs
Ενότητα με τίτλο «maxLateMs»Όταν ένα 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 });timeoutMs
Ενότητα με τίτλο «timeoutMs»Μετά από αυτόν τον αριθμό χιλιοστών του δευτερολέπτου, το 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 δεν μπλοκάρει την αυριανήoverlap
Ενότητα με τίτλο «overlap»Απαιτεί backend μίσθωσης.
Τι συμβαίνει όταν μια εκτέλεση πρέπει να ξεκινήσει ενώ η προηγούμενη εκτέλεση της ίδιας εργασίας βρίσκεται ακόμη σε εξέλιξη:
'skip'(προεπιλογή): η νέα εκτέλεση παραλείπεται, με ένα eventskipμε αιτία'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 είναι αυτοτελέςmaxAttempts
Ενότητα με τίτλο «maxAttempts»Απαιτεί 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 }); // ασφαλές να ξαναγίνει: συνεχίζει αλλούcatchUpMs
Ενότητα με τίτλο «catchUpMs»Απαιτεί 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
Ενότητα με τίτλο «historySize και historyTtlMs»Το 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 });
// Μια εργασία που δεν πρέπει να ξεκινήσει δύο φορές για το ίδιο slotscheduler.cron('send-invoices', '0 9 1 * *', send, { maxAttempts: 1, catchUpMs: 12 * 60 * 60_000 });