Skip to content

The MySQL plugin compare server and client timezone #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 26, 2017

Conversation

hiroyuki-sato
Copy link
Member

@muga @hito4t

Reference implementation This comment.

The embulk-input-mysql to compare server and client timezone.

I would like to request comments about this changes.

Questions

  • How to check server timezone parameter before load data.
    • I made before_load method for executing timezone check SQL.
  • How to get server timezone parameter
    • I executed the query select @@system_time_zone for get server timezone.
    • Older MySQL version doesn't support @@system_time_zone parameter. (I'll comment later)
    • I tried to use getServerTimezoneTZ() method. But It always returns null
  • How to get the client side parameter.
    • I used System.getProperty("user.timezone");
  • How to compare client and server timezone.
    • I used sys_tz.hasSameRules(usr_tz).
    • This method return false when I compare GMT+9 and JST (See below)

Same timezone

2017-04-29 21:57:47.042 +0900: Embulk v0.8.18
2017-04-29 21:57:48.972 +0900 [INFO] (0001:preview): Loaded plugin embulk/input/mysql from a load path
2017-04-29 21:57:49.031 +0900 [INFO] (0001:preview): Fetch size is 10000. Using server-side prepared statement.
2017-04-29 21:57:49.446 +0900 [INFO] (0001:preview): Fetch size is 10000. Using server-side prepared statement.
2017-04-29 21:57:49.455 +0900 [WARN] (0001:preview): The plugin will set `useLegacyDatetimeCode=false` by default in future.
2017-04-29 21:57:49.456 +0900 [INFO] (0001:preview): SQL: SELECT * FROM `time_test`
2017-04-29 21:57:49.461 +0900 [INFO] (0001:preview): > 0.00 seconds

Difference timezone

2017-04-29 22:00:55.618 +0900 [INFO] (0001:preview): Loaded plugin embulk/input/mysql from a load path
2017-04-29 22:00:55.671 +0900 [INFO] (0001:preview): Fetch size is 10000. Using server-side prepared statement.
2017-04-29 22:00:56.103 +0900 [INFO] (0001:preview): Fetch size is 10000. Using server-side prepared statement.
2017-04-29 22:00:56.119 +0900 [WARN] (0001:preview): The server timezone and client timezone are different, the plugin will fetch wrong datetime values.
2017-04-29 22:00:56.119 +0900 [WARN] (0001:preview): Use `options: { useLegacyDatetimeCode: false }`
2017-04-29 22:00:56.119 +0900 [WARN] (0001:preview): The plugin will set `useLegacyDatetimeCode=false` by default in future.
2017-04-29 22:00:56.120 +0900 [INFO] (0001:preview): SQL: SELECT * FROM `time_test`
2017-04-29 22:00:56.125 +0900 [INFO] (0001:preview): > 0.00 seconds

Compare timezone

import java.util.TimeZone;

class CompareTimeZone {
  public static void main(String args[]){
    TimeZone tz_jst   = TimeZone.getTimeZone("JST");
    TimeZone tz_tokyo = TimeZone.getTimeZone("Asia/Tokyo");
    TimeZone tz_gmt9  = TimeZone.getTimeZone("GMT+9");

    System.out.println(tz_jst);
    System.out.println(tz_tokyo);
    System.out.println(tz_gmt9);

    System.out.println(tz_jst.hasSameRules(tz_jst));
    System.out.println(tz_jst.hasSameRules(tz_gmt9));

  }
}
sun.util.calendar.ZoneInfo[id="JST",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null]
sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null]
sun.util.calendar.ZoneInfo[id="GMT+09:00",offset=32400000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
true
false

@hito4t
Copy link
Contributor

hito4t commented May 2, 2017

Thank you for the PR!
It will be very helpful if embulk-output-mysql can output such warning messages.

Though select @@system_time_zone may return "SYSTEM", we need to write more codes to detect server timezone.
http://stackoverflow.com/questions/2934258/how-do-i-get-the-current-time-zone-of-mysql

@hiroyuki-sato
Copy link
Member Author

@hito4t. Thank you for replying.

It seems @@global.time_zone not @@system_time_zone.
(It seems that @@system_time_zone does not return SYSTEM).

  • Do you know how to set @@system_time_zone to SYSTEM?
  • Should we care @@system_time_zone != @@global.time_zone case?

I tried MySQL Server version: 5.0.95.
It's the RPM package on CentOS5.

It @@system_time_zone worked.
It may support on the all of the supported MySQL versions.

mysql> SELECT @@global.time_zone, @@session.time_zone,@@system_time_zone;
+--------------------+---------------------+--------------------+
| @@global.time_zone | @@session.time_zone | @@system_time_zone |
+--------------------+---------------------+--------------------+
| SYSTEM             | SYSTEM              | JST                |
+--------------------+---------------------+--------------------+
1 row in set (0.00 sec)

@hito4t
Copy link
Contributor

hito4t commented May 2, 2017

I'm sorry, I've mistaken.

In my environment (Windows 7 and MySQL 5.6), the string value returned by SELECT @@system_time_zone was corrupt.
(regardless of whether MySQL character set is utf8 or sjis)

I could get right value by the following code.

ResultSet rs = statement.executeQuery("SELECT @@system_time_zone");
if (rs.next()) {
    byte[] b = rs.getBytes(1);
    String tz = new String(b, "MS932");
}

The result value was "東京 (標準時)" (Japanese).
I couldn't get right timezone by TimeZone.getTimeZone("東京 (標準時)");
It returned GMT, not JST.

The value of @@system_time_zone may depend on OS.

@hiroyuki-sato
Copy link
Member Author

hiroyuki-sato commented May 2, 2017

OMG, we can't use @@system_time_zone.

I'm investigating an alternative idea.

How about this? (need implement TODO part).

SELECT IF(TIMEDIFF(NOW(), UTC_TIMESTAMP) = TIME('00:00:00'),"UTC","TODO");

I want to output "GMT-07:00".

mysql> SELECT IF(TIMEDIFF(NOW(), UTC_TIMESTAMP) = TIME('00:00:00'),"UTC",concat("GMT",EXTRACT(HOUR FROM (TIMEDIFF(NOW(), UTC_TIMESTAMP))))) as timezone;
+----------+
| timezone |
+----------+
| GMT-7    |
+----------+
mysql> select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| PDT                |
+--------------------+
1 row in set (0.00 sec)
mysql> select timediff( now(), utc_timestamp());
+-----------------------------------+
| timediff( now(), utc_timestamp()) |
+-----------------------------------+
| -07:00:00                         |
+-----------------------------------+
1 row in set (0.00 sec)

@hiroyuki-sato
Copy link
Member Author

hiroyuki-sato commented May 2, 2017

@hito4t

Can you try the following SQL?

select IF(timediff(now(),utc_timestamp())=0,'UTC',
  IF(timediff(now(),utc_timestamp())>0,
    CONCAT('GMT+',TIME_FORMAT(timediff(now(),utc_timestamp()),'%h:%m')),
    CONCAT('GMT',TIME_FORMAT(timediff( now(), utc_timestamp()),'%h:%m')))) as offset;

OR

set @timediff = timediff(now(),utc_timestamp());
select IF(@timediff=0,'UTC',
  IF(@timediff>0,
    CONCAT('GMT+',TIME_FORMAT(@timediff,'%h:%m')),
    CONCAT('GMT',TIME_FORMAT(@timediff,'%h:%m')))) as offset;
+--------------------+
| @@system_time_zone |
+--------------------+
| JST                |
+--------------------+
1 row in set (0.00 sec)

mysql> select IF(timediff(now(),utc_timestamp())=0,'UTC',
    ->   IF(timediff(now(),utc_timestamp())>0,
    ->     CONCAT('GMT+',TIME_FORMAT(timediff(now(),utc_timestamp()),'%h:%m')),
    ->     CONCAT('GMT',TIME_FORMAT(timediff( now(), utc_timestamp()),'%h:%m')))) as offset;
+-----------+
| offset    |
+-----------+
| GMT+09:00 |
+-----------+
1 row in set (0.00 sec)
mysql> select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| PDT                |
+--------------------+
1 row in set (0.00 sec)

mysql> select IF(timediff(now(),utc_timestamp())=0,'UTC',
    ->   IF(timediff(now(),utc_timestamp())>0,
    ->     CONCAT('GMT+',TIME_FORMAT(timediff(now(),utc_timestamp()),'%h:%m')),
    ->     CONCAT('GMT',TIME_FORMAT(timediff( now(), utc_timestamp()),'%h:%m')))) as offset;
+-----------+
| offset    |
+-----------+
| GMT-07:00 |
+-----------+
1 row in set (0.00 sec)
mysql> select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| UTC                |
+--------------------+
1 row in set (0.00 sec)

mysql> select IF(timediff(now(),utc_timestamp())=0,'UTC',                                                                      ->   IF(timediff(now(),utc_timestamp())>0,
    ->     CONCAT('GMT+',TIME_FORMAT(timediff(now(),utc_timestamp()),'%h:%m')),
    ->     CONCAT('GMT',TIME_FORMAT(timediff( now(), utc_timestamp()),'%h:%m')))) as offset;
+--------+
| offset |
+--------+
| UTC    |
+--------+
1 row in set (0.00 sec)

@hiroyuki-sato
Copy link
Member Author

@hito4t

Please take a look again. I calculate GMT offset.

TZ=PST8PDT mysql.server start
mysql> select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| PDT                |
+--------------------+
1 row in set (0.00 sec)
2017-05-03 23:16:14.928 +0900 [WARN] (0001:preview): The server timezone offset(GMT-07:00) and client timezone(Asia/Tokyo) has different timezone offset. The plugin will fetch wrong datetime values.
2017-05-03 23:16:14.928 +0900 [WARN] (0001:preview): Use `options: { useLegacyDatetimeCode: false }`
2017-05-03 23:16:14.928 +0900 [WARN] (0001:preview): The plugin will set `useLegacyDatetimeCode=false` by default in future.

@hito4t
Copy link
Contributor

hito4t commented May 10, 2017

@hiroyuki-sato

Can you try the following SQL?

select IF(timediff(now(),utc_timestamp())=0,'UTC',
IF(timediff(now(),utc_timestamp())>0,
CONCAT('GMT+',TIME_FORMAT(timediff(now(),utc_timestamp()),'%h:%m')),
CONCAT('GMT',TIME_FORMAT(timediff( now(), utc_timestamp()),'%h:%m')))) as offset;

OR

set @timediff = timediff(now(),utc_timestamp());
select IF(@timediff=0,'UTC',
IF(@timediff>0,
CONCAT('GMT+',TIME_FORMAT(@timediff,'%h:%m')),
CONCAT('GMT',TIME_FORMAT(@timediff,'%h:%m')))) as offset;

Both worked fine!

I think SQL should return timezone offset as number and Java should convert it to timezone string because the timezone strings is interpreted in Java.

@hiroyuki-sato
Copy link
Member Author

@hito4t Thank you for your comment.

How do you think this query?

select TIME_TO_SEC(timediff(now(),utc_timestamp()))

Offset Timezone GMT
-25200 PST8PDT(PDT) GMT-07:00
0 UTC UTC
16200 Asia/Kabul GMT+04:50
32400 Asia/Tokyo GMT+09:00

Implementation Idea

  • SQL part

    • Query UTC offset using select TIME_TO_SEC(timediff(now(),utc_timestamp())).
    • We need query in seconds not in hour. because some area using half in hour (ex. Asia/Kabul: GMT+04:50 )
    • The query result will be 0, -25200, 16200... (called offset sec)
  • Java Part

    • If offset sec is zero, It's UTC.
    • Check offset sec is (+/-)
    • Calculate hour from offset sec 60sec * 60min
    • Calculate min from offset sec
    • make a zoneinfo like 'GMT+09:00'

Memo

@hiroyuki-sato
Copy link
Member Author

hiroyuki-sato commented May 10, 2017

@hito4t Please take a look again.

I think I should seperate TimeZone getter (builder?) class like the following.
How do you think?

public TimeZone MySQLTimeZoneBuilder.fromSystemConfig(Connection connection) throws SQLException;
public TimeZone MySQLTimeZoneBuilder.fromUTCOffsetSeconds(int offset);

This is the sample output.

2017-05-10 13:26:00.501 +0900 [WARN] (0001:preview): The server timezone offset(GMT-07:00) and client timezone(Asia/Tokyo) has different timezone offset. The plugin will fetch wrong datetime values.
2017-05-10 13:26:00.501 +0900 [WARN] (0001:preview): Use `options: { useLegacyDatetimeCode: false }`
2017-05-10 13:26:00.501 +0900 [WARN] (0001:preview): The plugin will set `useLegacyDatetimeCode=false` by default in future.

@hiroyuki-sato
Copy link
Member Author

hiroyuki-sato commented May 10, 2017

@hito4t, Please take a look again.
I made the class MySQLTimeZoneBuilder.

You can change timezone like the following quickly.

set global time_zone = "-07:30";

Copy link
Contributor

@hito4t hito4t left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hiroyuki-sato
Thank you!
Please see review comments.

//
String query = "select TIME_TO_SEC(timediff(now(),utc_timestamp()));";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statement and ResultSet are not closed.
They should be closed in finally clause.

Statement stmt = ...
try {
    ...
} finally {
    stmt.close();
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add stmt.close exception handling is still TODO.

ResultSet rs = stmt.executeQuery(query);

if (rs.next()) {
int offset_seconds = rs.getInt(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class names, method names and variable names (except for constant names) should be CamelCased in Java.
For example, offset_seconds should be offsetSeconds.
http://www.oracle.com/technetwork/java/codeconventions-135099.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable names use camel case like offsetSeconds.

}
}

public static TimeZone fromGMTOffsetSeconds(int offset_seconds)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to be public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to private.

int tz_min = abs_offset_sec % ONE_HOUR_SEC / ONE_MIN_SEC;
String tz_name = String.format(Locale.ENGLISH, "GMT%s%02d:%02d", sign, tz_hour, tz_min);
return TimeZone.getTimeZone(tz_name);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work for summer time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on the Twitter, I used getDSTSavings method like the below.
If this code is better,
I'll change this.

        TimeZone clientTimeZone = TimeZone.getDefault();
        Date today = new Date();
        int clientOffset = clientTimeZone.getRawOffset();

        if( clientTimeZone.inDaylightTime(today) ){
            clientOffset +=  clientTimeZone.getDSTSavings();
        }

TimeZone svr_tz = MySQLTimeZoneBuilder.fromSystemTimeZone(connection);

String usr_tz_name = System.getProperty("user.timezone");
TimeZone usr_tz = TimeZone.getTimeZone(usr_tz_name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get same value by TimeZone.getDefaultValue().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to TimeZone.getDefaultValue.

//
if( svr_tz.getRawOffset() != usr_tz.getRawOffset() ) {
logger.warn(String.format(Locale.ENGLISH,
"The server timezone offset(%s) and client timezone(%s) has different timezone offset. The plugin will fetch wrong datetime values.",svr_tz.getID(),usr_tz_name));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following message?
The client timezone(%s) is different from the server timezone(%s). ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to The client timezone(%s) is different from the server timezone(%s).

@@ -426,6 +426,8 @@ public TaskReport run(TaskSource taskSource,
LastRecordStore lastRecordStore = null;

try (JdbcInputConnection con = newConnection(task)) {
con.before_load();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newConnection is called by AbstractJdbcInputPlugin#transaction first, then called by AbstractJdbcInputPlugin#run.
So we should check timezone in the transaction method.

    @Override
    public ConfigDiff transaction(ConfigSource config,
            InputPlugin.Control control)
    {
        ...
        Schema schema;
        try (JdbcInputConnection con = newConnection(task)) {
            // TODO incremental_columns is not set => get primary key
            schema = setupTask(con, task);
        } catch (SQLException ex) {
            throw Throwables.propagate(ex);
        }

        return buildNextConfigDiff(task, control.run(task.dump(), schema, 1));
    }

Now setupTask method is private, but I think the method is appropriate to check timezone if it is changed to protected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed setupTask to protect and Override setupTask like the below.

    @Override
    protected Schema setupTask(JdbcInputConnection con, PluginTask task) throws SQLException
    {
        MySQLInputConnection mySQLCon = (MySQLInputConnection)con;
        mySQLCon.compareTimeZone();
        return super.setupTask(con,task);
    }

@hiroyuki-sato
Copy link
Member Author

@hito4t Thank you for your comment.

I wrote two implementations.
Which is better this PR and TimeZoneComparison version?

@hito4t
Copy link
Contributor

hito4t commented May 16, 2017

@hiroyuki-sato
Thank you!
I think the TimeZoneComparison version is better because codes about TimeZone are put into one class.
Can you implement "// TODO error check"?

@hiroyuki-sato
Copy link
Member Author

hiroyuki-sato commented May 17, 2017

@hito4t

Please take a look again. I implemented TODO part.
Some users recommend me to set serverTimeZone parameter too.
So I changed the error message like the following.

2017-05-17 15:16:13.994 +0900 [WARN] (0001:preview): The client timezone(Asia/Tokyo) is different from the server timezone(GMT-07:00). The plugin will fetch wrong datetime values.
2017-05-17 15:16:13.994 +0900 [WARN] (0001:preview): Use You may need to set options `useLegacyDatetimeCode` and `serverTimeZone`
2017-05-17 15:16:13.994 +0900 [WARN] (0001:preview): Ex. `options: { useLegacyDatetimeCode: false, serverTimeZone: UTC }`
2017-05-17 15:16:13.994 +0900 [WARN] (0001:preview): The plugin will set `useLegacyDatetimeCode=false` by default in future.

Copy link
Contributor

@hito4t hito4t left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hiroyuki-sato
Thank you.
I have a few comments.
Please check them.

serverTimeZone = getServerTimeZone();
}
catch (SQLException ex) {
logger.error(String.format(Locale.ENGLISH, "SQLException raised %s", ex.toString()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be logger.warn because it is not fatal even if timezone checking fails.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change logger.warn.

return fromGMTOffsetSeconds(offsetSeconds);
}
else {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If throwing SQLException instead of returning null, the caller doesn't have to check null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw SQLException if timezone comparison query fail. Simplified codes.

@hiroyuki-sato
Copy link
Member Author

@hito4t Thank you for reviewing.
Please take a look again when you have a time.

@hito4t
Copy link
Contributor

hito4t commented May 19, 2017

@hiroyuki-sato
Thank you!
That's good.
I'll merge it soon.

@hiroyuki-sato hiroyuki-sato changed the title [WIP] The MySQL plugin compare server and client timezone The MySQL plugin compare server and client timezone May 19, 2017
@hiroyuki-sato
Copy link
Member Author

@hito4t Thank you for reviewing.
Do I need to combine multiple commits into a single one?
(Do I need git rebase? )

@hito4t
Copy link
Contributor

hito4t commented May 19, 2017

@hiroyuki-sato
No, I can merge it as it is because it has no conflicts.

@hiroyuki-sato
Copy link
Member Author

@hito4t
OK, Thanks!

@hito4t hito4t merged commit ced0816 into embulk:master May 26, 2017
@hito4t hito4t added this to the 0.8.3 milestone May 26, 2017
@hiroyuki-sato hiroyuki-sato deleted the mysql_timezone_check branch May 26, 2017 06:46
@hiroyuki-sato
Copy link
Member Author

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants