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}.
وهناك أيضًا إمكانات أكثر تقدمًا لن نتناولها الآن.
لكي تتذكر ما تفعله دالة الاستدعاء الرجعي هذه من اسمها، تذكّر أن «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 كوسيط أول.