ট্র্যাক
/
Elixir
Elixir
/
অনুশীলনী
/
নম্বর নিন ডিলাক্স
নম্বর নিন ডিলাক্স

নম্বর নিন ডিলাক্স

লার্নিং অনুশীলনী

ভূমিকা

GenServer

GenServer (জেনেরিক সার্ভার) হলো একটি behaviour, যা Elixir প্রসেসগুলোর মধ্যে সাধারণ ক্লায়েন্ট-সার্ভার যোগাযোগকে বিমূর্ত করে।

প্রসেস সম্পর্কে শেখার সময়ের receive লুপটি কি মনে আছে? GenServer behaviour এমন লুপ বাস্তবায়ন এবং এমন একটি লুপ চালানো প্রসেসের সাথে মেসেজ আদান-প্রদানের জন্য বিমূর্তন সরবরাহ করে। এটি স্টেট ধরে রাখা এবং অ্যাসিঙ্ক্রোনাস কোড চালানো সহজ করে তোলে।

Note

সাবধান থাকুন, 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/1

GenServer.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 কলব্যাকটি সিঙ্ক্রোনাস মেসেজ হ্যান্ডল করা এবং তার উত্তর দেওয়ার দায়িত্বে থাকে। এটি তিনটি আর্গুমেন্ট গ্রহণ করে:

  1. message - GenServer.call/2-তে দ্বিতীয় আর্গুমেন্ট হিসেবে পাঠানো মান।
  2. from - GenServer.call/2 কল করা প্রসেসের pid। বেশিরভাগ ক্ষেত্রে এই আর্গুমেন্টটি উপেক্ষা করা যায়।
  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 behaviour handle_info/2-এর একটি ক্যাচ-অল বাস্তবায়ন সরবরাহ করে, যা অপ্রত্যাশিত মেসেজ সম্পর্কে এরর লগ করে। যদি আপনি সেই ডিফল্ট বাস্তবায়ন ওভাররাইড করেন, তবে সবসময় নিজের একটি ক্যাচ-অল বাস্তবায়ন যুক্ত করুন। ভুলে গেলে, কোনো অপ্রত্যাশিত মেসেজ পেলে সার্ভারটি ক্র্যাশ করবে।

টাইমআউট

উপরে বর্ণিত চারটি কলব্যাকের প্রতিটির রিটার্ন ভ্যালু আরও একটি টাপল এলিমেন্ট, অর্থাৎ একটি টাইমআউট, দিয়ে বাড়ানো যায়। যেমন, init/1 থেকে {:ok, state} রিটার্ন করার বদলে {:ok, state, timeout} রিটার্ন করুন।

নির্দিষ্ট একটি সময়ের জন্য মেইলবক্সে মেসেজের অভাব শনাক্ত করতে টাইমআউট ব্যবহার করা যায়। যদি সার্ভারটি তার কোনো কলব্যাক থেকে একটি টাইমআউট রিটার্ন করে, এবং নির্দিষ্ট মিলিসেকেন্ড পার হয়ে যায় কোনো মেসেজ না এসে, তবে handle_info/2 প্রথম আর্গুমেন্ট হিসেবে :timeout নিয়ে কল করা হয়।

নির্দেশনা

সাধারণ Take-A-Number মেশিনটি বেশ ভালোই বিক্রি হচ্ছিল, কিন্তু বাজারে থাকা অন্য মডেলগুলোর তুলনায় এতে উন্নত ফিচারের অভাব নিয়ে কিছু ব্যবহারকারী অভিযোগ করছিলেন।

নির্মাতা প্রতিষ্ঠান ব্যবহারকারীদের মতামত শুনে আরও ফিচারসহ একটি ডিলাক্স মডেল বাজারে ছাড়ার সিদ্ধান্ত নিল, আর এই মেশিনের সফটওয়্যার লেখার দায়িত্ব আবারও আপনার হাতে পড়ল।

ডিলাক্স মডেলে যুক্ত হওয়া নতুন ফিচারগুলোর মধ্যে রয়েছে:

  • বর্তমানে কিউতে থাকা নম্বরগুলোর হিসাব রাখা।
  • সর্বনিম্ন ও সর্বোচ্চ নম্বর নির্ধারণ করা। এতে একই প্রতিষ্ঠানের বিভিন্ন বিভাগে গ্রাহকদের কিউ করাতে একাধিক ডিলাক্স Take-A-Number মেশিন ব্যবহার করা যাবে, আর নম্বরের পরিসর দেখে বিভাগগুলো আলাদা করা যাবে।
  • নির্দিষ্ট কিছু নম্বরকে কিউ এড়িয়ে যাওয়ার সুযোগ দেওয়া, যাতে গর্ভবতী নারী ও প্রবীণরা অগ্রাধিকারমূলক সেবা পান।
  • ভুলে গিয়ে সারা সপ্তাহান্ত ধরে মেশিন চালু রেখে শক্তি নষ্ট করা আটকাতে অটো শাটডাউন।

মেশিনের বিজনেস লজিক আপনার সহকর্মী আগেই তৈরি করেছেন, যা TakeANumberDeluxe.State মডিউলে পাওয়া যাবে। এখন আপনার কাজ হলো সেটিকে একটি GenServer-এ মুড়ে দেওয়া।

1. মেশিন চালু করুন

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। এই অনুশীলনীর শেষ ধাপে আমরা এটি ব্যবহার করব।

2. মেশিনের স্টেট রিপোর্ট করুন

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,
#    }

3. নতুন নম্বর কিউ করুন

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}

4. কিউতে থাকা পরের নম্বরটি সার্ভ করুন

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}

5. স্টেট রিসেট করুন

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

6. অটো শাটডাউন ইমপ্লিমেন্ট করুন

মেশিন চালু করার অংশটি পরিবর্তন করুন। এটির উচিত 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
GitHub-এর মাধ্যমে সম্পাদনা করুন লিংকটি একটি নতুন উইন্ডো বা ট্যাবে খোলে
Elixir Exercism

নম্বর নিন ডিলাক্স শুরু করতে প্রস্তুত?

Exercism-এ সাইন আপ করুন, Elixir ট্র্যাকের 58টি কনসেপ্ট168টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।