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

Βασικές έννοιες

Το υπόλοιπο της τεκμηρίωσης χρησιμοποιεί ένα μικρό λεξιλόγιο όρων. Κάθε όρος ορίζεται εδώ μία φορά.

Ένα εκτελούμενο αντίγραφο της υπηρεσίας σας: μία διεργασία (process). Κάθε instance δημιουργεί τον δικό του Scheduler με τις ίδιες εργασίες. Το καθένα διαθέτει ένα instanceId, από προεπιλογή hostname:pid, το οποίο εμφανίζεται στο ιστορικό ώστε να μπορείτε να δείτε ποιος εκτέλεσε τι.

Μια επώνυμη εργασία με ένα χρονοδιάγραμμα (schedule) και έναν handler:

scheduler.every('sync', '10s', async (ctx) => { /* ο handler */ });
// name schedule

Το name (όνομα) ταυτοποιεί την εργασία στο store, επομένως πρέπει να είναι το ίδιο σε κάθε instance και δεν θα πρέπει να αλλάζει μεταξύ των deploys.

Ένας προγραμματισμένος χρόνος μιας εργασίας, ως χιλιοστά του δευτερολέπτου (milliseconds) από το 1970 (Unix epoch). «Η εκτέλεση 12:00:10 του sync» αντιστοιχεί στο slot 1791633610000.

Το slot είναι η καρδιά του σχεδιασμού. Κάθε instance υπολογίζει τα ίδια slots από το ίδιο χρονοδιάγραμμα, χωρίς επικοινωνία μεταξύ τους:

  • Το every('10s') χρησιμοποιεί ένα πλέγμα (grid) υπολογισμένο από τις 1970-01-01 UTC, επομένως τα slots είναι :00, :10, :20 ανεξάρτητα από την ώρα που ξεκίνησε κάθε instance.
  • Το cron('0 8 * * *') χρησιμοποιεί την έκφραση cron στη ζώνη ώρας (time zone) της.

Επειδή το slot είναι ο ίδιος αριθμός παντού, το ζεύγος (job name, slot) προσδιορίζει μία συγκεκριμένη εκτέλεση σε ολόκληρο το cluster. Για αυτό το ζεύγος ανταγωνίζονται τα instances.

flowchart LR
  subgraph grid["every('10s') στο πλέγμα UTC"]
    direction LR
    s0["12:00:00"] --> s1["12:00:10"] --> s2["12:00:20"] --> s3["12:00:30"]
  end
  A["Instance A<br/>εκκίνησε 11:58:03"] -.-> s1
  B["Instance B<br/>εκκίνησε 12:00:07"] -.-> s1

Ένα run είναι ένα slot μιας εργασίας. Συνήθως ένα run εκτελείται μία φορά. Αν το instance που το εκτελεί τερματιστεί απροσδόκητα, ένα άλλο instance το ξεκινά ξανά: αυτό αποτελεί μια δεύτερη προσπάθεια (attempt) του ίδιου run. Το ctx.attempt είναι 1 για την πρώτη προσπάθεια, και 2 ή περισσότερο μετά από μια ανάληψη (takeover).

Το αίτημα που στέλνει ένα instance στο store όταν φτάσει η ώρα ενός slot: «κατάγραψε ότι εκτελώ αυτή την εκτέλεση, εκτός αν είναι ήδη δεσμευμένη». Το store απαντά σε ένα ατομικό βήμα, έτσι ώστε ακριβώς ένα instance να κερδίζει.

Μια τυχαία καθυστέρηση, μεταξύ 0 και jitterMs, την οποία περιμένει κάθε instance πριν επιχειρήσει claim. Χωρίς αυτό, το instance με το ταχύτερο ρολόι θα κέρδιζε κάθε φορά. Με αυτό, διαφορετικό instance τείνει να κερδίζει κάθε εκτέλεση, ώστε το φορτίο να κατανέμεται.

sequenceDiagram
  participant A as Instance A
  participant B as Instance B
  participant C as Instance C
  participant S as Store
  Note over A,C: slot 12:00:10.000
  Note over B: περιμένει 310 ms
  Note over A: περιμένει 1 420 ms
  Note over C: περιμένει 1 880 ms
  B->>S: claim (12:00:10.310)
  S-->>B: won
  A->>S: claim (12:00:11.420)
  S-->>A: taken
  C->>S: claim (12:00:11.880)
  S-->>C: taken

Ο προσαρμογέας (adapter) μεταξύ του anarchon και ενός store. Τα RedisBackend, PostgresBackend, SqliteBackend, RabbitMQBackend και NatsBackend είναι ενσωματωμένα. Όλα τους εγγυώνται έναν νικητή ανά εκτέλεση. Ορισμένα υποστηρίζουν επίσης μισθώσεις (leases).

Μια υπόσχεση με χρόνο λήξης, αποθηκευμένη μαζί με την εκτέλεση: «το instance B εκτελεί αυτό το run, και είναι ζωντανό έως τις 12:00:40». Όσο η εργασία εκτελείται, ο runner μεταθέτει αυτόν τον χρόνο αργότερα κάθε heartbeatMs. Αυτό είναι το heartbeat (παλμός).

Αν ο runner καταρρεύσει, τα heartbeats σταματούν, η μίσθωση λήγει, και η εκτέλεση καθίσταται διαθέσιμη για την επόμενη προσπάθεια.

Μια μίσθωση δεν είναι lock. Κανείς δεν περιμένει σε ουρά για αυτήν και κανείς δεν υποχρεούται να την απελευθερώσει. Απαντά μόνο σε ένα ερώτημα: είναι ο runner ακόμα ζωντανός;

Ένα instance που έχασε το claim για μια εκτέλεση. Ελέγχει τη μίσθωση της εκτέλεσης κάθε 1 έως 2 heartbeats. Αν η μίσθωση λήξει πριν ολοκληρωθεί η εκτέλεση, ο εφεδρικός διεκδικεί την επόμενη προσπάθεια. Αυτό ονομάζεται ανάληψη (takeover).

Μια τυχαία τιμή που δημιουργείται για κάθε προσπάθεια. Το store απομνημονεύει το token της τρέχουσας προσπάθειας, και τα heartbeats καθώς και η ένδειξη ολοκλήρωσης («finished») γίνονται δεκτά μόνο με αυτό το token. Αν ένας εφεδρικός έχει αναλάβει την εκτέλεση (takeover), το token του παλιού runner δεν ταιριάζει πλέον, οπότε τίποτα από όσα στέλνει δεν επηρεάζει την εκτέλεση. Αυτό ονομάζεται fencing (περίφραξη/απομόνωση).

stateDiagram-v2
  [*] --> Due: φτάνει η ώρα του slot
  Due --> Claimed: ένα instance κερδίζει το claim
  Due --> Skipped: μια προηγούμενη εκτέλεση συνεχίζεται ακόμα (overlap)
  Claimed --> Running: ο handler ξεκινά, τα heartbeats αρχίζουν
  Running --> Success: ο handler επιστρέφει επιτυχώς
  Running --> Failed: ο handler πετά σφάλμα ή εξαντλεί το timeout
  Running --> Interrupted: ο runner κατέρρευσε, πάγωσε ή τερματίστηκε
  Interrupted --> Running: ένας εφεδρικός αναλαμβάνει (επόμενη προσπάθεια)
  Success --> [*]
  Failed --> [*]
  Skipped --> [*]

Backends με υποστήριξη μίσθωσης και backends μόνο για claim

Ενότητα με τίτλο «Backends με υποστήριξη μίσθωσης και backends μόνο για claim»

Δεν μπορεί κάθε store να διατηρεί μια μίσθωση την οποία μπορούν να ελέγχουν οι άλλοι.

Backends με μίσθωση (Lease) Backends μόνο για claim
Ποια Redis και Valkey (στρατηγική lease), PostgreSQL, SQLite Στρατηγικές Redis stream-id, idmp και script, RabbitMQ, NATS
Ένας νικητής ανά εκτέλεση Ναι Ναι
Εφεδρικοί (backups) και ανάληψη (takeover) Ναι Όχι
Χωρίς αλληλεπικαλυπτόμενες εκτελέσεις (overlap) Ναι Όχι
Ανάκτηση μετά από downtime (catch-up) Ναι Όχι
Παράδοση (hand-over) στο stop() Ναι Όχι

Οι οδηγοί επισημαίνουν τις λειτουργίες που απαιτούν backend με υποστήριξη μίσθωσης.