باگی که هر همگام‌سازی دوطرفه باید حلش کند

تاریخ سررسید یک تسک را به تقویم گوگل بفرستید. بعداً، تغییرات را از گوگل بگیرید. اگر منطق گرفتن‌تان نتواند «کاربر واقعاً این رویداد را در گوگل جابه‌جا کرد» را از «این دقیقاً همان رویدادی است که یک ثانیه پیش فرستادم» تشخیص دهد، سیستمی ساخته‌اید که خودش را به‌روز می‌کند، به‌روزرسانی خودش را یک تغییر می‌بیند، دوباره می‌نویسدش، آن نوشتن را یک تغییر می‌بیند، و همین‌طور ادامه می‌دهد. فوری حلقه نمی‌زند — با هر بازه‌ای که کار همگام‌سازی‌تان اجرا می‌شود حلقه می‌زند — که بدترش می‌کند، نه بهترش: در لاگ‌ها شبیه فعالیت عادی به‌نظر می‌رسد تا وقتی کسی متوجه شود تاریخ سررسید یک تسک بی‌صدا دارد جابه‌جا می‌شود یا سهمیه‌ی API دارد توسط تسکی که کسی لمسش نکرده خورده می‌شود.

راه‌حل ساده‌لوحانه، و چرا کافی نیست

محافظ بدیهی این است: «فقط اگر تسک تغییر کرد بفرست.» اما این جلوی سمت گرفتن را نمی‌گیرد که فرستادن خودتان را با یک ویرایش بیرونی اشتباه بگیرد — فید تغییرات گوگل «یک تماس API از همین یکپارچه‌سازی این را تغییر داد» را از «یک انسان این را در تقویمش کلیک کرد» تشخیص نمی‌دهد. باید گرفتن نسبت به یک دسته‌ی خاص از «تغییرات» شکاک باشد: آن‌هایی که فقط بازتاب نوشته‌های خودتان هستند.

کاری که WKFGo واقعاً می‌کند: مقایسه‌ی محتوا، نه وجود

هر پیوند تسک-به-رویداد یک هش از فیلدهایی که آخرین بار فرستاده شدند نگه می‌دارد:

func contentHash(t TaskDue) string {
    sum := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%d|%d",
        t.Title, t.ProjectName, t.Due.UTC().Unix(), t.Progress)))
    return hex.EncodeToString(sum[:])
}

هنگام فرستادن، WKFGo هش را برای وضعیت فعلی تسک دوباره محاسبه می‌کند و با هش ذخیره‌شده روی پیوند مقایسه می‌کند. اگر یکی باشند، هیچ‌چیز UpdateEvent را صدا نمی‌زند — چیز جدیدی برای گفتن نیست، پس هیچ تماس API اتفاق نمی‌افتد، و هیچ «تغییر»ی برای گرفتن آینده ساخته نمی‌شود که اشتباه تفسیر شود:

if link.Hash != h {
    client.UpdateEvent(link.EventID, ev)
    link.Hash = h
}

در سمت گرفتن، محافظ حتی مستقیم‌تر است — یک تغییر ورودی از گوگل فقط زمانی روی تسک نوشته می‌شود که زمان شروع رویداد واقعاً با چیزی که تسک از قبل دارد فرق کند:

if !c.Start.IsZero() && !c.Start.Equal(due) {
    st.UpdateTaskDue(link.TaskID, c.Start)
}

رویدادی که دقیقاً همان چیزی را بازتاب می‌دهد که WKFGo تازه فرستاده، زمان شروعی دارد که با تاریخ سررسید فعلی تسک یکی است — پس این چک نادرست است، هیچ‌چیز نوشته نمی‌شود، و حلقه جایی برای ادامه ندارد. کامنت داخل کد صریح می‌گوید: «گرفتن فقط زمانی تاریخ سررسید تسک را بازمی‌نویسد که شروع رویداد واقعاً با سررسید فعلی تسک فرق کند، که همان چیزی است که جلوی برگشت‌خوردن فرستاده‌های خودمان به‌عنوان تغییر را می‌گیرد.»

ترتیب مهم است: گرفتن قبل از فرستادن

Sync هر چرخه اول pull را اجرا می‌کند، بعد push را — عمداً. اگر اول فرستادن اجرا می‌شد، یک تغییر که لحظاتی پیش در گوگل انجام شده بود می‌توانست بی‌صدا با مقدار قدیمی تسک بازنویسی شود، پیش از اینکه گرفتن اصلاً فرصت دیدنش را داشته باشد. اول گرفتن یعنی ویرایش‌های بیرونی قبل از تصمیم WKFGo درباره‌ی اینکه چیزی برای فرستادن هست یا نه، روی تسک می‌نشینند — پس یک ویرایش واقعی از سمت گوگل و یک ویرایش واقعی از سمت WKFGo با ترتیب اشتباه روی هم نمی‌روند.

وقتی یک پیوند کهنه می‌شود چه اتفاقی می‌افتد

اگر کاربر رویداد را مستقیم در گوگل حذف کند، گرفتن بعدی آن را Deleted می‌بیند و پیوند محلی را حذف می‌کند — خود تسک دست‌نخورده می‌ماند، پس فرستادن آینده به‌سادگی رویداد را دوباره می‌سازد به‌جای اینکه همگام‌سازی خطا بدهد. اگر تسکی دوباره تخصیص یابد یا تاریخ سررسیدش کاملاً پاک شود، فرستادن بعدی هر رویدادی که تسکش دیگر در مجموعه‌ی «سررسیددار و تخصیص‌یافته» نیست را هرس می‌کند، ورودی تقویم یتیم را حذف می‌کند به‌جای اینکه یک رویداد کهنه باقی بگذارد.

اشتباهات رایج

مقایسه‌ی برچسب زمانی به‌جای محتوا. «فقط همگام کن اگر updated_at تغییر کرد» همان لحظه‌ای خراب می‌شود که نوشته‌ی خودتان updated_at را به‌روز می‌کند — به همان حلقه برگشته‌اید، فقط روی فیلد دیگری.

اعتماد به فید تغییرات ارائه‌دهنده برای تشخیص ویرایش خودی از بیرونی. معمولاً نمی‌تواند — توکن همگام‌سازی تقویم گوگل هر تغییری از آخرین توکن را برمی‌گرداند، شامل آن‌هایی که یکپارچه‌سازی خودتان همین الان انجام داده.

گرفتن و فرستادن در همان پاس بدون تضمین ترتیب. اگر هر دو جهت بتوانند همزمان روی همان پیوند اجرا شوند، می‌توانید یک فرستادن در حال پرواز داشته باشید که با یک گرفتن که تازه نشسته مسابقه می‌دهد — ترتیبیِ گرفتن-سپس-فرستادن کاملاً از این ابهام دوری می‌کند.

سؤالات متداول

اگر توکن همگام‌سازی گوگل منقضی شود چه؟

ListChanges دوباره مقداردهی اولیه می‌کند و توکن تازه‌ای با هیچ تغییر گزارش‌شده‌ای برای آن تماس برمی‌گرداند — رویدادهای از‌پیش‌موجود فقط به این خاطر که توکن ریست شده به‌عنوان ویرایش‌های جدید اشتباه گرفته نمی‌شوند.

آیا شکست گرفتن، فرستادن را مسدود می‌کند؟

نه — خطای گرفتن ثبت می‌شود و همگام‌سازی به فرستادن ادامه می‌دهد، پس یک شکست موقت خواندن جلوی رسیدن تغییرات تاریخ سررسید تسک به تقویم را نمی‌گیرد.

می‌توانم یک حساب گوگل را به دو کاربر WKFGo وصل کنم؟

هر اتصال و توکن همگام‌سازی‌اش به‌ازای هر کاربر است؛ دو نفری که همان تقویم زیرین را همگام می‌کنند هرکدام یک اتصال جداگانه‌ی معتبر است، نه یک اتصال مشترک.

جمع‌بندی

باگ‌های همگام‌سازی دوطرفه تقریباً همیشه همین شکل را دارند: سیستم A نمی‌تواند انعکاس خودش در سیستم B را از یک تغییر بیرونیِ واقعی تشخیص دهد. راه‌حل زمان‌بندی هوشمندانه‌تر نیست — این است که «آیا واقعاً چیزی تغییر کرده» را یک مقایسه‌ی محتوا کنید، نه چک وجود یا برچسب زمانی، هم در سمت فرستادن هم در سمت گرفتن.