এই লেখা পড়ে আপনি কী পাবেন
একবার পড়লেই যেন নিচের প্রশ্নগুলোর উত্তর নিজে দিতে এবং অন্যকে বোঝাতে পারেন:
- 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-কে করতে হবে:
- Current membership খুঁজে বের করা
- Target membership ঠিক করা
- Eligibility check
- Purchase rule evaluate
- Renewal status update
- 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
রেস্টুরেন্টের উদাহরণ ভাবুন। ওয়েটার (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 দেখে
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
সবাই একই queue থেকে টানে, প্রতিটা message শুধু একজন পায়। কাজ বাড়লে worker instance বাড়ান।
৭. Failure isolation। Worker crash করলে API চলতেই থাকে।
৪. Queue বনাম Topic
Queue = একটা message, একজন consumer।
Producer → Queue → একজন Consumer
যেমন: bulk renewal, invoice generation, email পাঠানো, report তৈরি।
Topic + Subscription = একটা message, অনেক আগ্রহী পক্ষ।
┌── Billing Subscription
Producer → Topic ├── Notification Subscription
└── Audit Subscription
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
৮. সবচেয়ে বড় ফাঁদ: Dual-Write Problem
অনেকে ভাবেন Service Bus ব্যবহার করলেই data consistency নিশ্চিত। না। এই কোডটা দেখুন:
// ❌ বিপজ্জনক
await _db.SaveChangesAsync(); // 1. DB commit সফল
await _sender.SendMessageAsync(message); // 2. এখানে connection failure!
ফলাফল: 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" চিহ্নিত
- 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
);
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();
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
}
}
লক্ষ্য করুন, 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
গুরুত্বপূর্ণ: "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);
}
};
চেকলিস্ট:
- Message ছোট রাখুন। বড় data DB বা blob storage-এ রাখুন, message-এ শুধু reference পাঠান:
{ "processId": 259, "itemIds": [26114] }
(Standard tier-এ message size-এর সীমা আছে, তাই বড় payload এমনিতেই সমস্যা।)
- Batch-wise process করুন। ১০,০০০ জনের জন্য একটা বিশাল message নয়, বরং ছোট ছোট batch (যেমন ৫০টা item) আলাদা message। এতে retry ছোট থাকে আর progress সুন্দরভাবে দেখানো যায়।
- Error classify করুন: transient হলে retry, permanent বা business failure হলে retry নয়।
- Poison message-কে অনন্ত retry করবেন না। Max delivery count আর DLQ আছে এজন্যই।
- DLQ নিয়মিত দেখুন। DLQ থাকাই যথেষ্ট নয়। কোন message, কতবার, কেন fail করল, আবার চালানো যাবে কি না, manual হস্তক্ষেপ লাগবে কি না, এগুলো দেখার প্রক্রিয়া থাকতে হবে।
- Correlation ID রাখুন, যাতে পুরো পথ trace করা যায়:
API request → DB update → Message publish → Worker processing → Final result
- Cancellation / shutdown সামলান। Application বন্ধ হওয়ার সময় চলমান message-এর কী হবে তা পরিষ্কার রাখুন। Complete না করলে lock শেষে message আবার আসবে, আর সেটা সামলানোর জন্যই idempotency।
- 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 দেখে
Client-এর জন্য:
{ "processId": 259, "status": "Queued" }
GET /bulk-renew/processes/{processGuid}
→ { "status": "Processing", "total": 10000, "succeeded": 4200, "failed": 12 }
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 যোগ করার আগে নিজেকে তিনটা প্রশ্ন করুন:
- এই কাজটা কি সত্যিই asynchronous হওয়া দরকার?
- Message হারালে বা duplicate হলে কী হবে?
- Database update আর message publish-এর মাঝখানে failure হলে recovery plan কী?
এই তিনটার উত্তর পরিষ্কার থাকলে আপনি Service Bus সঠিক জায়গায় ব্যবহার করছেন।
আপনার system-এ কি এমন কোনো কাজ আছে যা এখনো API request-এর ভেতরেই চলছে? কমেন্টে জানান, আলোচনা করব।
Top comments (0)