Cron + database প্যাটার্ন: কিউ ছাড়া ব্যাকগ্রাউন্ড জব
ক্রন + ডেটাবেস প্যাটার্ন শিখুন: পূর্ণ queue সিস্টেম ছাড়া নির্ধারিত ব্যাকগ্রাউন্ড জব চলান, রিট্রাই, লকিং ও আইডেম্পোটেন্সি সহ।

সমস্যা: অতিরিক্ত ইনফ্রাস্ট্রাকচার ছাড়া নির্ধারিত কাজ
অধিকাংশ অ্যাপে কাজ পরে বা নির্ধারিত সময়েই করা লাগে: ফলো-আপ ইমেইল পাঠানো, রাতে বিলিং চেক চালানো, পুরনো রেকর্ডগুলো পরিস্কার করা, রিপোর্ট পুনর্নির্মাণ করা, অথবা ক্যাশ রিফ্রেশ করা।
শুরুতে, ব্যাকগ্রাউন্ড জবের জন্য পূর্ণQueue সিস্টেম যোগ করা প্রলুব্ধ করে কারণ তা মনে হয় “ঠিক” উপায়। কিন্তু queue গুলো আরও অনেক moving part যোগ করে: আরেকটি সার্ভিস চালানো, মনিটার করা, ডিপ্লয় করা এবং ডিবাগ করা। ছোট টীম (বা একক প্রতিষ্ঠাতা) জন্য সেই অতিরিক্ত ওজন আপনাকে ধীর করতে পারে।
তাহলে মূল প্রশ্ন হল: অতিরিক্ত ইনফ্রাস্ট্রাকচার চালানো ছাড়া নির্ধারিত কাজ কিভাবে নির্ভরযোগ্যভাবে চালাবেন?
প্রথম সাধারণ চেষ্টা সাধারণত সহজ: একটি cron এন্ট্রি যোগ করে একটি endpoint হিট করানো, এবং সেই endpoint কাজটি করবে। এটি কাজ করে যতদিন না করে। একবার আপনার একটির বেশি সার্ভার থাকে, ভুল সময়ে ডিপ্লয় হয়, বা একটি কাজ প্রত্যাশার চেয়ে বেশি সময় নেয়, তখন বিভ্রান্তিকর ব্যর্থতা দেখা শুরু করে।
নির্ধারিত কাজ সাধারণত কয়েকটি পূর্বানুমানযোগ্যভাবে ভেঙে পড়ে:
- ডাবল রান: দুইটি সার্ভার একসঙ্গে একই টাস্ক চালায়, ফলে চালান দুবার তৈরি হয় বা ইমেইল দুবার পাঠে।
- হারানো রান: ডিপ্লয়ের সময় cron কল ব্যর্থ হয় এবং কেউ লক্ষ্য করে না যতক্ষণ না ব্যবহারকারী অভিযোগ করে।
- নীরব ব্যর্থতা: জব একবার এরর করে, তারপর আর চালানো হয় না কারণ কোনো রিট্রাই পরিকল্পনা নেই।
- আংশিক কাজ: জব মাঝপথে ক্র্যাশ করে এবং ডেটা অদ্ভুত অবস্থায় রেখে যায়।
- অডিট ট্রেইল নেই: আপনি জানাতে পারেন না “শেষে কখন এটি চলেছে?” বা “গত রাতে কি হয়েছে?”
cron + database প্যাটার্ন একটি মধ্যপথ। আপনি এখনও cron ব্যবহার করে নির্ধারিত সময়ে "ওয়েক আপ" করছেন, কিন্তু কাজের উদ্দেশ্য ও অবস্থান ডাটাবেসে সংরক্ষণ করা হয় যাতে সিস্টেম সমন্বয়, রিট্রাই এবং কি ঘটেছে তা রেকর্ড করতে পারে।
এটি ভাল মানায় যখন আপনার ইতিমধ্যে একটি ডেটাবেস (অften PostgreSQL), সীমিত সংখ্যক জব টাইপ আছে, এবং আপনি কম অপস কাজের সাথে পূর্বানুমানযোগ্য আচরণ চান। এটি দ্রুত তৈরি করা আধুনিক স্ট্যাকের অ্যাপগুলোর জন্যও প্রাকৃতিক পছন্দ (উদাহরণস্বরূপ, React + Go + PostgreSQL সেটআপ)।
এটি সঠিক নয় যখন আপনাকে খুব উচ্চ থ্রুপুট, দীর্ঘ-চলমান কাজ যা প্রগতি স্ট্রিম করে চালাতে হয়, বিভিন্ন জব টাইপের মধ্যে কঠোর ordering প্রয়োজন, বা ভারী fan-out (প্রতি মিনিটে হাজারো সাব-টাস্ক) দরকার। সেক্ষেত্রে একটি প্রকৃত queue এবং ডেডিকেটেড ওয়ার্কার সাধারণত নিজেকে মেটায়।
সরল ভাষায় মূল ধারণা
cron + database প্যাটার্ন একটি পূর্ণ queue সিস্টেম চালানো ছাড়া নির্ধারিত ব্যাকগ্রাউন্ড কাজ চালায়। আপনি এখনও cron (অথবা যেকোনো scheduler) ব্যবহার করেন, কিন্তু cron নির্ধারণ করে না কী চালানো হবে। এটি শুধু worker-কে ঘুম থেকে জাগায় (একমিনিটে একবার সাধারণ)। ডেটাবেস সিদ্ধান্ত নেয় কোন কাজ অনিবার্য এবং নিশ্চিত করে যে প্রতিটি জব শুধু একটিই নেয়।
এটিকে ভাবুন একটি শেয়ার্ড চেকলিস্টের মতো। Cron হচ্ছে সেই ব্যক্তি যে প্রতি মিনিটে ঘরে এসে বলে, “কারো কিছু করবার আছে কি?” ডেটাবেস হচ্ছে সেই হোয়াইটবোর্ড যা দেখায় কী নির্ধারিত, কী নেওয়া হয়েছে, এবং কী সমাপ্ত।
পর্দা-পেছনের অংশগুলো সরল:
- একটি একক scheduler trigger ঘনঘন চলে।
- একটি jobs টেবিল থাকে যা “কি” এবং “কখন” (due time), সাথে status ও attempt count ধারণ করে।
- এক বা একাধিক worker টেবিল পোল করে, একটি জব দাবি করে, এবং কাজটি করে।
- দাবি করার সময় ডেটাবেস লক ব্যবহার করা হয় যাতে দুটি worker একই row নিতে না পারে।
- ডেটাবেস কি রন করেছে, কী ব্যর্থ হয়েছে, এবং কী রিট্রাই হওয়া উচিত—সবকিছুর source of truth থাকে।
উদাহরণ: প্রতিদিন সকালে invoice রিমাইন্ডার পাঠাতে চান, প্রতিটি 10 মিনিটে ক্যাশ রিফ্রেশ করতে চান, এবং রাতের বেলা পুরনো সেশন ক্লিনআপ করতে চান। আলাদা তিনটি cron কমান্ডের বদলে (প্রতিটিতেই ওভারল্যাপ ও ব্যর্থতার মোড আছে), আপনি জব এন্ট্রিগুলো এক জায়গায় স্টোর করেন। Cron একই worker প্রসেস শুরু করে। Worker Postgres-কে জিজ্ঞাসা করে, “এখন কী_due?” এবং Postgres নিরাপদভাবে একে একে প্রতিটি জব কে claim করতে দেয়।
এটি ধীরে ধীরে স্কেল করে। আপনি এক সার্ভারে একটি worker দিয়ে শুরু করতে পারেন। পরে, আপনি পাঁচটি worker বিভিন্ন সার্ভারে চালাতে পারেন। কনট্রাক্ট একই থাকে: টেবিলই কনট্রাক্ট।
মনোভাব পরিবর্তন সহজ: cron কেবল ওয়েক-আপ কল। ডেটাবেস ট্রাফিক কপ যা সিদ্ধান্ত নেয় কী চালানো যাবে, কী হয়েছে তা রেকর্ড করে, এবং কিছু ভুল হলে পরিষ্কার ইতিহাস দেয়।
jobs টেবিল ডিজাইন করা (প্র্যাকটিক্যাল স্কিমা)
এই প্যাটার্নটি সবচেয়ে ভাল কাজ করে যখন আপনার ডেটাবেসই কী চালানো উচিত, কখন চালানো উচিত, এবং শেষবার কী ঘটেছিল—এর source of truth হয়। স্কিমাটা জটিল নয়, কিন্তু ছোট ডিটেইলগুলো (লক ফিল্ড এবং সঠিক ইনডেক্স) লোড বাড়ার সাথে বড় প্রভাব ফেলে।
এক টেবিল না দুই টেবিল?
দুইটি সাধারণ পদ্ধতি:
- একটি সংযুক্ত টেবিল যখন আপনি প্রতিটি জবের সর্বশেষ অবস্থা নিয়ে কেবল আগ্রহী (সরল, কম joins)।
- দুটি টেবিল যখন আপনি “এই জব কি” এবং “প্রতিবার এটি চালানো হয়েছিল” আলাদা রাখতে চান (ভিত্তিহীন ইতিহাস, ডিবাগ করা সহজ)।
আপনি যদি আশা করেন যে বারবার ফেইলিউর ডিবাগ করতে হবে, ইতিহাস রাখুন। সবচেয়ে ছোট সেটআপ চাইলে এক টেবিল দিয়েই শুরু করুন এবং পরে ইতিহাস যোগ করুন।
একটি ব্যবহারিক স্কিমা (দুই-টেবিল ভার্সন)
নিচে একটি PostgreSQL-বান্ধব বিন্যাস। আপনি যদি Go দিয়ে PostgreSQL তৈরি করেন, এই কলামগুলো struct-এ সহজে মানচিত্র হয়।
-- What should exist (the definition)
create table job_definitions (
id bigserial primary key,
job_type text not null,
payload jsonb not null default '{}'::jsonb,
schedule text, -- optional: cron-like text if you store it
max_attempts int not null default 5,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- What should run (each run / attempt group)
create table job_runs (
id bigserial primary key,
definition_id bigint references job_definitions(id),
job_type text not null,
payload jsonb not null default '{}'::jsonb,
run_at timestamptz not null,
status text not null, -- queued | running | succeeded | failed | dead
attempts int not null default 0,
max_attempts int not null default 5,
locked_by text,
locked_until timestamptz,
last_error text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
কয়েকটি ডিটেইল যা পরে ব্যথা বাঁচায়:
- job_type একটি ছোট স্ট্রিং রাখুন যাতে আপনি রাউটিং করতে পারেন (যেমন
send_invoice_emails)। - payload
jsonbহিসেবে রাখুন যাতে মাইগ্রেশন ছাড়া এটি পরিবর্তন করা যায়। - run_at হচ্ছে আপনার “পরবর্তী নির্ধারিত সময়”। Cron (অথবা scheduler script) এটি সেট করে, worker এটি গ্রহণ করে।
- locked_by এবং locked_until worker-দের জব claim করার সুযোগ দেয় যাতে তারা একে অপরের ওপর পা না রাখে।
- last_error সংক্ষিপ্ত এবং মানুষের জন্য পাঠ যোগ্য রাখুন। স্ট্যাক ট্রেস দরকার হলে সেটা আলাদা রাখুন।
প্রয়োজনীয় ইনডেক্সগুলো
ইনডেক্স না থাকলে worker-রা বেশি সার্চ করে ফেলবে। শুরুতে রাখুন:
- দ্রুত due কাজ খুঁজতে ইনডেক্স:
(status, run_at) - মেয়াদোত্তীর্ণ লক শনাক্ত করতে সহায়াকারী ইনডেক্স:
(locked_until) - ঐচ্ছিক: active work-এ partial index (উদাহরণ: status
queuedএবংfailed)
এগুলো “পরবর্তী runnable job খুঁজুন” কুয়েরিটিকে দ্রুত রাখে এমনকি টেবিল বড় হলেও।
নিরাপদভাবে লকিং এবং জব দাবি করা
লক্ষ্য সহজ: অনেক worker চলতে পারে, কিন্তু একটি নির্দিষ্ট জব কেবল একজনই গ্রহণ করবে। যদি দুটি worker একই row প্রসেস করে, আপনি ডাবল ইমেইল, ডাবল চার্জ বা বিশৃঙ্খল ডেটা পাবেন।
নিরাপদ উপায় একটি জব ক্লেইমকে একটি “লিজ” হিসেবে বিবেচনা করা। Worker জবটিকে একটি সংক্ষিপ্ত সময়ের জন্য লক করে। Worker ক্র্যাশ করলে লিজ মেয়াদ শেষ হয়ে অন্য worker এটি নিতে পারে। এ কারণেই locked_until আছে।
ক্র্যাশ কাজগুলো চিরস্থায়ী ব্লক না করার জন্য লিজ ব্যবহার করুন
লিজ না থাকলে, একটি worker জব লক করে এবং কখনো আনলক না করেও যেতে পারে (প্রক্রিয়া নিহত, সার্ভার রিবুট, ভুল ডিপ্লয়)। locked_until থাকলে সময় পেরিয়ে গেলে জবটি আবার পাওয়া যায়।
সাধারণ নিয়ম: একটি জব তখনই claim করা যাবে যখন locked_until NULL বা locked_until <= now()।
এক এটমিক আপডেটে জব claim করুন
কী ডিটেইল হল: জবকে একটি সিঙ্গেল স্টেটমেন্ট (বা এক ট্রানজ্যাকশনে) এ দাবি করা। আপনি চান ডাটাবেস রেফারি হিসেবে কাজ করুক।
নিচে একটি সাধারণ PostgreSQL প্যাটার্ন: একটি due জব বেছে নিন, সেটি লক করুন, এবং worker-কে রিটার্ন করুন। (এই উদাহরণটি একটি সিঙ্গেল jobs টেবিল ব্যবহার করে; একই ধারণা job_runs থেকেই প্রয়োগ করা যাবে.)
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'queued'
AND run_at <= now()
AND (locked_until IS NULL OR locked_until <= now())
ORDER BY run_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs j
SET status = 'running',
locked_until = now() + interval '2 minutes',
locked_by = $1,
attempts = attempts + 1,
updated_at = now()
FROM next_job
WHERE j.id = next_job.id
RETURNING j.*;
কেন এটি কাজ করে:
FOR UPDATE SKIP LOCKEDএকাধিক worker-কে প্রতিদ্বন্দ্বিতা করার সুযোগ দেয় ব্লক না করে।- লিজ দাবি করার সময় সেট করা হয়, তাই অন্য worker গুলো একে মেয়াদ শেষ না হওয়া পর্যন্ত উপেক্ষা করে।
RETURNINGরেস জয় করা worker-কে row হস্তান্তর করে।
লিজ কতক্ষণ হওয়া উচিত, এবং কীভাবে এটি নবায়ন করবেন?
লিজটি সাধারণ রান থেকে লম্বা হতে হবে, কিন্তু যথেষ্ট সংক্ষিপ্ত যাতে ক্র্যাশ দ্রুত পুনরুদ্ধার হয়। যদি বেশিরভাগ জব 10 সেকেন্ডে শেষ হয়, 2 মিনিট লিজ যথেষ্ট।
দীর্ঘ টাস্কের জন্য, কাজ করার সময়ে লিজ নবায়ন করুন (একটি হার্টবিট)। সহজ পদ্ধতি: প্রতি 30 সেকেন্ডে যদি আপনি এখনও জবের মালিক হন, locked_until বাড়িয়ে দিন।
- লিজ দৈর্ঘ্য: আপনার সাধারণ কাজ সময়ের 5x থেকে 20x
- হার্টবিট ইন্টারভাল: লিজের 1/4 থেকে 1/2
- নবায়ন আপডেটে থাকা উচিত
WHERE id = $job_id AND locked_by = $worker_id
এই শেষ শর্তটি গুরুত্বপূর্ণ। এটি প্রতিরোধ করে যে একটি worker সে আর মালিক নয় এমন জবের লিজ বাড়িয়ে দেয়।
রিট্রাই এবং ব্যাকঅফ যা পূর্বানুমানযোগ্যভাবে আচরণ করে
রিট্রাই-এই প্যাটার্নটি শান্ত মনে হবে নাকি বিশৃঙ্খল হবে সেটাই নির্ধারণ করে। লক্ষ্য সহজ: যখন একটি জব ব্যর্থ হয়, পরবর্তীতে পুনরায় চেষ্টা করুন এমনভাবে যা ব্যাখ্যা করা যায়, মাপা যায়, এবং বন্ধ করা যায়।
প্রথমে জব স্টেট স্পষ্ট এবং সসীম রাখুন: queued, running, succeeded, failed, dead। বাস্তবে, বেশিরভাগ টীম failed-কে “ব্যর্থ কিন্তু আবার রিট্রাই করা হবে” এবং dead-কে “ব্যর্থ এবং আমরা ছাড় দিয়েছি” হিসেবে ব্যবহার করে। এই পার্থক্যটি ইনফিনিট লুপ প্রতিরোধ করে।
চেষ্টা গণনা দ্বিতীয় গার্ডরেইল। attempts (কতবার চেষ্টা করা হয়েছে) এবং max_attempts (কতবার অনুমোদিত) রাখুন। যখন worker একটি ত্রুটি ধরে, এটি উচিত:
attemptsবাড়ানো- যদি
attempts < max_attemptsতাহলে স্টেটfailedসেট করা, অন্যথায়dead - পরবর্তী ট্রাই-এর জন্য
run_atগণনা করা (শুধুfailed-এর ক্ষেত্রে)
ব্যাকঅফ হল কেবল নিয়ম যা পরবর্তী run_at নির্ধারণ করে। একটি পদ্ধতি বেছে নিন, ডকুমেন্ট করুন, এবং ধারাবাহিক রাখুন:
- স্থির বিলম্ব: সর্বদা 1 মিনিট অপেক্ষা করুন
- এক্সপোনেনশিয়াল: 1m, 2m, 4m, 8m
- সীমাবদ্ধ এক্সপোনেনশিয়াল: এক্সপোনেনশিয়াল কিন্তু সর্বোচ্চ, ধরুন 30m পর্যন্ত নয়
- জিটার যোগ করুন: একটু এলোমেলো করুন যাতে সব জব একই সেকেন্ডে রিট্রাই না করে
জিটার তখনই গুরুত্বপূর্ণ যখন একটি নির্ভরতা ডাউন হয় এবং ফিরে আসে। জিটারের ছাড়া, শত শত জব একসঙ্গে রিট্রাই করে আবার ব্যর্থ হয়ে যায়।
ব্যর্থতা দৃশ্যমান ও ডিবাগ যোগ্য করে রাখার জন্য পর্যাপ্ত ত্রুটি বিবরণ স্টোর করুন। পুরো লগিং সিস্টেমের প্রয়োজন নেই, কিন্তু মৌলিক জিনিসগুলো দরকার:
last_error(সংক্ষিপ্ত বার্তা, admin স্ক্রিনে দেখানোর মতো)error_codeবাerror_type(গ্রুপিং-এ সাহায্য করে)failed_atএবংnext_run_at- ঐচ্ছিক
last_stack(সাইজ আপনি নিয়ন্ত্রণ করলে)
একটি বাস্তব নিয়ম যেটি ভাল কাজ করে: 10 চেষ্টা পর dead চিহ্নিত করুন, এবং জিটারের সাথে এক্সপোনেনশিয়াল ব্যাকঅফ ব্যবহার করুন। এতে ট্রানজিয়েন্ট ব্যর্থতা পুনরায় চেষ্টা হয়, কিন্তু ভাঙা জবগুলো CPU নিরবলে খেয়ে ফেলে না।
আইডেম্পোটেন্সি: রিট্রাই-এও ডুপ্লিকেট প্রতিরোধ করা
আইডেম্পোটেন্সি মানে আপনার জব দুবার চালালে শেষ ফল একই থাকা। এই প্যাটার্নে এটি গুরুত্বপূর্ণ কারণ একই row ক্র্যাশ, টাইমআউট, বা রিট্রাই নিয়ে আবার নেওয়া হতে পারে। যদি আপনার কাজ হয় “ইনভয়েস ইমেইল পাঠানো”, তা দুবার চালানো নিরপত্তে নয়।
প্রাযুক্তিকভাবে ভাবুন: প্রতিটি জবকে (1) কাজ করা এবং (2) প্রভাব প্রয়োগ করা—এই দুই ভাগে ভাগ করুন। আপনি চান যে প্রভাব একবারই ঘটুক, যদিও কাজটি একাধিকবার চেষ্টা করা হতে পারে।
ব্যবসায়িক ইভেন্ট-ভিত্তিক একটি idempotency কী ব্যবহার করুন
একটি idempotency কী হওয়া উচিত জব যা প্রতিনিধিত্ব করে তার সঙ্গে সম্পর্কিত, worker অ্যাটেম্পট থেকে নয়। ভালো কী গুলো স্থির এবং সহজে বর্ণনীয়, যেমন invoice_id, user_id + day, বা report_name + report_date। যদি দুইটি জব অ্যাটেম্পট একই বাস্তব-বিশ্বের ইভেন্টকে বোঝায়, তাদের একই কী থাকা উচিত।
উদাহরণ: “2026-01-14-র জন্য দৈনিক সেলস রিপোর্ট জেনারেট” এর কী হতে পারে sales_report:2026-01-14। “Invoice 812 চার্জ করা” হতে পারে invoice_charge:812।
ডেটাবেস কনস্ট্রেইন্ট দিয়ে “কেবল একবার” প্রয়োগ করুন
সবচেয়ে সহজ গার্ডরেইল হল PostgreSQL-কে ডুপ্লিকেট নাকচ করতে দেয়া। idempotency কী কোথাও স্টোর করুন যা ইনডেক্স করা যায়, তারপর একটি unique constraint যোগ করুন।
-- Example: ensure one logical job/effect per business key
ALTER TABLE jobs
ADD COLUMN idempotency_key text;
CREATE UNIQUE INDEX jobs_idempotency_key_uniq
ON jobs (idempotency_key)
WHERE idempotency_key IS NOT NULL;
এটি একই কী সহ দুটি row একই সময়ে থাকা প্রতিরোধ করে। যদি আপনার ডিজাইন ইতিহাস বজায় রাখে (একাধিক row অনুমোদন করে), তাহলে uniqueness আরেকটায় রাখুন—যেমন sent_emails(idempotency_key) বা payments(idempotency_key)।
সাধারণ সাইড-ইফেক্ট যা রক্ষা করা উচিত:
- ইমেইল:
sent_emailsটেবিলে একটি ইউনিক কী দিয়ে রেকর্ড তৈরি করুন আগে পাঠানোর, অথবা পাঠানোর পরে provider message id রেকর্ড করুন। - ওয়েবহুক:
delivered_webhooks(event_id)স্টোর করুন এবং যদি সেটা থাকে তাহলে স্কিপ করুন। - পেমেন্ট: সর্বদা payment provider-এর idempotency ফিচার ব্যবহার করুন সাথে নিজের ডাটাবেস ইউনিক কী।
- ফাইল লিখা: টেম্প নাম দিয়ে লিখুন, তারপর রিনেম করুন, বা
(type, date)দিয়ে কীবদ্ধ করাfile_generatedরেকর্ড স্টোর করুন।
আপনি যদি Postgres-backed স্ট্যাকে তৈরি করছেন (উদাহরণ, Go + PostgreSQL ব্যাকএন্ড), এই ইউনিকনেস চেকগুলো দ্রুত এবং ডেটার কাছে রাখা সহজ। মূল ধারণা সহজ: রিট্রাই স্বাভাবিক, ডুপ্লিকেট অপশনাল।
ধাপে ধাপে: একটি মিনিমাল worker এবং scheduler বানানো
একটি বিরক্তিকর runtime বাছুন এবং সেটাতে থাকুন। cron + database প্যাটার্নের লক্ষ্য কম moving part, তাই একটি ছোট Go, Node, বা Python প্রসেস যা PostgreSQL-এ কথা বলে সাধারণত যথেষ্ট।
পাঁচটি ছোট ধাপে বানান
-
টেবিল ও ইনডেক্স তৈরি করুন। একটি
jobsটেবিল (ওপরের মতো) যোগ করুন, তারপরrun_atইনডেক্স করুন, এবং একটি ইনডেক্স যোগ করুন যা আপনার worker-কে দ্রুত উপলব্ধ জব খুঁজতে সাহায্য করবে (উদাহরণ:(status, run_at))। -
একটি ছোট enqueue ফাংশন লিখুন। আপনার অ্যাপ একটি রো ইনসার্ট করবে
run_atএখন বা ভবিষ্যত সময় দিয়ে। payload ছোট ও পূর্বানুমানযোগ্য রাখুন (IDs এবং job type, বড় ব্লব নয়)।
INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
- claim লুপ ইমপ্লিমেন্ট করুন। এটি একটি ট্রানজ্যাকশনে চালান। কয়েকটি due জব সিলেক্ট করুন, সেগুলো লক করুন যাতে অন্য worker ছাড়ে, এবং একই ট্রানজ্যাকশনে সেগুলোকে
runningহিসেবে চিহ্নিত করুন।
WITH picked AS (
SELECT id
FROM jobs
WHERE status = 'queued' AND run_at <= now()
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT 10
)
UPDATE jobs
SET status = 'running', started_at = now()
WHERE id IN (SELECT id FROM picked)
RETURNING *;
-
প্রক্রিয়া এবং ফাইনালাইজ করুন। প্রত্যেক claimed job-এর জন্য কাজটি করুন, তারপর
done-এfinished_at-সহ আপডেট করুন। যদি ব্যর্থ হয়, একটি ত্রুটি বার্তা রেকর্ড করে সেটিকে আবারqueued-এ নিয়ে যান নতুনrun_at(backoff) সহ। ফাইনালাইজেশন আপডেটগুলো ছোট রাখুন এবং সবসময় চালান, এমনকি আপনার প্রসেস বন্ধ হলে ও। -
ব্যাখ্যাযোগ্য retry নিয়ম যোগ করুন। একটি সহজ সূত্র ব্যবহার করুন যেমন
run_at = now() + (attempts^2) * interval '10 seconds', এবংmax_attemptsপেরলেstatus = 'dead'সেট করুন।
মৌলিক ভিজিবিলিটি যোগ করুন
প্রথম দিনেই পূর্ণ ড্যাশবোর্ড দরকার নেই, কিন্তু সমস্যা লক্ষ্য করার মতো কিছু দরকার।
- প্রতিটি জবের জন্য একটি লাইন লগ করুন: claimed, succeeded, failed, retried, dead।
- “dead jobs” এবং “old running jobs” এর জন্য একটি সহজ admin কুয়েরি বা ভিউ তৈরি করুন।
- বাড়তে থাকা ফেইলিউর কাউন্টে অ্যালার্ট দিন (উদাহরণ: গত একঘন্টায় N-র বেশি dead jobs)।
আপনি যদি আগে থেকেই Go + PostgreSQL স্ট্যাকে থাকেন, এটি একটি single worker binary এবং cron-এ ভালো মানায়।
এমন একটি বাস্তব উদাহরণ যা আপনি কপি করতে পারেন
ধরা যাক একটি ছোট SaaS অ্যাপের দুটি নির্ধারিত কাজ:
- রাতের ক্লিনআপ যা মেয়াদোত্তীর্ণ সেশন এবং পুরনো টেম্পরারি ফাইল মুছে দেয়।
- সাপ্তাহিক “আপনার কার্যকলাপ রিপোর্ট” ইমেইল যা প্রতি সোমবার প্রত্যেক ব্যবহারকারীকে পাঠানো হয়।
সরল রাখুন: একটি PostgreSQL টেবিল জবগুলো ধরে রাখে, এবং একটি worker প্রতি মিনিটে চলে (cron দ্বারা ট্রিগার)। Worker due জব claim করে, চালায়, এবং সাফল্য বা ব্যর্থতা রেকর্ড করে।
কি_enqueue হয়, এবং কখন
আপনি বিভিন্ন জায়গা থেকে জব enqueue করতে পারেন:
- প্রতিদিন 02:00: একটি
cleanup_nightlyজব enqueue করুন “আজকের” জন্য। - সাইনআপে: ব্যবহারকারীর পরবর্তী সোমবারের জন্য
send_weekly_reportজব enqueue করুন। - একটি ইভেন্টের পরে (যেমন “ব্যবহারকারী Export report ক্লিক করেছে”): নির্দিষ্ট তারিখ-রেঞ্জের জন্য তাৎক্ষণিকভাবে
send_weekly_reportenqueue করুন।
পে-লোড হল কেবল worker-কে যা লাগে তা—ছোট রাখুন যাতে রিট্রাই সহজ হয়।
{
"type": "send_weekly_report",
"payload": {
"user_id": 12345,
"date_range": {
"from": "2026-01-01",
"to": "2026-01-07"
}
}
}
কিভাবে idempotency ডাবল-সেন্ড রোধ করে
একটি worker সবচেয়ে খারাপ মুহূর্তে ক্র্যাশ করতে পারে: ইমেইল পাঠানোর ঠিক পরে, কিন্তু জবটিকে “done” চিহ্নিত করার আগে। পুনরায় শুরু হলে এটি একই জব আবার নিতে পারে।
ডাবল-সেন্ড বন্ধ করতে, কাজটিকে একটি প্রাকৃতিক dedupe কী দিন এবং ডেটাবেস যেখানে এটি প্রয়োগ করতে পারে সেখানে এটি সংরক্ষণ করুন। সাপ্তাহিক রিপোর্টের জন্য ভাল কী হতে পারে (user_id, week_start_date)। পাঠানোর আগে worker রেকর্ড করে “আমি রিপোর্ট X পাঠাতে যাচ্ছি”। যদি সেই রেকর্ড ইতিমধ্যেই থাকে, worker স্কিপ করে।
এটি একটি sent_reports টেবিল যেটির (user_id, week_start_date)-এ ইউনিক কনস্ট্রেইন্ট থাকতে পারে, অথবা জবটিকেই একটি ইউনিক idempotency_key দেওয়া হতে পারে।
একটি ব্যর্থতা কেমন দেখায় (এবং কিভাবে পুনরুদ্ধার করে)
ধরা যাক আপনার ইমেইল প্রোভাইডার টাইমআউট দেয়। জব ব্যর্থ হয়, তখন worker:
attemptsবাড়ায়- ডিবাগের জন্য ত্রুটি বার্তা সংরক্ষণ করে
- ব্যাকঅফ সহ পরবর্তী ট্রাই শিডিউল করে (উদাহরণ: +1 মিনিট, +5 মিনিট, +30 মিনিট, +2 ঘন্টা)
যদি এটি আপনার সীমা (উদাহরণ 10 অ্যাটেম্পট) পেরিয়ে যায়, সেটি dead হিসেবে চিহ্নিত করুন এবং পুনরায় চেষ্টা বন্ধ করুন। জব বা তো সাফল্য একবার পাবে, বা এটি স্পষ্ট পরিকল্পনায় পুনরায় চেষ্টা করবে, এবং idempotency রিট্রাইকে নিরাপদ করে।
সাধারণ ভুল ও ফাঁদগুলি
cron + database প্যাটার্নটি সরল, কিন্তু ছোট ভুলগুলো এটিকে ডুপ্লিকেট, আটকে থাকা কাজ, বা হঠাৎ লোডে পরিণত করতে পারে। বেশিরভাগ সমস্যা প্রথম ক্র্যাশ, ডিপ্লয়, বা ট্রাফিক স্পাইকে দেখা যায়।
ডাবল বা আটকে থাকা কাজ ঘটানোর ভুলগুলো
বাস্তব-জীবনের বেশিরভাগ ইনসিডেন্ট কয়েকটি ফাঁদ থেকে আসে:
- একাধিক cron এন্ট্রি থেকে একই জব চালানো লিজ না থাকলে। যদি দুই সার্ভার একই মিনিটে টিক করে, তারা একই কাজ claim করতে পারে যদি claim step এটমিক না হয় এবং একই ট্রানজ্যাকশনে লক/লিজ সেট না করে।
locked_untilবাদ দেয়া। যদি একটি worker claim করার পরে ক্র্যাশ করে, rowটি “in progress” হতেই থাকতে পারে। একটি লিজ টাইমস্ট্যাম্প অন্য worker-কে পরে নিতে দেয়।- ব্যর্থতার উপর তাত্ক্ষণিক রিট্রাই করা। যখন কোনো API ডাউন, ইনস্ট্যান্ট রিট্রাই স্পাইক তৈরি করে, রেট লিমিট বার্ন করে, এবং সংকীর্ণ লুপে ব্যর্থতা চালিয়ে যায়। সর্বদা পরবর্তী চেষ্টা ভবিষ্যতে শিডিউল করুন।
- “at least once” কে “exactly once” মনে করা। একটি জব দুবার চলতে পারে (টাইমআউট, worker রিস্টার্ট, নেটওয়ার্ক সমস্যা)। যদি দুইবার চালালে ক্ষতি হয়, side effects-কে পুনরাবৃত্তি-নিরাপদ বানান।
- জব রো-তে বিশাল পে-লোড রাখা। বড় JSON ব্লব টেবিলকে ফোলা করে, ইনডেক্স ধীর করে, এবং লকিংকে ভারী করে। একটি রেফারেন্স (যেমন
user_id,invoice_id, বা ফাইল কী) রাখুন এবং রান করার সময় বাকি ডেটা ফেচ করুন।
উদাহরণ: আপনি সাপ্তাহিক ইনভয়েস ইমেইল পাঠান। যদি worker পাঠানোর পরে কিন্তু জব ডান না করা পর্যন্ত টাইমআউট করে, একই জব পুনরায় চালানো হতে পারে এবং ডুপ্লিকেট ইমেইল পাঠাতে পারে। এই প্যাটার্নের জন্য এটি স্বাভাবিক, যদি না আপনি একটি গার্ডরেইল যোগ করেন (উদাহরণ: invoice id দিয়ে ইউনিক "email sent" ইভেন্ট রেকর্ড)।
কম স্পষ্ট গটচাস
একই দীর্ঘ ট্রানজ্যাকশনে scheduling এবং execution মিশিয়ে দেবেন না। যদি আপনি নেটওয়ার্ক কল করার সময় একটি ট্রানজ্যাকশন খোলা রাখেন, আপনি লকগুলো অপ্রয়োজনীয়ভাবে দীর্ঘ সময় ধরে রাখেন এবং অন্য worker-দের ব্লক করেন।
মেশিনগুলোর মধ্যে ঘড়ির পার্থক্য নজর রাখুন। run_at এবং locked_until এর জন্য্ ডাটাবেস টাইম (NOW() in PostgreSQL) ব্যবহার করুন, অ্যাপ সার্ভারের ঘড়ি নয়।
একটি স্পষ্ট সর্বোচ্চ runtime সেট করুন। যদি একটি জব 30 মিনিট নিতে পারে, লিজটি তার চেয়েও বড় করুন, এবং প্রয়োজন হলে নবায়ন করুন। অন্যথায় আরেকটি worker মাঝখানে নিয়ে নিতে পারে।
জব টেবিলটি সুস্থ রাখুন। যদি সম্পন্ন জব চিরতরে জমে থাকে, কুয়েরি ধীর হয় এবং লক contention বাড়ে। টেবিল খুব বড় হওয়ার আগে একটি retention নিয়ম নির্ধারণ করুন (archive বা delete)।
দ্রুত চেকলিস্ট এবং পরবর্তী ধাপ
দ্রুত চেকলিস্ট
এই প্যাটার্ন শিপ করার আগে মৌল বিষয়গুলো চেক করুন। এখানে ছোট একটি ভুল সাধারণত আটকে থাকা জব, আশ্চর্যের ডুপ্লিকেট, বা ডেটাবেসে hammer করা হয়ে ওঠে।
- আপনার jobs টেবিলে মৌলিক কলাম আছে:
run_at,status,attempts,locked_until, এবংmax_attempts(সাথেlast_errorবা অনুরূপ যেন আপনি কী হয়েছে দেখেন)। - প্রতিটি জব নিরাপদে দুইবার চালানো যাবে। যদি নিশ্চিত না হন, একটি idempotency কী বা সাইড-ইফেক্টের চারপাশে একটি uniqueness নিয়ম যোগ করুন (যেমন, প্রতি
invoice_id-এর জন্য একমাত্র invoice)। - ব্যর্থতা পর্যবেক্ষণ এবং সিদ্ধান্ত নেওয়ার স্থান আছে: failed jobs দেখুন, একটি জব পুনরায় চালান, বা যখন আর চালানো উচিত নয় তখন সেটিকে dead চিহ্নিত করুন।
- আপনার লিজ (লক) টাইমআউট কাজের জন্য যুক্তিযুক্ত। এটি সাধারণ রান থেকে লম্বা, কিন্তু এতটাই সংক্ষিপ্ত যে ক্র্যাশ করা worker ঘন্টার পর ঘন্টা ব্লক না করে।
- রিট্রাই ব্যাকঅফ পূর্বানুমানযোগ্য। এটি পুনরাবৃত্ত ব্যর্থতাকে ধীর করে, এবং
max_attemptsপেরলে থামে।
এগুলো সত্য হলে, cron + database প্যাটার্ন সাধারণত বাস্তব ওয়ার্কলোডের জন্য পর্যাপ্ত স্থিতিশীল।
পরবর্তী ধাপ
চেকলিস্ট ঠিকঠাক হলে, প্রতিদিনের অপারেশনের দিকে মনোযোগ দিন।
- দুটি ছোট admin অ্যাকশন যোগ করুন: “now retry” (
run_at = now()এবং লক ক্লিয়ার) এবং “cancel” (ট্রানজিশন টার্মিনাল স্থিতিতে)। এগুলো ইনসিডেন্ট সময়ে সময় বাঁচায়। - worker প্রত্যেক জবের জন্য একটি লাইন লগ করা উচিত: job type, job id, attempt সংখ্যা, এবং ফল। বাড়তে থাকা ব্যর্থতা কনের ওপর অ্যালার্ট দিন।
- বাস্তবসম্মত স্পাইকের সঙ্গে লোড টেস্ট করুন: একই মিনিটে অনেক জব শিডিউল করা। যদি জব claim করা ধীর হয়, সঠিক ইনডেক্স দিন (সাধারণত
status, run_at)।
দ্রুত এমন একটি সেটআপ বানাতে, Koder.ai (koder.ai) আপনাকে schema থেকে একটি ডিপ্লয়ড Go + PostgreSQL অ্যাপে দ্রুত নিয়ে যেতে সাহায্য করতে পারে, যাতে আপনি লকিং, রিট্রাই, এবং idempotency নিয়মগুলোর উপর মনোযোগ দিতে পারেন।
পরবর্তীতে যদি আপনি এই সেটআপ ছেড়ে আরও বড় সিস্টেমে যান, আপনি তখনও জব lifecycle সম্পর্কে স্পষ্ট ধারণা পাবেন, এবং একই ধারণা একটি পূর্ণ queue সিস্টেমে ভালোভাবে মানায়।
সাধারণ প্রশ্ন
ব্যাকগ্রাউন্ড জবের জন্য কখন ডেটাবেসের সঙ্গে cron ব্যবহার করা উচিত?
আপনার কাছে আগে থেকেই একটি ডেটাবেস থাকলে, জবের ধরন সীমিত হলে এবং আলাদা কিউ চালু না করেই নির্ধারিত কাজ চালাতে চাইলে এই প্যাটার্ন ব্যবহার করুন। ইমেল রিমাইন্ডার, পরিষ্কার-পরিচ্ছন্নতা, রিপোর্ট এবং ক্যাশ রিফ্রেশের মতো কাজের জন্য এটি উপযোগী।
এই সেটআপে cron কী করে?
এই সেটআপে cron-এর কাজ শুধু নিয়মিত বিরতিতে ওয়ার্কার চালু করা, সাধারণত প্রতি মিনিটে একবার। কোন জব কখন চলবে তা ডেটাবেস ঠিক করে, জবের অবস্থা সংরক্ষণ করে এবং একটি ওয়ার্কারকে প্রতিটি জব নেওয়ার সুযোগ দেয়।
একটি jobs টেবিলে কোন ফিল্ডগুলো রাখা উচিত?
জবের ধরন, একটি ছোট JSON পে-লোড, নির্ধারিত সময়, স্ট্যাটাস, চেষ্টার সংখ্যা, পুনঃচেষ্টার সীমা, লক মালিক, লকের মেয়াদ শেষের সময় এবং সংক্ষিপ্ত ত্রুটি বার্তা সংরক্ষণ করুন। এই ফিল্ডগুলো ওয়ার্কারকে নিরাপদে জব চালানো ও পুনরুদ্ধারের জন্য যথেষ্ট তথ্য দেয়।
একাধিক ওয়ার্কার কীভাবে একই জব চালানো এড়ায়?
PostgreSQL-এর FOR UPDATE SKIP LOCKED-এর মতো রো লকিং ব্যবহার করে একটি ডেটাবেস ট্রানজ্যাকশনে জব দাবি করুন। ওয়ার্কার আসল কাজ শুরু করার আগে রোটি running অবস্থায় আপডেট করুন এবং তার লিজ সেট করুন।
ব্যাকগ্রাউন্ড জবের জন্য লিজ টাইমআউট কেন দরকার?
লিজ হলো locked_until-সহ সংরক্ষিত একটি অস্থায়ী লক। কোনো ওয়ার্কার ক্র্যাশ করলে লিজের মেয়াদ শেষ হয় এবং জবটি চিরতরে আটকে না থেকে অন্য ওয়ার্কার সেটি নিতে পারে।
ব্যর্থ জবের পুনঃচেষ্টা কীভাবে হওয়া উচিত?
পরিষ্কার ব্যাকঅফ নিয়ম মেনে পরে আবার চেষ্টা করুন, যেমন 1 মিনিট, 2 মিনিট, 4 মিনিট, তারপর সামান্য র্যান্ডমনেসসহ একটি সীমাবদ্ধ বিলম্ব। অনুমোদিত সংখ্যক চেষ্টার পর জবটিকে dead হিসেবে চিহ্নিত করুন, যাতে এটি আর ওয়ার্কারের সময় খরচ না করে।
idempotency কী এবং এটি কেন গুরুত্বপূর্ণ?
পার্শ্বপ্রতিক্রিয়াটি যেন পুনরাবৃত্তি করলেও নিরাপদ থাকে, তা নিশ্চিত করুন। উদাহরণ হিসেবে, ইনভয়েস ID বা রিপোর্টের তারিখসহ ব্যবহারকারী ID-এর মতো একটি স্থিতিশীল idempotency key ব্যবহার করুন, তারপর PostgreSQL-এ বা বাইরের প্রোভাইডারের কাছে স্বতন্ত্রতা নিশ্চিত করুন।
এই প্যাটার্ন কি exactly-once প্রক্রিয়াকরণের নিশ্চয়তা দেয়?
না। কোনো ওয়ার্কার ইমেল পাঠানো বা পেমেন্ট প্রোভাইডারকে কল করার পর, কিন্তু সফলতা সংরক্ষণের আগে ক্র্যাশ করতে পারে। at-least-once প্রক্রিয়াকরণের জন্য নকশা করুন, তারপর ডুপ্লিকেট প্রভাব ঠেকাতে idempotency ব্যবহার করুন।
ওয়ার্কার কি জব চালানোর সময় ডেটাবেস ট্রানজ্যাকশন ধরে রাখবে?
ডেটাবেস ট্রানজ্যাকশন ছোট রাখুন। জব দাবি করুন, কমিট করুন, ট্রানজ্যাকশনের বাইরে নেটওয়ার্ক কল করুন, তারপর আলাদা আপডেটে সফলতা বা ব্যর্থতা সংরক্ষণ করুন। দীর্ঘ ট্রানজ্যাকশন লক ধরে রাখে এবং অন্য ওয়ার্কারকে ধীর করে।
cron এবং ডেটাবেস জব কীভাবে মনিটর করব?
dead জব এবং দীর্ঘ সময় ধরে চলা পুরোনো জব ট্র্যাক করুন, প্রতিটি দাবি ও ফলাফল লগ করুন এবং ব্যর্থতা বাড়লে সতর্কতা দিন। এখনই জব পুনরায় চালানো, বাতিল করা বা সর্বশেষ ত্রুটি দেখার জন্য সহজ অ্যাডমিন অ্যাকশন যোগ করুন।