From 1ff230f7c2ff709620ed06d25c33fea3faa4765b Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Oct 2022 11:20:31 +0200 Subject: [PATCH] [CONJ-1017] concurrent data/time/timestamp when using same calendar parameter missing synchronization --- .../mariadb/jdbc/plugin/codec/DateCodec.java | 13 ++-- .../mariadb/jdbc/plugin/codec/TimeCodec.java | 37 +++++---- .../jdbc/plugin/codec/TimestampCodec.java | 41 +++++----- .../mariadb/jdbc/integration/BatchTest.java | 77 ++++++++++++++++++- 4 files changed, 126 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java index a4f463ad2..4e83e631a 100644 --- a/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java +++ b/src/main/java/org/mariadb/jdbc/plugin/codec/DateCodec.java @@ -79,11 +79,14 @@ public void encodeText( public void encodeBinary(Writer encoder, Object value, Calendar providedCal, Long maxLength) throws IOException { Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal; - cal.setTimeInMillis(((java.util.Date) value).getTime()); - encoder.writeByte(4); // length - encoder.writeShort((short) cal.get(Calendar.YEAR)); - encoder.writeByte(((cal.get(Calendar.MONTH) + 1) & 0xff)); - encoder.writeByte((cal.get(Calendar.DAY_OF_MONTH) & 0xff)); + synchronized (cal) { + cal.clear(); + cal.setTimeInMillis(((java.util.Date) value).getTime()); + encoder.writeByte(4); // length + encoder.writeShort((short) cal.get(Calendar.YEAR)); + encoder.writeByte(((cal.get(Calendar.MONTH) + 1) & 0xff)); + encoder.writeByte((cal.get(Calendar.DAY_OF_MONTH) & 0xff)); + } } public int getBinaryEncodeType() { diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java index 0b3aa755b..0772bd24a 100644 --- a/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java +++ b/src/main/java/org/mariadb/jdbc/plugin/codec/TimeCodec.java @@ -82,23 +82,26 @@ public void encodeText( public void encodeBinary(Writer encoder, Object value, Calendar providedCal, Long maxLength) throws IOException { Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal; - cal.setTime((Time) value); - cal.set(Calendar.DAY_OF_MONTH, 1); - if (cal.get(Calendar.MILLISECOND) > 0) { - encoder.writeByte((byte) 12); - encoder.writeByte((byte) 0); - encoder.writeInt(0); - encoder.writeByte((byte) cal.get(Calendar.HOUR_OF_DAY)); - encoder.writeByte((byte) cal.get(Calendar.MINUTE)); - encoder.writeByte((byte) cal.get(Calendar.SECOND)); - encoder.writeInt(cal.get(Calendar.MILLISECOND) * 1000); - } else { - encoder.writeByte((byte) 8); // length - encoder.writeByte((byte) 0); - encoder.writeInt(0); - encoder.writeByte((byte) cal.get(Calendar.HOUR_OF_DAY)); - encoder.writeByte((byte) cal.get(Calendar.MINUTE)); - encoder.writeByte((byte) cal.get(Calendar.SECOND)); + synchronized (cal) { + cal.clear(); + cal.setTime((Time) value); + cal.set(Calendar.DAY_OF_MONTH, 1); + if (cal.get(Calendar.MILLISECOND) > 0) { + encoder.writeByte((byte) 12); + encoder.writeByte((byte) 0); + encoder.writeInt(0); + encoder.writeByte((byte) cal.get(Calendar.HOUR_OF_DAY)); + encoder.writeByte((byte) cal.get(Calendar.MINUTE)); + encoder.writeByte((byte) cal.get(Calendar.SECOND)); + encoder.writeInt(cal.get(Calendar.MILLISECOND) * 1000); + } else { + encoder.writeByte((byte) 8); // length + encoder.writeByte((byte) 0); + encoder.writeInt(0); + encoder.writeByte((byte) cal.get(Calendar.HOUR_OF_DAY)); + encoder.writeByte((byte) cal.get(Calendar.MINUTE)); + encoder.writeByte((byte) cal.get(Calendar.SECOND)); + } } } diff --git a/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java b/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java index d35101167..b8389bbbd 100644 --- a/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java +++ b/src/main/java/org/mariadb/jdbc/plugin/codec/TimestampCodec.java @@ -90,26 +90,29 @@ public void encodeText( @Override public void encodeBinary(Writer encoder, Object value, Calendar providedCal, Long maxLength) throws IOException { - Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal; Timestamp ts = (Timestamp) value; - cal.setTimeInMillis(ts.getTime()); - if (ts.getNanos() == 0) { - encoder.writeByte(7); // length - encoder.writeShort((short) cal.get(Calendar.YEAR)); - encoder.writeByte((cal.get(Calendar.MONTH) + 1)); - encoder.writeByte(cal.get(Calendar.DAY_OF_MONTH)); - encoder.writeByte(cal.get(Calendar.HOUR_OF_DAY)); - encoder.writeByte(cal.get(Calendar.MINUTE)); - encoder.writeByte(cal.get(Calendar.SECOND)); - } else { - encoder.writeByte(11); // length - encoder.writeShort((short) cal.get(Calendar.YEAR)); - encoder.writeByte((cal.get(Calendar.MONTH) + 1)); - encoder.writeByte(cal.get(Calendar.DAY_OF_MONTH)); - encoder.writeByte(cal.get(Calendar.HOUR_OF_DAY)); - encoder.writeByte(cal.get(Calendar.MINUTE)); - encoder.writeByte(cal.get(Calendar.SECOND)); - encoder.writeInt(ts.getNanos() / 1000); + Calendar cal = providedCal == null ? Calendar.getInstance() : providedCal; + synchronized (cal) { + cal.clear(); + cal.setTimeInMillis(ts.getTime()); + if (ts.getNanos() == 0) { + encoder.writeByte(7); // length + encoder.writeShort((short) cal.get(Calendar.YEAR)); + encoder.writeByte((cal.get(Calendar.MONTH) + 1)); + encoder.writeByte(cal.get(Calendar.DAY_OF_MONTH)); + encoder.writeByte(cal.get(Calendar.HOUR_OF_DAY)); + encoder.writeByte(cal.get(Calendar.MINUTE)); + encoder.writeByte(cal.get(Calendar.SECOND)); + } else { + encoder.writeByte(11); // length + encoder.writeShort((short) cal.get(Calendar.YEAR)); + encoder.writeByte((cal.get(Calendar.MONTH) + 1)); + encoder.writeByte(cal.get(Calendar.DAY_OF_MONTH)); + encoder.writeByte(cal.get(Calendar.HOUR_OF_DAY)); + encoder.writeByte(cal.get(Calendar.MINUTE)); + encoder.writeByte(cal.get(Calendar.SECOND)); + encoder.writeInt(ts.getNanos() / 1000); + } } } diff --git a/src/test/java/org/mariadb/jdbc/integration/BatchTest.java b/src/test/java/org/mariadb/jdbc/integration/BatchTest.java index cb28a0871..00178b218 100644 --- a/src/test/java/org/mariadb/jdbc/integration/BatchTest.java +++ b/src/test/java/org/mariadb/jdbc/integration/BatchTest.java @@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.*; import java.sql.*; +import java.util.Calendar; +import java.util.stream.Stream; import org.junit.jupiter.api.*; import org.mariadb.jdbc.Connection; import org.mariadb.jdbc.Statement; @@ -20,11 +22,14 @@ public static void beforeAll2() throws SQLException { stmt.execute( "CREATE TABLE BatchTest (t1 int not null primary key auto_increment, t2 LONGTEXT)"); createSequenceTables(); + stmt.execute("CREATE TABLE timestampCal(id int, val TIMESTAMP)"); } @AfterAll public static void after2() throws SQLException { - sharedConn.createStatement().execute("DROP TABLE IF EXISTS BatchTest"); + Statement stmt = sharedConn.createStatement(); + stmt.execute("DROP TABLE IF EXISTS timestampCal"); + stmt.execute("DROP TABLE IF EXISTS BatchTest"); } @Test @@ -414,4 +419,74 @@ private void batchWithError(Connection con) throws SQLException { assertThrows(BatchUpdateException.class, prep::executeBatch); } } + + private class TimestampCal { + private Timestamp val; + private int id; + + public TimestampCal(Timestamp val, int id) { + this.val = val; + this.id = id; + } + + public Timestamp getVal() { + return val; + } + + public int getId() { + return id; + } + + @Override + public String toString() { + return "TimestampCal{" + "val=" + val + ", id=" + id + '}'; + } + } + + @Test + public void ensureCalendarSync() throws SQLException { + Assumptions.assumeTrue(isMariaDBServer() && !isXpand()); + // to ensure that calendar is use at the same time, using BULK command + TimestampCal[] t1 = new TimestampCal[50]; + for (int i = 0; i < 50; i++) { + t1[i] = new TimestampCal(Timestamp.valueOf((1970 + i) + "-01-31 12:00:00.0"), i); + } + TimestampCal[] t2 = new TimestampCal[50]; + for (int i = 0; i < 50; i++) { + t2[i] = new TimestampCal(Timestamp.valueOf((1970 + i) + "-12-01 01:12:15.0"), i + 50); + } + + Calendar cal = Calendar.getInstance(); + + int inserts = Stream.of(t1, t2).parallel().mapToInt(l -> insertTimestamp(l, cal)).sum(); + assertEquals(100, inserts); + Statement stmt = sharedConn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM timestampCal order by ID"); + for (int i = 0; i < 50; i++) { + rs.next(); + assertEquals(t1[i].getVal().toString(), rs.getTimestamp(2, cal).toString()); + } + for (int i = 0; i < 50; i++) { + rs.next(); + assertEquals(t2[i].getVal().toString(), rs.getTimestamp(2, cal).toString()); + } + } + + private int insertTimestamp(TimestampCal[] vals, Calendar cal) { + try (Connection con = createCon()) { + try (PreparedStatement prep = + con.prepareStatement("INSERT INTO timestampCal(val, id) VALUES (?,?)")) { + for (int i = 0; i < vals.length; i++) { + System.out.println(vals[i]); + prep.setTimestamp(1, vals[i].getVal(), cal); + prep.setInt(2, vals[i].getId()); + prep.addBatch(); + } + return prep.executeBatch().length; + } + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } }