Διαδρομές
/
Elixir
Elixir
/
Ασκήσεις
/
Πάρε νούμερο Deluxe
Πάρε νούμερο Deluxe

Πάρε νούμερο Deluxe

Άσκηση εκμάθησης

Εισαγωγή

GenServer

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

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

Note

Να έχεις υπόψη σου ότι το όνομα 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

Callback

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}. Ο διακομιστής θα ξεκινήσει τον βρόχο λήψης του χρησιμοποιώντας το 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 είναι υπεύθυνο για τον χειρισμό και την απάντηση σε σύγχρονα μηνύματα. Δέχεται τρία ορίσματα:

  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 παρέχει μια προεπιλεγμένη υλοποίηση του handle_info/2 που καλύπτει όλες τις περιπτώσεις και καταγράφει σφάλματα για απροσδόκητα μηνύματα. Αν παρακάμψεις αυτή την προεπιλεγμένη υλοποίηση, φρόντισε να συμπεριλάβεις πάντα τη δική σου υλοποίηση που καλύπτει όλες τις περιπτώσεις. Αν το ξεχάσεις, ο διακομιστής θα καταρρεύσει αν λάβει ένα απροσδόκητο μήνυμα.

Χρονικά όρια

Η τιμή επιστροφής καθενός από τα τέσσερα callback που περιγράφηκαν παραπάνω μπορεί να επεκταθεί με ένα ακόμη στοιχείο πλειάδας, ένα χρονικό όριο. Π.χ. αντί να επιστρέψεις {:ok, state} από το init/1, επέστρεψε {:ok, state, timeout}.

Το χρονικό όριο μπορεί να χρησιμοποιηθεί για να εντοπίσει την απουσία μηνυμάτων στο γραμματοκιβώτιο για ένα συγκεκριμένο διάστημα. Αν ο διακομιστής επιστρέψει ένα χρονικό όριο από ένα από τα callback του και περάσει ο καθορισμένος αριθμός χιλιοστών του δευτερολέπτου χωρίς να φτάσει κανένα μήνυμα, το handle_info/2 καλείται με το :timeout ως πρώτο όρισμα.

Οδηγίες

Η βασική μηχανή Take-A-Number πουλούσε πάρα πολύ καλά, αλλά κάποιοι χρήστες διαμαρτύρονταν ότι της έλειπαν προηγμένες λειτουργίες σε σύγκριση με άλλα μοντέλα που κυκλοφορούν στην αγορά.

Ο κατασκευαστής άκουσε τα σχόλια των χρηστών και αποφάσισε να κυκλοφορήσει ένα μοντέλο deluxe με περισσότερες λειτουργίες, και για άλλη μια φορά σου ανατέθηκε να γράψεις το λογισμικό για αυτή τη μηχανή.

Οι νέες λειτουργίες που προστέθηκαν στο μοντέλο deluxe περιλαμβάνουν:

  • Παρακολούθηση των αριθμών που βρίσκονται αυτή τη στιγμή στην ουρά.
  • Ορισμό του ελάχιστου και του μέγιστου αριθμού. Αυτό θα επιτρέψει τη χρήση πολλών μηχανών Take-A-Number deluxe, για να μπαίνουν οι πελάτες σε ουρά για διαφορετικά τμήματα της ίδιας εγκατάστασης, καθώς και τη διάκριση των τμημάτων με βάση το εύρος αριθμών.
  • Δυνατότητα σε ορισμένους αριθμούς να παρακάμπτουν την ουρά, ώστε να παρέχεται υπηρεσία προτεραιότητας σε εγκύους και ηλικιωμένους.
  • Αυτόματο τερματισμό λειτουργίας, ώστε να μην αφήνεις κατά λάθος τη μηχανή αναμμένη όλο το Σαββατοκύριακο και σπαταλάς ενέργεια.

Η επιχειρηματική λογική της μηχανής έχει ήδη υλοποιηθεί από τον συνάδελφό σου και βρίσκεται στο module TakeANumberDeluxe.State. Τώρα η αποστολή σου είναι να την περιτυλίξεις σε ένα GenServer.

1. Ξεκίνα τη μηχανή

Χρησιμοποίησε το 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. Θα το χρησιμοποιήσουμε στο τελευταίο βήμα αυτής της άσκησης.

2. Ανάφερε την κατάσταση της μηχανής

Υλοποίησε τη συνάρτηση 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,
#    }

3. Βάλε νέους αριθμούς στην ουρά

Υλοποίησε τη συνάρτηση 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}

4. Εξυπηρέτησε τον επόμενο αριθμό στην ουρά

Υλοποίησε τη συνάρτηση 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}

5. Επαναφορά κατάστασης

Υλοποίησε τη συνάρτηση 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

6. Υλοποίησε τον αυτόματο τερματισμό λειτουργίας

Τροποποίησε την εκκίνηση της μηχανής. Πρέπει να διαβάσει την τιμή κάτω από το κλειδί :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
Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
Elixir Exercism

Έτοιμος να ξεκινήσεις την άσκηση Πάρε νούμερο Deluxe;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις Elixir με 58 έννοιες168 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.