GenServer (الخادم العام) هو سلوك يوفّر تجريدًا للتفاعلات الشائعة بين العميل والخادم بين عمليات Elixir.
أتذكر حلقة الاستقبال من حين تعلّمنا عن العمليات؟ يوفّر سلوك GenServer تجريدات لتنفيذ حلقات كهذه، ولتبادل الرسائل مع عملية تشغّل حلقة كهذه. وهو يسهّل الحفاظ على الحالة وتنفيذ كود لاتزامني.
احترس، فاسم 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 معالجة الرسائل المتزامنة والرد عليها. وهي تستقبل ثلاثة وسائط:
message: القيمة التي مُرّرت بوصفها الوسيط الثاني إلى GenServer.call/2.from: pid العملية التي تستدعي GenServer.call/2. وفي أغلب الأحيان يمكن تجاهل هذا الوسيط.state: الحالة الراهنة للخادم. وتذكّر أن قيمتها الابتدائية حُدّدت في دالة رد النداء init/1.عادةً ما تُرجع دالة رد النداء handle_call/3 ثلاثيًا بالشكل {:reply, reply, state}. وهذا يعني أن العنصر الثاني في الثلاثي، وهو reply الذي يمكن أن يكون من أي نوع، سيُرسَل عائدًا إلى المستدعي. أما العنصر الثالث في الثلاثي، state، فهو الحالة الجديدة للخادم بعد معالجة هذه الرسالة.
وهناك أيضًا احتمالات أكثر تقدمًا لن نتناولها الآن.
لتتذكر ما تفعله دالة رد النداء هذه من اسمها، فكّر فيها على أنها «اتصال» بشخص ما عبر الهاتف.
وإذا كان ذلك الشخص متاحًا، فستتلقى ردًّا فورًا (بشكل متزامن).
handle_cast/2يمكن إرسال رسالة لا تتطلب ردًّا إلى عملية خادم باستخدام GenServer.cast/2. ووسائطها مماثلة لوسائط GenServer.call/2.
تتولّى دالة رد النداء handle_cast/2 معالجة تلك الرسائل. وهي تستقبل وسيطين، message وstate، وهما الوسيطان نفسهما في دالة رد النداء handle_call/3 (باستثناء from).
عادةً ما تُرجع دالة رد النداء handle_cast/2 زوجًا بالشكل {:noreply, state}.
وهناك أيضًا احتمالات أكثر تقدمًا لن نتناولها الآن.
لتتذكر ما تفعله دالة رد النداء هذه من اسمها، تذكّر أن كلمة «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 بوصفه الوسيط الأول.
كانت آلة أخذ الأرقام الأساسية تُبيع جيدًا جدًا، لكن بعض المستخدمين اشتكوا من افتقارها إلى ميزات متقدمة مقارنة بطرازات أخرى متاحة في السوق.
استمعت الشركة المصنّعة إلى ملاحظات المستخدمين وقررت إطلاق طراز فاخر بميزات أكثر، وأُوكلت إليك مرة أخرى مهمة كتابة البرمجيات لهذه الآلة.
تشمل الميزات الجديدة التي أُضيفت إلى الطراز الفاخر ما يلي:
لقد نفّذ زميلك منطق العمل الخاص بالآلة بالفعل، ويمكنك العثور عليه في الوحدة TakeANumberDeluxe.State. مهمتك الآن هي تغليفه داخل GenServer.
استخدم سلوك GenServer في الوحدة TakeANumberDeluxe.
نفّذ الدالة start_link/1 ودالة الاستدعاء الراجعة اللازمة في GenServer.
الوسيط الذي يُمرَّر إلى start_link/1 هو قائمة كلمات مفتاحية. تحتوي على المفتاحين :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 ودالة الاستدعاء الراجعة اللازمة في 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 ودالة الاستدعاء الراجعة اللازمة في 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 ودالة الاستدعاء الراجعة اللازمة في 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 ودالة الاستدعاء الراجعة اللازمة في 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 في قائمة الكلمات المفتاحية المُمرَّرة باسم init_arg وأن تمرّرها كوسيط ثالث إلى TakeANumberDeluxe.State.new/3. استخدم القيمة الافتراضية :infinity إذا لم يُعطَ :auto_shutdown_timeout.
عدّل إعادة ضبط حالة الآلة لتمرّر أيضًا auto_shutdown_timeout إلى TakeANumberDeluxe.State.new/3.
عدّل القيم المُرجَعة من جميع دوال الاستدعاء الراجعة المنفَّذة (init/1 وجميع دوال الاستدعاء الراجعة handle_*) لتعيين مهلة. استخدم القيمة الموجودة تحت المفتاح :auto_shutdown_timeout في الحالة الحالية للآلة. لا تُضِف المهلة إلى القيمة المُرجَعة {:stop, reason} من init/1. فالمهلات لا تنطبق إلا بعد أن يبدأ الخادم حلقة الاستقبال.
نفّذ دالة استدعاء راجعة في GenServer للتعامل مع رسالة :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 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.