Elixir 1.7

ETS




elixir

ETS

यह अध्याय मिक्स और ओटीपी गाइड का हिस्सा है और यह इस गाइड में पिछले अध्यायों पर निर्भर करता है। अधिक जानकारी के लिए, परिचय मार्गदर्शिका पढ़ें या साइडबार में अध्याय सूचकांक देखें।

हर बार जब हमें एक बाल्टी देखने की आवश्यकता होती है, तो हमें रजिस्ट्री को एक संदेश भेजने की आवश्यकता होती है। यदि हमारी रजिस्ट्री को कई प्रक्रियाओं द्वारा समवर्ती रूप से एक्सेस किया जा रहा है, तो रजिस्ट्री एक अड़चन बन सकती है!

इस अध्याय में, हम ईटीएस (एरलंग टर्म स्टोरेज) के बारे में जानेंगे और कैशे तंत्र के रूप में इसका उपयोग कैसे करें।

चेतावनी! समय से पहले कैश के रूप में ईटीएस का उपयोग न करें! लॉग करें और अपने आवेदन के प्रदर्शन का विश्लेषण करें और पहचानें कि कौन से हिस्से अड़चन हैं, इसलिए आपको पता है कि आपको कैश करना चाहिए, और आपको क्या कैश करना चाहिए। यह अध्याय केवल एक उदाहरण है कि ईटीएस का उपयोग कैसे किया जा सकता है, एक बार जब आप आवश्यकता निर्धारित कर लेते हैं।

कैश के रूप में ईटीएस

ईटीएस हमें एक इन-मेमोरी टेबल में किसी भी अमृत शब्द को संग्रहीत करने की अनुमति देता है। ईटीएस टेबल के साथ काम करना एर्लांग के :ets ईटीएस मॉड्यूल के माध्यम से किया जाता है:

iex> table = :ets.new(:buckets_registry, [:set, :protected])
#Reference<0.1885502827.460455937.234656>
iex> :ets.insert(table, {"foo", self()})
true
iex> :ets.lookup(table, "foo")
[{"foo", #PID<0.41.0>}]

ETS तालिका बनाते समय, दो तर्कों की आवश्यकता होती है: तालिका का नाम और विकल्पों का एक सेट। उपलब्ध विकल्पों में से, हमने टेबल प्रकार और इसके उपयोग के नियमों को पारित किया। हमने चुना है :set प्रकार, जिसका अर्थ है कि कुंजी को डुप्लिकेट नहीं किया जा सकता है। हमने तालिका की पहुँच भी निर्धारित की है :protected , जिसका अर्थ है कि तालिका बनाने वाली प्रक्रिया ही इसे लिख सकती है, लेकिन सभी प्रक्रियाएँ इससे पढ़ सकती हैं। वे वास्तव में डिफ़ॉल्ट मान हैं, इसलिए हम उन्हें अभी से छोड़ देंगे।

ETS की तालिकाओं को भी नाम दिया जा सकता है, जिससे हम उन्हें दिए गए नाम से एक्सेस कर सकते हैं:

iex> :ets.new(:buckets_registry, [:named_table])
:buckets_registry
iex> :ets.insert(:buckets_registry, {"foo", self()})
true
iex> :ets.lookup(:buckets_registry, "foo")
[{"foo", #PID<0.41.0>}]

आइए, ईटीएस तालिकाओं का उपयोग करने के लिए KV.Registry को बदलें। नाम परिवर्तन की आवश्यकता के लिए हमारी रजिस्ट्री को संशोधित करने के लिए पहला परिवर्तन है, हम इसका उपयोग ईटीएस तालिका और रजिस्ट्री प्रक्रिया को स्वयं करने के लिए करेंगे। ईटीएस नाम और प्रक्रिया के नाम अलग-अलग स्थानों में संग्रहीत किए जाते हैं, इसलिए टकराव की कोई संभावना नहीं है।

lib/kv/registry.ex खोलें, और इसके कार्यान्वयन को बदल दें। हमने अपने द्वारा किए गए परिवर्तनों को उजागर करने के लिए स्रोत कोड में टिप्पणियाँ जोड़ी हैं:

defmodule KV.Registry do
  use GenServer

  ## Client API

  @doc """
  Starts the registry with the given options.

  `:name` is always required.
  """
  def start_link(opts) do
    # 1. Pass the name to GenServer's init
    server = Keyword.fetch!(opts, :name)
    GenServer.start_link(__MODULE__, server, opts)
  end

  @doc """
  Looks up the bucket pid for `name` stored in `server`.

  Returns `{:ok, pid}` if the bucket exists, `:error` otherwise.
  """
  def lookup(server, name) do
    # 2. Lookup is now done directly in ETS, without accessing the server
    case :ets.lookup(server, name) do
      [{^name, pid}] -> {:ok, pid}
      [] -> :error
    end
  end

  @doc """
  Ensures there is a bucket associated with the given `name` in `server`.
  """
  def create(server, name) do
    GenServer.cast(server, {:create, name})
  end

  ## Server callbacks

  def init(table) do
    # 3. We have replaced the names map by the ETS table
    names = :ets.new(table, [:named_table, read_concurrency: true])
    refs  = %{}
    {:ok, {names, refs}}
  end

  # 4. The previous handle_call callback for lookup was removed

  def handle_cast({:create, name}, {names, refs}) do
    # 5. Read and write to the ETS table instead of the map
    case lookup(names, name) do
      {:ok, _pid} ->
        {:noreply, {names, refs}}
      :error ->
        {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
        ref = Process.monitor(pid)
        refs = Map.put(refs, ref, name)
        :ets.insert(names, {name, pid})
        {:noreply, {names, refs}}
    end
  end

  def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
    # 6. Delete from the ETS table instead of the map
    {name, refs} = Map.pop(refs, ref)
    :ets.delete(names, name)
    {:noreply, {names, refs}}
  end

  def handle_info(_msg, state) do
    {:noreply, state}
  end
end

ध्यान दें कि हमारे परिवर्तन से पहले KV.Registry.lookup/2 ने सर्वर को अनुरोध भेजा था, लेकिन अब यह सीधे ईटीएस तालिका से पढ़ता है, जिसे सभी प्रक्रियाओं में साझा किया जाता है। हम जिस कैश मैकेनिज़्म को लागू कर रहे हैं, उसके पीछे यह मुख्य विचार है।

कैश तंत्र को काम करने के लिए, बनाई गई ईटीएस तालिका में पहुंच :protected (डिफ़ॉल्ट) की आवश्यकता होती है, इसलिए सभी क्लाइंट इसे पढ़ सकते हैं, जबकि केवल KV.Registry प्रक्रिया इसे लिखती है। हमने read_concurrency: true भी सेट read_concurrency: true तालिका शुरू करते समय read_concurrency: true , समवर्ती पढ़ने के संचालन के सामान्य परिदृश्य के लिए तालिका का अनुकूलन।

हमारे द्वारा ऊपर किए गए परिवर्तनों ने हमारे परीक्षणों को तोड़ दिया है क्योंकि रजिस्ट्री की आवश्यकता है :name शुरू करते समय :name विकल्प। इसके अलावा, कुछ रजिस्ट्री संचालन जैसे lookup/2 को एक PID के बजाय तर्क के रूप में नाम देने की आवश्यकता होती है, इसलिए हम ETS टेबल लुकअप कर सकते हैं। चलिए दोनों मुद्दों को हल करने के लिए test/kv/registry_test.exs में सेटअप फ़ंक्शन बदलते हैं:

  setup context do
    _ = start_supervised!({KV.Registry, name: context.test})
    %{registry: context.test}
  end

चूंकि प्रत्येक परीक्षण का एक अद्वितीय नाम होता है, हम अपनी रजिस्ट्रियों के नाम के लिए परीक्षण नाम का उपयोग करते हैं। इस तरह, हमें अब रजिस्ट्री PID को पास करने की आवश्यकता नहीं है, इसके बजाय हम इसे परीक्षण नाम से पहचानते हैं। यह भी ध्यान दें कि हमने start_supervised! का परिणाम सौंपा है start_supervised! अंडरस्कोर ( _ ) के लिए। इस मुहावरे का उपयोग अक्सर संकेत देने के लिए किया जाता है कि हम start_supervised! के परिणाम में रुचि नहीं रखते हैं start_supervised!

एक बार जब हम setup बदलते हैं, तो कुछ परीक्षण विफल होते रहेंगे। तुम भी परीक्षण पास नोटिस और रन के बीच असंगत विफल हो सकता है। उदाहरण के लिए, "स्पॉन बकेट्स" टेस्ट:

test "spawns buckets", %{registry: registry} do
  assert KV.Registry.lookup(registry, "shopping") == :error

  KV.Registry.create(registry, "shopping")
  assert {:ok, bucket} = KV.Registry.lookup(registry, "shopping")

  KV.Bucket.put(bucket, "milk", 1)
  assert KV.Bucket.get(bucket, "milk") == 1
end

इस पंक्ति में विफल हो सकता है:

{:ok, bucket} = KV.Registry.lookup(registry, "shopping")

यदि हम पिछली पंक्ति में केवल बाल्टी ही बनाते हैं तो यह रेखा कैसे विफल हो सकती है?

जो असफलताएं हो रही हैं, उनका कारण यह है कि उपदेशात्मक उद्देश्यों के लिए, हमने दो गलतियाँ की हैं:

  1. हम समय से पहले अनुकूलन कर रहे हैं (इस कैश परत को जोड़कर)
  2. हम cast/2 का उपयोग कर रहे हैं (जबकि हमें call/2 का उपयोग करना चाहिए)

दौर कि शर्ते?

Elixir में विकास करने से आपका कोड रेस की स्थिति से मुक्त नहीं होता है। हालांकि, एलिक्सिर के अमूर्त जहां कुछ भी डिफ़ॉल्ट रूप से साझा नहीं किया जाता है, एक रेस की स्थिति के मूल कारण को जानना आसान बनाता है।

हमारे परीक्षणों में जो कुछ हो रहा है, वह यह है कि एक ऑपरेशन के बीच में देरी हो रही है और जिस समय हम ईटीएस तालिका में इस बदलाव का निरीक्षण कर सकते हैं। यहाँ हम क्या होने की उम्मीद कर रहे थे:

  1. हम KV.Registry.create(registry, "shopping") आह्वान करते हैं
  2. रजिस्ट्री बाल्टी बनाती है और कैश टेबल को अपडेट करती है
  3. हम KV.Registry.lookup(registry, "shopping") साथ तालिका से जानकारी तक KV.Registry.lookup(registry, "shopping")
  4. ऊपर दिए गए आदेश {:ok, bucket}

हालाँकि, KV.Registry.create/2 एक कास्ट ऑपरेशन है, इससे पहले कि हम वास्तव में टेबल पर लिखें, कमांड वापस आ जाएगी! दूसरे शब्दों में, यह हो रहा है:

  1. हम KV.Registry.create(registry, "shopping") आह्वान करते हैं
  2. हम KV.Registry.lookup(registry, "shopping") साथ तालिका से जानकारी तक KV.Registry.lookup(registry, "shopping")
  3. रिटर्न के ऊपर कमांड :error
  4. रजिस्ट्री बाल्टी बनाती है और कैश टेबल को अपडेट करती है

विफलता को ठीक करने के लिए हमें call/2 बजाय call/2 का उपयोग करके KV.Registry.create/2 तुल्यकालिक बनाने की आवश्यकता है। यह गारंटी देगा कि तालिका में परिवर्तन किए जाने के बाद ही ग्राहक जारी रहेगा। चलिए फ़ंक्शन और उसके कॉलबैक को इस प्रकार बदलते हैं:

def create(server, name) do
  GenServer.call(server, {:create, name})
end

def handle_call({:create, name}, _from, {names, refs}) do
  case lookup(names, name) do
    {:ok, pid} ->
      {:reply, pid, {names, refs}}
    :error ->
      {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
      ref = Process.monitor(pid)
      refs = Map.put(refs, ref, name)
      :ets.insert(names, {name, pid})
      {:reply, pid, {names, refs}}
  end
end

हमने कॉलबैक को handle_cast/2 से handle_call/3 बदल दिया और इसे बनाई गई बाल्टी की सहायता से उत्तर में बदल दिया। सामान्यतया, एलिक्ज़िर डेवलपर्स cast/2 call/2 बजाय call/2 का उपयोग करना पसंद करते हैं क्योंकि यह बैक-प्रेशर भी प्रदान करता है - जब तक आपको उत्तर नहीं मिलता तब तक आप ब्लॉक करें। जब आवश्यक नहीं हो तो cast/2 का उपयोग करना भी एक समयपूर्व अनुकूलन माना जा सकता है।

चलो एक बार फिर से परीक्षण चलाते हैं। इस बार हालांकि, हम - विकल्प विकल्प पास करेंगे:

$ mix test --trace

जब आपके परीक्षण गतिरोध होते हैं या दौड़ की स्थिति होती है, तो यह विकल्प उपयोगी होता है, क्योंकि यह सभी परीक्षणों को समकालिक रूप से चलाता है ( async: true का कोई प्रभाव नहीं है) और प्रत्येक परीक्षण के बारे में विस्तृत जानकारी दिखाता है। आप एक या दो आंतरायिक विफलताओं को देख सकते हैं:

  1) test removes buckets on exit (KV.RegistryTest)
     test/kv/registry_test.exs:19
     Assertion with == failed
     code: KV.Registry.lookup(registry, "shopping") == :error
     lhs:  {:ok, #PID<0.109.0>}
     rhs:  :error
     stacktrace:
       test/kv/registry_test.exs:23

विफलता संदेश के अनुसार, हम उम्मीद कर रहे हैं कि बाल्टी अब मेज पर मौजूद नहीं है, लेकिन यह अभी भी करता है! यह समस्या हमारे द्वारा अभी-अभी हल की गई के विपरीत है: जबकि पहले कमांड में एक बाल्टी बनाने और टेबल को अपडेट करने के बीच देरी होती थी, अब बाल्टी प्रक्रिया के मरने और मेज से इसके प्रवेश के बीच देरी हो रही है। चूंकि यह एक दौड़ की स्थिति है, आप इसे अपनी मशीन पर पुन: पेश करने में सक्षम नहीं हो सकते हैं, लेकिन यह वहां है।

पिछली बार हमने एक call द्वारा एसिंक्रोनस ऑपरेशन, एक cast , जो सिंक्रोनस है, को बदलकर दौड़ की स्थिति तय की। दुर्भाग्य से, handle_info/2 कॉलबैक handle_info/2 हम उपयोग कर रहे हैं :DOWN संदेश को डाउनलोड करें और ईटीएस तालिका से प्रविष्टि को हटा दें एक तुल्यकालिक समतुल्य नहीं है। इस बार, हमें रजिस्ट्री को संसाधित करने की गारंटी के लिए एक तरीका खोजने की आवश्यकता है :DOWN जब बाल्टी प्रक्रिया समाप्त हो गई है तो अधिसूचना को भेजा गया।

ऐसा करने का एक आसान तरीका यह है कि हम बकेट लुकअप करने से पहले रजिस्ट्री में एक सिंक्रोनस अनुरोध भेज दें। Agent.stop/2 ऑपरेशन तुल्यकालिक है और केवल बाल्टी प्रक्रिया समाप्त होने के बाद ही रिटर्न और सभी :DOWN किए गए संदेश डिलीवर हो जाते हैं। इसलिए, एक बार Agent.stop/2 वापस आने के बाद, रजिस्ट्री को पहले से ही :DOWN मैसेज मिल जाता है, लेकिन हो सकता है कि उसने अभी तक इसे संसाधित न किया हो। के प्रसंस्करण की गारंटी करने के लिए :DOWN संदेश, हम एक तुल्यकालिक अनुरोध कर सकते हैं। चूंकि संदेशों को क्रम में संसाधित किया जाता है, एक बार जब रजिस्ट्री सिंक्रोनस अनुरोध का जवाब देती है, तो :DOWN संदेश निश्चित रूप से संसाधित किया जाएगा।

चलो ऐसा "फर्जी" बाल्टी बनाकर करते हैं, जो दोनों Agent.stop/2 बाद एक तुल्यकालिक अनुरोध है:

  test "removes buckets on exit", %{registry: registry} do
    KV.Registry.create(registry, "shopping")
    {:ok, bucket} = KV.Registry.lookup(registry, "shopping")
    Agent.stop(bucket)

    # Do a call to ensure the registry processed the DOWN message
    _ = KV.Registry.create(registry, "bogus")
    assert KV.Registry.lookup(registry, "shopping") == :error
  end

  test "removes bucket on crash", %{registry: registry} do
    KV.Registry.create(registry, "shopping")
    {:ok, bucket} = KV.Registry.lookup(registry, "shopping")

    # Stop the bucket with non-normal reason
    Agent.stop(bucket, :shutdown)

    # Do a call to ensure the registry processed the DOWN message
    _ = KV.Registry.create(registry, "bogus")
    assert KV.Registry.lookup(registry, "shopping") == :error
  end

हमारे परीक्षण अब (हमेशा) पास होना चाहिए!

ध्यान दें कि परीक्षण का उद्देश्य यह जांचना है कि रजिस्ट्री बाल्टी के शटडाउन संदेश को सही ढंग से संसाधित करती है या नहीं। तथ्य यह है कि KV.Registry.lookup/2 हमें एक वैध बाल्टी भेजता है इसका मतलब यह नहीं है कि जब तक आप इसे कॉल करते हैं तब तक बाल्टी अभी भी जीवित है। उदाहरण के लिए, यह किसी कारण से दुर्घटनाग्रस्त हो सकता है। निम्नलिखित परीक्षण में इस स्थिति को दर्शाया गया है:

  test "bucket can crash at any time", %{registry: registry} do
    KV.Registry.create(registry, "shopping")
    {:ok, bucket} = KV.Registry.lookup(registry, "shopping")

    # Simulate a bucket crash by explicitly and synchronously shutting it down.
    Agent.stop(bucket, :shutdown)

    # Now trying to call the dead process causes a :noproc exit
    catch_exit KV.Bucket.put(bucket, "milk", 3)
  end

इससे हमारा अनुकूलन अध्याय समाप्त होता है। हमने ETS को एक कैश मेकेनिज़्म के रूप में उपयोग किया है जहाँ रीड किसी भी प्रक्रिया से हो सकते हैं लेकिन लिखते हैं फिर भी एक प्रक्रिया के माध्यम से क्रमबद्ध होते हैं। इससे भी महत्वपूर्ण बात यह है कि हमने यह भी जान लिया है कि एक बार डेटा को एसिंक्रोनस रूप से पढ़ा जा सकता है, हमें इसकी दौड़ की स्थितियों से अवगत होना चाहिए।

व्यवहार में, यदि आप खुद को ऐसी स्थिति में पाते हैं जहाँ आपको गतिशील प्रक्रियाओं के लिए एक प्रक्रिया रजिस्ट्री की आवश्यकता होती है, तो आपको अमृत के भाग के रूप में प्रदान किए गए Registry मॉड्यूल का उपयोग करना चाहिए। यह उसी तरह की कार्यक्षमता प्रदान करता है जैसा कि हमने जेनरवर + :ets ईट्स का उपयोग करके बनाया है, जबकि समवर्ती लिखने और पढ़ने दोनों का प्रदर्शन करने में सक्षम है। यह 40 कोर के साथ मशीनों पर भी सभी कोर के पैमाने पर बेंचमार्क किया गया है

अगला, आइए बाहरी और आंतरिक निर्भरता पर चर्चा करें और मिक्स हमें बड़े कोडबेस को प्रबंधित करने में मदद करता है।