নিচের হাইপোথেটিক্যাল সিনেরিও कल্পনা করুন: একটি রেন্টাল প্রপার্টি ম্যানেজমেন্ট সিস্টেমে, Employee A রেন্টাল X-এর কন্ট্যাক্ট ইনফো এডিট করা শুরু করেন, কিছু এক্সট্রা ফোন নম্বর যোগ করতে। একই সময়, Employee B রেন্টাল X-এর কন্ট্যাক্ট ইনফোতে একটি টাইপো লক্ষ্য করেন এবং একটি আপডেট করেন। কয়েক মিনিট পর, Employee A রেন্টাল X-এর কন্ট্যাক্ট ইনফো নতুন ফোন নম্বর দিয়ে আপডেট করেন, এবং ... টাইপো ফিক্স করার আপডেটটি এখন গেয়ে গেছে!
এটি অবশ্যই ভালো না! এবং এটি একটি ট্রিভিয়াল সিনেরিও। একটি ফাইন্যান্সিয়াল সিস্টেমে একই কনফ্লিক্ট হলে কী হবে সেটি কল্পনা করুন!
কি আমরা ভবিষ্যতে এমন সিনেরিও এড়াতে পারি? সৌভাগ্যিত, উত্তর হ্যাঁ! আমাদের কনকারেন্সি প্রোটেকশন এবং লকিং দরকার — স্পেসিফিক্যালি, অপটিমিস্টিক লকিং — এমন সমস্যাগুলো রোধ করতে।
চলুন রেইল্স REST API-তে অপটিমিস্টিক লকিং এক্সপ্লোর করি।
'Lost Updates' এবং অপটিমিস্টিক লকিং vs পেসিমিস্টিক লকিং
আমরা যেটি এখন 겪েছি সেটি একটি ধরনের 'Lost Update'। যখন দুটি কনকারেন্ট ট্রানজ্যাকশন একই রোয়ের একই কলাম আপডেট করে, দ্বিতীয়টি প্রথমটির পরিবর্তনগুলিকে ওভাররাইড করে দেয়, যেন প্রথম ট্রানজ্যাকশনটা কখনো হয়নি সেভাবে।
সাধারণত এই সমস্যাটি সমাধান করা যায়:
- সঠিক Transaction Isolation level সেট করে, যা ডাটাবেস লেভেলে সমস্যাটি হ্যান্ডেল করে।
- Pessimistic locking — কনকারেন্ট ট্রানজ্যাকশনকে একই রো আপডেট করার থেকে রোধ করে। দ্বিতীয় ট্রানজ্যাকশন প্রথম ট্রানজ্যাকশন ফিনিশ হওয়ার জন্য অপেক্ষা করে ডেটা পড়ার আগেও। এখান véhে বড় সুবিধা হলো স্টেল ডেটা ওপারেট করা অসম্ভব। বড় ডিসঅ্যাডভান্টেজ হলো এটি ডেটা পড়াকেও ব্লক করে একটি রো থেকে।
- Optimistic locking — যদি রোয়ের স্টেট মডিফিকেশন টাইমে পড়ার সময় থেকে ভিন্ন থাকে, তবে মডিফিকেশন রোধ করে।
আমাদের সমস্যাটি কনকারেন্ট ডাটাবেস ট্রানজ্যাকশন সম্পর্কে নয় (বла মালিখে বিজনেস ট্রানজ্যাকশন) — তাই প্রথম সমাধানটি সত্যিকারের প্রয়োজনে নয়। এর মানে আমরা পেসিমিস্টিক লকিং এবং অপটিমিস্টিক লকিং-এ আছি।
পেসিমিস্টিক লকিং আমাদের হাইপোথেটিক্যাল সিনেরিওতে লост আপডেট রোধ করতো প্রথমেই। কিন্তু এটি ইউজারদের জীবন কষ্টদায়ক করতো যদি এটি খুব দীর্ঘ সময় ডেটা অ্যাক্সেস ব্লক করে (৩০ মিনিট বা তার বেশি পড়া ও এডিট করার কাল্পনিক করুন)।
অপটিমিস্টিক লকিং বেছে কম রস্ট্রিক্টিভ, কারণ এটি একাধিক ইউজারকে ডেটা অ্যাক্সেস করতে দেয়। যাইহোক, যদি একাধিক ইউজার একসাথে ডেটা এডিট করা শুরু করে, তবে শুধু একটি ইউজার অপারেশন পারফর্ম করতে পারবে। বাকিগুলো একটি এরর দেখবে যে তারা স্টেল ডেটা ওপারেট করেছে এবং রিট্রাই করতে হবে। আিডিয়াল নয়, কিন্তু সঠিক UX-এর সাথে এটি उतনা পেইনফুল নাও হতে পারে।
চলুন দেখি কীভাবে আমরা একটি হাইপোথেটিক্যাল রেইল্স REST API-তে অপটিমিস্টিক লকিং ইমপ্লিমেন্ট করতে পারি।
REST API-তে অপটিমিস্টিক লকিং
আসল রেইল্স অ্যাপে ইমপ্লিমেন্টেশনের আগে, চিন্তা করি সাধারণ REST API-তে অপটিমিস্টিক লকিং কীভাবে দেখাবে।
উপরে বর্ণনা করা মতে, আমাদের একটি অবজেক্টের আসল স্টেট ট্র্যাক করতে হবে পড়ার সময়, যাতে পরবর্তীতে আপডেটের সময় তার স্টেটের সাথে তুলনা করা যায়। যদি স্টেট শেষ পড়ার পর থেকে পরিবর্তন না হয়, তবে অপারেশন অনুমোদিত। যদি পরিবর্তন হয়, তবে ফেইল করবে।
REST API-এর কনটেক্সটে আমাদের কী কী বের করতে হবে:
- একটি রিসোর্সের ডেটা পড়ার সময়, কীভাবে একটি অবজেক্টের বর্তমান স্টেট এক্সপ্রেস করব এবং রেসপন্সে কনসিউমারের জন্য রিটার্ন করব?
- কনসিউমাররা আপডেট করার সময় রিসোর্সের আসল স্টেট API-তে কীভাবে প্রোপাগেট করবে?
- যদি স্টেট পরিবর্তন হয়ে যায় এবং আপডেট সম্ভব নয়, তবে API কনসিউমারের কাছে কী রিটার্ন করবে?
খুশির খবর হলো এই সব প্রশ্নের উত্তর HTTP সেম্যান্টিক্স দিয়ে দেওয়া যায় এবং হ্যান্ডেল করা যায়।
রিসোর্সের স্টেট ট্র্যাক করার জন্য, আমরা Entity Tags (বা ETags) এর সুবিধা নিতে পারি। আমরা রিসোর্সের ফিঙ্গারপ্রিন্ট/চেকসাম/ভার্সন নম্বর ডেডিকেটেড ETag হেডারে API কনসিউমারদের রিটার্ন করতে পারি যাতে সেগুলো PATCH রিকোয়েস্টের সাথে পাঠানো যায়। আমরা If-Match হেডার ব্যবহার করতে পারি, যা API সার্ভারের জন্য খুব সহজ করে দেয় রিসোর্স পরিবর্তন হয়েছে কিনা চেক করতে। এটার মানে শুধু চেকসাম/ভার্সন নম্বর/যা কিছুই ETag হিসেবে বেছে নেওয়া হয়েছে সেগুলো তুলনা করা।
রিকোয়েস্ট সাকসেসফুল হবে যদি বর্তমান ETag এবং If-Match ভ্যালু একই হয়। যদি না হয়, তবে API সengl 412 Precondition Failed স্ট্যাটাস রেসপন্ড করবে, যা এই উদ্দেশ্যে সবচেয়ে উপযুক্ত এবং এক্সপ্রেসিভ স্ট্যাটাস।
একটি অন্য সম্ভাব্য সিনেরিও আছে। আমরা ETags তুলনা করতে পারি শুধু তখন যখন API কনসিউমার If-Match হেডার প্রদান করে। যদি সেটি প্রদান না করে? আপনি কনকারেন্সি প্রোটেকশন ইগনোর করতে পারেন এবং অপটিমিস্টিক লকিং ভুলে যেতে পারেন, কিন্তু তা আইডিয়াল নয়। অন্য একটি সমাধান হলো If-Match হেডার আবশ্যক করা এবং যদি না থাকে তবে 428 Precondition Required স্ট্যাটাস রেসপন্ড করা।
এখন REST API-তে অপটিমিস্টিক লকিং কীভাবে কাজ করতে পারে সেটির একটি স olid ওভারভিউ পাওয়া গেলে, চলুন রেইলসে ইমপ্লিমেন্ট করি।
রেইলসে অপটিমিস্টিক লকিং
খুশির খবর হলো রেইলস অপটিমিস্টিক লকিং আউট-অফ-দ্য-বক্স অফার করে — আমরা ActiveRecord::Locking::Optimistic প্রদত্ত ফিচার ব্যবহার করতে পারি। যখন আপনি lock_version কলাম যোগ করেন (অথবা যা কিছুই চান, যদিও তখন মডেল লেভেলে অতিরিক্ত ডিক্লেরেশন দরকার লকিং কলাম ডিফাইন করার জন্য), ActiveRecord এটি প্রতিটি পরিবর্তনের পর ইনক্রিমেন্ট করবে এবং চেক করবে বর্তমান অ্যাসাইন করা ভার্সন আসলেই এক্সপেক্টেডটি কিনা। যদি সেটি স্টেল হয়, ActiveRecord::StaleObjectError এক্সেপশন আপডেট/ডিস্ট্রয় অটেম্প্টে রেইজ হবে।
আমাদের API-তে অপটিমিস্টিক লকিং হ্যান্ডেল করার সবচেয়ে সহজ পথ হলো lock_version-এর ভ্যালু ETag হিসেবে ব্যবহার করা। আমাদের হাইপোথেটিক্যাল RentalsController-এ প্রথম ধাপ হিসেবে এটা করি:
class RentalsController
after_action :assign_etag, only: [:show]
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
private
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
end
এটি অবশ্যই কন্ট্রোলারের খুব সিম্পলিফাইড ভার্সন কারণ আমরা শুধু অপটিমিস্টিক লকিং-এর জন্য যা দরকার সেটি মাত্র আগ্রহী, অথেন্টিকেশন, অথরাইজেশন, বা অন্যান্য কনসেপ্ট নয়। এটি কনসিউমারের কাছে প্রপার ETag এক্সপোজ করার জন্য যথেষ্ট। এখন If-Match হেডার নিয়ে যত্ন নিই যা কনসিউমাররা প্রদান করতে পারে:
class RentalsController
after_action :assign_etag, only: [:show, :update]
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
def update
@rental = Rental.find(params[:id])
@rental.update(rental_params)
respond_with @rental
end
private
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
def rental_params
params
.require(:rental)
.permit(:some, :permitted, :attributes)
.merge(lock_version: lock_version_from_if_match_header)
end
def lock_version_from_if_match_header
request.headers["If-Match"].to_i
end
end
এইটাই আসলে মিনিমাল অপটিমিস্টিক লকিং কাজ করার জন্য যথেষ্ট! যদিও স্পষ্টই, আমরা ৫০০ রেসপন্স রিটার্ন করতে চাই না কোনো কনফ্লিক্ট হলে। আমরা If-Match আবশ্যক করব যেকোনো আপডেটের জন্যও:
class RentalsController
before_action :ensure_if_match_header_provided, only: [:update]
after_action :assign_etag, only: [:show, :update]
rescue_from ActiveRecord::StaleObjectError do
head 412
end
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
def update
@rental = Rental.find(params[:id])
@rental.update(rental_params)
respond_with @rental
end
private
def ensure_if_match_header_provided
request.headers["If-Match"].present? or head 428 and return
end
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
def rental_params
params
.require(:rental)
.permit(:some, :permitted, :attributes)
.merge(lock_version: lock_version_from_if_match_header)
end
def lock_version_from_if_match_header
request.headers["If-Match"].to_i
end
end
এটাই প্রায় সকল ফাংশনালিটি ইমপ্লিমেন্ট করার জন্য যা আমরা আগে আলোচনা করেছি। আমরা আরও অনেক কিছু উন্নত করতে পারি — যেমন শুধু রেসপন্স কোড ছাড়া কিছু এক্সট্রা এরর মেসেজ দিয়ে — কিন্তু এই আর্টিকেলের স্কোপের বাইরে।
র্যাপ-আপ: রেইল্স API-তে অপটিমিস্টিক লকিং-এর গুরুত্ব
কনকারেন্সি প্রোটেকশন প্রায়শই উপেক্ষিত থাকে REST API ডিজাইন করার সময়, যা তীব্র পরিণতিতে লিড করতে পারে।
তবুও, রেইল্স API-তে অপটিমিস্টিক লকিং ইমপ্লিমেন্ট করা খুব স্ট্রেটফরওয়ার্ড — যেমন এই আর্টিকেলে ডেমোনস্ট্রেট করা হয়েছে — এবং এটি সম্ভাব্য ক্রিটিক্যাল ইস্যু এড়াতে সাহায্য করবে।
হ্যাপি কোডিং!
P.S. Ruby Magic পোস্টগুলো প্রেস থেকে বের होतेই পেতে চাইলে, আমাদের Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন এবং কোনো পোস্ট মিস করবেন না!