Ge

GenServer σε Elixir

4 ασκήσεις

Σχετικά με την έννοια GenServer

Το GenServer (γενικός διακομιστής) είναι μια συμπεριφορά που παρέχει αφαιρέσεις για τις κοινές αλληλεπιδράσεις πελάτη-διακομιστή μεταξύ διεργασιών της Elixir.

Θυμάσαι τον βρόχο receive από τότε που μάθαμε για τις διεργασίες; Η συμπεριφορά GenServer παρέχει αφαιρέσεις για την υλοποίηση τέτοιων βρόχων και για την ανταλλαγή μηνυμάτων με μια διεργασία που εκτελεί έναν τέτοιο βρόχο. Κάνει ευκολότερη τη διατήρηση κατάστασης και την εκτέλεση ασύγχρονου κώδικα.

Note

Προσοχή: το όνομα 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

Callbacks

init/1

Ένας διακομιστής μπορεί να ξεκινήσει καλώντας το GenServer.start/3 ή το GenServer.start_link/3. Μάθαμε για τη διαφορά ανάμεσα σε αυτές τις συναρτήσεις στην έννοια links.

Αυτές οι δύο συναρτήσεις:

  • Δέχονται ως πρώτο όρισμα ένα module που υλοποιεί τη συμπεριφορά 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 είναι υπεύθυνο για τη διαχείριση και την απάντηση σε σύγχρονα μηνύματα. Δέχεται τρία ορίσματα:

  1. message - η τιμή που περνάει ως δεύτερο όρισμα στο GenServer.call/2.
  2. from - το pid της διεργασίας που καλεί το GenServer.call/2. Τις περισσότερες φορές αυτό το όρισμα μπορεί να αγνοηθεί.
  3. state - η τρέχουσα κατάσταση του διακομιστή. Θυμήσου ότι η αρχική της τιμή ορίστηκε στο callback init/1.

Το callback handle_call/3 συνήθως επιστρέφει μια πλειάδα 3 στοιχείων {:reply, reply, state}. Αυτό σημαίνει ότι το δεύτερο στοιχείο της πλειάδας, μια reply που μπορεί να είναι οποιουδήποτε τύπου, θα σταλεί πίσω στον καλούντα. Το τρίτο στοιχείο της πλειάδας, state, είναι η νέα κατάσταση του διακομιστή μετά τη διαχείριση αυτού του μηνύματος.

Υπάρχουν επίσης πιο προχωρημένες δυνατότητες που δεν θα καλύψουμε τώρα.

Note

Για να θυμάσαι τι κάνει αυτό το 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}.

Υπάρχουν επίσης πιο προχωρημένες δυνατότητες που δεν θα καλύψουμε τώρα.

Note

Για να θυμάσαι τι κάνει αυτό το 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 ως πρώτο όρισμα.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα

Μάθε την έννοια GenServer

Η εξάσκηση είναι κλειδωμένη

Ξεκλείδωσε 2 ακόμη ασκήσεις για να εξασκηθείς στην έννοια GenServer