Κύκλος ζωής εκτέλεσης
Αυτή η σελίδα παρακολουθεί μία εκτέλεση μίας εργασίας μέσα από τον κώδικα, σε ένα lease backend. Η εργασία είναι η every('sync', '10s', handler) και η εκτέλεση είναι εκείνη που αναμένεται στις 12:00:10.
Ολόκληρη η διαδρομή
Ενότητα με τίτλο «Ολόκληρη η διαδρομή»sequenceDiagram
participant T as JobTimers
participant S as Scheduler
participant L as LeaseRunner
participant B as Backend
participant E as Executor
participant H as Ο handler σας
T->>T: arm(): επόμενο slot 12:00:10, χρονόμετρο στο slot + jitter
Note over T: 12:00:10.310, το χρονόμετρο ενεργοποιείται
T->>S: fire(job, slot)
S->>T: arm(): προγραμματισμός 12:00:20 αμέσως
S->>S: πολύ αργά (now - slot > maxLateMs);
S->>L: run(job, slot, 'scheduled')
L->>B: claim(job, slot, token, leaseMs, ...)
B-->>L: won, προσπάθεια 1
L->>L: εκκίνηση χρονομέτρου heartbeat
L->>E: execute(job, slot, run)
E-->>S: συμβάν: start
E->>H: handler(ctx)
loop κάθε heartbeatMs
L->>B: heartbeat(job, slot, token)
B-->>L: ok
end
H-->>E: επιστρέφει
E->>B: finish(job, slot, token, 'success')
E->>B: saveResult(job, result)
E-->>S: συμβάν: success
L->>L: διακοπή χρονομέτρου heartbeat
Βήμα προς βήμα
Ενότητα με τίτλο «Βήμα προς βήμα»1. Όπλιση του χρονομέτρου
Ενότητα με τίτλο «1. Όπλιση του χρονομέτρου»Όταν ξεκινά ο scheduler, καθώς και μετά από κάθε εκτέλεση, η JobTimers.arm() ζητά από το πρόγραμμα της εργασίας το πρώτο slot μετά το «τώρα», και ορίζει ένα χρονόμετρο για slot + τυχαίο jitter.
Τα χρονόμετρα δεν ορίζονται ποτέ για διάστημα μεγαλύτερο από ένα λεπτό. Μια μεγαλύτερη αναμονή χωρίζεται σε επιμέρους διαστήματα, και το ρολόι ελέγχεται ξανά κάθε φορά. Αυτό διατηρεί το χρονόμετρο ακριβές σε βάθος ωρών και επιτρέπει στον scheduler να αντιληφθεί μια αλλαγή ρολογιού.
2. Το χρονόμετρο ενεργοποιείται
Ενότητα με τίτλο «2. Το χρονόμετρο ενεργοποιείται»Η Scheduler.fire() κάνει τρία πράγματα:
- Οπλίζει πρώτα την επόμενη εκτέλεση. Το επόμενο slot προγραμματίζεται πριν ξεκινήσει η επεξεργασία του τρέχοντος, ώστε μια χρονοβόρα εργασία να μην καθυστερεί ποτέ το πρόγραμμα.
- Ελέγχει τοπικά την καθυστέρηση (lateness). Αν το instance έφτασε σε αυτή την εκτέλεση περισσότερο από
maxLateMsμετά το slot της (λόγω παγώματος ή μπλοκαρίσματος του event loop), εκπέμπειskipμε αιτιολογία'late'και σταματά εδώ. - Παραδίδει την εκτέλεση στον runner.
3. Η διεκδίκηση (claim)
Ενότητα με τίτλο «3. Η διεκδίκηση (claim)»Η LeaseRunner.run() κατασκευάζει τη διεκδίκηση. Εκτός από την εργασία και το slot, περιλαμβάνει ένα νέο τυχαίο token, τη διάρκεια της μίσθωσης (lease length), τον κανόνα αλληλεπικάλυψης (overlap) και το όριο προσπαθειών.
Το backend απαντά με ένα από έξι αποτελέσματα:
flowchart TD
claim["runs.claim()"] --> o{"αποτέλεσμα"}
o -- "won" --> exec["εκτέλεση της εργασίας,<br/>ανανεώνοντας τη μίσθωση"]
o -- "running" --> watch1["skip (taken)<br/>παρακολούθηση της εκτέλεσης ως backup"]
o -- "overlap" --> watch2["skip (overlap)<br/>παρακολούθηση της παλαιότερης εκτέλεσης,<br/>και αυτής αν τυχόν ξεκινήσει"]
o -- "late" --> skipLate["skip (late)"]
o -- "done" --> skipDone["skip (taken)<br/>τίποτα προς παρακολούθηση"]
o -- "none" --> none["κανείς δεν την ξεκίνησε:<br/>συνέχιση ελέγχων όσο μπορεί να ξεκινήσει"]
o -- "η κλήση απέτυχε" --> err["συμβάν error"]
| Αποτέλεσμα | Σημασία | Τι κάνει αυτό το instance |
|---|---|---|
won |
Αυτό το instance κατέχει αυτή την προσπάθεια | Εκτελεί τον handler |
running |
Ένα άλλο instance την εκτελεί, και η μίσθωσή του είναι ενεργή | Γίνεται εφεδρικός (backup) |
overlap |
Μια παλαιότερη εκτέλεση της εργασίας έχει ακόμα ενεργή μίσθωση | Παραλείπει (skip) και παρακολουθεί και τις δύο εκτελέσεις |
late |
Η διεκδίκηση έφτασε στο store υπερβολικά αργά μετά το slot, βάσει του ρολογιού του store | Παραλείπει (skip) |
done |
Η εκτέλεση έχει ήδη ολοκληρωθεί ή εξάντλησε τις προσπάθειές της | Τίποτα |
none |
Ένα backup ρώτησε για μια εκτέλεση που κανείς δεν έχει διεκδικήσει | Συνεχίζει τους ελέγχους για ένα διάστημα |
4. Εκτέλεση του handler
Ενότητα με τίτλο «4. Εκτέλεση του handler»Η executeLeased() ξεκινά ένα χρονόμετρο που στέλνει ένα heartbeat κάθε heartbeatMs, και στη συνέχεια καλεί τον Executor.
Ο Executor:
- Εκπέμπει
start. - Καλεί τον handler σας με το context του. Το
ctx.signalανήκει αποκλειστικά σε αυτή την εκτέλεση. - Αν έχει οριστεί
timeoutMs, θέτει σε αντιπαραβολή (race) τον handler με ένα χρονόμετρο. Όταν κερδίζει το χρονόμετρο, ακυρώνει το signal και θεωρεί την εκτέλεση αποτυχημένη (failed).
5. Απόφαση για το πώς ολοκληρώθηκε
Ενότητα με τίτλο «5. Απόφαση για το πώς ολοκληρώθηκε»Όταν ο handler επιστρέφει ή πετάει εξαίρεση (throws), ο Executor κατηγοριοποιεί την εκτέλεση:
flowchart TD
ended{"ο handler..."}
ended -- "επέστρεψε κανονικά" --> success["success"]
ended -- "πέταξε εξαίρεση" --> why{"ακυρώθηκε το ctx.signal<br/>από stop() ή ανάληψη (takeover);"}
why -- "όχι" --> failed["failed"]
why -- "ναι" --> interrupted["interrupted"]
ended -- "υπερέβη το timeoutMs" --> failed
Μια εξαίρεση μετά από stop() ή ανάληψη (takeover) καταγράφεται ως interrupted, όχι ως failed. Ζητήθηκε από τον handler να σταματήσει· δεν υπήρξε σφάλμα.
6. Καταγραφή της ολοκλήρωσης
Ενότητα με τίτλο «6. Καταγραφή της ολοκλήρωσης»Δύο εγγραφές, με αυτή τη σειρά:
finish: σηματοδοτεί τη λήξη της εκτέλεσης στην κατάσταση μίσθωσης, συνοδευόμενη από το token. Το heartbeat συνεχίζει να εκτελείται μέχρι να αποθηκευτεί αυτό: αν το store είναι στιγμιαία μη προσβάσιμο εκείνη τη στιγμή, ένα backup δεν πρέπει να επανεκτελέσει μια εργασία που έχει ήδη ολοκληρωθεί.saveResult: αποθηκεύει την εγγραφή ιστορικού: κατάσταση, χρόνος έναρξης και λήξης, διάρκεια, και το μήνυμα σφάλματος αν υπήρξε.
Αν η εκτέλεση διακόπηκε από stop(), το πρώτο βήμα είναι ένα release αντί για finish: η μίσθωση λήγει αμέσως, ώστε ένα backup να μπορεί να αναλάβει (takeover) χωρίς αναμονή.
Τέλος, ο Executor εκπέμπει success, failure ή interrupted.
Η ίδια εκτέλεση, στα instances που έχασαν
Ενότητα με τίτλο «Η ίδια εκτέλεση, στα instances που έχασαν»sequenceDiagram
participant A as Instance A (έχασε)
participant B as Backend
A->>B: claim sync @ 12:00:10
B-->>A: running (προσπάθεια 1)
Note over A: συμβάν: skip (taken)
loop κάθε 1 έως 2 heartbeats
A->>B: claim sync @ 12:00:10, mode 'takeover'
B-->>A: running: η μίσθωση είναι ενεργή
end
A->>B: claim sync @ 12:00:10, mode 'takeover'
B-->>A: done: η εκτέλεση ολοκληρώθηκε
Note over A: σταματά την παρακολούθηση
Ο έλεγχος ενός backup είναι μια διεκδίκηση σε λειτουργία ανάληψης (takeover mode): «δώσε μου την επόμενη προσπάθεια αυτής της εκτέλεσης αν η μίσθωσή της έχει λήξει». Όσο η μίσθωση παραμένει ενεργή, η απάντηση είναι running και τίποτα δεν αλλάζει. Ο έλεγχος πραγματοποιείται σε μια τυχαία χρονική στιγμή μεταξύ ενός και δύο heartbeats, ώστε τα backups να μην ρωτούν όλα ταυτόχρονα.
Σε ένα claim-only backend
Ενότητα με τίτλο «Σε ένα claim-only backend»Η διαδρομή είναι συντομότερη. Δεν υπάρχει μίσθωση, επομένως δεν υπάρχουν heartbeats ούτε backups:
sequenceDiagram
participant S as Scheduler
participant C as ClaimOnlyRunner
participant B as Backend
participant E as Executor
S->>C: run(job, slot)
C->>B: claim(job, slot, token)
alt won
B-->>C: true
C->>E: execute(job, slot)
E->>B: saveResult(job, result)
else lost
B-->>C: false
Note over C: συμβάν: skip (taken)
end
Τι προσθέτει το start()
Ενότητα με τίτλο «Τι προσθέτει το start()»Σε ένα lease backend, κάθε εργασία περνά από ανάκαμψη (recovery) όταν ξεκινά ο scheduler, πριν ενεργοποιηθεί το πρώτο της χρονόμετρο:
flowchart TD
start["start()"] --> active["ερώτημα στο store: ποιες εκτελέσεις αυτής της εργασίας είναι ημιτελείς;"]
active --> each{"για καθεμία"}
each -- "η μίσθωσή της είναι ενεργή" --> watch["παρακολούθηση ως backup"]
each -- "η μίσθωσή της έληξε,<br/>και είναι ακόμα η πιο πρόσφατη εκτέλεση" --> take["διεκδίκηση της επόμενης προσπάθειας (takeover)"]
each -- "η μίσθωσή της έληξε,<br/>αλλά αναμένεται νεότερη εκτέλεση" --> leave["αγνόηση: η επόμενη εκτέλεση την αντικαθιστά"]
start --> catchup{"αναμενόταν το πιο πρόσφατο slot<br/>πριν από λιγότερο από catchUpMs;"}
catchup -- "ναι" --> claimc["αναμονή τυχαίου jitter,<br/>έπειτα διεκδίκηση (catch-up)"]
catchup -- "όχι" --> nothing["τίποτα"]
Αυτό καθιστά ασφαλή μια πλήρη επανεκκίνηση: όποιο instance επιστρέψει πρώτο αναλαμβάνει ό,τι βρισκόταν σε εξέλιξη.
Στη συνέχεια
Ενότητα με τίτλο «Στη συνέχεια»- Αλγόριθμοι διεκδίκησης: τι συμβαίνει μέσα στο
claim() - Μισθώσεις (Leases): heartbeats, ανάληψη και fencing λεπτομερώς