باگی که فقط زیر بار واقعی خودش را نشان میدهد
خلاصهی هفتگی سادهترین کار زمانبندیشده به نظر میرسد: هفتهای یکبار، روی ادمینها حلقه بزن، ایمیل بفرست. بعد دو نمونه از بکاند را پشت یک لودبالانسر اجرا میکنید، هر دو با یک ساعت، و هر دو در همان دقیقه بیدار میشوند تا ببینند «آیا الان دوشنبه ساعت ۸ صبح است؟» هر دو میگویند بله. هر دو میفرستند.
در محیط توسعه کسی این را نمیبیند، چون توسعه فقط یک پردازه دارد. اولین هفتهای که از یک نمونه فراتر میروید نمایان میشود — یک ادمین همان خلاصه را دوبار میگیرد، سه دقیقه فاصله، و از پشتیبانی میپرسد چیزی خراب شده؟
چرا یک پرچم بولی درستش نمیکند
راهحل غریزی یک پرچم است: alreadySent را چک کن، اگر نبود، بگذارش true و بفرست. این خوب کار میکند تا وقتی دو درخواست همزمان پرچم را میخوانند — قبل از اینکه هیچکدام true را برگردانند. هر دو میبینند «هنوز فرستاده نشده.» هر دو میفرستند. پرچم هیچوقت اشتباه نبود؛ فقط توالی چککن-بعد-عملکن اتمیک نیست، و هیچ تعداد پرچم اضافه یک شرط رقابتی متشکل از دو مرحلهی جدا را درست نمیکند.
راهحل واقعی: بگذار پایگاهداده خودش تکرار را رد کند
جدول خلاصه در WKFGo یک ایندکس یکتا روی (org_id, week_key) دارد، جایی که week_key شمارهی هفتهی سال به سبک ایزو است، مثل 2026-W28. فرستادن خلاصه یعنی اول ردیف را درج کن، بعد ایمیل را بساز:
if err := s.db.Create(&Send{OrgID: orgID, WeekKey: weekKey, ...}).Error; err != nil {
// unique violation → this week's digest was already sent (or raced)
return 0, nil
}
اگر دو نمونه مسابقه بدهند، پایگاهداده دو INSERT را به ترتیب میکند. یکی موفق میشود و ایمیل را میسازد و میفرستد. دیگری خطای نقض محدودیت یکتا میگیرد و فوراً برمیگردد، هیچ ایمیلی ساخته نمیشود، هیچچیز فرستاده نمیشود. هیچ بازهای نیست که هر دو چک کنند و هر دو رد شوند — چک کردن همان نوشتن است، و پایگاهداده فقط اجازه میدهد یکی از این نوشتنها موفق شود.
این همان اصل کلید ایمپوتنسی در یک API پرداخت است: نپرس «آیا این اتفاق افتاده؟» و بعد عمل کن — سعی کن ثبت کنی که اتفاق افتاده، و بگذار لایهی ذخیرهسازی داور باشد.
چرا این تعمیمپذیر است
هر مسئلهی «این دقیقاً یکبار اتفاق بیفتد» همین شکل را دارد: یک ایمیل یادآوری، تحویل یک وبهوک، ساختهشدن یک تسک تکرارشونده (به «تسکهای تکرارشونده بدون تکرار» نگاه کنید برای همین ایده در ساخت تسک). راهحل هیچوقت «دقیقتر چک کن» نیست — ساختن رکورد «این اتفاق افتاد» به همان عملیات چیزی است که قرار است فقط یکبار اتفاق بیفتد، پشتوانهاش محدودیتی که پایگاهداده اجرایش میکند، صرفنظر از اینکه چند پردازه در همان لحظه میپرسند.
چه چیزی هنوز به منطق برنامه نیاز دارد
محدودیت یکتا جلوی ارسالهای تکراری را میگیرد. تصمیم نمیگیرد چه کسی خلاصه بگیرد، یا ارسال دستی خارج از برنامه چه شکلی است. WKFGo حالت دوم را با یک کلید قابلتشخیص حل میکند — ارسال اجباری از کلید manual-<timestamp> بهجای هفتهی واقعی ایزو استفاده میکند، تا نه با هفتهی برنامهریزیشدهی بعدی تداخل کند و نه جلوی آن را بگیرد.
اشتباهات رایج
تکرارزدایی در سطح سرویس ایمیل بهجای جدول خودتان. برخی ریلههای SMTP قابلیت «سرکوب محتوای تکراری» دارند. اینها بر اساس هش موضوع/متن کار میکنند، که همان لحظهای که ایمیل را برای هر گیرنده شخصیسازی کنید خراب میشود.
استفاده از SELECT و بعد INSERT بهعنوان دو دستور جدا. این دقیقاً همان شرط رقابتی که میخواستید ببندید را دوباره باز میکند — فاصلهی بین خواندن و نوشتن همان جایی است که دو نمونه هر دو رد میشوند.
فراموش کردن اینکه ایندکس باید یکتا باشد، نه فقط موجود. یک ایندکس معمولی (غیریکتا) روی (org_id, week_key) جستجو را سریع میکند اما چیزی را اجرا نمیکند؛ فقط ایندکس یکتا باعث میشود INSERT دوم شکست بخورد.
سؤالات متداول
اگر ارسال نیمهکاره شکست بخورد — ردیف درج شده، اما SMTP تایماوت بدهد؟
ردیف درجشده باقی میماند (ارسال «ادعاشده» حساب میشود)، و تحویل واقعی ایمیل از صف تلاشمجدد موجود WKFGo عبور میکند، که از چک تکرارزدایی جداست. یک هفتهی ادعاشده-اما-تحویلنشده بهصورت خاموش دور بعدی بهعنوان تکراری دوباره تلاش نمیکند — این همان بدهبستان «حداکثر یکبار» در برابر «حداقل یکبار» است، و برای ایمیل خلاصه انتخاب درستی است.
آیا برای این به کرون جدا روی هر نمونه نیاز است؟
نه — هر نمونه همان تیکر را اجرا میکند و هر نمونه همان درج را امتحان میکند؛ محدودیت تصمیم میگیرد کدام برنده میشود. برای این تضمین خاص نیازی به انتخاب رهبر نیست (WKFGo برای کارهای پسزمینهی تکنمونهی دیگر مثل پاکسازی سطل بازیافت همچنان از انتخاب رهبر استفاده میکند، پس فقط یک نمونه تلاش میکند برای اکثر کارهای پسزمینه — این تکرارزدایی پشتیبان همان کاری است که ارزانش میکند بگذاریم هر نمونه امتحان کند).
آیا ادمین میتواند انصراف بدهد؟
بله، weekly_digest یک بولی بهازای هر کاربر است؛ کوئری گیرندگان قبل از اجرای این منطق تکرارزدایی روی آن فیلتر میکند.
جمعبندی
اگر دارید یک قابلیت «مطمئن شو این فقط یکبار اتفاق بیفتد» میسازید، سراغ پرچم و چک نروید — سراغ محدودیت یکتا و درج بروید. پایگاهداده دقیقاً برای داوری همین نوع شرط رقابتی ساخته شده، و آن را در یک قدم اتمیک انجام میدهد نه دو قدم رقابتی.