Skip to content

Commit c0f72df

Browse files
committed
Align behaviors about time zone
- Add `preserveInstants`, `connectionTimeZone` and `forceConnectionTimeZoneToSession` - Default `connectionTimeZone` to "LOCAL" - Mark `serverZoneId` as deprecated. It will notice users to use `connectionTimeZone` instead - Add `TimeZoneIntegrationTest` to test JDBC alignment of time zone behavior - Correct `OffsetTimeCodec` to not convert time zone
1 parent 068c0e3 commit c0f72df

27 files changed

+919
-229
lines changed

r2dbc-mysql/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,4 @@
151151
</dependency>
152152
</dependencies>
153153

154-
</project>
154+
</project>

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ public final class ConnectionContext implements CodecContext {
4848

4949
private final int localInfileBufferSize;
5050

51+
private final boolean preserveInstants;
52+
5153
@Nullable
52-
private ZoneId serverZoneId;
54+
private ZoneId timeZone;
5355

5456
/**
5557
* Assume that the auto commit is always turned on, it will be set after handshake V10 request message, or
@@ -60,12 +62,18 @@ public final class ConnectionContext implements CodecContext {
6062
@Nullable
6163
private volatile Capability capability = null;
6264

63-
ConnectionContext(ZeroDateOption zeroDateOption, @Nullable Path localInfilePath,
64-
int localInfileBufferSize, @Nullable ZoneId serverZoneId) {
65+
ConnectionContext(
66+
ZeroDateOption zeroDateOption,
67+
@Nullable Path localInfilePath,
68+
int localInfileBufferSize,
69+
boolean preserveInstants,
70+
@Nullable ZoneId timeZone
71+
) {
6572
this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null");
6673
this.localInfilePath = localInfilePath;
6774
this.localInfileBufferSize = localInfileBufferSize;
68-
this.serverZoneId = serverZoneId;
75+
this.preserveInstants = preserveInstants;
76+
this.timeZone = timeZone;
6977
}
7078

7179
/**
@@ -101,27 +109,33 @@ public CharCollation getClientCollation() {
101109
}
102110

103111
@Override
104-
public ZoneId getServerZoneId() {
105-
if (serverZoneId == null) {
112+
public boolean isPreserveInstants() {
113+
return preserveInstants;
114+
}
115+
116+
@Override
117+
public ZoneId getTimeZone() {
118+
if (timeZone == null) {
106119
throw new IllegalStateException("Server timezone have not initialization");
107120
}
108-
return serverZoneId;
121+
return timeZone;
109122
}
110123

111-
@Override
112-
public boolean isMariaDb() {
113-
return capability.isMariaDb() || serverVersion.isMariaDb();
124+
public boolean isTimeZoneInitialized() {
125+
return timeZone != null;
114126
}
115127

116-
boolean shouldSetServerZoneId() {
117-
return serverZoneId == null;
128+
@Override
129+
public boolean isMariaDb() {
130+
Capability capability = this.capability;
131+
return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb();
118132
}
119133

120-
void setServerZoneId(ZoneId serverZoneId) {
121-
if (this.serverZoneId != null) {
134+
void setTimeZone(ZoneId timeZone) {
135+
if (isTimeZoneInitialized()) {
122136
throw new IllegalStateException("Server timezone have been initialized");
123137
}
124-
this.serverZoneId = serverZoneId;
138+
this.timeZone = timeZone;
125139
}
126140

127141
@Override

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java

Lines changed: 40 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.r2dbc.spi.IsolationLevel;
3535
import io.r2dbc.spi.Lifecycle;
3636
import io.r2dbc.spi.R2dbcNonTransientResourceException;
37+
import io.r2dbc.spi.Readable;
3738
import io.r2dbc.spi.TransactionDefinition;
3839
import io.r2dbc.spi.ValidationDepth;
3940
import org.jetbrains.annotations.Nullable;
@@ -64,12 +65,6 @@ public final class MySqlConnection implements Connection, Lifecycle, ConnectionS
6465

6566
private static final String PING_MARKER = "/* ping */";
6667

67-
private static final String ZONE_PREFIX_POSIX = "posix/";
68-
69-
private static final String ZONE_PREFIX_RIGHT = "right/";
70-
71-
private static final int PREFIX_LENGTH = 6;
72-
7368
private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true);
7469

7570
private static final ServerVersion MYSQL_8_0_3 = ServerVersion.create(8, 0, 3);
@@ -333,7 +328,8 @@ public Mono<Void> setTransactionIsolationLevel(IsolationLevel isolationLevel) {
333328
requireNonNull(isolationLevel, "isolationLevel must not be null");
334329

335330
// Set subsequent transaction isolation level.
336-
return QueryFlow.executeVoid(client, "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql())
331+
return QueryFlow.executeVoid(client,
332+
"SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql())
337333
.doOnSuccess(ignored -> {
338334
this.sessionLevel = isolationLevel;
339335
if (!this.isInTransaction()) {
@@ -436,7 +432,7 @@ public Mono<Void> setStatementTimeout(Duration timeout) {
436432
final ServerVersion serverVersion = context.getServerVersion();
437433
final long timeoutMs = timeout.toMillis();
438434
final String sql = isMariaDb ? "SET max_statement_time=" + timeoutMs / 1000.0
439-
: "SET SESSION MAX_EXECUTION_TIME=" + timeoutMs;
435+
: "SET SESSION MAX_EXECUTION_TIME=" + timeoutMs;
440436

441437
// mariadb: https://mariadb.com/kb/en/aborting-statements/
442438
// mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/
@@ -485,10 +481,10 @@ static Mono<MySqlConnection> init(
485481
Mono<MySqlConnection> connection = initSessionVariables(client, sessionVariables)
486482
.then(loadSessionVariables(client, codecs, context))
487483
.map(data -> {
488-
ZoneId serverZoneId = data.serverZoneId;
489-
if (serverZoneId != null) {
490-
logger.debug("Set server time zone to {} from init query", serverZoneId);
491-
context.setServerZoneId(serverZoneId);
484+
ZoneId timeZone = data.timeZone;
485+
if (timeZone != null) {
486+
logger.debug("Got server time zone {} from loading session variables", timeZone);
487+
context.setTimeZone(timeZone);
492488
}
493489

494490
return new MySqlConnection(client, context, codecs, data.level, data.lockWaitTimeout,
@@ -531,21 +527,21 @@ private static Mono<Void> initSessionVariables(Client client, List<String> sessi
531527
return QueryFlow.executeVoid(client, query.toString());
532528
}
533529

534-
private static Mono<InitData> loadSessionVariables(
530+
private static Mono<SessionData> loadSessionVariables(
535531
Client client, Codecs codecs, ConnectionContext context
536532
) {
537533
StringBuilder query = new StringBuilder(160)
538534
.append("SELECT ")
539535
.append(transactionIsolationColumn(context))
540536
.append(",@@innodb_lock_wait_timeout AS l,@@version_comment AS v");
541537

542-
Function<MySqlResult, Flux<InitData>> handler;
538+
Function<MySqlResult, Flux<SessionData>> handler;
543539

544-
if (context.shouldSetServerZoneId()) {
545-
query.append(",@@system_time_zone AS s,@@time_zone AS t");
546-
handler = MySqlConnection::fullInit;
540+
if (context.isTimeZoneInitialized()) {
541+
handler = r -> convertSessionData(r, false);
547542
} else {
548-
handler = MySqlConnection::init;
543+
query.append(",@@system_time_zone AS s,@@time_zone AS t");
544+
handler = r -> convertSessionData(r, true);
549545
}
550546

551547
return new TextSimpleStatement(client, codecs, context, query.toString())
@@ -569,70 +565,39 @@ private static Mono<Void> initDatabase(Client client, String database) {
569565
});
570566
}
571567

572-
private static Flux<InitData> init(MySqlResult r) {
573-
return r.map((row, meta) -> new InitData(convertIsolationLevel(row.get(0, String.class)),
574-
convertLockWaitTimeout(row.get(1, Long.class)),
575-
row.get(2, String.class), null));
576-
}
577-
578-
private static Flux<InitData> fullInit(MySqlResult r) {
579-
return r.map((row, meta) -> {
580-
IsolationLevel level = convertIsolationLevel(row.get(0, String.class));
581-
long lockWaitTimeout = convertLockWaitTimeout(row.get(1, Long.class));
582-
String product = row.get(2, String.class);
583-
String systemTimeZone = row.get(3, String.class);
584-
String timeZone = row.get(4, String.class);
585-
ZoneId zoneId;
586-
587-
if (timeZone == null || timeZone.isEmpty() || "SYSTEM".equalsIgnoreCase(timeZone)) {
588-
if (systemTimeZone == null || systemTimeZone.isEmpty()) {
589-
logger.warn("MySQL does not return any timezone, trying to use system default timezone");
590-
zoneId = ZoneId.systemDefault();
591-
} else {
592-
zoneId = convertZoneId(systemTimeZone);
593-
}
594-
} else {
595-
zoneId = convertZoneId(timeZone);
596-
}
568+
private static Flux<SessionData> convertSessionData(MySqlResult r, boolean timeZone) {
569+
return r.map(readable -> {
570+
IsolationLevel level = convertIsolationLevel(readable.get(0, String.class));
571+
long lockWaitTimeout = convertLockWaitTimeout(readable.get(1, Long.class));
572+
String product = readable.get(2, String.class);
597573

598-
return new InitData(level, lockWaitTimeout, product, zoneId);
574+
return new SessionData(level, lockWaitTimeout, product, timeZone ? readZoneId(readable) : null);
599575
});
600576
}
601577

602-
/**
603-
* Creates a {@link ZoneId} from MySQL timezone result, or fallback to system default timezone if not
604-
* found.
605-
*
606-
* @param id the ID/name of MySQL timezone.
607-
* @return the {@link ZoneId}.
608-
*/
609-
private static ZoneId convertZoneId(String id) {
610-
String realId;
578+
private static ZoneId readZoneId(Readable readable) {
579+
String systemTimeZone = readable.get(3, String.class);
580+
String timeZone = readable.get(4, String.class);
611581

612-
if (id.startsWith(ZONE_PREFIX_POSIX) || id.startsWith(ZONE_PREFIX_RIGHT)) {
613-
realId = id.substring(PREFIX_LENGTH);
582+
if (timeZone == null || timeZone.isEmpty() || "SYSTEM".equalsIgnoreCase(timeZone)) {
583+
if (systemTimeZone == null || systemTimeZone.isEmpty()) {
584+
logger.warn("MySQL does not return any timezone, trying to use system default timezone");
585+
return ZoneId.systemDefault().normalized();
586+
} else {
587+
return convertZoneId(systemTimeZone);
588+
}
614589
} else {
615-
realId = id;
590+
return convertZoneId(timeZone);
616591
}
592+
}
617593

594+
private static ZoneId convertZoneId(String id) {
618595
try {
619-
switch (realId) {
620-
case "Factory":
621-
// It seems like UTC.
622-
return ZoneOffset.UTC;
623-
case "America/Nuuk":
624-
// America/Godthab is the same as America/Nuuk, with DST.
625-
return ZoneId.of("America/Godthab");
626-
case "ROC":
627-
// It is equal to +08:00.
628-
return ZoneId.of("+8");
629-
}
630-
631-
return ZoneId.of(realId, ZoneId.SHORT_IDS);
596+
return StringUtils.parseZoneId(id);
632597
} catch (DateTimeException e) {
633598
logger.warn("The server timezone is unknown <{}>, trying to use system default timezone", id, e);
634599

635-
return ZoneId.systemDefault();
600+
return ZoneId.systemDefault().normalized();
636601
}
637602
}
638603

@@ -691,7 +656,7 @@ private static String transactionIsolationColumn(ConnectionContext context) {
691656
"@@transaction_isolation AS i" : "@@tx_isolation AS i";
692657
}
693658

694-
private static class InitData {
659+
private static class SessionData {
695660

696661
private final IsolationLevel level;
697662

@@ -701,14 +666,14 @@ private static class InitData {
701666
private final String product;
702667

703668
@Nullable
704-
private final ZoneId serverZoneId;
669+
private final ZoneId timeZone;
705670

706-
private InitData(IsolationLevel level, long lockWaitTimeout, @Nullable String product,
707-
@Nullable ZoneId serverZoneId) {
671+
private SessionData(IsolationLevel level, long lockWaitTimeout, @Nullable String product,
672+
@Nullable ZoneId timeZone) {
708673
this.level = level;
709674
this.lockWaitTimeout = lockWaitTimeout;
710675
this.product = product;
711-
this.serverZoneId = serverZoneId;
676+
this.timeZone = timeZone;
712677
}
713678
}
714679
}

0 commit comments

Comments
 (0)