7 মিনিট

ব্যাকগ্রাউন্ড জবের জন্য Go ওয়ার্কার পুল: রিট্রাই, বাতিল এবং শাটডাউন

Go ওয়ার্কার পুল ছোট টিমকে ব্যাকগ্রাউন্ড জবগুলো সহজভাবে চালাতে সাহায্য করে: রিট্রাই, ক্যানসেলেশন এবং পরিষ্কার শাটডাউন সরল প্যাটার্ন ব্যবহার করে—ভরসা যোগ করার আগে ভারী ইনফ্রাস্ট্রাকচার যোগ করার বদলে।

ব্যাকগ্রাউন্ড জবের জন্য Go ওয়ার্কার পুল: রিট্রাই, বাতিল এবং শাটডাউন

কেন ব্যাকগ্রাউন্ড জব দ্রুত জটিল হয়ে যায়

একটি ছোট Go সার্ভিসে, ব্যাকগ্রাউন্ড কাজ সাধারণত একটি সরল লক্ষ্য দিয়ে শুরু হয়: HTTP রেসপন্স দ্রুত ফেরত দিন, তারপর ধীর কাজগুলো পরে করুন। সেটা হতে পারে ইমেল পাঠানো, ইমেজ রিসাইজ করা, অন্য API-তে সিঙ্ক করা, সার্চ ইনডেক্স পুনর্নির্মাণ করা, বা রাত্রিবেলায় রিপোর্ট চালানো।

সমস্যা হল এগুলো বাস্তব প্রোডাকশন কাজ — শুধু যে রিকোয়েস্ট হ্যান্ডলিং-এ যে ধরণের নিরাপত্তা থাকে তা নেই। HTTP হ্যান্ডলার থেকে স্টার্ট করা একটি goroutine প্রথমে ঠিকঠাক লাগতে পারে, যতক্ষণ না কোনো ডিপ্লয় মাঝপথে ঘটে, কোনো আপস্ট্রিম API ধীর হয়ে পড়ে, বা একই রিকোয়েস্ট রি-ট্রাই হয়ে একই কাজ দুবার ট্রিগার করে।

প্রাথমিক যন্ত্রণা সম্মুখীন বিষয়গুলো ভবিষ্যদ্বাণীমূলক:

  • থেকে যাওয়া জব: কোনো কল আটকে গেলে, ওয়ার্কাররা অগ্রগতি থামায়।
  • ডুপ্লিকেট কাজ: HTTP স্তরে রিট্রাই একই জবকে পুনরায় চালায়।
  • শাটডাউন পরিকল্পনার অভাব: প্রসেসটি বন্ধ হলে কাজ হারানো বা অর্ধেক করা থাকে।
  • নীরব ব্যর্থতা: এররগুলো একবার লগ হয় (অথবা вовсе হয় না) এবং অদৃশ্য হয়ে যায়।
  • রিট্রাই ঝড়: ব্যর্থ জবগুলো অবিলম্বে রিট্রাই করে ডিপেন্ডেন্সিগুলোকে ওভারলোড করে।

এখানেই একটি ছোট, স্পষ্ট প্যাটার্ন হিসেবে Go ওয়ার্কার পুল সাহায্য করে। এটা কনকারেন্সিকে একটি পছন্দ করে তোলে (N ওয়ার্কার), “পরে কর” কে স্পষ্ট জব টাইপ হিসেবে রূপ দেয়, এবং আপনাকে রিট্রাই, টাইমআউট এবং বাতিলকরণ হ্যান্ডল করার একটি কেন্দ্রীয় জায়গা দেয়।

উদাহরণ: একটি SaaS অ্যাপে ইনভয়েস পাঠাতে হবে। ব্যাচ ইম্পোর্টের পরে ৫০০টি সমান্তরাল পাঠা শুরু করতে চান না, এবং একই ইনভয়েস রি-সেন্ডও চান না কারণ কোনো রিকোয়েস্ট রি-ট্রাই হয়েছে। ওয়ার্কার পুল আপনাকে থ্রুপুট ক্যাপ করতে দেয় এবং “ইনভয়েস #123 পাঠান” কে ট্র্যাক করা কাজ হিসেবে বিবেচনা করতে দেয়।

একটি ওয়ার্কার পুল উপযুক্ত নয় যদি আপনাকে ডিউরেবল, ক্রস-প্রসেস গ্যারান্টি দরকার হয়। যদি কাজগুলি ক্র্যাশ থেকে বেঁচে থাকতে হয়, ভবিষ্যতের জন্য শিডিউল করা লাগবে, বা একাধিক সার্ভিসে প্রসেস করতে হবে, তাহলে সাধারণত আপনাকে একটি প্রকৃত কিউ ও পার্সিস্টেন্ট স্টোরেজ দরকার হবে।

সাধারণ ভাষায় ওয়ার্কার পুল মডেল

একটি Go ওয়ার্কার পুল ইচ্ছাকৃতভাবে সরল: কাজকে একটি কিউতে রাখুন, একটি নির্দিষ্ট সংখ্যক ওয়ার্কার তা থেকে তুলি, এবং নিশ্চিত করুন পুরো সিস্টেমটি পরিষ্কারভাবে বন্ধ হতে পারে।

বেসিক টার্মগুলো:

  • Job: একটি কাজের ইউনিট, যেমন “এই ইমেজ রিসাইজ করুন” বা “এই ইনভয়েস ইমেল পাঠান”।
  • Queue: যেখানে জবগুলো অপেক্ষা করে।
  • Worker: একটি goroutine যা বারবার একটি জব নে এবং সেটি সম্পাদন করে।
  • Dispatcher: যে অংশটি জব গ্রহণ করে এবং কিউতে পাঠায়।

অনেক ইন-প্রসেস ডিজাইনে, একটি Go channel কিউ হিসেবে ব্যবহৃত হয়। একটি বাফার্ড চ্যানেল নির্দিষ্ট সংখ্যক জব ধরে রাখতে পারে তা না হলে প্রযোজকরা ব্লক করে। সেই ব্লকিংই ব্যাকপ্রেশার, এবং এটা প্রায়ই আপনার সার্ভিসকে সীমাহীন কাজ গ্রহণ করে মেমরি শেষ হয়ে যাওয়া থেকে রক্ষা করে যখন ট্রাফিক স্পাইক হয়।

বাফার সাইজ সিস্টেমের অনুভূতি বদলে দেয়। ছোট বাফার চাপ দ্রুত দৃশ্যমান করে (কলাররা তাড়াতাড়ি অপেক্ষা করে)। বড় বাফার সংক্ষিপ্ত স্ফীতিকে স্মুথ করে কিন্তু বেশি লোড লুকিয়ে রাখতে পারে। সঠিক সংখ্যা নেই, কেবল এমন একটি সংখ্যা আছে যা আপনি কতটুকু অপেক্ষা সহ্য করতে পারেন তা মেলে।

আপনি নির্ধারণ করবেন পুল সাইজ স্থির হবে নাকি পরিবর্তনশীল। স্থির পুল বুঝতে সহজ এবং রিসোর্স ব্যবহার পূর্বানুমানযোগ্য রাখে। অটো-স্কেলিং ওয়ার্কার অব্যবহারে সহায়তা করতে পারে, তবে এতে আরো সিদ্ধান্ত আসে যা আপনাকে বজায় রাখতে হবে (কখন স্কেল করবেন, কতটুকু, এবং কখন কম করবেন)।

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

ডিজাইন লক্ষ্য: রিট্রাই, বাতিলকরণ, এবং পরিষ্কার শাটডাউন

একটি ওয়ার্কার পুল মেকানিক্যালি সরল: একটি নির্দিষ্ট সংখ্যক ওয়ার্কার চালান, তাদের জব খাওয়ান, এবং প্রসেস করুন। মূল্য হলো নিয়ন্ত্রণ: পূর্বানুমানীয় কনকারেন্সি, স্পষ্ট ব্যর্থতা পরিচালনা, এবং এমন একটি শাটডাউন পথ যা অর্ধেক করা কাজ পিছনে রেখে যায় না।

ছোট টিমকে শান্ত রাখতে তিনটি লক্ষ্য:\n

  • কনকারেন্সি সীমিত করা যাতে এক স্পাইক ডাটাবেস বা বাইরের API কে ঝলসে না দেয়।
  • কাজ হারানো এড়ানো (অথবা কমপক্ষে কী কারণে ড্রপ হলো সেটা নিশ্চিত জানা)।
  • ডিবাগ যোজ্য রাখা: প্রতিটি জব লগ ও কিছু কাউন্টার দিয়ে ট্রেসযোগ্য হওয়া উচিত।

অধিকাংশ ব্যর্থতা সাধারণ, কিন্তু আপনি সেগুলোকে আলাদা ভাবে ট্রীট করতে চান:\n

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

বাতিলকরণ মানে এরর নয়। এটা একটি সিদ্ধান্ত: ব্যবহারকারী বাতিল করেছে, একটি ডিপ্লয় প্রসেস আপনার প্রক্রিয়াটি বদলে দিয়েছে, বা আপনার সার্ভিস শাটডাউন করছে। Go-তে বাতিলকরণকে কনটেক্সট বাতিলকরণ ব্যবহার করে প্রথম শ্রেণীর সিগন্যাল হিসাবে বিবেচনা করুন, এবং প্রতিটি জব শুরু করার আগে ও একাধিক স্থানে এক্সপেনসিভ কাজের মাঝে এটি চেক করতে নিশ্চিত করুন।

পরিষ্কার শাটডাউনই যেখানে অনেক পুল ব্যর্থ হয়। আগে থেকেই নির্ধারণ করুন আপনার জবের জন্য “সেফ” কি মানে: আপনি ইন-ফ্লাইট কাজ শেষ করবেন কি, না কি দ্রুত থেমে পরে পুনরায় চালাবেন? একটি বাস্তবসম্মত প্রবাহ:

  • নতুন জব গ্রহণ বন্ধ করুন।
  • ওয়ার্কারদের বলুন তাদের বর্তমান কাজ শেষ করার পরে থামতে (অথবাৎৎৎৎৎৎৎৎৎৎৎৎৎ)-
  • একটি ডেডলাইন পর্যন্ত অপেক্ষা করুন, তারপর জোর করে এক্সিট করুন।

যদি আপনি এগুলো আগে থেকেই সংজ্ঞায়িত করেন, তাহলে রিট্রাই, বাতিলকরণ, এবং শাটডাউন ছোট ও পূর্বানুমানযোগ্য থাকে, বড় কোনো হোমগ্রোন ফ্রেমওয়ার্কে পরিণত হয় না।

ধাপে ধাপে: একটি বেসিক ওয়ার্কার পুল তৈরি করা

একটি ওয়ার্কার পুল হলো কিছু goroutine যা একটি চ্যানেল থেকে জব টানে এবং কাজ করে। গুরুত্বপূর্ণ অংশ হলো ভিত্তি নির্ভরযোগ্য করা: জব কেমন দেখতে লাগে, ওয়ার্কাররা কিভাবে থামে, এবং কিভাবে বুঝবেন সব কাজ শেষ হয়েছে।

একটি সরল Job টাইপ দিয়ে শুরু করুন। লগের জন্য ID দিন, একটি পে-লোড দিন (কি প্রসেস করতে হবে), একটি অ্যাটেম্পট কাউন্টার (রিট্রাইয়ের জন্য দরকারী), টাইমস্ট্যাম্প, এবং প্রতি-জব কনটেক্সট ডেটা রাখার জায়গা।

package jobs

import (
	"context"
	"sync"
	"time"
)

type Job struct {
	ID        string
	Payload   any
	Attempt   int
	Enqueued  time.Time
	Started   time.Time
	Ctx       context.Context
	Meta      map[string]string
}

type Pool struct {
	ctx    context.Context
	cancel context.CancelFunc

	jobs chan Job
	wg   sync.WaitGroup
}

func New(size, queue int) *Pool {
	ctx, cancel := context.WithCancel(context.Background())
	p := \u00026Pool{ctx: ctx, cancel: cancel, jobs: make(chan Job, queue)}
	for i := 0; i \u0003c size; i++ {
		go p.worker(i)
	}
	return p
}

func (p *Pool) worker(_ int) {
	for {
		select {
		case \u0003c-p.ctx.Done():
			return
		case job, ok := \u0003c-p.jobs:
			if !ok {
				return
			}
			p.wg.Add(1)
			job.Started = time.Now()
			_ = job // call your handler here
			p.wg.Done()
		}
	}
}

// Submit blocks when the queue is full (backpressure).
func (p *Pool) Submit(job Job) error {
	if job.Enqueued.IsZero() {
		job.Enqueued = time.Now()
	}
	select {
	case \u0003c-p.ctx.Done():
		return context.Canceled
	case p.jobs \u0003c- job:
		return nil
	}
}

func (p *Pool) Stop() { p.cancel() }
func (p *Pool) Wait() { p.wg.Wait() }

কিছু ব্যবহারিক পছন্দ যা আপনি তৎক্ষণাত করবেন:

  • কতটুকু অপেক্ষা সহ্য করতে পারেন তার উপর ভিত্তি করে একটি কিউ সাইজ পছন্দ করুন।
  • কলারদের জন্য ব্যাকপ্রেশার মানে কি হবে: ব্লক করা, একটি এরর ফেরত দেওয়া, না কি ড্রপ করা।
  • Stop() এবং Wait() আলাদা রাখুন যাতে আপনি প্রথমে ইনটেক বন্ধ করে পরে ইন-ফ্লাইট কাজ শেষ হওয়া পর্যন্ত অপেক্ষা করতে পারেন।

ফ্রেমওয়ার্ক না বানিয়ে রিট্রাই যোগ করা

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

শুরুতে নির্ধারণ করুন কী রিট্রাইযোগ্য। সাময়িক সমস্যা (নেটওয়ার্ক হিচ, টাইমআউট, “পরে চেষ্টা করুন” রেসপন্স) সাধারণত রিট্রাই করার যোগ্য। পার্মানেন্ট সমস্যা (খারাপ ইনপুট, অনুপস্থিত রেকর্ড, পারমিশন নেই) রিট্রাই করা ঠিক না।

একটি ছোট রিট্রাই পলিসি সাধারণত যথেষ্ট:

  • এররগুলিকে রিট্রাইযোগ্য বনাম না-রিট্রাইযোগ্য হিসেবে চিহ্নিত করুন (উদাহরণ: একটি Retryable(err) হেল্পারের মাধ্যমে)।
  • সর্বোচ্চ অ্যাটেম্পট সেট করুন (প্রায়ই ৩ থেকে ৫)। এর বেশি হলে সাধারণত সময় নষ্ট করছেন।
  • এক্সপোনেনাশিয়াল ব্যাকঅফ ব্যবহার করুন জিটারের সাথে যাতে জবগুলো একসাথে রিট্রাই না করে।
  • ডিলে ক্যাপ করুন (উদাহরণ: কখনো ৩০ সেকেন্ডের বেশি স্লিপ করবেন না)।
  • লগ করুন রিট্রাই সহ: অ্যাটেম্পট নম্বর, পরবর্তী ডিলে, এবং জব ID।

ব্যাকঅফ খুব জটিল হওয়া প্রয়োজন নেই। সাধারণ ফর্ম হলো: delay = min(base * 2^(attempt-1), max), তারপর জিটার যোগ করুন (±২০% র‍্যান্ডম)। জিটার গুরুত্বপূর্ণ কারণ না হলে বহু ওয়ার্কার একই সময়ে ব্যর্থ হয়ে একসাথে রিট্রাই করবে।

ডিলে কোথায় রাখবেন? ছোট সিস্টেমের জন্য, ওয়ার্কারের ভেতরেই স্লিপ করা ঠিক আছে, কিন্তু এটি একটি ওয়ার্কার স্লট আটকে রাখে। যদি রিট্রাই বিরল হয়, সেটা গ্রহণযোগ্য। যদি রিট্রাই সাধারণ বা ডিলে দীর্ঘ হয়, তাহলে জবটিকে পুনঃএনকিউ করে একটি “run after” টাইমস্ট্যাম্প দেওয়া বিবেচনা করুন যাতে ওয়ার্কাররা অন্য কাজ দিয়ে ব্যস্ত থাকে।

চূড়ান্ত ব্যর্থতায় স্পষ্ট হোন। ব্যর্থ জবটি (এবং শেষ এরর) স্টোর করুন পর্যালোচনার জন্য, পুনরায় চালানোর জন্য পর্যাপ্ত কনটেক্সট লগ করুন, বা একটি ডেড-লিস্টে ঠেলে দিন যা নিয়মিত পরীক্ষা করা হয়। নীরবভাবে জব ড্রপ করা এড়িয়ে চলুন। এমন একটি পুল যা ব্যর্থতাগুলো লুকায় সেটা আর পুল না থাকা থেকেই খারাপ।

বাতিলকরণ এবং টাইমআউট যা সত্যিই কাজ থামায়

Earn credits for your next build
Publish a short write-up and get credits you can use on Koder.ai.

ওয়ার্কার পুল তখনই নিরাপদ মনে হয় যখন আপনি তাদের থামাতে পারেন। সবচেয়ে সরল নিয়ম: প্রতিটি ব্লকিং স্তরে context.Context পাস করুন। এর মানে সাবমিশন, এক্সিকিউশন, এবং ক্লিনআপ—সব জায়গায় কনটেক্সট ব্যবহার করা।

একটি ব্যবহারিক সেটআপে দুটি টাইম লিমিট থাকে:

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

কনটেক্সট এন্ড শেষ পর্যন্ত ব্যবহার করুন

প্রতিটি জবকে ওয়ার্কারের কনটেক্সট থেকে ডেরাইভ করা নিজস্ব কনটেক্সট দিন। তারপর প্রতিটি ধীর কল (ডাটাবেস, HTTP, কিউ, ফাইল I/O) সেই কনটেক্সট ব্যবহার করবে যেন তা আগেভাগেই ফিরে আসতে পারে।

func worker(ctx context.Context, jobs \u0003c-chan Job) {
	for {
		select {
		case \u0003c-ctx.Done():
			return
		case job, ok := \u0003c-jobs:
			if !ok { return }

			jobCtx, cancel := context.WithTimeout(ctx, job.Timeout)
			_ = job.Run(jobCtx) // Run must respect jobCtx
			cancel()
		}
	}
}

যদি Run আপনার DB বা কোনো API কল করে, ওই কলগুলিতে কনটেক্সট পাস করুন (উদাহরণ: QueryContext, NewRequestWithContext, বা ক্লায়েন্ট মেথডস যা কনটেক্সট নেয়)। যদি আপনি এক জায়গায় এটি উপেক্ষা করেন, বাতিলকরণ “সেরা প্রচেষ্টা” হয়ে যাবে এবং সাধারণত তখনই ব্যর্থ হয় যখন সবচেয়ে বেশি দরকার।

আংশিক কাজ এবং “পুনরায় চালানো নিরাপদ” স্টেপগুলি

বাতিলকরণ জব মধ্যেকারেও ঘটতে পারে, তাই আংশিক কাজ স্বাভাবিক ধরে নিন। পুনরাবৃত্তি নিরাপদ করতে চেষ্টা করুন যাতে পুনরায় চালালে ডুপ্লিকেট তৈরি না হয়। সাধারণ উপায়গুলো: ইনসার্টের জন্য ইউনিক কী (অথবা আপসার্ট) ব্যবহার, প্রগ্রেস মার্কার লেখা (started/done), ফলাফল সংরক্ষণ করে তারপর এগোনো, এবং ধাপগুলোর মধ্যে ctx.Err() চেক করা।

শাটডাউনকে একটি ডেডলাইন হিসেবে দেখুন: নতুন জব গ্রহণ বন্ধ করুন, ওয়ার্কার কনটেক্সট বাতিল করুন, এবং শাটডাউন টাইমআউট পর্যন্ত শুধু ইন-ফ্লাইট জবগুলোর জন্য অপেক্ষা করুন।

পরিষ্কার শাটডাউন: যখন প্রসেস বন্ধ করতে হবে তখন কি করবেন

একটি পরিষ্কার শাটডাউনের কাজ হলো: নতুন কাজ নেওয়া বন্ধ করা, ইন-ফ্লাইট কাজগুলোকে থামাতে বলা, এবং সিস্টেমকে অদ্ভুত অবস্থায় না রেখে এক্সিট করা।

সিগন্যাল দিয়ে শুরু করুন। বেশিরভাগ ডিপ্লয়মেন্টে আপনি লোকালভাবে SIGINT এবং প্রসেস ম্যানেজার বা কন্টেইনার রানটাইম থেকে SIGTERM পাবেন। একটি শাটডাউন কনটেক্সট ব্যবহার করুন যা সিগন্যাল আসলে বাতিল হয়, এবং সেটি আপনার পুল ও জব হ্যান্ডলারগুলোকে পাস করুন।

পরের ধাপ, নতুন জব গ্রহণ বন্ধ করুন। কাউকে এমন এক ফাংশনের পিছনে সাবমিশন রাখা উচিত যা শাটডাউন কনটেক্সট চেক করে বা একটি ক্লোজড ফ্ল্যাগ দেখিয়ে চ্যানেলে পাঠায় না — যাতে কোনো কলার চ্যানেলে পাঠাতে গিয়ে অনন্তকাল ব্লক না হয়।

তারপর কিউতে থাকা কাজগুলো কী হবে তা সিদ্ধান্ত নিন:

  • Drain: ইতিমধ্যে কিউতে যে কাজগুলো আছে সেগুলো শেষ করুন, কিন্তু নতুন সাবমিশন প্রত্যাখ্যান করুন।
  • Drop: এখনও শুরু হয়নি এমন কাজগুলো বাতিল করুন।

ড্রেইনিং পেমেন্ট বা ইমেলের মতো জিনিসের জন্য নিরাপদ। ড্রপ করা “আচ্ছা থাকলে” কাজগুলোর জন্য ঠিক আছে, যেমন কেশ পুনঃগণনা।

একটি বাস্তবসম্মত শাটডাউন সিকোয়েন্স:

  • SIGINT/SIGTERM ধরা এবং একটি শেয়ার্ড কনটেক্সট বাতিল করা।
  • সাবমিশন বন্ধ করা (সবসময় কাজ চ্যানেলই না বন্ধ করে সাবমিশন পাথ বন্ধ করুন)।
  • ওয়ার্কারদের কনটেক্সট দিয়ে শেষ করার বা বাতিল করার সুযোগ দেওয়া।
  • WaitGroup দিয়ে ওয়ার্কারদের জন্য অপেক্ষা করা।
  • একটি ডেডলাইন প্রয়োগ করা, তারপর এক্সিট।

ডেডলাইন গুরুত্বপূর্ণ। উদাহরণস্বরূপ, ইন-ফ্লাইট জবগুলোকে থামানোর জন্য ১০ সেকেন্ড দিন। এর পরও যেগুলো চলছে সেগুলো লগ করুন এবং বেরিয়ে পড়ুন। এটা ডিপ্লয়গুলো পূর্বানুমানযোগ্য রাখে এবং আটকে থাকা প্রসেস এড়ায়।

ওয়ার্কার পুলের জন্য লগিং এবং সিম্পল মেট্রিক্স

Move to a PostgreSQL job queue
Turn in-process jobs into a simple PostgreSQL queue without rewriting your handlers.

যখন একটি ওয়ার্কার পুল ভেঙে পড়ে, সাধারণত সেটা জোরে ফলপ্রসূ ব্যর্থতা দেখায় না। জবগুলো ধীর হয়, রিট্রাই জমে যায়, এবং কেউ রিপোর্ট করে “কিছুই হচ্ছে না।” লগিং এবং কিছু মৌলিক কাউন্টার সেটা পরিষ্কার গল্পে পরিণত করে।

প্রতিটি জবের জন্য একটি স্থিতিশীল ID রাখুন (অথবা সাবমিট হলে জেনারেট করুন) এবং প্রতিটি লগ লাইনে তা অন্তর্ভুক্ত করুন। লগ কনসিস্টেন্ট রাখুন: একটি লাইন জব শুরুতে, একটি লাইন শেষ হলে, এবং একটি লাইন ব্যর্থ হলে। যদি আপনি রিট্রাই করেন, অ্যাটেম্পট নম্বর এবং পরবর্তী ডিলে লগ করুন।

একটি সরল লগ ফরম্যাট:

  • start: job_id, worker_id, attempt, kind
  • finish: job_id, worker_id, attempt, duration_ms
  • fail/retry: job_id, worker_id, attempt, err, next_delay_ms

মেট্রিক্সও মিনিমাল থাকতে পারে কিন্তু উপকারী। কিউ দৈর্ঘ্য, ইন-ফ্লাইট জব, মোট সাকসেস ও ফেলিওর, এবং জব লেটেন্সি (অন্তত গড় ও সর্বোচ্চ) ট্র্যাক করুন। যদি কিউ লেন্গথ বাড়তে থাকে এবং ইন-ফ্লাইট কনস্ট্যান্টভাবে ওয়ার্কার কাউন্টের সমান থাকে, তাহলে আপনি স্যাচুরেটেড। যদি সাবমিটাররা জব চ্যানেলে পাঠাতে গিয়ে ব্লক করে, তাহলে ব্যাকপ্রেশার কলারে পৌঁছেছে। এটা সব সময় খারাপ না, কিন্তু ইচ্ছাকৃত হওয়া উচিত।

“জব আটকে আছে” বললে দেখা দরকার প্রসেস এখনও জব নিচ্ছে কি না, কিউ লেন্গথ বাড়ছে কিনা, ওয়ার্কাররা জীবিত আছে কি না, এবং কোন জবগুলো সবচেয়ে বেশি সময় ধরে চলছে। দীর্ঘ রানটাইম সাধারণত নিয়ত টাইমআউট অনুপস্থিতি, ধীর ডিপেনডেন্সি, বা এমন একটি রিট্রাই লুপের ইঙ্গিত দেয় যা কখনই থামে না।

বাস্তবসম্মত উদাহরণ: একটি ছোট SaaS ব্যাকগ্রাউন্ড কিউ

ভাবুন একটি ছোট SaaS-এ একটি অর্ডার PAID-এ বদলেছে। পেমেন্টের পরে আপনাকে ইনভয়েস PDF পাঠাতে হবে, কাস্টমারকে ইমেল পাঠাতে হবে, এবং ইনার্নাল টিমকে নোটিফাই করতে হবে। এই কাজগুলো ওয়েব রিকোয়েস্ট ব্লক করলে ঠিক হবে না। এটি ওয়ার্কার পুলের জন্য উপযুক্ত কারণ কাজগুলো বাস্তব, কিন্তু সিস্টেম এখনও ছোট।

জব পে-লোড মিনিমাল হতে পারে: বাকি ডেটা ডাটাবেস থেকে আনার জন্য যথেষ্ট। API হ্যান্ডলার অর্ডার আপডেটের একই ট্রানজ্যাকশনে একটি সারি লেখে যেমন jobs(status='queued', type='send_invoice', payload, attempts=0), তারপর একটি ব্যাকগ্রাউন্ড লুপ কিউড জবগুলোর জন্য পোল করে এবং সেগুলোকে ওয়ার্কার চ্যানেলে ঠেলে দেয়।

type SendInvoiceJob struct {
    OrderID    string
    CustomerID string
    Email      string
}

যখন একটি ওয়ার্কার এটি নেয়, হ্যাপি পাথ সরল: অর্ডার লোড করুন, ইনভয়েস জেনারেট করুন, ইমেল প্রোভাইডার কল করুন, তারপর জবকে ডান হিসেবে মার্ক করুন।

রিট্রাই এখানে বাস্তবে আসে। যদি আপনার ইমেল প্রোভাইডারের সাময়িক আউটেজ হয়, আপনি চান না ১,০০০ জব চিরতরে ফেল হোক বা প্রতি সেকেন্ডে প্রোভাইডারকে হামার করা হোক। একটি ব্যবহারিক পদ্ধতি হলো:

  • নেটওয়ার্ক এরর এবং 5xx রেসপন্সকে রিট্রাইযোগ্য হিসেবে ট্রিট করুন।
  • এক্সপোনেনশিয়াল ব্যাকঅফ ব্যবহার করুন একটি সর্বোচ্চ ডিলে সহ (উদাহরণ: 5s, 15s, 45s, 2m)।
  • অ্যাটেম্পট সীমা দিন (উদাহরণ: 10) এবং তারপর জবকে ফেল হিসেবে মার্ক করুন।
  • শেষ এরর রেকর্ড করুন যাতে সাপোর্ট দেখতে পারে কী হয়েছিল।

আউটেজ চলাকালীন, জবগুলো queued থেকে in_progress তে যায়, তারপর আবার queued-তে ভবিষ্যৎ রান টাইম সহ ফিরে আসে। প্রোভাইডার ফিরে এলে, ওয়ার্কাররা স্বাভাবিকভাবে ব্যাকলগ ড্রেইন করে।

এখন ধরুন একটি ডিপ্লয়। আপনি SIGTERM পাঠালেন। প্রসেসটি নতুন কাজ নেবে না কিন্তু চলমান কাজগুলো শেষ করবে। পোলিং বন্ধ করুন, ওয়ার্কার চ্যানেলে ফিড বন্ধ করুন, এবং ওয়ার্কারদের জন্য একটি ডেডলাইনসহ অপেক্ষা করুন। যেসব জব শেষ হয় সেগুলো ডান হিসেবে মার্ক হবে। ডেডলাইন এলে এখনও চলমান থাকা জবগুলোকে আবার queued হিসেবে মার্ক করুন (অথবা একটি ওয়াচডগ দিয়ে in_progress রেখে দিন) যাতে নতুন ভার্সন শুরু হলে সেগুলো পুনরায় পিকআপ করা যায়।

সাধারণ ভুল এবং ফাঁদ

ব্যাকগ্রাউন্ড প্রসেসিং-এ বেশিরভাগ বাগ জব লজিকেই নেই। এগুলো সমন্বয়জনিত ত্রুটি যা কেবল লোড বা শাটডাউনের সময় প্রকাশ পায়।

একটি ক্লাসিক ফাঁদ হলো একই চ্যানেল একাধিক জায়গা থেকে close করা। ফলাফল একটি panic যা পুনরুত্থাপন করা কঠিন। প্রতিটি চ্যানেলের জন্য এক মালিক নির্ধারণ করুন (সাধারণত প্রোডিউসার), এবং শুধুই সেই জায়গাই close(jobs) কল করুক।

রিট্রাই আরেকটি জায়গা যেখানে ভাল ইচ্ছা আউটেজ তৈরি করে। যদি আপনি সবকিছু রিট্রাই করেন, আপনি পার্মানেন্ট ফেলিওরও রিট্রাই করবেন। এটা সময় নষ্ট করে, লোড বাড়ায়, এবং একটি ছোট সমস্যা বড় ইন্সিডেন্টে পরিণত করতে পারে। এররগুলো শ্রেণীবদ্ধ করুন এবং রিট্রাই কপি সীমাবদ্ধ রাখুন।

ডুপ্লিকেট ঘটবেই এমনকি যত্নশীল ডিজাইনের সাথেও। ওয়ার্কার ক্র্যাশ হতে পারে মিদ-জব, একটি টাইমআউট কাজ শেষ হওয়ার পরে ফায়ার করতে পারে, বা ডিপ্লয়ের সময় পুনঃকিউ হতে পারে। যদি জব আইডেমপোটেন্ট না হয়, ডুপ্লিকেট বাস্তব ক্ষতি করে: দুটি ইনভয়েস, দুটি ওয়েলকাম ইমেল, দুটি রিফাণ্ড।

সবচেয়ে সাধারণ ভুলগুলো:

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

অনবাউন্ডেড কিউ বিশেষভাবে ছলনায় ভরা। কাজের একটি স্পাইক RAM-এ চুপচাপ জমতে পারে। বাফারড চ্যানেল ব্যবহার করুন এবং নির্ধারণ করুন পূর্ণ হলে কী হবে: ব্লক, ড্রপ, বা এরর ফেরত।

শিপিং-এর আগে দ্রুত চেকলিস্ট

Own the generated source code
Get full Go, React, and database code you can own and extend.

প্রোডাকশনে ওয়ার্কার পুল পাঠানোর আগে, আপনি জব লাইফসাইকেলটি জিবর্ষণ করে বলতে পারেন। যদি কেউ জিজ্ঞাসা করে “এই জব এখন কোথায়?”, উত্তরটি অনুধাবনের বাইরে থাকা উচিৎ নয়।

একটি ব্যবহারিক প্রিফ্লাইট চেকলিস্ট:

  • আপনি প্রতিটি স্টেট ও ট্রানজিশন নাম বলতে পারেন: queued, picked up, running, finished, failed (এবং কি তাদের পরিবর্তন করে)।
  • কনকারেন্সি একটি একক নিয়ামক (যেমন workerCount) এবং পরিবর্তন করলে কোড রিরাইট করার দরকার নেই।
  • রিট্রাই সীমিত: সর্বোচ্চ অ্যাটেম্পট স্পষ্ট, ব্যাকঅফ বাড়ে, এবং পার্মানেন্ট ফেলিওরকে ইচ্ছাকৃতভাবে কোথাও পাঠানো হয়।
  • শাটডাউন আচরণ প্রমাণিত: আপনি ইনটেক বন্ধ করেন, ইন-ফ্লাইট জব শেষ হতে দেন, এবং একটি হার্ড টাইমআউট আছে।
  • লগগুলো মৌলিক প্রশ্নের উত্তর দেয়: job ID, attempt number, duration, এবং এরর কারণ।

রিলিজের আগে একটি বাস্তব অনুশীলন করুন: ১০০টি “সেন্ড রিসিট ইমেল” জব এনকিউ করুন, ২০টি জবকে ব্যর্থ করতে জোর করুন, তারপর রান মাঝপথে সার্ভিস রিস্টার্ট করুন। আপনি দেখতে পাবেন রিট্রাই প্রত্যাশিতভাবে আচরণ করে, ডুপ্লিকেট সাইড-ইফেক্ট নেই, এবং ক্যানসেলেশন ডেডলাইনে প্রকৃতভাবে কাজ থামায়।

যদি কোনো আইটেম ধূসর হয়, এখনই তা কঠোর করুন। ছোট সংস্কারগুলো পরে দিন বাঁচাতে পারে।

পরবর্তী ধাপ: কখন ভারি ইনফ্রাস্ট্রাকচার যোগ করবেন (আর কখন নয়)

একটি সাদামাটা ইন-প্রসেস পুল প্রোডাক্ট তরুণ থাকাকালীন প্রায়ই যথেষ্ট। যদি আপনার জবগুলো “আচ্ছা থাকলে” ধরনের (ইমেল পাঠান, কেশ রিফ্রেশ, রিপোর্ট জেনারেট) এবং আপনি সেগুলো পুনরায় চালাতে পারেন, একটি ওয়ার্কার পুল সিস্টেমটিকে সহজে বোঝার যোগ্য রাখে।

কখন আপনি ইন-প্রসেস পুল ছাড়বেন

এই চাপের জায়গাগুলো দেখুন:

  • আপনি একাধিক অ্যাপ ইনস্ট্যান্স চালান এবং শুধুমাত্র একটি ইনস্ট্যান্সকে কোনো জব পিক করতে হবে।
  • আপনার ডিউরেবিলিটি দরকার (জব ক্র্যাশ সহ বেঁচে থাকতে হবে)।
  • আপনাকে অডিট ট্রেইল দরকার: কে কখন কী কিউ করেছে, কখন চলছে, এবং সঠিক ফলাফল।
  • সার্ভিসগুলির মধ্যে ব্যাকপ্রেশার কন্ট্রোল দরকার, শুধু একটি প্রসেসের ভিতরে নয়।
  • কঠোর শিডিউলিং বা দীর্ঘ ডিলে (ঘন্টার বা দিনের) প্রয়োজনীয়তা এবং নির্ভরযোগ্য ওয়েক-আপ দরকার।

যদি এগুলোর কোনোটি সত্য না, ভারি টুলগুলো মূল্য যোগ করার চেয়ে বেশি চলাচলের অংশ যোগ করতে পারে।

ধাপে ধাপে মাইগ্রেট করুন, পুরো রিরাইট না করেই

সেরা হেজ হলো একটি স্থিতিশীল জব ইন্টারফেস: একটি ছোট পে-লোড টাইপ, একটি ID, এবং একটি হ্যান্ডলার যা স্পষ্ট রেজাল্ট রিটার্ন করে। তারপর আপনি কিউ ব্যাকএন্ড পরিবর্তন করতে পারেন পরে (ইন-মেমরি চ্যানেল থেকে ডাটাবেস টেবিল, এবং তারপর একটি ডেডিকেটেড কিউ) ব্যবসায়িক কোড বদলানো ছাড়া।

একটি ব্যবহারিক মধ্যবর্তী ধাপ হলো একটি ছোট Go সার্ভিস যা PostgreSQL থেকে জব পড়ে, তাদের লক করে ক্লেইম করে, এবং স্ট্যাটাস আপডেট করে। আপনি ডিউরেবিলিটি ও বেসিক অডিটেবিলিটি পান একই ওয়ার্কার লজিক রাখা অবস্থায়।

যদি দ্রুত প্রোটোটাইপ করতে চান, Koder.ai (koder.ai) একটি চ্যাট প্রম্পট থেকে Go + PostgreSQL স্টার্টার জেনারেট করতে পারে, ব্যাকগ্রাউন্ড জব টেবিল এবং ওয়ার্কার লুপ সহ, এবং এর স্ন্যাপশট ও রোলব্যাক আপনাকে রিট্রাই ও শাটডাউন আচরণ টিউন করার সময় সহায়তা করতে পারে।

সাধারণ প্রশ্ন

ব্যাকগ্রাউন্ড জবের জন্য কখন Go worker pool ব্যবহার করা উচিত?

যখন ব্যাকগ্রাউন্ডের কাজগুলো একটি সার্ভিস প্রসেসের মধ্যেই চালানো যায় এবং আপনার মূলত নিয়ন্ত্রিত সমান্তরালতা দরকার হয়, তখন worker pool ব্যবহার করুন। এটি ইমেইল পাঠানো, ছবি প্রক্রিয়াকরণ, ক্যাশ রিফ্রেশ এবং ছোট রিপোর্ট তৈরির জন্য ভালো কাজ করে। কাজগুলোকে ক্র্যাশের পরও টিকে থাকতে হলে, একাধিক অ্যাপ ইনস্ট্যান্সে চালাতে হলে বা ভবিষ্যতের কোনো সময় পর্যন্ত অপেক্ষা করাতে হলে স্থায়ী job storage বা আলাদা queue ব্যবহার করুন।

আমার Go pool-এ কতজন worker থাকা উচিত?

শুধু CPU সংখ্যার ওপর নয়, সবচেয়ে ধীর নির্ভরতার ভিত্তিতে নির্দিষ্ট worker সংখ্যা বেছে নিন। উদাহরণ হিসেবে, কোনো ইমেইল প্রোভাইডার বা ডেটাবেস নিরাপদে ওই মাত্রার সমান্তরাল কাজ সামলাতে পারলে 5টি worker দিয়ে শুরু করুন। Queue কতটা বাড়ছে এবং job কতক্ষণ চলছে তা মাপুন, তারপর ধীরে ধীরে সংখ্যা সমন্বয় করুন।

Job queue হিসেবে buffered channel কেন ব্যবহার করব?

Buffered channel jobগুলোর জন্য অপেক্ষার জায়গা দেয় এবং মেমরিতে কতটা কাজ জমে থাকতে পারে তা সীমিত রাখে। এটি পূর্ণ হলে, অন্য নীতি না বাছলে job জমা দেওয়া কোড ব্লক হয়ে যায়। এই চাপ আপনার সার্ভিসকে জানায় যে ব্যস্ততার সময় এটি সীমাহীন ব্যাকগ্রাউন্ড কাজ গ্রহণ করতে পারে না।

কোন job ব্যর্থতায় পুনরায় চেষ্টা করা উচিত?

নেটওয়ার্ক ত্রুটি, timeout, rate limit এবং সার্ভার-পক্ষের 5xx response-এর মতো সাময়িক ব্যর্থতায় পুনরায় চেষ্টা করুন। অবৈধ ইনপুট, অনুপস্থিত record বা permission error-এ পুনরায় চেষ্টা করবেন না। সর্বোচ্চ চেষ্টার সংখ্যা নির্ধারণ করুন এবং শেষ ত্রুটিটি সংরক্ষণ করুন, যাতে কেউ jobটি পরীক্ষা বা পুনরায় চালাতে পারে।

Worker pool-এ retry backoff কীভাবে কাজ করা উচিত?

Jitter-সহ exponential backoff ব্যবহার করুন। প্রতিটি ব্যর্থতার পর বিলম্ব বাড়ান, যুক্তিসঙ্গত সীমায় আটকে দিন এবং সামান্য এলোমেলো করুন, যাতে অনেক ব্যর্থ job একই মুহূর্তে আবার চেষ্টা না করে। পুনরায় চেষ্টা বিরল হলে worker-এর ভেতরে অল্প বিলম্ব রাখা ঠিক আছে; দীর্ঘ বা ঘন ঘন বিলম্ব স্থায়ী queue-তে রাখা ভালো।

আটকে থাকা job কীভাবে বন্ধ করব?

প্রতিটি job-কে deadline-সহ একটি context দিন এবং সেটি database, HTTP ও অন্যান্য blocking call-এ পাঠান। বাতিল করলে প্রকৃত কাজ থামাতে ওই callগুলোকে context মানতে হবে। একাধিক ধাপের কাজের মাঝেও context পরীক্ষা করুন, বিশেষ করে কোনো side effect তৈরির আগে।

ডুপ্লিকেট ব্যাকগ্রাউন্ড কাজ কীভাবে ঠেকাব?

প্রতিটি side effect idempotent করুন, যাতে একই job দুইবার চললেও দুটি invoice, email বা refund তৈরি না হয়। একটি idempotency ID সংরক্ষণ করুন, unique database constraint বা upsert ব্যবহার করুন এবং jobটির একাধিক ধাপ থাকলে অগ্রগতি সংরক্ষণ করুন। Crash ও timeout-এর পর duplicate হওয়ার সম্ভাবনা ধরে নিন।

Go worker pool বন্ধ করার সবচেয়ে নিরাপদ উপায় কী?

আগে নতুন submission গ্রহণ বন্ধ করুন। তারপর queue-তে থাকা ও চলমান jobগুলো শেষ হতে দিন, অথবা কাজটি পরে নিরাপদে আবার চালানো গেলে সেগুলো বাতিল করুন। কেবল shutdown deadline পর্যন্ত অপেক্ষা করুন, যেমন 10 সেকেন্ড, তারপর exit করুন এবং অসমাপ্ত durable jobগুলো পরের process-এর জন্য উপলভ্য রাখুন।

ব্যাকগ্রাউন্ড job-এর জন্য কী log ও measure করা উচিত?

কাজ শুরু, শেষ, ব্যর্থতা ও পুনরায় চেষ্টার সময় একটি স্থিতিশীল job ID log করুন। Queue-এর দৈর্ঘ্য, সক্রিয় worker, সফল ও ব্যর্থতার মোট সংখ্যা, retry count এবং job duration ট্র্যাক করুন। সব worker ব্যস্ত থাকা অবস্থায় queue বাড়তে থাকলে সাধারণত বোঝায় pool বা কোনো dependency তাল মেলাতে পারছে না।

কখন in-process worker pool খুব সীমিত হয়ে যায়?

যখন একাধিক instance-কে সমন্বয় করতে হয়, crash-এর পর job হারিয়ে যেতে পারে না, সম্পূর্ণ audit trail দরকার হয় বা নির্ভরযোগ্য scheduled execution দরকার হয়, তখন in-process pool-এর সীমা ছাড়িয়ে যান। আলাদা queue system যোগ করার আগে PostgreSQL jobs table প্রায়ই একটি ব্যবহারিক পরবর্তী ধাপ হয়।

Related posts