ट्रैक
/
Elixir
Elixir
/
अभ्यास
/
नंबर लीजिए डीलक्स
नंबर लीजिए डीलक्स

नंबर लीजिए डीलक्स

सीखने का अभ्यास

परिचय

GenServer

GenServer (जेनेरिक सर्वर) एक बिहेवियर है जो Elixir प्रोसेसों के बीच होने वाली आम क्लाइंट-सर्वर बातचीत के लिए एब्स्ट्रैक्शन देता है।

याद है जब हमने प्रोसेस के बारे में सीखा था, तब का receive लूप? 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}। सर्वर अपना receive लूप शुरू करता है और state को अपनी शुरुआती स्थिति बनाता है। 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 - उस प्रोसेस का pid जो GenServer.call/2 कॉल कर रहा है। ज़्यादातर मामलों में इस आर्गुमेंट को नज़रअंदाज़ किया जा सकता है।
  3. state - सर्वर की मौजूदा स्थिति। याद रखिए कि इसकी शुरुआती वैल्यू init/1 कॉलबैक में तय हुई थी।

handle_call/3 कॉलबैक आम तौर पर {:reply, reply, state} वाला 3-टपल लौटाता है। इसका मतलब है कि टपल का दूसरा एलिमेंट, यानी 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} वाला 2-टपल लौटाता है।

ऐसी और भी एडवांस संभावनाएँ हैं, जिन पर हम अब बात नहीं करेंगे।

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 का एक कैच-ऑल कार्यान्वयन देता है, जो अनपेक्षित संदेशों के बारे में एरर लॉग करता है। अगर आप उस डिफ़ॉल्ट कार्यान्वयन को बदल देते हैं, तो ध्यान रखिए कि हमेशा अपना कैच-ऑल कार्यान्वयन भी शामिल करें। अगर आप भूल जाते हैं, तो अनपेक्षित संदेश मिलने पर सर्वर क्रैश हो जाएगा।

टाइमआउट

ऊपर बताए गए चारों कॉलबैकों की रिटर्न वैल्यू में एक और टपल एलिमेंट, यानी टाइमआउट, जोड़ा जा सकता है। जैसे, 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 कॉलबैक लागू कीजिए।

इसे TakeANumberDeluxe.State.new/2 फंक्शन को कॉल करना चाहिए और वर्तमान स्थिति के min_number तथा max_number इस्तेमाल करके एक नई स्थिति बनानी चाहिए। मशीन को नई स्थिति को ही अपनी स्थिति बना लेना चाहिए। इसे कॉल करने वाले को कोई जवाब नहीं देना चाहिए।

{: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 डिफॉल्ट वैल्यू इस्तेमाल कीजिए।

मशीन की स्थिति रीसेट करने का तरीका भी बदलिए, ताकि auto_shutdown_timeout भी TakeANumberDeluxe.State.new/3 को दिया जाए।

लागू किए गए सभी कॉलबैक (init/1 और सभी handle_* कॉलबैक) की रिटर्न वैल्यू इस तरह बदलिए कि वे टाइमआउट सेट करें। इसके लिए वर्तमान मशीन स्थिति में :auto_shutdown_timeout कीज़ के नीचे की वैल्यू इस्तेमाल कीजिए। init/1 की {:stop, reason} रिटर्न वैल्यू में टाइमआउट न जोड़ें, क्योंकि टाइमआउट तभी लागू होते हैं जब सर्वर अपना रिसीव लूप शुरू कर चुका हो।

एक 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
GitHub के ज़रिए संपादित करें यह लिंक एक नई विंडो या टैब में खुलता है
Elixir Exercism

नंबर लीजिए डीलक्स शुरू करने के लिए तैयार हैं?

Exercism पर साइन अप कीजिए और Elixir को 58 कॉन्सेप्ट168 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।