رفتن به محتوا

منطقه‌های زمانی: بزرگ‌ترین چالش مهندسی در UTasks

پیش از ساختن UTasks هدفی بلندپروازانه برای خودمان گذاشتیم: ابزاری بسازیم که هم‌زمان ساده، راحت و قابل‌اعتماد باشد. اما از میان همه مسئله‌های فنی که باید حل می‌کردیم، کار با منطقه‌های زمانی یکی از سخت‌ترین‌ها از آب درآمد.

چطور فهمیدیم منطقه‌های زمانی واقعاً سخت‌اند

Section titled “چطور فهمیدیم منطقه‌های زمانی واقعاً سخت‌اند”

تقریباً از همان ابتدا، آدم‌هایی از کشورهای مختلف شروع به استفاده از برنامه مدیریت وظایف ما کردند و بلافاصله با چالش پشتیبانی از چند منطقه زمانی روبه‌رو شدیم.

منطقه‌های زمانی فقط اختلاف ساعت میان مناطق نیستند. پیچیدگی شگفت‌آوری در خود پنهان دارند: تغییر ساعت فصلی (ساعت تابستانی و زمستانی)، تغییرات تاریخی قوانین، تاریخ‌های متفاوت تغییر ساعت در کشورها و حتی در مناطق مختلف یک کشور. ترجیحات کاربران، زمان‌بندی وظایف برای تاریخ‌های مشخص و همگام‌سازی خودکار میان دستگاه‌ها را هم به این اضافه کنید – و ابعاد مسئله روشن می‌شود.

اصطلاحات. چون گریزی از آن‌ها نیست.

Section titled “اصطلاحات. چون گریزی از آن‌ها نیست.”

منطقه زمانی دو ویژگی اصلی دارد: یک نام (مثلاً مسکو) و یک آفست (offset) – اختلاف زمان با منطقه زمانی صفر، بر حسب دقیقه.

مثال: منطقه زمانی مسکو – نام: مسکو، آفست: سه ساعت جلوتر از UTC، معادل ‎−۱۸۰ دقیقه.

UTC (زمان هماهنگ جهانی) – نقطه مرجع پذیرفته‌شده جهانی برای زمان روی کره زمین.

GMT (زمان میانگین گرینویچ) – منطقه زمانی صفر، با آفست ۰. هرچند GMT و UTC آفست یکسانی دارند، GMT بیشتر در رابط‌ها دیده می‌شود، چون اصطلاح آشناتری است. برای نمونه، در UTasks ساعت مسکو به‌صورت GMT+3 نمایش داده می‌شود.

همه منطق منطقه زمانی در UTasks نسبت به UTC کار می‌کند، اما رابط برای خوانایی، آفست‌ها را نسبت به GMT نشان می‌دهد.

UTasks چطور با منطقه‌های زمانی کار می‌کند

Section titled “UTasks چطور با منطقه‌های زمانی کار می‌کند”

UTasks زمان را با در نظر گرفتن دو ورودی مدیریت می‌کند:

  • منطقه زمانی دستگاه کاربر
  • منطقه زمانی انتخاب‌شده در تنظیمات UTasks

وقتی کاربر به منطقه زمانی دستگاه تکیه می‌کند

Section titled “وقتی کاربر به منطقه زمانی دستگاه تکیه می‌کند”

اگر کاربر منطقه زمانی دلخواهی تنظیم نکرده باشد، UTasks به‌طور خودکار با منطقه زمانی سیستم دستگاه همگام می‌شود.

مثال: کاربری در مسکو از ساعت مسکو (GMT+3) استفاده می‌کند. اگر به نووسیبیرسک سفر کند، UTasks منطقه زمانی تازه را از تنظیمات سیستم تلفن برمی‌دارد.

وقتی کاربر منطقه زمانی را دستی در UTasks تنظیم می‌کند

Section titled “وقتی کاربر منطقه زمانی را دستی در UTasks تنظیم می‌کند”

کاربر می‌تواند منطقه زمانی مشخصی را در تنظیمات UTasks ثابت کند. این منطقه زمانی، هر جا که دستگاه عملاً باشد، ثابت می‌ماند.

مثال: کاربری به وقت مسکو کار می‌کند، اما در سفر کاری به ولادی‌وستوک (GMT+10) است. در برنامه، مسکو (GMT+3) را تنظیم می‌کند تا وظایف به وقت شهر خودش نمایش داده شوند. برای اینکه گیج نشود، باید هر دو منطقه زمانی را در ذهن نگه دارد – و ما به حالش غبطه نمی‌خوریم.

علاوه بر تفاوت منطقه زمانی سیستم و برنامه، لایه دیگری از پیچیدگی هم به نحوه کار زمان وظیفه مربوط است.

اگر وظیفه‌ای فقط تاریخ داشته باشد و ساعت مشخصی نداشته باشد، برای کاربران مسکو، ولادی‌وستوک و لیسبون در همان روز ظاهر می‌شود – مثلاً دوشنبه ۲۸ اکتبر.

اما اگر وظیفه ساعت مشخصی داشته باشد، کاربران مسکو و ولادی‌وستوک آن را در ساعت‌های متفاوتی می‌بینند، چون این شهرها در منطقه‌های زمانی متفاوتی هستند. کاربر لیسبون هم علاوه بر این با تغییر ساعت تابستانی سروکار خواهد داشت.

فرض کنیم (کاملاً فرضی) دنیس در کازان زندگی می‌کند که ساعت ثابت UTC+3 دارد و ساعت تابستانی ندارد.

دنیس UTasks را باز می‌کند و سیستم:

  • منطقه زمانی او را از دستگاه می‌خواند
  • آفست این منطقه زمانی را ثبت می‌کند: ‎−۱۸۰ دقیقه (برای تبدیل زمان محلی به UTC، باید ۱۸۰ دقیقه کم کرد – شاید در مقایسه با قالب آشنای «GMT+3» خلاف انتظار به نظر برسد، اما در برنامه‌نویسی معمول است که با زمان محلی کار کنند و برای ذخیره، آن را به UTC تبدیل کنند)

دنیس برای ۱۰ نوامبر ساعت ۱۰:۰۰ به وقت محلی وظیفه‌ای می‌سازد. برای ذخیره آن، UTasks آفست ذخیره‌شده (‎−۱۸۰ دقیقه) را می‌خواند و زمان محلی (۱۰:۰۰) را با کم کردن ۱۸۰ دقیقه به UTC تبدیل می‌کند.

نتیجه: وظیفه به‌صورت ۱۰ نوامبر، ۰۷:۰۰ به وقت UTC ذخیره می‌شود.

چرا همه چیز را به UTC ذخیره کنیم؟

Section titled “چرا همه چیز را به UTC ذخیره کنیم؟”

ذخیره همه داده‌های زمانی به UTC، پردازش را در برنامه‌هایی که کاربرانش از منطقه‌های زمانی مختلف با آن کار می‌کنند ساده می‌کند:

  • اعلان‌ها برای منطقه زمانی همه شرکت‌کنندگان درست ارسال می‌شوند
  • یک بازه زمانی برای کاربران مناطق مختلف درست نمایش داده می‌شود
  • تبدیل‌های تکراری روی سرور لازم نیست – همه تبدیل‌ها در سمت کلاینت انجام می‌شود

وقتی دنیس برمی‌گردد تا وظیفه‌اش را ببیند، سرور زمان را به UTC برمی‌گرداند (۱۰ نوامبر، ۰۷:۰۰). UTasks آفست (‎−۱۸۰ دقیقه) را برعکس اعمال می‌کند و دنیس وظیفه را در ۱۰ نوامبر، ۱۰:۰۰ می‌بیند – به وقت محلی خودش.

هم‌تیمی‌های دنیس همین وظیفه را چطور می‌بینند:

Section titled “هم‌تیمی‌های دنیس همین وظیفه را چطور می‌بینند:”

دنیس برای کل تیم وظیفه‌ای می‌سازد: ۱۰ نوامبر، ساعت ۱۰:۰۰ به وقت مسکو.

وظیفه به‌صورت ۰۷:۰۰ به وقت UTC ذخیره می‌شود.

  • لیوشا (کالینینگراد، GMT+2): ۰۷:۰۰ به وقت UTC به‌علاوه ۲ ساعت = ۰۹:۰۰ به وقت کالینینگراد
  • کامیل (آستانه، GMT+6): ۰۷:۰۰ به وقت UTC به‌علاوه ۶ ساعت = ۱۳:۰۰ به وقت آستانه
  • لیسان (مالدیو، GMT+5): ۰۷:۰۰ به وقت UTC به‌علاوه ۵ ساعت = ۱۲:۰۰ به وقت مالدیو (اوج گرما)

این الگوریتم ساده تا وقتی درست کار می‌کند که منطقه زمانی سیستم کاربر با مکان واقعی او یکی باشد و تغییر نکند.

اوضاع وقتی پیچیده می‌شود که کاربر در برنامه منطقه زمانی‌ای انتخاب کند که با منطقه زمانی سیستم دستگاه فرق دارد.

فرض کنیم دنیس به لیسبون نقل مکان کرده – صبحانه پاستل د ناتا می‌خورد، عصرها در کافه با چای و شیرینی خستگی در می‌کند و به کلاس دفاع شخصی می‌رود – اما در ذهنش هنوز در کازان است: گاراژش و پنجاه شیشه خالی را می‌فروشد. در تنظیمات UTasks، ساعت مسکو (UTC+3) را دستی انتخاب می‌کند تا همه وظایفش در همان برنامه زمانی آشنا بمانند.

دنیس عملاً در لیسبون است، اما وظیفه‌ای برای «هفته بعد ساعت ۱۰:۰۰» می‌سازد و انتظار دارد این ساعت به وقت مسکو باشد.

مشکل: همه عملیات روی تاریخ در کد، در بستر منطقه زمانی سیستم دستگاه اجرا می‌شوند – در اینجا، لیسبون (UTC+1). اگر UTasks از یک الگوریتم ساده‌لوحانه استفاده می‌کرد، تاریخ را با منطقه زمانی سیستم (لیسبون) پردازش می‌کرد، نه مسکو.

در روش ساده‌لوحانه چه چیزی خراب می‌شود:

Section titled “در روش ساده‌لوحانه چه چیزی خراب می‌شود:”

دنیس وظیفه‌ای برای ساعت ۱۰:۰۰ به وقت مسکو تنظیم می‌کند ← آفست لیسبون (در تابستان یک ساعت) اعمال می‌شود ← وظیفه به‌جای ۰۷:۰۰ به وقت UTC مورد نظر (۱۰:۰۰ به وقت مسکو)، به‌صورت ۰۹:۰۰ به وقت UTC (یعنی ۱۲:۰۰ به وقت مسکو) به سرور فرستاده می‌شود ← وظیفه با زمان اشتباه ذخیره می‌شود، هرچند برنامه ظاهراً همان ۱۰:۰۰ درست را نشان می‌دهد ← اعلان‌ها دو ساعت دیر می‌رسند.

  • UTasks اختلاف میان منطقه زمانی سیستم دستگاه (لیسبون) و منطقه زمانی انتخاب‌شده کاربر در تنظیمات (مسکو) را حساب می‌کند. در این مثال: ۲ ساعت (UTC+3 منهای UTC+1).
  • پیش از ذخیره، UTasks زمان محلی را به اندازه همین اختلاف اصلاح می‌کند: دنیس ۱۰:۰۰ به وقت مسکو را وارد کرده ← زمان محلی دستگاه (لیسبون) اصلاح می‌شود: ۱۰:۰۰ منهای ۲ ساعت = ۰۸:۰۰ به وقت لیسبون.
  • UTasks زمان اصلاح‌شده را به UTC تبدیل می‌کند: ۰۷:۰۰ به وقت UTC – که دقیقاً ۱۰:۰۰ به وقت مسکو است و با انتظار دنیس جور درمی‌آید.

سخت‌ترین حالت: تغییر آفست درون یک منطقه زمانی

Section titled “سخت‌ترین حالت: تغییر آفست درون یک منطقه زمانی”

وقتی آفست درون یک منطقه زمانی به خاطر تغییر ساعت تابستانی عوض می‌شود، منطق از این هم پیچیده‌تر می‌شود.

لیسبون را در نظر بگیرید، جایی که ساعت‌ها در ۲۷ اکتبر، ساعت ۰۲:۰۰ به وقت تابستانی محلی، یک ساعت عقب کشیده می‌شوند.

در ۱۰ اکتبر (وقتی لیسبون هنوز ساعت تابستانی دارد)، دنیس بلیت کنسرت بیانسه در ۱۰ نوامبر را می‌خرد (وقتی لیسبون دیگر ساعت زمستانی خواهد داشت) و در UTasks یادآوری می‌سازد: ۱۰ نوامبر، ۱۰:۰۰ – «خرید گل برای بیانسه».

این چند سؤال پیش می‌آورد:

  • منظور دنیس ۱۰:۰۰ به وقت زمستانی (UTC+0) است یا ۱۰:۰۰ به وقت تابستانی (UTC+1)؟
  • UTasks چطور باید تغییر آفست را در نظر بگیرد تا زمان درست را ذخیره کند؟
  • دسته‌گل ۲۰ یورویی برای بیانسه مناسب است، یا بهتر است شب قبلش از چای و شیرینی در کافه بگذرد و ۲۵ یورو خرج کند؟

تغییر آفست به قوانین جاری منطقه زمانی بستگی دارد – قوانینی که دولت‌ها می‌توانند تغییر دهند و تغییر هم می‌دهند (لغو ساعت تابستانی، جابه‌جایی تاریخ تغییر ساعت و غیره). همین، پیش‌بینی رفتار دقیق را بدون اطلاعات به‌روز ناممکن می‌کند.

راه‌حل: استفاده از زمان سیستم دستگاه

Section titled “راه‌حل: استفاده از زمان سیستم دستگاه”

سیستم‌عامل قوانین جاری ساعت تابستانی را می‌داند. UTasks به‌جای نگهداری پایگاه داده منطقه‌های زمانی خودش، این کار را به سیستم‌عامل می‌سپارد:

  • درخواست زمان قالب‌بندی‌شده برای منطقه زمانی مقصد. وقتی دنیس تاریخی را در تقویم انتخاب می‌کند (مثلاً ۱۰ نوامبر، ۱۰:۰۰)، UTasks از سیستم می‌پرسد: «این تاریخ را برای منطقه زمانی لیسبون قالب‌بندی کن.»
  • برای ۱۰ اکتبر (ساعت تابستانی)، سیستم ۱۰:۰۰، ۱۰ اکتبر، UTC+1 را برمی‌گرداند
  • برای ۱۰ نوامبر (ساعت زمستانی)، سیستم ۱۰:۰۰، ۱۰ نوامبر، UTC+0 را برمی‌گرداند. سیستم‌عامل آفست درست را به‌طور خودکار برای تاریخ درخواست‌شده اعمال می‌کند.
  • استخراج آفست از پاسخ سیستم. نتیجه قالب‌بندی‌شده آفست را هم در خود دارد (یک ساعت برای تابستان، صفر برای زمستان). UTasks آن را می‌خواند و به کار می‌برد.
  • اصلاح زمان پیش از ارسال به سرور. UTasks اختلاف میان منطقه زمانی سیستم دستگاه (مسکو، UTC+3) و منطقه زمانی انتخاب‌شده کاربر (لیسبون) را حساب می‌کند و زمان را بر همین اساس اصلاح می‌کند: دنیس ۱۰ نوامبر، ۱۰:۰۰ را انتخاب می‌کند. منطقه زمانی دستگاه: مسکو (UTC+3). محاسبه: ۱۰:۰۰ منهای ۳ به‌علاوه ۰ = ۰۷:۰۰ به وقت UTC. سرور زمانی را دریافت می‌کند که از پیش برای منطقه زمانی کاربر و آفست جاری آن اصلاح شده است.

برای به‌روز نگه داشتن آفست‌ها، UTasks مرتب بررسی می‌کند که آیا آفست منطقه زمانی سیستم تغییر کرده است یا نه (مثلاً پس از تغییر ساعت ۲۸ اکتبر). اگر تغییر کرده باشد، برنامه به‌روز می‌شود و از آن پس مقادیر تازه را به کار می‌برد.

پس وقتی دنیس در ۱۰ اکتبر وظیفه‌ای برای ۱۰ نوامبر ساعت ۱۰:۰۰ می‌سازد، UTasks فرض می‌کند که او از تغییر ساعت پیش رو خبر دارد و آفست زمستانی (UTC+0) را در نظر دارد. آفست درست ۱۰ نوامبر هنگام ذخیره اعمال می‌شود و زمان دقیق می‌ماند.

کار با منطقه‌های زمانی یکی از دشوارترین چالش‌های مهندسی در ساختن UTasks از آب درآمد – و یکی از آموزنده‌ترین‌ها.

اصل اساسی: ذخیره همه چیز به UTC پایه‌ای قابل‌اعتماد است که کاربران مناطق مختلف را با هم هماهنگ نگه می‌دارد.

اما ساختن رابطی واقعاً راحت نیازمند رسیدگی به لایه‌های زیادی از ظرافت است: نمایش درست زمان بر اساس منطقه زمانی محلی و منطقه زمانی انتخاب‌شده کاربر، ارسال به‌موقع اعلان‌ها و رعایت قوانین فصلی آفست.

راه‌حل ما میان خودکارسازی (همگام‌سازی با زمان سیستم دستگاه) و انعطاف‌پذیری (تنظیم دستی منطقه زمانی) توازن برقرار می‌کند.

یک نکته دیگر: لیوشا، برنامه‌نویس ما، مرتب این عضله را ورزش می‌دهد – منطقه‌های زمانی را در برنامه دستی عوض می‌کند و نتیجه‌ها را دستی بررسی می‌کند. این‌طور است که موارد مرزی ظریفی را پیدا می‌کنیم که هیچ مقدار استدلال انتزاعی نمی‌تواند کاملاً پیش‌بینی‌شان کند.