Wednesday, June 28, 2023

Handling time zones in distributed systems, Part 1

Learn how to use Java APIs to handle time zone conversions in your applications.


Handling dates and times in applications is a common and challenging problem requiring a particular design and implementation to ensure a correct solution. The problem is compounded because applications are required to handle customers across the globe, each with a different time zone. In many cases, those users’ countries (or regions within their countries) use Daylight Saving Time (DST), which must be considered.

Many countries, even large ones such as China and India, have only a single official time zone. On the other hand, some countries have several time zones. The United States has 4 time zones in the contiguous states and 5 more in Alaska, Hawaii, and other US territories. Russia has 11 time zones, and Australia has 3 time zones. And each of these countries has one or more separate daylight-saving policies. For example, part of the US state of Arizona follows DST. However, most parts of Arizona don’t follow DST.

Such challenges affect saving and searching for data with date filters, which can affect the functionality of applications.

This article explores the issue and presents a sample application implementation, with an API, to provide an end-to-end solution for a distributed environment. The sample code works well in Java 18 or later.

Stating the problem


In this article’s example business case, your customers are spread across multiple locations, and they might also travel through different time zones while using your software. You need to anticipate that your customers will create data in different time zones and access data while in different time zones, as shown in Figure 1.

Oracle Java Certification, Oracle Java Career, Oracle Java Skills, Oracle Java Jobs, Oracle Java Tutorial and Materials, Oracle Java Systems

Figure 1. A scenario for handling time zones in distributed systems

Imagine there’s a customer who will create data while in Egypt today but tomorrow will travel to Morocco and access the data. Meanwhile, that same data will be accessed by a colleague in Australia.

You can see that your infrastructure should spread across multiple time zones; otherwise, your users may gain access to the system from different time zones. This issue is not straightforward, as you can see in Figure 2. In this figure, one user and a server are in the US time zone called Central Time (“1” in the figure). Similarly, a user in Sydney is in the same time zone as a server there (“2” in the figure).

Oracle Java Certification, Oracle Java Career, Oracle Java Skills, Oracle Java Jobs, Oracle Java Tutorial and Materials, Oracle Java Systems

Figure 2. Some users and servers in the same time zone

What if a server doesn’t have users in the same time zone? That’s the situation shown in “3” of Figure 3.

Oracle Java Certification, Oracle Java Career, Oracle Java Skills, Oracle Java Jobs, Oracle Java Tutorial and Materials, Oracle Java Systems

Figure 3. A server in a time zone that doesn’t have users

Meanwhile, users in some time zones might need to access servers in different time zones, as shown in “4” and “5” of Figure 4.

Oracle Java Certification, Oracle Java Career, Oracle Java Skills, Oracle Java Jobs, Oracle Java Tutorial and Materials, Oracle Java Systems

Figure 4. Customers accessing a server from a different time zone

Time zone configuration


How are time zones configured in users’ computers (such as desktops or mobile devices) and in servers? Usually, when you install a service or application—especially a JVM-based application, a database server, or an application server—it will get the default time zone as well as the date and time configuration from the operating system. In the case of a mobile device, the time zone may be reset periodically by the operating system when the device talks to a local network, such as when you turn your phone back on after a flight.

Because this article’s example service is running on a distributed infrastructure, you can’t rely on the server’s default time zone; you need to override the default time zone configuration and assume that customers might create data from a different time zone.

In such a situation, the general approach is to capture the date and time, in database files and in logs, in a standard time format, which is usually Coordinated Universal Time (UTC), also known as Greenwich Mean Time (GMT). Therefore, when a customer is accessing date-related attributes from a different time zone, you need to identify the user’s local time zone and convert the UTC time zone data attribute into the user’s local time zone on the fly.

Figure 5 shows that a customer from Berlin (“1” in the figure) created a data record, and his local time was 21:30. (I’m using 24-hour time to keep things simple.) The record will be saved on the server as 20:30 after converting it to UTC (“2” in the figure). The customers from Tokyo (“3” in the figure) and Sydney (“4” in the figure) will access the same record and read the record creation time as 05:30 and 07:30 local time, respectively, of the following day.

Oracle Java Certification, Oracle Java Career, Oracle Java Skills, Oracle Java Jobs, Oracle Java Tutorial and Materials, Oracle Java Systems

Figure 5. Distributed time zone architecture

Representing date and time information in Java


Java provides several ways to represent date and time information.

The java.sql Date and Timestamp classes. Java offers two classes, Date and Timestamp, in the JDBC API to represent date and time information separately. They reside in the package java.sql. I don’t recommend using these classes because they will tie your Java Persistence API (JPA) entities’ date and time representation to JDBC-related API classes.

The util.Date class. The single class java.util.Date represents both date and time information. In JPA entities, the date and time will be differentiated with a @Temporal type annotation such as the following:

@Temporal(TemporalType.DATE)
private Date date;
@Temporal(TemporalType.TIMESTAMP)
private Date time;

I don’t recommend this approach either, because you need to use annotations to differentiate the date and time.

Java Date and Time API. With Java 8 in 2014, the java.time package was added to the platform as JSR 310, and it has been enhanced occasionally. It provides the Date and Time API, which is supported by all tools and frameworks and is considered the standard API for perfect date and time conversion.

By the way, if you are using the built-in java.util.Date and java.util.Calendar classes, you should migrate to the Data and Time API. Here’s why.

◉ For the most part, these date and calendar classes are not thread-safe.
◉ There are few practical methods to do date and time conversions.
◉ The time zone support they provide, especially for time zone conversions, is not straightforward.

The following are the advantages of using the java.time APIs:

◉ The java.time classes are immutable and thread-safe.
◉ They are ISO-centric especially with respect to the ISO 8601 date and time format, and they use consistent domain models for dates, time, durations, and periods. In addition, they have comprehensive utility methods for date and time operations.
◉ The java.time classes provide comprehensive functionality to manipulate date and time attributes among multiple time zones.
◉ Finally, java.time classes have been backported to Java 6 and Java 7.

The ZonedDateTime class. I want to call attention to the dedicated domain model in java.time, ZonedDateTime, which has a wide selection of interfaces and methods for time zone operations. In part, the documentation says the following:

public final class ZonedDateTime
extends Object
implements Temporal, ChronoZonedDateTime<LocalDate>, Serializable

A date-time with a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30+01:00 Europe/Paris.

ZonedDateTime is an immutable representation of a date-time with a time-zone. This class stores all date and time fields, to a precision of nanoseconds, and a time-zone, with a zone offset used to handle ambiguous local date-times. For example, the value “2nd October 2007 at 13:45.30.123456789 +02:00 in the Europe/Paris time-zone” can be stored in a ZonedDateTime.

This class handles conversion from the local time-line of LocalDateTime to the instant time-line of Instant. The difference between the two time-lines is the offset from UTC/Greenwich, represented by a ZoneOffset.

Converting between the two time-lines involves calculating the offset using the rules accessed from the ZoneId. Obtaining the offset for an instant is simple, as there is exactly one valid offset for each instant. By contrast, obtaining the offset for a local date-time is not straightforward. There are three cases:

◉ Normal, with one valid offset. For the vast majority of the year, the normal case applies, where there is a single valid offset for the local date-time.
◉ Gap, with zero valid offsets. This is when clocks jump forward typically due to the spring daylight savings change from “winter” to “summer”. In a gap there are local date-time values with no valid offset.
◉ Overlap, with two valid offsets. This is when clocks are set back typically due to the autumn daylight savings change from “summer” to “winter”. In an overlap there are local date-time values with two valid offsets.

External libraries


Several external libraries exist to address the deficiencies of the old java.util package classes. One is Joda-Time; however, that library’s authors recommended that you use Java 8’s java.time package.

Another alternative is Apache’s DateUtils class, which is described as a suite of utilities surrounding the use of the Calendar and Date object. Its documentation says

DateUtils contains a lot of common methods considering manipulations of Dates or Calendars. Some methods require some extra explanation. The truncate, ceiling and round methods could be considered the Math.floor(), Math.ceil() or Math.round versions for dates This way date-fields will be ignored in bottom-up order. As a complement to these methods we’ve introduced some fragment-methods. With these methods the Date-fields will be ignored in top-down order. Since a date without a year is not a valid date, you have to decide in what kind of date-field you want your result, for instance milliseconds or days.

Several methods are provided for adding to Date objects, of the form addXXX(Date date, int amount). It is important to note these methods use a Calendar internally (with default time zone and locale) and may be affected by changes to daylight saving time (DST).

One more library I’ll mention here is Apache Commons Lang3’s DateUtils class.

No matter which library you use, I urge you to follow a single approach or use a compatible library to avoid date and time inconsistencies. Think about maintenance when you’re working with multiple external code libraries. If you use external libraries extensively, evaluate each library’s current health and the future of each library, especially the number of active contributions.

I always tend to use one approach in my personal and business projects. I use Java 8’s java.time package classes as they are very fluent and easy to use; in addition, because of all the advantages mentioned above, those classes are always maintained as part of the standard JDK.

Zoned date and time conversion in action


Consider the following story: Mohamed is accepted as a speaker at Oracle’s JavaOne conference and would like to talk with Duke prior to the event.

Mohamed lives in the Europe/Belgrade time zone, also known as Central European time, and he created a calendar appointment by entering a date and time for when a phone call with Duke will occur. A meeting invitation was sent to Duke, who lives in San Francisco, California. When Duke views the invitation, it should reflect his time zone: Pacific Time.

Let’s see how this is done in code. The vital point is that the meeting will be held at the same instant worldwide; therefore, the code uses the Instant object for persistence, and without any time zone knowledge, the object’s value should be only in UTC. The rest of the story is shown in the code comments.

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class MeetingApplication {

  public static void main(String[] args) {
  
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /*
      1. Mohamed (in the "Europe/Belgrade" time zone)
     is creating a meeting slot with Duke in San Francisco.
    */

    // Mohamed's meeting date and time
    String mohamedTime = "2022-07-10 10:30:00";

    // Mohamed's time zone
    ZoneId mohamedZone = ZoneId.of("Europe/Belgrade");

    // Creating the meeting
    ZonedDateTime parsed = LocalDateTime.parse(mohamedTime, formatter)
                       .atZone(mohamedZone);

    System.out.println("1. Mohamed booked a meeting according to his time zone at: " + parsed);
    // will print: 2022-07-10T10:30+02:00[Europe/Belgrade]

    // 2. Send the calendar invite and save the event
    Instant instant = parsed.toInstant();

    // Invitation (instant) is stored in the database
    System.out.println("2. Mohamed meeting time saved in database as UTC equivalent: " + instant);
    // will print: 2022-07-10T08:30:00Z

    /*
      Duke (in the "US/San Francisco" time zone) is viewing the meeting
  DateTime Mohamed has booked to determine when exactly the meeting is.
    */

    // Initialize Duke time zone.
    ZoneId dukeZone = ZoneId.of("America/Los_Angeles");

    // The meeting time is retrieved from the database (instant) with Duke's time zone.
    ZonedDateTime dukeTime = ZonedDateTime.ofInstant(instant, dukeZone);

    System.out.println("3.1. Duke meeting will be at (formatted): " + dukeTime.format(formatter));
    // will print: 2022-07-10 01:30:00

    System.out.println("3.2. Duke meeting will be at: " + dukeTime);
    // will print: 2022-07-10T01:30-07:00[America/Los_Angeles]

    // Mohamed would like to make sure of the meeting time
    System.out.println("4. Again, Mohamed is checking the meeting time: " + ZonedDateTime
            .ofInstant(instant, mohamedZone)
            .format(formatter)); // will print: 2022-07-10 10:30:00
  }
}

This simple example gives you an idea about how to convert from one zoned date and time to another in a distributed system.

Consider that the Instant class is for computers. Date and time variant classes are for humans. The Instant class is the natural way of representing the time for computers, but it is often useless to humans. Instant is usually preferred for storage (for example, in a database), but you may need to use other classes such as ZonedDateTime when presenting data to a user. Thus

◉ The date and time are always saved in the database in the UTC format.
◉ The time zone should always be present for conversion from Instant to any date and time equivalent classes.

Source: oracle.com

Related Posts

0 comments:

Post a Comment