Ge

GenServer في Elixir

4 تمارين

نبذة عن GenServer

GenServer (الخادم العام) هو سلوك يجرّد التفاعلات الشائعة بين العميل والخادم في عمليات Elixir.

أتذكر حلقة الاستقبال من حين تعلّمنا عن العمليات؟ يوفر سلوك GenServer تجريدات لتنفيذ مثل هذه الحلقات، ولتبادل الرسائل مع عملية تشغّل مثل هذه الحلقة. وهو يسهّل الحفاظ على الحالة وتنفيذ كود غير متزامن.

Note

احترس، فاسم GenServer محمّل بأكثر من معنى. فهو يُستخدم أيضًا لوصف وحدة تستخدم سلوك GenServer، وكذلك لوصف عملية بدأت من وحدة تستخدم سلوك GenServer.

يعرّف سلوك GenServer دالة استدعاء رجعي واحدة مطلوبة، init/1، وعددًا من دالات الاستدعاء الرجعي الاختيارية المهمة: handle_call/3 وhandle_cast/2 وhandle_info/3. لا يُفترض أن تستدعي العملاء الذين يستخدمون GenServer دالات الاستدعاء الرجعي تلك مباشرةً. بدلًا من ذلك، توفر وحدة GenServer دوال يمكن للعملاء استخدامها للتواصل مع عملية GenServer.

غالبًا ما تعرّف وحدة واحدة كلاً من واجهة API الخاصة بالعميل، وهي مجموعة من الدوال التي يمكن لأجزاء أخرى من تطبيق Elixir الخاص بك استدعاؤها للتواصل مع عملية GenServer هذه، و_تنفيذات دالات الاستدعاء الرجعي الخاصة بالخادم_، التي تحتوي على منطق GenServer هذا.

لنلقِ نظرة أولًا على مثال بسيط لـ GenServer، ثم نتعلّم ماذا تعني كل دالة استدعاء رجعي.

مثال

هذا خادم مثال يستطيع الرد على استجوابات الركاب المزعجين المتكررة خلال رحلة برية طويلة، وبشكل أدق على السؤال: «هل وصلنا بعد؟». وهو يتتبّع عدد المرات التي طُرح فيها هذا السؤال، ويُرجع ردودًا متزايدة الانزعاج.

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. تعلّمنا الفرق بين هاتين الدالتين في مفهوم الروابط.

هاتان الدالتان:

  • تقبلان وحدة تنفّذ سلوك GenServer كوسيط أول.
  • تقبلان أي شيء كوسيط ثانٍ يُسمى init_arg. وكما يوحي الاسم، يُمرَّر هذا الوسيط إلى دالة الاستدعاء الرجعي init/1.
  • تقبلان وسيطًا ثالثًا اختياريًا يحتوي على خيارات متقدمة لتشغيل العملية لن نتناولها الآن.

بدء تشغيل خادم عبر استدعاء GenServer.start/3 أو GenServer.start_link/3 سيستدعي دالة الاستدعاء الرجعي init/1 بطريقة حاجزة. والقيمة التي تُرجعها init/1 تحدد ما إذا كان يمكن بدء تشغيل الخادم بنجاح.

عادةً ما تُرجع دالة الاستدعاء الرجعي 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 لعملية خادم قيد التشغيل كوسيط أول، والرسالة كوسيط ثانٍ. ويمكن أن تكون الرسالة من أي نوع.

دالة الاستدعاء الرجعي handle_call/3 مسؤولة عن معالجة الرسائل المتزامنة والرد عليها. وهي تستقبل ثلاثة وسائط:

  1. message: القيمة الممرَّرة كوسيط ثانٍ إلى GenServer.call/2.
  2. from: قيمة pid الخاصة بالعملية التي تستدعي GenServer.call/2. وفي أغلب الأحيان يمكن تجاهل هذا الوسيط.
  3. state: الحالة الحالية للخادم. وتذكّر أن قيمتها الابتدائية حُدِّدت في دالة الاستدعاء الرجعي init/1.

عادةً ما تُرجع دالة الاستدعاء الرجعي handle_call/3 صفًا ثلاثيًا بالشكل {:reply, reply, state}. وهذا يعني أن العنصر الثاني في الصف، وهو reply الذي يمكن أن يكون من أي نوع، سيُرسل مرة أخرى إلى المستدعي. والعنصر الثالث في الصف، state، هو الحالة الجديدة للخادم بعد معالجة هذه الرسالة.

وهناك أيضًا إمكانات أكثر تقدمًا لن نتناولها الآن.

Note

لكي تتذكر ما تفعله دالة الاستدعاء الرجعي هذه من اسمها، فكّر فيها كأنك «تتصل» بشخص ما على الهاتف.

إذا كان ذلك الشخص متاحًا، فستتلقى ردًا على الفور (بشكل متزامن).

handle_cast/2

يمكن إرسال رسالة لا تتطلب ردًا إلى عملية خادم باستخدام GenServer.cast/2. ووسائطها مطابقة لوسائط GenServer.call/2.

دالة الاستدعاء الرجعي handle_cast/2 مسؤولة عن معالجة تلك الرسائل. وهي تستقبل وسيطين، message وstate، وهما الوسيطان نفسهما كما في دالة الاستدعاء الرجعي handle_call/3 (باستثناء from).

عادةً ما تُرجع دالة الاستدعاء الرجعي handle_cast/2 صفًا ثنائيًا بالشكل {:noreply, state}.

وهناك أيضًا إمكانات أكثر تقدمًا لن نتناولها الآن.

Note

لكي تتذكر ما تفعله دالة الاستدعاء الرجعي هذه من اسمها، تذكّر أن «to cast» تعني أيضًا «يقذف».

إذا قذفت رسالة في زجاجة إلى البحر، فأنت لا تتوقع أن تتلقى ردًا على الفور، أو ربما لن تتلقاه أبدًا.

هل أستخدم call أم cast؟

استخدم call في جميع الحالات تقريبًا حتى لو لم يكن كود العميل الخاص بك بحاجة إلى الرد من الخادم.

استخدام call ينتظر الرد، وهو ما يعمل كآلية ضغط عكسي (لمنع العملاء من إرسال رسائل كثيرة جدًا دفعة واحدة). كما أن تلقّي رد من الخادم هو الطريقة الوحيدة للتأكد من أن الخادم استقبل رسالة العميل وعالجها.

handle_info/2

يمكن أن تنتهي الرسائل أيضًا في صندوق وارد الخادم بوسائل غير استدعاء GenServer.call/2 أو GenServer.cast/2، على سبيل المثال استدعاء الدالة العادية send/2.

لمعالجة مثل هذه الرسائل، استخدم دالة الاستدعاء الرجعي handle_info/2. وتعمل هذه الدالة بالطريقة نفسها تمامًا كما handle_cast/2.

يوفر سلوك GenServer تنفيذًا شاملًا لـ handle_info/2 يسجّل أخطاءً بشأن الرسائل غير المتوقعة. وإذا تجاوزت هذا التنفيذ الافتراضي، فتأكد من تضمين تنفيذ شامل خاص بك دائمًا. وإذا نسيت، فسينهار الخادم إذا استقبل رسالة غير متوقعة.

المهلات الزمنية

يمكن توسيع القيمة التي تُرجعها كل دالة من دالات الاستدعاء الرجعي الأربع الموصوفة أعلاه بعنصر إضافي واحد في الصف، وهو المهلة الزمنية. فمثلًا، بدلًا من إرجاع {:ok, state} من init/1، أرجِع {:ok, state, timeout}.

يمكن استخدام المهلة الزمنية لاكتشاف غياب الرسائل في صندوق الرسائل لفترة محددة. وإذا أرجع الخادم مهلة زمنية من إحدى دالات الاستدعاء الرجعي الخاصة به، وانقضى عدد المللي ثانية المحدد دون وصول أي رسالة، فسيُستدعى handle_info/2 مع :timeout كوسيط أول.

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم GenServer

التدريب مقفل

افتح تمرينين إضافيين لممارسة GenServer