GenServer (জেনেরিক সার্ভার) হলো একটি behaviour, যা Elixir প্রসেসগুলোর মধ্যে সাধারণ ক্লায়েন্ট-সার্ভার যোগাযোগকে বিমূর্ত করে।
প্রসেস সম্পর্কে শেখার সময়ের receive লুপটি কি মনে আছে? GenServer behaviour এমন লুপ বাস্তবায়ন এবং এমন একটি লুপ চালানো প্রসেসের সাথে মেসেজ আদান-প্রদানের জন্য বিমূর্তন সরবরাহ করে। এটি স্টেট ধরে রাখা এবং অ্যাসিঙ্ক্রোনাস কোড চালানো সহজ করে তোলে।
সাবধান থাকুন, GenServer নামটি একাধিক অর্থ বহন করে। যে মডিউল GenServer behaviour ব্যবহার করে, তাকে বোঝাতেও এটি ব্যবহৃত হয়, আবার যে প্রসেস GenServer behaviour ব্যবহার করা একটি মডিউল থেকে চালু করা হয়েছে, তাকেও।
GenServer behaviour একটি আবশ্যক কলব্যাক, 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/1GenServer.start/3 বা GenServer.start_link/3 কল করে একটি সার্ভার চালু করা যায়। এই ফাংশনগুলোর মধ্যে পার্থক্য আমরা লিংক কনসেপ্টে শিখেছি।
এই দুটি ফাংশন:
GenServer behaviour বাস্তবায়নকারী একটি মডিউল গ্রহণ করে।init_arg নামের দ্বিতীয় আর্গুমেন্ট হিসেবে যেকোনো কিছু গ্রহণ করে। নাম থেকে বোঝা যায়, এই আর্গুমেন্টটি init/1 কলব্যাকে পাঠানো হয়।GenServer.start/3 বা GenServer.start_link/3 কল করে সার্ভার চালু করলে init/1 কলব্যাকটি ব্লকিং পদ্ধতিতে আহ্বান করা হয়। init/1-এর রিটার্ন ভ্যালু ঠিক করে দেয় সার্ভারটি সফলভাবে চালু হতে পারবে কি না।
init/1 কলব্যাক সাধারণত এই মানগুলোর একটি রিটার্ন করে:
{:ok, state}। সার্ভারটি state-কে তার প্রাথমিক স্টেট হিসেবে ব্যবহার করে তার receive লুপ শুরু করবে। state যেকোনো টাইপের হতে পারে।{:stop, reason}। reason যেকোনো টাইপের হতে পারে। সার্ভারটি তার receive লুপ শুরু করবে না। প্রসেসটি প্রদত্ত কারণটি নিয়ে বন্ধ হবে।এছাড়াও আরও উন্নত কিছু সম্ভাবনা আছে, যা আমরা এখন আলোচনা করব না।
সার্ভারের receive লুপ শুরু হলে 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 - GenServer.call/2 কল করা প্রসেসের pid। বেশিরভাগ ক্ষেত্রে এই আর্গুমেন্টটি উপেক্ষা করা যায়।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/2GenServer.call/2 বা GenServer.cast/2 কল করা ছাড়াও অন্য উপায়ে মেসেজগুলো সার্ভারের ইনবক্সে পৌঁছাতে পারে, যেমন সাধারণ send/2 ফাংশনটি কল করে।
এমন মেসেজ হ্যান্ডল করতে handle_info/2 কলব্যাকটি ব্যবহার করুন। এই কলব্যাকটি handle_cast/2-এর মতো ঠিক একইভাবে কাজ করে।
GenServer behaviour handle_info/2-এর একটি ক্যাচ-অল বাস্তবায়ন সরবরাহ করে, যা অপ্রত্যাশিত মেসেজ সম্পর্কে এরর লগ করে। যদি আপনি সেই ডিফল্ট বাস্তবায়ন ওভাররাইড করেন, তবে সবসময় নিজের একটি ক্যাচ-অল বাস্তবায়ন যুক্ত করুন। ভুলে গেলে, কোনো অপ্রত্যাশিত মেসেজ পেলে সার্ভারটি ক্র্যাশ করবে।
উপরে বর্ণিত চারটি কলব্যাকের প্রতিটির রিটার্ন ভ্যালু আরও একটি টাপল এলিমেন্ট, অর্থাৎ একটি টাইমআউট, দিয়ে বাড়ানো যায়। যেমন, init/1 থেকে {:ok, state} রিটার্ন করার বদলে {:ok, state, timeout} রিটার্ন করুন।
নির্দিষ্ট একটি সময়ের জন্য মেইলবক্সে মেসেজের অভাব শনাক্ত করতে টাইমআউট ব্যবহার করা যায়। যদি সার্ভারটি তার কোনো কলব্যাক থেকে একটি টাইমআউট রিটার্ন করে, এবং নির্দিষ্ট মিলিসেকেন্ড পার হয়ে যায় কোনো মেসেজ না এসে, তবে handle_info/2 প্রথম আর্গুমেন্ট হিসেবে :timeout নিয়ে কল করা হয়।
সাধারণ Take-A-Number মেশিনটি বেশ ভালোই বিক্রি হচ্ছিল, কিন্তু বাজারে থাকা অন্য মডেলগুলোর তুলনায় এতে উন্নত ফিচারের অভাব নিয়ে কিছু ব্যবহারকারী অভিযোগ করছিলেন।
নির্মাতা প্রতিষ্ঠান ব্যবহারকারীদের মতামত শুনে আরও ফিচারসহ একটি ডিলাক্স মডেল বাজারে ছাড়ার সিদ্ধান্ত নিল, আর এই মেশিনের সফটওয়্যার লেখার দায়িত্ব আবারও আপনার হাতে পড়ল।
ডিলাক্স মডেলে যুক্ত হওয়া নতুন ফিচারগুলোর মধ্যে রয়েছে:
মেশিনের বিজনেস লজিক আপনার সহকর্মী আগেই তৈরি করেছেন, যা TakeANumberDeluxe.State মডিউলে পাওয়া যাবে। এখন আপনার কাজ হলো সেটিকে একটি GenServer-এ মুড়ে দেওয়া।
TakeANumberDeluxe মডিউলে GenServer বিহেভিয়ার ব্যবহার করুন।
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 কলব্যাকটি ইমপ্লিমেন্ট করুন।
এটির উচিত মেশিনের বর্তমান স্টেট এবং এর দ্বিতীয় ঐচ্ছিক আর্গুমেন্ট priority_number নিয়ে TakeANumberDeluxe.State.serve_next_queued_number/2 ফাংশনটি কল করা।
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 কলব্যাকটি ইমপ্লিমেন্ট করুন।
এটির উচিত বর্তমান স্টেটের min_number ও max_number ব্যবহার করে নতুন একটি স্টেট তৈরি করতে TakeANumberDeluxe.State.new/2 ফাংশনটি কল করা। মেশিনটির উচিত নতুন স্টেটটিকে তার স্টেট হিসেবে সেট করা। কল করা পক্ষকে উত্তর দেওয়া উচিত নয়।
{:ok, machine} = TakeANumberDeluxe.start_link(min_number: 1, max_number: 10)
TakeANumberDeluxe.reset_state(machine)
# => :ok
মেশিন চালু করার অংশটি পরিবর্তন করুন। এটির উচিত init_arg হিসেবে পাঠানো কিওয়ার্ড লিস্টে :auto_shutdown_timeout কী-এর অধীন মানটি পড়া এবং সেটি TakeANumberDeluxe.State.new/3-এ তৃতীয় আর্গুমেন্ট হিসেবে পাঠানো। :auto_shutdown_timeout দেওয়া না থাকলে ডিফল্ট মান :infinity ব্যবহার করুন।
মেশিনের স্টেট রিসেট করার অংশটিও এমনভাবে পরিবর্তন করুন যাতে TakeANumberDeluxe.State.new/3-এ auto_shutdown_timeout পাঠানো হয়।
ইমপ্লিমেন্ট করা সব কলব্যাকের (init/1 এবং সব handle_* কলব্যাক) রিটার্ন ভ্যালু পরিবর্তন করে একটি টাইমআউট সেট করুন। এর জন্য বর্তমান মেশিন স্টেটের :auto_shutdown_timeout কী-এর অধীন মানটি ব্যবহার করুন। init/1-এর {:stop, reason} রিটার্ন ভ্যালুতে টাইমআউট যোগ করবেন না; টাইমআউট কেবল সার্ভার তার রিসিভ লুপ শুরু করার পরেই প্রযোজ্য।
নির্দিষ্ট টাইমআউটের মধ্যে অন্য কোনো মেসেজ না পেলে মেশিনে যে :timeout মেসেজটি পাঠানো হবে, সেটি সামলাতে একটি GenServer কলব্যাক ইমপ্লিমেন্ট করুন। এটির উচিত :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টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।