Timezone handling pitfalls - part 2
This time I will focus on issues that arise when multiple users have to coordinate to a particular time, but they still can be in different timezones.
Note that this is a continuation from the previous post which you can find here.
Let’s recall our example domain:
- we are dealing with task scheduling
- the user can schedule a task for an arbitrary day in the future, and when that day comes she can mark it as done
- our users travel a lot, so they do not stick to just one timezone, we would like to allow them to complete the tasks wherever they currently are
which led us to implement floating timezones as I like to call it - business logic dependent on the user’s local timezone, and storing the dates in the naive form (not timezone-aware). All the details are in the previous part.
But the latest requirement destroyed our previous concept:
- We would like to introduce Teams, where users could rival on how many Tasks they complete. To make it fair, this time we will stick to one timezone for Tasks which will count towards the score
This time we cannot use the user’s local time, as it would not be fair to other users. We need to stick to a particular timezone against which we will validate the business rules, but additionally, we would still like to show to the users the time left to complete a particular Team Task considering their current timezone, as our users still travel a lot.
Timezone handling isn’t simple and is not easy
The time zones stuff tends to boil the brains of people, even experienced developers, and it wasn’t different from me. I’ll try to give you some examples of the problems that arise here.
As a User I would like to see the deadline to finish a task
when I’m in the Team common timezone
e.g CET (UTC-1), and assuming tasks usually last until an end of a day, but for the local time of the whole Team. So this means that we can have a Task which can be done between
24.09.2020 - 00:00to
24.09.2020 - 23:59CET, but when you convert it to UTC to store it into the database it is then
23.09.2020 - 23:00to
24.09.2020 - 23:59, then when the client (for example a mobile app) in CET timezone is requesting for details of such Task the time has to be shifted to CET, usually that shift is done by the client, as the client knows in which timezone it is.
when I’m traveling to a different timezone e.g. New York time
This part is quite similar: a Task is stored in UTC, New York is UTC-5 so the datetimes visible in the client are
23.09.2020 - 17:00to
24.09.2020 - 16:59, however, please notice one thing: if previously all the Tasks had a deadline at the end of the day (local time), then it now turns out that depending on the location of the client the deadline and availability of the Task to finish might happen somewhere in the mid-day!
For both of these examples also notice that it is the mobile client which “knows” in which timezone it is, thus we could do the conversion on the mobile client, the backend would return UTC datetimes, and the client would shift them appropriately, this is a common approach, right? But it will not work in our case, or would work poorly, let’s examine why.
We have a Task scheduled for
24.09.2020 - 00:00 to
24.09.2020 - 23:59 UTC, the mobile client is in New York (-5h) which makes the task available from
23.09.2020 - 19:00 to
24.09.2020 - 18:59 local time. When the mobile client what’s to view tasks for 23.09 it has to send that date in the request obviously, but look what happens now: the server does not know in which timezone is the mobile client, so it does not know how to convert 23.09 into UTC datetime period which matches that date, so the only possibility would be to convert that period using maximal and minimal possible timezone shifts (-11 / +14 hours) which makes the period greater than 24h, resulting in returning Tasks to the client that are possibly one day before or after the requested date, leaving to the mobile client the task of shifting and cleaning the data.
There is a better approach: just send to the server not only the local time but also the timezone, leaving the conversion for the server, so that it perfectly knows for what period return the Tasks. To make this conversion behavior consistent we should also do the conversion back from UTC to local time on the server.
When timezones change their time…
Another problem that arises here is the changes in the common timezone of the Team. That’s exactly the thing that previously mentioned floating timezones handle so well by just using local time without any timezone information, but due to previously mentioned “fairness” reasons, we cannot use them here. A perfect example of a very common time shift is the introduction of daylight saving time, still so common in Europe. What it does is changing the relation of the Team’s common timezone from for example -1 to -2 in Summer.
There’s no simple solution to this problem, the easiest is to take into account the appropriate shift when first converting the time to UTC, so for example when 2 Tasks are being scheduled, the first one for the date when there will be still CET time apply -1h, and the second already in the CEST - apply -2h shift. That’s the easy part, because if you know the date you know if it will be with or without DST shift.
If you ever encounter however a whole timezone shift, as it happened in Samoa Island around 2011 then I guess that it will require changing affected dates already written to the database.
Another problem, however, lies in scheduled jobs, which often are triggered based on a schedule. For example, when a Competition starts all of the competitors get a notification, it happens every Monday at 7:00, local time of course. Three solutions to that:
- Hardcode the schedule and reconfigure the application when required. Easiest but let’s face it, not the most pleasant approach, and will not work if you need to support multiple different timezones.
- Fire a scheduled job at least once per hour but base the job on a datetime taken from the database, which will be a UTC datetime
- Schedule jobs dynamically using a library like APScheduler
Converting to local time sometimes does not make sense
While developing the project it occurred to us that sometimes it does not make sense to convert times to the local timezone, or at least it feels clumsy to do so. One of the views in the mobile app was showing an aggregated number of completed Tasks in a particular day: if the user finished 3 Tasks of 4 at 25.09.2020 the view showed these numbers, it worked fine for so-called floating timezones when the datetimes where saved in the naive form, but when we introduced timezone-awareness it got weird because depending on the local timezone that finished Task might have counted for different dates or often two dates at once (recall a Task being available to do between mid-day hours despite being scheduled for the start of the day but in a different timezone). How to treat some cases? To be honest I do not know, as I think depends purely on the UX, in our case we just ditched that feature.