DEV Community

Cover image for Service Bus কেন ব্যবহার করবেন, আর কখন করবেন না? A2Z গাইড (বাস্তব Bulk Renewal উদাহরণসহ)
Rakib Ahasan
Rakib Ahasan

Posted on Edited on

Service Bus কেন ব্যবহার করবেন, আর কখন করবেন না? A2Z গাইড (বাস্তব Bulk Renewal উদাহরণসহ)

এই লেখা পড়ে আপনি কী পাবেন

একবার পড়লেই যেন নিচের প্রশ্নগুলোর উত্তর নিজে দিতে এবং অন্যকে বোঝাতে পারেন:

  • Service Bus আসলে কোন সমস্যার সমাধান?
  • Queue আর Topic কখন কোনটা?
  • কখন Service Bus ব্যবহার করা উচিত নয়?
  • Message হারালে বা duplicate হলে কী হয়, আর কীভাবে সামলাবেন?
  • Dual-write problem কী, আর Outbox Pattern সেটা কীভাবে ঠিক করে?
  • Service Bus, RabbitMQ, Kafka, Hangfire: কোনটা কোথায়?

১. সমস্যা দিয়ে শুরু: ১০,০০০ member-এর renewal

ধরুন, একজন admin একটা button চাপলেন: "সব ১০,০০০ member-এর membership renew করো।"

প্রতিটি member-এর জন্য system-কে করতে হবে:

  1. Current membership খুঁজে বের করা
  2. Target membership ঠিক করা
  3. Eligibility check
  4. Purchase rule evaluate
  5. Renewal status update
  6. Result database-এ save

সবচেয়ে সহজ উপায় হলো API-র ভেতরেই loop চালিয়ে ১০,০০০ জনকে process করা। কিন্তু তাহলে:

সমস্যা কেন হয়
Request অনেকক্ষণ open থাকে কাজ মিনিটের হিসাবে চলে, client ঝুলে থাকে
Timeout Load balancer, gateway বা browser একটা সময় পর connection কেটে দেয়
Server resource শেষ Thread, memory, DB connection আটকে থাকে
একটা failure-এ সব শেষ ৭,৩০০ নম্বরে error এলে বাকি কাজের কী হবে?
Progress জানা যায় না User জানে না ২% হয়েছে না ৯০%
Restart করলে ঝুঁকি আবার শুরু করলে কেউ দুইবার renew হয়ে যেতে পারে

মূল সমস্যা একটাই: কাজটা request-এর জীবনকালের মধ্যে শেষ হওয়ার মতো নয়।


২. Service Bus কী?

সহজ কথায়: Service Bus হলো application-গুলোর মধ্যে message আদান-প্রদানের একটা reliable ডাকবাক্স।

API সরাসরি worker-কে call করে না। সে কাজের বর্ণনা একটা message হিসেবে queue-তে রেখে দেয়। Worker নিজের সুবিধামতো সেটা তুলে নিয়ে কাজ করে।

API  →  Queue  →  Background Worker
Enter fullscreen mode Exit fullscreen mode

রেস্টুরেন্টের উদাহরণ ভাবুন। ওয়েটার (API) অর্ডার নিয়ে কিচেনের টিকেট-রেলে (Queue) গেঁথে দেয়, তারপর সঙ্গে সঙ্গে পরের টেবিলে চলে যায়। বাবুর্চি (Worker) নিজের গতিতে টিকেট নিয়ে রান্না করে। ওয়েটার রান্না শেষ হওয়া পর্যন্ত কিচেনের সামনে দাঁড়িয়ে থাকে না।

Bulk renewal-এ flow হবে এরকম:

POST /bulk-renew
   │
   ├─ BulkRenewProcess তৈরি (status: Queued)
   ├─ Message publish
   └─ Response: 202 Accepted { "processId": 259, "status": "Queued" }

(পরে, background-এ)
Worker → message তুলে নেয় → batch করে process করে → status update করে

Client → GET /bulk-renew/processes/{id}  → progress দেখে
Enter fullscreen mode Exit fullscreen mode

Client-কে ১০ মিনিট অপেক্ষা করতে হলো না। Progress দেখা যাচ্ছে। কাজ ব্যর্থ হলেও API বসে যায়নি।


৩. Service Bus কী কী সুবিধা দেয়

১. Long-running কাজ asynchronous হয়। API শুধু "কাজ নিলাম" বলে ছেড়ে দেয়।

২. API আর Worker আলাদা হয় (decoupling)। API শুধু জানে "আমি message পাঠিয়েছি।" Worker শুধু জানে "আমি message নিয়ে কাজ করব।" দুটো আলাদা deploy, scale ও maintain করা যায়।

৩. Retry করা যায়। Network glitch বা temporary DB সমস্যায় message আবার process করা যায়। তবে blind retry নয়:

Error-এর ধরন কী করবেন
Temporary (DB timeout, service unavailable) Retry করুন
Invalid input (ভুল product ID) Retry করে লাভ নেই
Business rule failure (eligible নয়) Retry করা উচিত নয়, result হিসেবে save করুন

৪. Dead Letter Queue (DLQ) পাওয়া যায়। কোনো message বারবার fail করলে সেটা হারিয়ে যায় না, আলাদা একটা sub-queue-তে জমা হয়। Team পরে কারণ দেখে, message inspect করে, ঠিক করে আবার process করতে পারে।

৫. Load control (back pressure / load smoothing)। ১০,০০০ কাজ একসাথে server-এর ঘাড়ে না চাপিয়ে queue-তে রাখুন। Worker নিজের সক্ষমতা অনুযায়ী টানবে।

৬. Scale করা সহজ (competing consumers)।

Queue
 ├── Worker 1
 ├── Worker 2
 ├── Worker 3
 └── Worker 4
Enter fullscreen mode Exit fullscreen mode

সবাই একই queue থেকে টানে, প্রতিটা message শুধু একজন পায়। কাজ বাড়লে worker instance বাড়ান।

৭. Failure isolation। Worker crash করলে API চলতেই থাকে।


৪. Queue বনাম Topic

Queue = একটা message, একজন consumer।

Producer → Queue → একজন Consumer
Enter fullscreen mode Exit fullscreen mode

যেমন: bulk renewal, invoice generation, email পাঠানো, report তৈরি।

Topic + Subscription = একটা message, অনেক আগ্রহী পক্ষ।

                 ┌── Billing Subscription
Producer → Topic ├── Notification Subscription
                 └── Audit Subscription
Enter fullscreen mode Exit fullscreen mode

Topic-এ publish করলে প্রতিটা subscription নিজের একটা copy পায়। যেমন MembershipRenewed event একসাথে Billing, Notification, Audit ও Analytics শুনতে চাইলে Topic।

সহজ নিয়ম:

  • "এই কাজটা কেউ একজন করুক" → Queue
  • "এই ঘটনাটা ঘটেছে, যার যার জানা দরকার জানুক" → Topic

৫. Service Bus-এর কিছু জরুরি ধারণা (এগুলো না জানলে production-এ ধাক্কা খাবেন)

Peek-Lock বনাম Receive-and-Delete

  • Peek-Lock (default, প্রায় সবসময় এটাই): Worker message পায়, কিন্তু message queue থেকে মুছে যায় না, শুধু কিছুক্ষণের জন্য lock হয়। কাজ শেষ হলে worker Complete করে, তখন message মোছা হয়। Worker crash করলে lock-এর সময় শেষে message আবার অন্য worker পায়।
  • Receive-and-Delete: তুলে নেওয়ার সঙ্গে সঙ্গেই মুছে যায়। Worker crash করলে message চিরতরে হারায়। সাধারণ business কাজে এড়িয়ে চলুন।

Lock duration আর renewal। কাজ lock-এর সময়ের চেয়ে বেশি লাগলে lock শেষ হয়ে message আরেকজন পেয়ে যেতে পারে, ফলে একই কাজ দুইজন করবে। দীর্ঘ কাজের ক্ষেত্রে lock renew করুন, নতুবা কাজকে ছোট batch-এ ভাঙুন।

Max Delivery Count। একটা message নির্দিষ্ট সংখ্যকবার (default ১০) ব্যর্থ হলে Service Bus নিজেই সেটাকে DLQ-তে পাঠায়। এটাই poison message-এর প্রাথমিক প্রতিরক্ষা।

Duplicate detection। Producer-এর MessageId দিয়ে নির্দিষ্ট time window-এর মধ্যে একই message দ্বিতীয়বার পাঠানো আটকানো যায়। এটা সাহায্য করে, কিন্তু consumer-side idempotency-র বিকল্প নয়।

Sessions। একই SessionId-এর message গুলো ক্রমানুসারে (FIFO) ও একজন consumer-এর হাতে যায়। Order গুরুত্বপূর্ণ হলে (যেমন একই member-এর একাধিক event) ব্যবহার করুন।

Scheduled / deferred message। "৩০ মিনিট পর এটা process করো" ধরনের কাজের জন্য message-কে ভবিষ্যতের সময়ে schedule করা যায়।


৬. Service Bus কখন ব্যবহার করবেন

নিচের এক বা একাধিক মিললে ভাবুন:

  • কাজটা long-running
  • User-কে এখনই final result দেখাতে হবে না
  • Retry দরকার
  • Traffic হঠাৎ বাড়ে (burst)
  • এক service সাময়িক বন্ধ থাকলেও message হারানো যাবে না
  • একাধিক service-এর মধ্যে যোগাযোগ
  • API আর worker আলাদাভাবে scale করতে হবে
  • Progress track করতে হবে

বাস্তব উদাহরণ: bulk membership renewal, payment processing, email/SMS, file ও image/video processing, invoice generation, বড় report, search indexing, audit event, data synchronization, integration event publishing।

৭. Service Bus কখন ব্যবহার করবেন না

সবকিছুতে queue ঢোকানো ইঞ্জিনিয়ারিং নয়, অভ্যাস। নিচের ক্ষেত্রে সাধারণ method call-ই যথেষ্ট:

  • Simple CRUD (GET /users/123, POST /users, PUT /profile)
  • ১০০-২০০ ms-এর মধ্যে শেষ হওয়া ছোট query
  • User-কে সঙ্গে সঙ্গে final result বা error দেখাতে হবে
  • কাজটা অবশ্যই একই database transaction-এর মধ্যে থাকতে হবে
  • Message handling-এর overhead কাজের মূল্যের চেয়ে বেশি
  • ছোট team, ছোট system, operate করার মতো কেউ নেই

একটা বিষয় আলাদা করে মনে রাখুন: user button চেপে সঙ্গে সঙ্গে চূড়ান্ত ফল চাইলে async queue UX জটিল করে দেয়। মাঝামাঝি পথ (hybrid) ভালো:

API → ছোট synchronous validation
API → ভারী কাজ queue-তে
API → processId return
Client → status polling
Enter fullscreen mode Exit fullscreen mode

৮. সবচেয়ে বড় ফাঁদ: Dual-Write Problem

অনেকে ভাবেন Service Bus ব্যবহার করলেই data consistency নিশ্চিত। না। এই কোডটা দেখুন:

// ❌ বিপজ্জনক
await _db.SaveChangesAsync();                       // 1. DB commit সফল
await _sender.SendMessageAsync(message);            // 2. এখানে connection failure!
Enter fullscreen mode Exit fullscreen mode

ফলাফল: database বলছে কাজ "Pending", কিন্তু queue-তে কোনো message নেই। কেউ কাজটা করবে না, কেউ জানতেও পারবে না। উল্টো ক্রমে (আগে publish, পরে DB commit) করলে worker এমন message পেতে পারে যার ডেটা এখনো DB-তে নেই।

DB আর broker দুটো আলাদা system, দুটোকে একসাথে atomically update করা যায় না। শুধু retry দিয়ে এটা পুরোপুরি সারে না, কারণ retry-র আগেই process crash করতে পারে।

সমাধান: Outbox Pattern

ধারণা: message সরাসরি broker-এ না পাঠিয়ে একই DB transaction-এ একটা Outbox table-এ লিখে রাখুন। আলাদা একটা publisher সেখান থেকে তুলে broker-এ পাঠাবে।

Transaction শুরু
   ├─ Business data update
   ├─ Outbox table-এ event insert
Transaction commit
        ↓
Background publisher: Outbox পড়ে → Service Bus-এ publish → Outbox row "Published" চিহ্নিত
Enter fullscreen mode Exit fullscreen mode
  • Transaction fail করলে business data-ও নেই, outbox event-ও নেই: সামঞ্জস্য বজায় থাকল।
  • Service Bus সাময়িক বন্ধ থাকলে event outbox-এ বসে থাকে, publisher পরে আবার চেষ্টা করে।

একটা সরল Outbox table:

CREATE TABLE OutboxMessages (
    Id            UNIQUEIDENTIFIER PRIMARY KEY,
    Type          NVARCHAR(200)  NOT NULL,
    Payload       NVARCHAR(MAX)  NOT NULL,
    CorrelationId NVARCHAR(100)  NULL,
    CreatedAt     DATETIME2      NOT NULL,
    PublishedAt   DATETIME2      NULL,
    RetryCount    INT            NOT NULL DEFAULT 0,
    NextRetryAt   DATETIME2      NULL,
    Error         NVARCHAR(MAX)  NULL
);
Enter fullscreen mode Exit fullscreen mode

Transaction-এর ভেতরে (Dapper উদাহরণ):

using var tx = connection.BeginTransaction();

await connection.ExecuteAsync(
    "INSERT INTO BulkRenewProcess (...) VALUES (...)", process, tx);

await connection.ExecuteAsync(
    @"INSERT INTO OutboxMessages (Id, Type, Payload, CorrelationId, CreatedAt)
      VALUES (@Id, @Type, @Payload, @CorrelationId, SYSUTCDATETIME())",
    new {
        Id = Guid.NewGuid(),
        Type = "BulkRenewRequested",
        Payload = JsonSerializer.Serialize(new { processId = process.Id }),
        CorrelationId = correlationId
    }, tx);

tx.Commit();
Enter fullscreen mode Exit fullscreen mode

Publisher (BackgroundService) এরকম কাজ করে:

var pending = await GetUnpublishedAsync(batchSize: 50);   // PublishedAt IS NULL AND NextRetryAt <= now

foreach (var row in pending)
{
    try
    {
        var msg = new ServiceBusMessage(row.Payload)
        {
            MessageId = row.Id.ToString(),        // duplicate detection-এর জন্য
            CorrelationId = row.CorrelationId,
            Subject = row.Type
        };
        await _sender.SendMessageAsync(msg, ct);
        await MarkPublishedAsync(row.Id);
    }
    catch (Exception ex)
    {
        await ScheduleRetryAsync(row.Id, ex);     // RetryCount++, NextRetryAt = backoff
    }
}
Enter fullscreen mode Exit fullscreen mode

লক্ষ্য করুন, publisher নিজেও একই message দুইবার পাঠাতে পারে (publish সফল হলো কিন্তু MarkPublished-এর আগে crash)। তাই Outbox একা যথেষ্ট নয়, consumer-কেও idempotent হতে হবে।

কখন Outbox লাগবে না? ছোট application-এ যেখানে কিছু event হারানো মেনে নেওয়া যায়। কারণ Outbox-এর খরচ আছে: table, publisher, retry, monitoring।


৯. Delivery guarantee ও Idempotency

Peek-Lock-এ Service Bus-এর গ্যারান্টি at-least-once: message অন্তত একবার পৌঁছাবে, কিন্তু একাধিকবারও আসতে পারে (worker কাজ শেষ করে Complete করার আগেই crash করলে, বা lock শেষ হয়ে গেলে)।

এর মানে: আপনার consumer অবশ্যই idempotent হতে হবে, অর্থাৎ একই message দুইবার প্রক্রিয়া হলেও ফল যেন একবারের মতোই হয়।

উদাহরণ: payment আগেই সম্পন্ন হলে আবার charge করা যাবে না। Renewal আগেই হয়ে থাকলে আবার renew করা যাবে না।

সহজ কৌশল: একটা ProcessedMessages table বা item-এর নিজের status ব্যবহার করা।

// Item-level idempotency: status দেখেই সিদ্ধান্ত
var item = await GetItemAsync(itemId);
if (item.Status is ItemStatus.Renewed or ItemStatus.Skipped)
    return;                                  // আগেই হয়ে গেছে, নিরাপদে বাদ দিন

// Message-level idempotency: MessageId unique constraint
// INSERT INTO ProcessedMessages (MessageId) -> duplicate হলে unique violation -> skip
Enter fullscreen mode Exit fullscreen mode

গুরুত্বপূর্ণ: "check করে তারপর করা" আর "করার চিহ্ন রাখা" যেন একই transaction-এ থাকে, নইলে race condition হবে।


১০. Consumer লেখার সময় যা যা সামলাতে হবে

processor.ProcessMessageAsync += async args =>
{
    var body = args.Message.Body.ToObjectFromJson<BulkRenewRequested>();

    try
    {
        await _handler.HandleAsync(body, args.CancellationToken);
        await args.CompleteMessageAsync(args.Message);          // সফল: মুছে ফেলুন
    }
    catch (TransientException)                                  // temporary: retry হবে
    {
        await args.AbandonMessageAsync(args.Message);           // delivery count বাড়বে
    }
    catch (PermanentException ex)                               // retry করে লাভ নেই
    {
        await args.DeadLetterMessageAsync(args.Message,
            deadLetterReason: "PermanentFailure",
            deadLetterErrorDescription: ex.Message);
    }
};
Enter fullscreen mode Exit fullscreen mode

চেকলিস্ট:

  1. Message ছোট রাখুন। বড় data DB বা blob storage-এ রাখুন, message-এ শুধু reference পাঠান:
   { "processId": 259, "itemIds": [26114] }
Enter fullscreen mode Exit fullscreen mode

(Standard tier-এ message size-এর সীমা আছে, তাই বড় payload এমনিতেই সমস্যা।)

  1. Batch-wise process করুন। ১০,০০০ জনের জন্য একটা বিশাল message নয়, বরং ছোট ছোট batch (যেমন ৫০টা item) আলাদা message। এতে retry ছোট থাকে আর progress সুন্দরভাবে দেখানো যায়।
  2. Error classify করুন: transient হলে retry, permanent বা business failure হলে retry নয়।
  3. Poison message-কে অনন্ত retry করবেন না। Max delivery count আর DLQ আছে এজন্যই।
  4. DLQ নিয়মিত দেখুন। DLQ থাকাই যথেষ্ট নয়। কোন message, কতবার, কেন fail করল, আবার চালানো যাবে কি না, manual হস্তক্ষেপ লাগবে কি না, এগুলো দেখার প্রক্রিয়া থাকতে হবে।
  5. Correlation ID রাখুন, যাতে পুরো পথ trace করা যায়:
   API request → DB update → Message publish → Worker processing → Final result
Enter fullscreen mode Exit fullscreen mode
  1. Cancellation / shutdown সামলান। Application বন্ধ হওয়ার সময় চলমান message-এর কী হবে তা পরিষ্কার রাখুন। Complete না করলে lock শেষে message আবার আসবে, আর সেটা সামলানোর জন্যই idempotency।
  2. Ordering লাগলে Session, partition key বা aggregate-ভিত্তিক processing ব্যবহার করুন। সব queue এ strict ordering দেয় না।

Production-এ যেসব metric চোখে রাখবেন: queue length, active message count, dead-letter count, processing latency, retry count, failure rate, consumer throughput। এবং এগুলোর উপর alert।


১১. Bulk Renewal-এর recommended design (শুরু থেকে শেষ)

1. API request গ্রহণ ও প্রাথমিক validation
2. BulkRenewProcess এবং BulkRenewItems তৈরি (status: Queued)
3. একই transaction-এ Outbox event save
4. processId return (HTTP 202)
5. Outbox publisher → Service Bus-এ publish
6. Worker message নেয় (Peek-Lock)
7. Batch-wise প্রতিটি member-এর জন্য:
     current membership → target membership → eligibility → purchase rule
8. Item ও Process-এর status update (idempotent ভাবে)
9. সব শেষে process status: Completed / CompletedWithErrors
10. Client status endpoint দিয়ে progress দেখে
Enter fullscreen mode Exit fullscreen mode

Client-এর জন্য:

{ "processId": 259, "status": "Queued" }
Enter fullscreen mode Exit fullscreen mode
GET /bulk-renew/processes/{processGuid}
→ { "status": "Processing", "total": 10000, "succeeded": 4200, "failed": 12 }
Enter fullscreen mode Exit fullscreen mode

Synchronous bulk processing-এর চেয়ে এটা অনেক বেশি scalable ও reliable, আর failure হলে কোন কোন item ব্যর্থ হয়েছে সেটা আলাদাভাবে দেখে শুধু সেগুলো নতুন করে চালানো যায়।


১২. Service Bus, RabbitMQ, Kafka, Hangfire: কোনটা কখন?

টুল কখন ভালো
Azure Service Bus Azure ecosystem, enterprise queue/topic, DLQ, scheduled delivery, managed service (নিজে server চালাতে চান না), .NET integration সহজ
RabbitMQ নিজের infrastructure নিয়ন্ত্রণ, on-premise/hybrid, flexible routing, open-source, ops team আছে
Kafka অতি high-throughput event streaming, event history replay, data pipeline/analytics, অনেক consumer একই stream পড়বে। এটা সাধারণ business queue-এর বিকল্প নয়
AWS SQS/SNS, Google Pub/Sub আপনার cloud যেটা, সেটারটা
Hangfire / Quartz.NET Application-কেন্দ্রিক background job, schedule করা কাজ, পুরো message broker-এর দরকার নেই
MassTransit / CAP Broker-এর উপর abstraction; retry, outbox, saga ইত্যাদি অনেকটা তৈরি অবস্থায় পাওয়া যায়

দ্রুত সিদ্ধান্তের নিয়ম:

  • শুধু "একটা heavy কাজ background-এ চালাতে চাই", আর একই app-এর ভেতরে: Hangfire যথেষ্ট হতে পারে।
  • একাধিক service-এর মধ্যে নির্ভরযোগ্য messaging, Azure-এ: Service Bus।
  • Event stream, replay, বিশাল volume: Kafka।

১৩. সাধারণ ভুল (Anti-patterns)

  • সবকিছুতে queue ঢোকানো, এমনকি simple CRUD-এও
  • Idempotency ছাড়া consumer লেখা
  • সব error-এ retry করা
  • DLQ আছে, কিন্তু কেউ দেখে না
  • DB update আর publish আলাদা আলাদা করা (dual-write)
  • বিশাল payload message-এ ঢোকানো
  • Correlation ID ছাড়া production চালানো, ফলে debug অসম্ভব
  • Lock duration-এর চেয়ে বেশি সময়ের কাজে lock renew না করা
  • Receive-and-Delete ব্যবহার করে message হারানোর ঝুঁকি নেওয়া

শেষ কথা

Service Bus কোনো জাদুর কাঠি নয়। এটা ব্যবহারের আসল কারণগুলো:

  • কাজ আলাদা করা
  • ভারী কাজ asynchronous করা
  • System decouple করা
  • Retry ও failure handling
  • Load নিয়ন্ত্রণ
  • স্বাধীনভাবে scale

কিন্তু Service Bus যোগ করলেই system নির্ভরযোগ্য হয়ে যায় না। নির্ভরযোগ্য করতে একসাথে লাগে: idempotent consumer, সঠিক retry policy, DLQ monitoring, correlation ID, timeout handling, Outbox Pattern আর স্পষ্ট failure strategy।

Queue ব্যবহার করা সহজ। Queue-based system নির্ভরযোগ্য করা কঠিন।

Service Bus যোগ করার আগে নিজেকে তিনটা প্রশ্ন করুন:

  1. এই কাজটা কি সত্যিই asynchronous হওয়া দরকার?
  2. Message হারালে বা duplicate হলে কী হবে?
  3. Database update আর message publish-এর মাঝখানে failure হলে recovery plan কী?

এই তিনটার উত্তর পরিষ্কার থাকলে আপনি Service Bus সঠিক জায়গায় ব্যবহার করছেন।


আপনার system-এ কি এমন কোনো কাজ আছে যা এখনো API request-এর ভেতরেই চলছে? কমেন্টে জানান, আলোচনা করব।

Top comments (0)