Το GenServer (γενικός διακομιστής) είναι μια συμπεριφορά που παρέχει μια αφαίρεση για τις κοινές αλληλεπιδράσεις πελάτη-διακομιστή μεταξύ διεργασιών της Elixir.
Θυμάσαι τον βρόχο λήψης από τότε που μάθαμε για τις διεργασίες; Η συμπεριφορά GenServer παρέχει αφαιρέσεις για την υλοποίηση τέτοιων βρόχων και για την ανταλλαγή μηνυμάτων με μια διεργασία που εκτελεί έναν τέτοιο βρόχο. Κάνει ευκολότερη τη διατήρηση της κατάστασης και την εκτέλεση ασύγχρονου κώδικα.
Να έχεις υπόψη σου ότι το όνομα GenServer έχει πολλαπλές σημασίες. Χρησιμοποιείται επίσης για να περιγράψει ένα module που χρησιμοποιεί τη συμπεριφορά GenServer, καθώς και μια διεργασία που ξεκίνησε από ένα module που χρησιμοποιεί τη συμπεριφορά GenServer.
Η συμπεριφορά GenServer ορίζει ένα υποχρεωτικό callback, το init/1, και μερικά ενδιαφέροντα προαιρετικά callback: handle_call/3, handle_cast/2 και handle_info/3. Οι πελάτες που χρησιμοποιούν ένα GenServer δεν πρέπει να καλούν αυτά τα callback απευθείας. Αντίθετα, το module GenServer παρέχει συναρτήσεις που μπορούν να χρησιμοποιήσουν οι πελάτες για να επικοινωνήσουν με μια διεργασία GenServer.
Συχνά, ένα μόνο module ορίζει και το API πελάτη, ένα σύνολο συναρτήσεων που μπορούν να καλέσουν άλλα μέρη της εφαρμογής σου σε Elixir για να επικοινωνήσουν με αυτή τη διεργασία GenServer, και τις υλοποιήσεις των callback του διακομιστή, που περιέχουν τη λογική αυτού του 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}. Ο διακομιστής θα ξεκινήσει τον βρόχο λήψης του χρησιμοποιώντας το state ως αρχική κατάσταση. Το state μπορεί να είναι οποιουδήποτε τύπου.{:stop, reason}. Το reason μπορεί να είναι οποιουδήποτε τύπου. Ο διακομιστής δεν θα ξεκινήσει τον βρόχο λήψης του. Η διεργασία θα τερματιστεί με τη δεδομένη αιτία.Υπάρχουν επίσης πιο προχωρημένες δυνατότητες που δεν θα καλύψουμε τώρα.
Αν ξεκινήσει ο βρόχος λήψης του διακομιστή, οι συναρτήσεις 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 παρέχει μια προεπιλεγμένη υλοποίηση του handle_info/2 που καλύπτει όλες τις περιπτώσεις και καταγράφει σφάλματα για απροσδόκητα μηνύματα. Αν παρακάμψεις αυτή την προεπιλεγμένη υλοποίηση, φρόντισε να συμπεριλάβεις πάντα τη δική σου υλοποίηση που καλύπτει όλες τις περιπτώσεις. Αν το ξεχάσεις, ο διακομιστής θα καταρρεύσει αν λάβει ένα απροσδόκητο μήνυμα.
Η τιμή επιστροφής καθενός από τα τέσσερα callback που περιγράφηκαν παραπάνω μπορεί να επεκταθεί με ένα ακόμη στοιχείο πλειάδας, ένα χρονικό όριο. Π.χ. αντί να επιστρέψεις {:ok, state} από το init/1, επέστρεψε {:ok, state, timeout}.
Το χρονικό όριο μπορεί να χρησιμοποιηθεί για να εντοπίσει την απουσία μηνυμάτων στο γραμματοκιβώτιο για ένα συγκεκριμένο διάστημα. Αν ο διακομιστής επιστρέψει ένα χρονικό όριο από ένα από τα callback του και περάσει ο καθορισμένος αριθμός χιλιοστών του δευτερολέπτου χωρίς να φτάσει κανένα μήνυμα, το handle_info/2 καλείται με το :timeout ως πρώτο όρισμα.
Η βασική μηχανή Take-A-Number πουλούσε πάρα πολύ καλά, αλλά κάποιοι χρήστες διαμαρτύρονταν ότι της έλειπαν προηγμένες λειτουργίες σε σύγκριση με άλλα μοντέλα που κυκλοφορούν στην αγορά.
Ο κατασκευαστής άκουσε τα σχόλια των χρηστών και αποφάσισε να κυκλοφορήσει ένα μοντέλο deluxe με περισσότερες λειτουργίες, και για άλλη μια φορά σου ανατέθηκε να γράψεις το λογισμικό για αυτή τη μηχανή.
Οι νέες λειτουργίες που προστέθηκαν στο μοντέλο deluxe περιλαμβάνουν:
Η επιχειρηματική λογική της μηχανής έχει ήδη υλοποιηθεί από τον συνάδελφό σου και βρίσκεται στο module TakeANumberDeluxe.State. Τώρα η αποστολή σου είναι να την περιτυλίξεις σε ένα GenServer.
Χρησιμοποίησε το behaviour GenServer στο module TakeANumberDeluxe.
Υλοποίησε τη συνάρτηση start_link/1 και το απαραίτητο callback του GenServer.
Το όρισμα που περνάς στη συνάρτηση start_link/1 είναι μια keyword list. Περιέχει τα κλειδιά :min_number και :max_number. Οι τιμές κάτω από αυτά τα κλειδιά πρέπει να περαστούν στη συνάρτηση TakeANumberDeluxe.State.new/2.
Αν η TakeANumberDeluxe.State.new/2 επιστρέψει μια πλειάδα {:ok, state}, η μηχανή πρέπει να ξεκινήσει, χρησιμοποιώντας την κατάσταση που επέστρεψε ως κατάστασή της. Αν αντ' αυτού επιστρέψει μια πλειάδα {:error, error}, η μηχανή πρέπει να σταματήσει, δίνοντας το σφάλμα που επιστράφηκε ως αιτία τερματισμού.
TakeANumberDeluxe.start_link(min_number: 1, max_number: 9)
# => {:ok, #PID<0.174.0>}
TakeANumberDeluxe.start_link(min_number: 9, max_number: 1)
# => {:error, :invalid_configuration}
Μπορεί να πρόσεξες ότι η συνάρτηση TakeANumberDeluxe.State.new/2 δέχεται και ένα προαιρετικό τρίτο όρισμα, το auto_shutdown_timeout. Θα το χρησιμοποιήσουμε στο τελευταίο βήμα αυτής της άσκησης.
Υλοποίησε τη συνάρτηση report_state/1 και το απαραίτητο callback του GenServer. Η μηχανή πρέπει να απαντήσει στον καλούντα με την τρέχουσα κατάστασή της.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.report_state(machine)
# => %TakeANumberDeluxe.State{
# max_number: 10,
# min_number: 1,
# queue: %TakeANumberDeluxe.Queue{in: [], out: []},
# auto_shutdown_timeout: :infinity,
# }
Υλοποίησε τη συνάρτηση queue_new_number/1 και το απαραίτητο callback του GenServer.
Πρέπει να καλέσει τη συνάρτηση TakeANumberDeluxe.State.queue_new_number/1 με την τρέχουσα κατάσταση της μηχανής.
Αν η TakeANumberDeluxe.State.queue_new_number/1 επιστρέψει μια πλειάδα {:ok, new_number, new_state}, η μηχανή πρέπει να απαντήσει στον καλούντα με τον νέο αριθμό και να ορίσει τη νέα κατάσταση ως κατάστασή της. Αν αντ' αυτού επιστρέψει μια πλειάδα {:error, error}, η μηχανή πρέπει να απαντήσει στον καλούντα με το σφάλμα και να μην αλλάξει την κατάστασή της.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 2)
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 2}
TakeANumberDeluxe.queue_new_number(machine)
# => {:error, :all_possible_numbers_are_in_use}
Υλοποίησε τη συνάρτηση serve_next_queued_number/2 και το απαραίτητο callback του GenServer.
Πρέπει να καλέσει τη συνάρτηση TakeANumberDeluxe.State.serve_next_queued_number/2 με την τρέχουσα κατάσταση της μηχανής και το δεύτερο προαιρετικό όρισμά της, priority_number.
Αν η TakeANumberDeluxe.State.serve_next_queued_number/2 επιστρέψει μια πλειάδα {:ok, next_number, new_state}, η μηχανή πρέπει να απαντήσει στον καλούντα με τον επόμενο αριθμό και να ορίσει τη νέα κατάσταση ως κατάστασή της. Αν αντ' αυτού επιστρέψει μια πλειάδα {:error, error}, η μηχανή πρέπει να απαντήσει στον καλούντα με το σφάλμα και να μην αλλάξει την κατάστασή της.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.queue_new_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.serve_next_queued_number(machine)
# => {:ok, 1}
TakeANumberDeluxe.serve_next_queued_number(machine)
# => {:error, :empty_queue}
Υλοποίησε τη συνάρτηση reset_state/1 και το απαραίτητο callback του GenServer.
Πρέπει να καλέσει τη συνάρτηση TakeANumberDeluxe.State.new/2 για να δημιουργήσει μια νέα κατάσταση, χρησιμοποιώντας το min_number και το max_number της τρέχουσας κατάστασης. Η μηχανή πρέπει να ορίσει τη νέα κατάσταση ως κατάστασή της. Δεν πρέπει να απαντήσει στον καλούντα.
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.reset_state(machine)
# => :ok
Τροποποίησε την εκκίνηση της μηχανής. Πρέπει να διαβάσει την τιμή κάτω από το κλειδί :auto_shutdown_timeout στη keyword list που περνάς ως init_arg και να την περάσει ως τρίτο όρισμα στη TakeANumberDeluxe.State.new/3. Χρησιμοποίησε την προεπιλεγμένη τιμή :infinity αν το :auto_shutdown_timeout δεν έχει δοθεί.
Τροποποίησε την επαναφορά της κατάστασης της μηχανής ώστε να περνάει και το auto_shutdown_timeout στη TakeANumberDeluxe.State.new/3.
Τροποποίησε τις τιμές επιστροφής όλων των callback που έχεις υλοποιήσει (init/1 και όλα τα callback handle_*) ώστε να ορίζουν ένα timeout. Χρησιμοποίησε την τιμή κάτω από το κλειδί :auto_shutdown_timeout στην τρέχουσα κατάσταση της μηχανής. Μην προσθέσεις το timeout στην τιμή επιστροφής {:stop, reason} της init/1. Τα timeouts ισχύουν μόνο αφού ο server ξεκινήσει τον βρόχο λήψης μηνυμάτων του.
Υλοποίησε ένα callback του GenServer για να χειριστείς το μήνυμα :timeout που θα σταλεί στη μηχανή αν δεν λάβει άλλα μηνύματα μέσα στο δεδομένο timeout. Πρέπει να τερματίσει τη διεργασία με αιτία :normal.
Φρόντισε επίσης να χειρίζεσαι τυχόν απροσδόκητα μηνύματα αγνοώντας τα.
{:ok, machine} =
TakeANumberDeluxe.start_link(
min_number: 1,
max_number: 10,
auto_shutdown_timeout: :timer.hours(2)
)
# after 3 hours...
TakeANumberDeluxe.queue_new_number(machine)
# => ** (exit) exited in: GenServer.call(#PID<0.171.0>, :queue_new_number, 5000)
# ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
# (elixir 1.13.0) lib/gen_server.ex:1030: GenServer.call/3
Γράψου στο Exercism για να μάθεις και να κατακτήσεις Elixir με 58 έννοιες168 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.