Το GenServer (γενικός διακομιστής) είναι μια συμπεριφορά που παρέχει αφαιρέσεις για τις κοινές αλληλεπιδράσεις πελάτη-διακομιστή μεταξύ διεργασιών της Elixir.
Θυμάσαι τον βρόχο receive από τότε που μάθαμε για τις διεργασίες; Η συμπεριφορά GenServer παρέχει αφαιρέσεις για την υλοποίηση τέτοιων βρόχων και για την ανταλλαγή μηνυμάτων με μια διεργασία που εκτελεί έναν τέτοιο βρόχο. Κάνει ευκολότερη τη διατήρηση κατάστασης και την εκτέλεση ασύγχρονου κώδικα.
Προσοχή: το όνομα GenServer είναι φορτισμένο. Χρησιμοποιείται επίσης για να περιγράψει ένα module που χρησιμοποιεί τη συμπεριφορά GenServer, καθώς και μια διεργασία που ξεκίνησε από ένα module που χρησιμοποιεί τη συμπεριφορά GenServer.
Η συμπεριφορά GenServer ορίζει ένα υποχρεωτικό callback, το init/1, και μερικά ενδιαφέροντα προαιρετικά callbacks: handle_call/3, handle_cast/2 και handle_info/3. Οι πελάτες που χρησιμοποιούν ένα GenServer δεν πρέπει να καλούν αυτά τα callbacks απευθείας. Αντ' αυτού, το module GenServer παρέχει συναρτήσεις που μπορούν να χρησιμοποιήσουν οι πελάτες για να επικοινωνήσουν με μια διεργασία GenServer.
Συχνά, ένα μόνο module ορίζει και ένα client API, ένα σύνολο συναρτήσεων που μπορούν να καλέσουν άλλα μέρη της εφαρμογής σου σε Elixir για να επικοινωνήσουν με αυτή τη διεργασία GenServer, και υλοποιήσεις server callbacks, οι οποίες περιέχουν τη λογική αυτού του GenServer.
Ας δούμε πρώτα ένα απλό παράδειγμα ενός GenServer και μετά ας μάθουμε τι σημαίνει κάθε callback.
Αυτός είναι ένας παράδειγμα διακομιστή που μπορεί να απαντά στις επαναλαμβανόμενες ερωτήσεις ενοχλητικών επιβατών κατά τη διάρκεια ενός μεγάλου ταξιδιού, και πιο συγκεκριμένα στην ερώτηση: "φτάσαμε;". Κρατάει τον αριθμό των φορών που έχει τεθεί αυτή η ερώτηση, επιστρέφοντας ολοένα και πιο εκνευρισμένες απαντήσεις.
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1Ένας διακομιστής μπορεί να ξεκινήσει καλώντας το GenServer.start/3 ή το GenServer.start_link/3. Μάθαμε για τη διαφορά ανάμεσα σε αυτές τις συναρτήσεις στην έννοια links.
Αυτές οι δύο συναρτήσεις:
GenServer.init_arg, οτιδήποτε. Όπως υποδηλώνει το όνομα, αυτό το όρισμα περνάει στο callback init/1.Η εκκίνηση ενός διακομιστή με κλήση του GenServer.start/3 ή του GenServer.start_link/3 θα καλέσει το callback init/1 με τρόπο που μπλοκάρει. Η τιμή επιστροφής του init/1 καθορίζει αν ο διακομιστής μπορεί να ξεκινήσει με επιτυχία.
Το callback init/1 συνήθως επιστρέφει μία από αυτές τις τιμές:
{:ok, state}. Ο διακομιστής θα ξεκινήσει τον βρόχο receive χρησιμοποιώντας το state ως αρχική κατάσταση. Το state μπορεί να είναι οποιουδήποτε τύπου.{:stop, reason}. Το reason μπορεί να είναι οποιουδήποτε τύπου. Ο διακομιστής δεν θα ξεκινήσει τον βρόχο receive. Η διεργασία θα τερματιστεί με τη δεδομένη αιτία.Υπάρχουν επίσης πιο προχωρημένες δυνατότητες που δεν θα καλύψουμε τώρα.
Αν ξεκινήσει ο βρόχος receive του διακομιστή, οι συναρτήσεις GenServer.start/3 και GenServer.start_link/3 επιστρέφουν μια πλειάδα {:ok, pid}. Διαφορετικά, επιστρέφουν {:error, reason}
handle_call/3Ένα μήνυμα που απαιτεί απάντηση μπορεί να σταλεί σε μια διεργασία διακομιστή με το GenServer.call/2. Αυτή η συνάρτηση περιμένει ως πρώτο όρισμα το pid μιας εκτελούμενης διεργασίας διακομιστή και ως δεύτερο όρισμα το μήνυμα. Το μήνυμα μπορεί να είναι οποιουδήποτε τύπου.
Το callback handle_call/3 είναι υπεύθυνο για τη διαχείριση και την απάντηση σε σύγχρονα μηνύματα. Δέχεται τρία ορίσματα:
message - η τιμή που περνάει ως δεύτερο όρισμα στο GenServer.call/2.from - το pid της διεργασίας που καλεί το GenServer.call/2. Τις περισσότερες φορές αυτό το όρισμα μπορεί να αγνοηθεί.state - η τρέχουσα κατάσταση του διακομιστή. Θυμήσου ότι η αρχική της τιμή ορίστηκε στο callback init/1.Το callback handle_call/3 συνήθως επιστρέφει μια πλειάδα 3 στοιχείων {:reply, reply, state}. Αυτό σημαίνει ότι το δεύτερο στοιχείο της πλειάδας, μια reply που μπορεί να είναι οποιουδήποτε τύπου, θα σταλεί πίσω στον καλούντα. Το τρίτο στοιχείο της πλειάδας, state, είναι η νέα κατάσταση του διακομιστή μετά τη διαχείριση αυτού του μηνύματος.
Υπάρχουν επίσης πιο προχωρημένες δυνατότητες που δεν θα καλύψουμε τώρα.
Για να θυμάσαι τι κάνει αυτό το callback από το όνομά του, σκέψου το σαν να "καλείς" κάποιον στο τηλέφωνο.
Αν αυτό το άτομο είναι διαθέσιμο, θα λάβεις απάντηση αμέσως (σύγχρονα).
handle_cast/2Ένα μήνυμα που δεν απαιτεί απάντηση μπορεί να σταλεί σε μια διεργασία διακομιστή με το GenServer.cast/2. Τα ορίσματά του είναι ίδια με αυτά του GenServer.call/2.
Το callback handle_cast/2 είναι υπεύθυνο για τη διαχείριση αυτών των μηνυμάτων. Δέχεται δύο ορίσματα, message και state, τα οποία είναι τα ίδια ορίσματα όπως στο callback handle_call/3 (εκτός από το from).
Το callback handle_cast/2 συνήθως επιστρέφει μια πλειάδα 2 στοιχείων {:noreply, state}.
Υπάρχουν επίσης πιο προχωρημένες δυνατότητες που δεν θα καλύψουμε τώρα.
Για να θυμάσαι τι κάνει αυτό το callback από το όνομά του, θυμήσου ότι το "to cast" σημαίνει επίσης "to throw".
Αν πετάξεις ένα μήνυμα σε ένα μπουκάλι στη θάλασσα, δεν περιμένεις να λάβεις απάντηση αμέσως, ή ίσως ποτέ.
call ή cast;Σχεδόν πάντα χρησιμοποίησε το call, ακόμα και αν ο κώδικας του πελάτη σου δεν χρειάζεται την απάντηση από τον διακομιστή.
Η χρήση του call περιμένει την απάντηση, η οποία λειτουργεί ως μηχανισμός backpressure (για να εμποδίζει τους πελάτες να στέλνουν πάρα πολλά μηνύματα ταυτόχρονα). Το να λάβεις απάντηση από τον διακομιστή είναι επίσης ο μόνος τρόπος να είσαι σίγουρος ότι ο διακομιστής έλαβε και διαχειρίστηκε το μήνυμα του πελάτη.
handle_info/2Τα μηνύματα μπορούν επίσης να καταλήξουν στα εισερχόμενα του διακομιστή με άλλα μέσα εκτός από την κλήση του GenServer.call/2 ή του GenServer.cast/2, για παράδειγμα καλώντας την απλή συνάρτηση send/2.
Για να διαχειριστείς τέτοια μηνύματα, χρησιμοποίησε το callback handle_info/2. Αυτό το callback λειτουργεί με ακριβώς τον ίδιο τρόπο όπως το handle_cast/2.
Η συμπεριφορά GenServer παρέχει μια υλοποίηση catch-all του handle_info/2 που καταγράφει σφάλματα για απροσδόκητα μηνύματα. Αν παρακάμψεις αυτή την προεπιλεγμένη υλοποίηση, φρόντισε να συμπεριλάβεις πάντα τη δική σου υλοποίηση catch-all. Αν το ξεχάσεις, ο διακομιστής θα καταρρεύσει αν λάβει ένα απροσδόκητο μήνυμα.
Η τιμή επιστροφής καθενός από τα τέσσερα callbacks που περιγράφηκαν παραπάνω μπορεί να επεκταθεί με ένα ακόμη στοιχείο πλειάδας, ένα χρονικό όριο. Π.χ. αντί να επιστρέφεις {:ok, state} από το init/1, επέστρεψε {:ok, state, timeout}.
Το χρονικό όριο μπορεί να χρησιμοποιηθεί για να εντοπίσεις την απουσία μηνυμάτων στο γραμματοκιβώτιο για μια συγκεκριμένη περίοδο. Αν ο διακομιστής επιστρέψει ένα χρονικό όριο από ένα από τα callbacks του και έχουν περάσει τα καθορισμένα χιλιοστά του δευτερολέπτου χωρίς να φτάσει κάποιο μήνυμα, το handle_info/2 καλείται με το :timeout ως πρώτο όρισμα.