কম্পিউটার টিউটোরিয়াল

রেইলস ক্যাশ (Rails Cache) মাস্টার করুন: ActiveSupport-এ Increment ও Decrement সিক্রেটস

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

এই আর্টিকেলে, আমরা Rails.cache-এ increment এবং decrement মেথডগুলো ব্যাখ্যা করব।

The Rails.cache Helper (Rails.cache হেল্পার)

Rails.cache আপনার অ্যাপ্লিকেশনে ক্যাশের সাথে ইন্টারঅ্যাক্ট করার এন্ট্রি পয়েন্ট। এটি একটি অ্যাবস্ট্রাকশনও, যা আপনাকে একটি কমন API কল করার সুবিধা দেয় যেন আন্ডার দ্য হুড (under the hood) কোনো ক্যাশ "স্টোর" (store) ব্যবহার করা হচ্ছে। বক্সের বাইরে, রেইলস নিচেরগুলো সাপোর্ট করে:

  • FileStore
  • MemoryStore
  • MemCacheStore
  • NullStore
  • RedisCacheStore

Rails.cache ইন্সপেক্ট করলে দেখাবে আপনি কোনটি রান করছেন:

> Rails.cache
=> #nil, ...

আমরা সকলটাকে বিস্তারিত দেখব না, কিন্তু কুইক সামারি হিসেবে:

  • NullStore কিছুই স্টোর করে না; এ থেকে রিড ব্যাক করার সর্বদা nil রিটার্ন করবে। এটি একটি নতুন রেইলস অ্যাপের ডিফল্ট।
  • FileStore ক্যাশকে হার্ড ড্রাইভে ফাইল হিসেবে স্টোর করে, তাই আপনি আপনার রেইলস সার্ভার রিস্টার্ট করলেও সেগুলো পERSIST করে।
  • MemoryStore ক্যাশকে RAM-এ রাখে, তাই আপনি আপনার রেইলস সার্ভার বন্ধ করলে ক্যাশও 와ইপ (wipe) হয়ে যাবে।
  • MemCacheStore এবং RedisCacheStore এক্সটার্নাল প্রোগ্রাম (क्रमশः MemCache এবং Redis) ব্যবহার করে ক্যাশ মেইনটেইন করে।

বিভিন্ন কারণে, প্রথম তিনটি ডেভেলপমেন্ট/টেস্টিং-এর জন্য সবচেয়ে বেশি ব্যবহার হয়। প্রোডাকশনে, আপনি সম্ভবত Redis বা MemCache ব্যবহার করবেন।

কারণ Rails.cache এই সার্ভিসগুলোর পার্থক্য অ্যাবস্ট্রাক্ট করে দিয়েছে, এটি সহজে ডিফারেন্ট এনভায়রনমেন্টে ডিফারেন্ট ভার্সন রান করা যায় কোড পরিবর্তন ছাড়া; উদাহরণস্বরূপ, আপনি ডেভেলপমেন্টে NullStore, টেস্টিং-এ MemoryStore, এবং প্রোডাকশনে RedisCacheStore ব্যবহার করতে পারেন।

Cached Data (ক্যাশড ডেটা)

রেইলসের Rails.cache.read, Rails.cache.write, এবং Rails.cache.fetch দিয়ে, আমাদের কাছে যেকোনো আর্বিট্রারি ডেটা ক্যাশ থেকে স্টোর এবং রিট্রিভ করার একটি সহজ উপায় রয়েছে। একটি আগের আর্টিকেল এগুলো বিস্তারিত কভার করেছিল; এই আর্টিকেলের জন্য গুরুত্বপূর্ণ বিষয় হলো এই মেথডগুলোর উপর কোনো বিল্ট-ইন থ্রেড-সেফটি (thread-safety) নেই। ধরা যাক আমরা ক্যাশড ডেটা একাধিক থ্রেড থেকে আপডেট করছি একটি রানিং কাউন্ট রাখতে; আমাদের রিড/রাইট অপারেশনকে একটি লক (lock) দিয়ে র‍্যাপ করতে হবে রেস কন্ডিশন (race conditions) এড়াতে। এই উদাহরণটি বিবেচনা করুন, ধরে নিই আমরা রেডিস ক্যাশ স্টোর ব্যবহার করার জন্য সেট আপ করেছি:

threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0)
4.times do
  threads << Thread.new do
    100.times do
      current_count = Rails.cache.read(:test_counter)
      current_count += 1
      Rails.cache.write(:test_counter, current_count)
    end
  end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter)

এখানে চারটি থ্রেড রয়েছে, প্রতিটি আমাদের ক্যাশড ভ্যালু ১০০ বার ইনক্রিমেন্ট করছে। রেজাল্ট হওয়া উচিত ৪০০, কিন্তু অধিকাংশ সময় এটি অনেক কম হবে - আমার টেস্ট রানে ২৬৯। এখানে কী হচ্ছে? একটি রেস কন্ডিশন (race condition)। আমি আগের আর্টিকেলে এই বিষয়ে বিস্তারিত আলোচনা করেছি, কিন্তু কুইক সামারি হিসেবে, কারণ থ্রেডগুলো একই "শেয়ার্ড" (shared) ডেটা ওপর অপারেট করছে, তারা একে অপরের সাথে আউট-অফ-সিঙ্ক (out-of-sync) হতে পারে। উদাহরণস্বরূপ, একটি থ্রেড ভ্যালুটা পড়তে পারে, তারপর অন্য থ্রেড ওভারটেক করে এবং ভ্যালুটাও পড়ে, ইনক্রিমেন্ট করে, এবং স্টোর করে। প্রথম থ্রেডটি তারপর রিজিউম করে, এর নাউ-আউট-অফ-ডেট ভ্যালু ব্যবহার করে।

এই সমস্যা সমাধান করার সাধারণ উপায় হলো কোডকে একটি মিউচুয়ালি এক্সক্লুসিভ লক (mutually exclusive lock বা Mutex) দিয়ে ঘেরে দেয়া যাতে একবারে শুধু একটি থ্রেড লকের ভিতরে কোড এক্সিকিউট করতে পারে। আমাদের কেসে, যাইহোক, Rails.cache-এ এই সিনারিও হ্যান্ডেল করার কিছু মেথড রয়েছে।

Rails Cache Increment এবং Decrement (বৃদ্ধি ও হ্রাস)

Rails.cache অবজেক্টে increment এবং decrement মেথড দুটি রয়েছে ক্যাশড ডেটা সরাসরি অ্যাক্ট করার জন্য আমাদের কাউন্টার সিনারিওর মতো:

threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0, raw: true)
4.times do
  threads << Thread.new do
    100.times do
      Rails.cache.increment(:test_counter)
      # repeating the increment just to highlight the thread safety
      Rails.cache.decrement(:test_counter)
      Rails.cache.increment(:test_counter)
    end
  end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter, raw: true)

increment এবং decrement ব্যবহার করতে, আমাদের ক্যাশ স্টোরকে বলতে হবে এটি একটি 'raw' ভ্যালু (via raw: true)। আপনাকে এটি ভ্যালু রিড ব্যাক করার সময়ও করতে হবে; অন্যথায়, আপনি একটি এরর পাবেন। মূলত, আমরা ক্যাশকে বলছি আমরা এই ভ্যালুটিকে একটি বেয়ার ইন্টিজার (bare integer) হিসেবে স্টোর করতে চাই যাতে আমরা ইনক্রিমেন্ট/ডিক্রিমেন্ট কল করতে পারি, কিন্তু আপনি এখনও expires_in এবং অন্যান্য ক্যাশ ফ্ল্যাগস একসাথে ব্যবহার করতে পারেন।

খেয়াল রাখবেন, যদিও আমি এখানে এটি ব্যবহার করিনি, দুটি মেথডও নতুন ভ্যালু রিটার্ন করে। তাই, আপনি যদি কাউন্টার ভ্যালু আপডেট করার বাইরে ব্যবহার করতে চান, আপনি একটি অতিরিক্ত read কল ছাড়া সেটিও করতে পারেন।

রিয়েল-ওয়ার্ল্ড অ্যাপ্লিকেশন (Real-World Applications)

বাহ্যিক দৃষ্টিতে, এই increment এবং decrement মেথডগুলো লো-লেভেল হেল্পার মেথডের মনে হয়, যেগুলো আপনি সম্ভবত তभी কেয়ার করবেন যদি আপনি কিছুইম্প্লিমেন্ট বা মেইনটেইন করছেন যেন একটি ব্যাকগ্রাউন্ড জব প্রসেসিং জেম। কিন্তু একবার আপনি এই সম্পর্কে জানলে, আপনি আশ্চর্য হতে পারেন সেগুলো কোথায় কাজে আসতে পারে।

আমি একটি প্রোডাকশন অ্যাপ্লিকেশনে এগুলো ব্যবহার করেছি ডুপ্লিকেট শিডিউলড ব্যাকগ্রাউন্ড জবস একসাথে রান হওয়া থেকে বাঁচাতে। আমাদের কেসে, আমাদের বিভিন্ন শিডিউলড জব রয়েছে সার্চ ইনডেক্স আপডেট করার জন্য, অ্যাব্যান্ডন্ড কার্ট চিহ্নিত করার জন্য, ইত্যাদি। সাধারণত এটি ঠিকই কাজ করে; ক্যাচ হলো কিছু জব (সার্চ ইনডেক্স erityে) প্রচুর মেমোরি কনসাম করে—প্রচুর পরিমাণে যে, যদি দুটি একসাথে রান করে, আমাদের Heroku dyno-এর লিমিট অতিক্রম করবে, এবং ওয়ার্কার কিল্ড অফ (killed off) হয়ে যাবে। কারণ আমাদের এমন কিছু জব রয়েছে, এটি এতটা সিম্পল নয় সেগুলো no-retries বা ফোর্সিং ইউনিক জবস দিয়ে مار্ক করা; দুটি ভিন্ন (অতএব ইউনিক) জব একই সময়ে রান করার চেষ্টা করতে পারে এবং ওয়ার্কার ডাউন করাতে পারে।

এটি প্রতিরোধ করতে, আমরা শিডিউলড জবের জন্য একটি বেস ক্লাস তৈরি করেছি যা একটি কাউন্টার রাখে কতগুলো বর্তমানে রান করছে। যদি কাউন্ট খুব বেশি হয়, জবটি সেলফ রি-এনকিউ (re-enqueue) করে এবং অপেক্ষা করে।

আমার একটি সাইড প্রজেক্টে আরেকটি উদাহরণ ছিল যেখানে একটি ব্যাকগ্রাউন্ড জব (বা একাধিক জব) কিছু প্রসেসিং করে যখন ইউজারকে অপেক্ষা করতে হয়। এটি ব্যাকগ্রাউন্ড জবের কারেন্ট প্রগ্রেস ইউজারের সাথে কমিউনিকেট করার কমন ইস্যু তুলে ধরে। যদিও এটিকে সমাধান করার অনেক উপায় রয়েছে, একটি এক্সপেরিমেন্ট হিসেবে, আমি Rails.cache.increment ব্যবহার করলাম একটি গ্লোবালি অবেইলেবল কাউন্টার আপডেট করার জন্য। স্ট্রাকচারটি নিচের মতো ছিল:

  1. প্রথম, আমি /app/models-এ একটি নতুন ক্লাস যোগ করলাম ক্যাশে কাউন্টার থাকে সেটি অ্যাবস্ট্র্যাক্ট করার জন্য। এটি হলো সকল অ্যাক্সেস টু দ্য ভ্যালু ফ্লো থ্রু হবে। এর অংশ হলো জব সম্পর্কিত یونিক ক্যাশ কী জেনারেট করা।
  2. জবটি এই মডেলের একটি ইনস্ট্যান্স তৈরি করে এবং আইটেমস প্রসেস হলে আপডেট করে।
  3. একটি সিম্পল JSON এন্ডপয়েন্ট এই মডেলের একটি ইনস্ট্যান্স তৈরি করে কারেন্ট ভ্যালু গ্র্যাব করার জন্য।
  4. ফ্রন্ট-এন্ড এই এন্ডপয়েন্টকে প্রতি بضعة সেকেন্ডে পোল (poll) করে UI আপডেট করতে। আপনি অবশ্যই এটি ফ্যান্সি করতে পারেন ActionCable দিয়ে আপডেট পুশ আউট করে।

উপসংহার

সত্যিকারের কথা, Rails.cache.increment এমন একটি টুল নয় যেটি আমি সচরাচর ব্যবহার করব, কারণ এটি নয় এমন যা আমি চাই ক্যাশে স্টোর করা ডেটা আপডেট করা (যেটি, নেচারের ভাবেই, কিছুটা টেম্পোরারি)। যেমন উপরে নোট করা হয়েছে, যে সময়গুলো আমি এর জন্য রিচ করি সেগুলো সাধারণত ব্যাকগ্রাউন্ড জবস সম্পর্কিত যেহেতু জবগুলো ইতিমধ্যে তাদের ডেটা রেডিসে (রব অ্যাপস যেগুলো আমি ওয়ার্ক করেছি) স্টোর করে এবং সাধারণত টেম্পোরারি। таких কেসে, রিলেটেড ডেটা (পার্সেন্টেজ কমপ্লিট, উদাহরণস্বরূপ) একই জায়গায় একই শর্ট-টার্ম পারসিস্টেন্স লেভেলের সাথে স্টোর করা স্বাভাবিক মনে হয়।

সকল "বিটেন পাথ" (off the beaten path) জিনিসের মতো, আপনাকে সতর্ক থাকা উচিত এমন জিনিস আপনার কোডবেসে ইন্ট্রোডিউস করার সময়। আমি রিকমেন্ড করব, কমপক্ষে, কিছু কমেন্ট যোগ করা ভবিষ্যৎ ডেভেলপারদের বুঝাতে আপনি কেন এই মেথডটি ব্যবহার করছেন যেটি তারা সম্ভবত পরিচিত নন।

  1. সঠিক রেলস ক্যাশ স্টোর নির্বাচন: একটি সমৃদ্ধ গাইড

  2. রেইলসে জেম ম্যানেজমেন্ট: Bundler এবং RubyGems কীভাবে একসাথে কাজ করে

  3. রেইলসে ক্যারিয়ারওয়েভ দিয়ে ফাইল আপলোডে দক্ষতা অর্জন

  4. Windows 11 মাস্টার করুন: এক্সপার্ট টিপস ও হিডেন ফিচারস প্রকাশিত