ءPriority بهتنهایی کافی نیست؛ Provider هم محدودیت دارد
فرض کن Queue ما آماده است، اما SMS Provider فقط اجازه میدهد:
100 پیامک در ثانیه
ارسال کنیم.
اگر Dispatcher با سرعت ۱۰ هزار پیامک در ثانیه به Provider درخواست بفرستد، اتفاقهای بدی رخ میدهد:
خطای Rate Limit ،Timeout،Retry زیاد ،فشار روی شبکه ،افزایش Backlog و در نهایت Retry Storm
پس بعد از Priority Queue به یک Rate Limiter نیاز داریم:
ءRate Limiter باید ظرفیت واقعی Provider را رعایت کند.
برای مثال:
Provider Limit:
100 SMS/sec
Dispatcher:
بیشتر از 100 ارسال در ثانیه انجام ندهد
اما اگر OTP در صف باشد، نباید با افزایش Priority، محدودیت Provider را دور بزنیم. Priority یعنی:
این پیام زودتر انتخاب شود.
نه اینکه:
این پیام بدون توجه به ظرفیت Provider ارسال شود.
🔁 ءRetry را داخل همان صف اصلی نریزیم
فرض کن ارسال یک پیامک شکست میخورد.
راه ساده اما خطرناک:
Send Failed
↓
Put back into Main Queue
اگر Provider مشکل داشته باشد، پیام دائماً از Queue خارج و دوباره وارد آن میشود.
راه بهتر، داشتن Retry Policy است:
Attempt 1:
بعد از 1 ثانیه
Attempt 2:
بعد از 5 ثانیه
Attempt 3:
بعد از 30 ثانیه
در عمل باید از:
Exponential Backoff
Jitter
Maximum Attempts
Error Classification
Provider Retry-After
استفاده شود.
همهی خطاها هم نباید Retry شوند.
مثلاً:
Temporary Timeout:
احتمالاً Retry شود
Rate Limit:
با Backoff و رعایت Retry-After
Invalid Phone Number:
Retry نکن
Blocked Destination:
Retry نکن
پیامهای شکستخوردهی موقت بهتر است وارد صفی مانند این شوند:
sms-retry
و پیامهایی که پس از تعداد مشخصی تلاش همچنان شکست خوردهاند، به اینجا منتقل شوند:
sms-dead-letter
☠️ ءDead Letter Queue
اگر یک پیام بعد از چند بار تلاش پردازش نشود، نباید برای همیشه در صف اصلی بماند.
مثلاً:
Max Attempts = 5
بعد از پنجمین شکست:
Main Queue
↓
Dead Letter Queue
ءDLQ برای این موارد مفید است:
بررسی علت شکست
اصلاح دادهی خراب
ءRetry دستی
گزارشگیری
جلوگیری از گیرکردن صف اصلی
اما DLQ نباید تبدیل به سطل زباله شود.
باید برای آن:
Monitoring
Alert
Retention Policy
و فرآیند بررسی داشته باشیم.
🧱 ءDuplicate؛ آیا یک پیامک ممکن است دوبار ارسال شود؟ بله.
فرض کن Provider پیامک را دریافت کرده، اما پاسخ موفقیت به سیستم ما نرسیده است:
Our Service → Provider
│
├── SMS sent
└── Response lost
سیستم ما فکر میکند ارسال شکست خورده و دوباره Retry میکند:
Retry → Provider
حالا ممکن است کاربر دو پیامک دریافت کند.
برای کاهش این مشکل باید از یک شناسهی یکتا استفاده کنیم:
{
"messageId": "sms-8f91",
"idempotencyKey": "otp-login-user-42-request-981"
}اما یک نکتهی مهم:
داشتن Idempotency Key در سیستم خودمان، تضمین نمیکند Provider نیز ارسال را دقیقاً یکبار انجام دهد.
این موضوع به قابلیتهای خود Provider وابسته است.
پس باید:
وضعیت ارسال را ذخیره کنیم؛
شناسهی پیام را به Provider منتقل کنیم، اگر پشتیبانی میشود؛
ءCallback و Delivery Report را مدیریت کنیم؛
و Duplicate احتمالی را در طراحی در نظر بگیریم.
⏱️ پیامک قدیمی همیشه ارزش ارسال ندارد
فرض کن پیامک OTP مربوط به ۲۰ دقیقه قبل هنوز در Queue است.
ارسال آن دیگر فایدهای ندارد.
یا پیامک وضعیت سفارش مربوط به سفارشی است که لغو شده.
پس هر پیام باید اطلاعاتی مانند این داشته باشد:
{
"messageId": "msg-123",
"priority": "Critical",
"createdAt": "2026-09-15T17:00:00Z",
"expiresAt": "2026-09-15T17:01:00Z",
"attempt": 0
}ءDispatcher قبل از ارسال بررسی میکند:
اگر now > expiresAt:
پیام را منقضی کن
ارسال نکن
این موضوع برای جلوگیری از Stale Work مهم است.
گاهی پردازشکردن یک پیام قدیمی بدتر از پردازشنکردن آن است.
🎯 نتیجهگیری
در سیستم بزرگ، Priority Queue فقط به معنی این نیست که:
پیام مهمتر را زودتر از صف بردار.
طراحی درست باید این مسائل را همزمان حل کند:
Priority
+
Fairness
+
Rate Limiting
+
Retry
+
Idempotency
+
Expiration
+
Dead Letter Queue
+
Observability
پس معماری مناسب برای سیستم پیامک بلکه چیزی شبیه این است:
Application
↓
Classification
↓
Priority Queues
↓
Fair Dispatcher
↓
Provider Rate Limiter
↓
SMS Provider
↓
Delivery Tracking
و مهمترین تصمیم مهندسی:
پیامک OTP و پیامک تبلیغاتی نباید فقط به خاطر اینکه هر دو «SMS» هستند، با یک SLA و یک مسیر پردازش مدیریت شوند.
چون در سیستمهای واقعی، همهی پیامها ارزش یکسانی ندارند. 📩