Skip to content

Commit fe4bc21

Browse files
authored
Fix timezone issue during reads and writes (#583)
1 parent 2c2f88d commit fe4bc21

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

dao-impl/ebean-dao/src/main/java/com/linkedin/metadata/dao/EbeanLocalAccess.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
import javax.persistence.PersistenceException;
4747
import lombok.extern.slf4j.Slf4j;
4848
import org.json.simple.JSONObject;
49+
import java.time.Instant;
50+
import java.time.ZoneOffset;
51+
import java.time.format.DateTimeFormatter;
4952

5053
import static com.linkedin.metadata.dao.EbeanLocalDAO.*;
5154
import static com.linkedin.metadata.dao.utils.EBeanDAOUtils.*;
@@ -59,6 +62,7 @@
5962
*/
6063
@Slf4j
6164
public class EbeanLocalAccess<URN extends Urn> implements IEbeanLocalAccess<URN> {
65+
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
6266
private final EbeanServer _server;
6367
private final Class<URN> _urnClass;
6468
private final String _entityType;
@@ -127,8 +131,12 @@ public <ASPECT extends RecordTemplate> int addWithOptimisticLocking(
127131
} else {
128132
sqlUpdate = _server.createSqlUpdate(SQLStatementUtils.createAspectUpsertSql(urn, aspectClass, urnExtraction, isTestMode));
129133
}
134+
135+
String utcTimestamp = Instant.ofEpochMilli(timestamp)
136+
.atZone(ZoneOffset.UTC)
137+
.format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT));
130138
sqlUpdate.setParameter("urn", urn.toString())
131-
.setParameter("lastmodifiedon", new Timestamp(timestamp).toString())
139+
.setParameter("lastmodifiedon", utcTimestamp)
132140
.setParameter("lastmodifiedby", actor);
133141

134142
// If a non-default UrnPathExtractor is provided, the user MUST specify in their schema generation scripts
@@ -146,7 +154,7 @@ public <ASPECT extends RecordTemplate> int addWithOptimisticLocking(
146154
.setAspect(RecordUtils.toJsonString(newValue))
147155
.setCanonicalName(aspectClass.getCanonicalName())
148156
.setLastmodifiedby(actor)
149-
.setLastmodifiedon(new Timestamp(timestamp).toString())
157+
.setLastmodifiedon(utcTimestamp)
150158
.setCreatedfor(impersonator, SetMode.IGNORE_NULL);
151159
if (ingestionTrackingContext != null) {
152160
auditedAspect.setEmitTime(ingestionTrackingContext.getEmitTime(), SetMode.IGNORE_NULL);
@@ -248,13 +256,16 @@ public <ASPECT_UNION extends RecordTemplate> int create(
248256

249257
sqlUpdate = _server.createSqlUpdate(insertStatement);
250258

259+
String utcTimestamp = Instant.ofEpochMilli(timestamp)
260+
.atZone(ZoneOffset.UTC)
261+
.format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT));
251262
// Set parameters for each aspect value
252263
for (int i = 0; i < aspectValues.size(); i++) {
253264
AuditedAspect auditedAspect = new AuditedAspect()
254265
.setAspect(RecordUtils.toJsonString(aspectValues.get(i)))
255266
.setCanonicalName(aspectCreateLambdas.get(i).getAspectClass().getCanonicalName())
256267
.setLastmodifiedby(actor)
257-
.setLastmodifiedon(new Timestamp(timestamp).toString())
268+
.setLastmodifiedon(utcTimestamp)
258269
.setCreatedfor(impersonator, SetMode.IGNORE_NULL);
259270
if (ingestionTrackingContext != null) {
260271
auditedAspect.setEmitTime(ingestionTrackingContext.getEmitTime(), SetMode.IGNORE_NULL);
@@ -270,7 +281,7 @@ public <ASPECT_UNION extends RecordTemplate> int create(
270281
sqlUpdate.setParameter("a_urn", toJsonString(urn));
271282
}
272283
sqlUpdate.setParameter("urn", urn.toString())
273-
.setParameter("lastmodifiedon", new Timestamp(timestamp).toString())
284+
.setParameter("lastmodifiedon", utcTimestamp)
274285
.setParameter("lastmodifiedby", actor);
275286

276287
return sqlUpdate.execute();
@@ -399,9 +410,11 @@ public <ASPECT extends RecordTemplate> ListResult<ASPECT> list(@Nonnull Class<AS
399410
final ASPECT aspect = RecordUtils.toRecordTemplate(aspectClass,
400411
extractAspectJsonString(sqlRow.getString(getAspectColumnName(urn.getEntityType(), aspectClass))));
401412
final ListResultMetadata listResultMetadata = new ListResultMetadata().setExtraInfos(new ExtraInfoArray());
413+
414+
Timestamp utcTimeStamp = timeStampStringToTimeStamp(sqlRow.getString("lastmodifiedon"));
402415
final ExtraInfo extraInfo = new ExtraInfo().setUrn(urn)
403416
.setVersion(LATEST_VERSION)
404-
.setAudit(makeAuditStamp(sqlRow.getTimestamp("lastmodifiedon"), sqlRow.getString("lastmodifiedby"),
417+
.setAudit(makeAuditStamp(utcTimeStamp, sqlRow.getString("lastmodifiedby"),
405418
sqlRow.getString("createdfor")));
406419
listResultMetadata.getExtraInfos().add(extraInfo);
407420
return toListResult(Collections.singletonList(aspect), Collections.singletonList(sqlRow), listResultMetadata,
@@ -427,9 +440,10 @@ public <ASPECT extends RecordTemplate> ListResult<ASPECT> list(@Nonnull Class<AS
427440
}
428441
final ListResultMetadata listResultMetadata = new ListResultMetadata().setExtraInfos(new ExtraInfoArray());
429442
final List<ASPECT> aspectList = sqlRows.stream().map(sqlRow -> {
443+
Timestamp utcTimeStamp = timeStampStringToTimeStamp(sqlRow.getString("lastmodifiedon"));
430444
final ExtraInfo extraInfo = new ExtraInfo().setUrn(getUrn(sqlRow.getString("urn"), _urnClass))
431445
.setVersion(LATEST_VERSION).setAudit(
432-
makeAuditStamp(sqlRow.getTimestamp("lastmodifiedon"), sqlRow.getString("lastmodifiedby"),
446+
makeAuditStamp(utcTimeStamp, sqlRow.getString("lastmodifiedby"),
433447
sqlRow.getString("createdfor")));
434448
listResultMetadata.getExtraInfos().add(extraInfo);
435449
return RecordUtils.toRecordTemplate(aspectClass,
@@ -664,6 +678,7 @@ private static <ASPECT extends RecordTemplate, URN> EbeanMetadataAspect findLate
664678
ebeanMetadataAspect.setMetadata(resultSet.getString("metadata"));
665679
ebeanMetadataAspect.setCreatedFor(resultSet.getString("createdFor"));
666680
ebeanMetadataAspect.setCreatedBy(resultSet.getString("createdBy"));
681+
// Not changing specifically to UTC because this method is specific to older schema
667682
ebeanMetadataAspect.setCreatedOn(resultSet.getTimestamp("createdOn"));
668683
return ebeanMetadataAspect;
669684
} else {

dao-impl/ebean-dao/src/main/java/com/linkedin/metadata/dao/utils/EBeanDAOUtils.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
import com.linkedin.metadata.query.UrnField;
2222
import io.ebean.EbeanServer;
2323
import io.ebean.SqlRow;
24+
import java.time.LocalDateTime;
25+
import java.time.ZoneOffset;
2426
import java.lang.reflect.InvocationTargetException;
2527
import java.lang.reflect.Method;
2628
import java.net.URISyntaxException;
2729
import java.sql.Connection;
2830
import java.sql.PreparedStatement;
2931
import java.sql.ResultSet;
3032
import java.sql.Timestamp;
33+
import java.time.format.DateTimeFormatter;
34+
import java.time.format.DateTimeFormatterBuilder;
35+
import java.time.temporal.ChronoField;
3136
import java.util.ArrayList;
3237
import java.util.HashMap;
3338
import java.util.HashSet;
@@ -52,7 +57,12 @@
5257
*/
5358
@Slf4j
5459
public class EBeanDAOUtils {
55-
60+
private static final DateTimeFormatter DATETIME_FORMATTER = new DateTimeFormatterBuilder()
61+
.appendPattern("yyyy-MM-dd HH:mm:ss")
62+
.optionalStart()
63+
.appendFraction(ChronoField.MILLI_OF_SECOND, 1, 3, true) // 1 to 3 digits
64+
.optionalEnd()
65+
.toFormatter();
5666
public static final String DIFFERENT_RESULTS_TEMPLATE = "The results of %s from the new schema table and old schema table are not equal. Reason: %s. "
5767
+ "Defaulting to using the value(s) from the old schema table.";
5868
// String stored in metadata_aspect table for soft deleted aspect
@@ -81,6 +91,14 @@ public static DeltaEntityAnnotationArrayMap getDeltaFromAnnotation(@Nonnull Stri
8191
}
8292
}
8393

94+
/**
95+
* Convert timestamp string to Timestamp.
96+
*/
97+
public static Timestamp timeStampStringToTimeStamp(String timestampString) {
98+
LocalDateTime ldt = LocalDateTime.parse(timestampString, DATETIME_FORMATTER);
99+
return Timestamp.from(ldt.toInstant(ZoneOffset.UTC));
100+
}
101+
84102
/**
85103
* Given urn string and Urn class, return Urn instance.
86104
* @param urn urn string
@@ -209,7 +227,9 @@ public static List<EbeanMetadataAspect> readSqlRows(List<SqlRow> sqlRows) {
209227
EbeanMetadataAspect.PrimaryKey primaryKey = new EbeanMetadataAspect.PrimaryKey(urn, auditedAspect.getCanonicalName(), LATEST_VERSION);
210228
ebeanMetadataAspect.setKey(primaryKey);
211229
ebeanMetadataAspect.setCreatedBy(auditedAspect.getLastmodifiedby());
212-
ebeanMetadataAspect.setCreatedOn(Timestamp.valueOf(auditedAspect.getLastmodifiedon()));
230+
231+
ebeanMetadataAspect.setCreatedOn(timeStampStringToTimeStamp(auditedAspect.getLastmodifiedon()));
232+
213233
ebeanMetadataAspect.setCreatedFor(auditedAspect.getCreatedfor());
214234
ebeanMetadataAspect.setMetadata(extractAspectJsonString(sqlRow.getString(columnName)));
215235
return ebeanMetadataAspect;
@@ -259,14 +279,18 @@ private static <ASPECT extends RecordTemplate> EbeanMetadataAspect readSqlRow(Sq
259279
if (isSoftDeletedAspect(sqlRow, columnName)) {
260280
primaryKey = new EbeanMetadataAspect.PrimaryKey(urn, aspectClass.getCanonicalName(), LATEST_VERSION);
261281
ebeanMetadataAspect.setCreatedBy(sqlRow.getString("lastmodifiedby"));
262-
ebeanMetadataAspect.setCreatedOn(sqlRow.getTimestamp("lastmodifiedon"));
282+
283+
ebeanMetadataAspect.setCreatedOn(timeStampStringToTimeStamp(sqlRow.getString("lastmodifiedon")));
284+
263285
ebeanMetadataAspect.setCreatedFor(sqlRow.getString("createdfor"));
264286
ebeanMetadataAspect.setMetadata(DELETED_VALUE);
265287
} else {
266288
AuditedAspect auditedAspect = RecordUtils.toRecordTemplate(AuditedAspect.class, sqlRow.getString(columnName));
267289
primaryKey = new EbeanMetadataAspect.PrimaryKey(urn, auditedAspect.getCanonicalName(), LATEST_VERSION);
268290
ebeanMetadataAspect.setCreatedBy(auditedAspect.getLastmodifiedby());
269-
ebeanMetadataAspect.setCreatedOn(Timestamp.valueOf(auditedAspect.getLastmodifiedon()));
291+
292+
ebeanMetadataAspect.setCreatedOn(timeStampStringToTimeStamp(auditedAspect.getLastmodifiedon()));
293+
270294
ebeanMetadataAspect.setCreatedFor(auditedAspect.getCreatedfor());
271295
ebeanMetadataAspect.setEmitTime(auditedAspect.getEmitTime());
272296
ebeanMetadataAspect.setEmitter(auditedAspect.getEmitter());

dao-impl/ebean-dao/src/test/java/com/linkedin/metadata/dao/EbeanLocalDAOTest.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
import java.sql.Timestamp;
8989
import java.time.Clock;
9090
import java.time.Instant;
91+
import java.time.ZoneOffset;
92+
import java.time.format.DateTimeFormatter;
9193
import java.util.ArrayList;
9294
import java.util.Arrays;
9395
import java.util.Collection;
@@ -119,6 +121,7 @@
119121
import pegasus.com.linkedin.metadata.events.IngestionAspectETagArray;
120122

121123
import static com.linkedin.common.AuditStamps.*;
124+
import static com.linkedin.metadata.dao.EbeanLocalAccess.*;
122125
import static com.linkedin.metadata.dao.internal.BaseGraphWriterDAO.RemovalOption.*;
123126
import static com.linkedin.metadata.dao.utils.EBeanDAOUtils.*;
124127
import static com.linkedin.metadata.dao.utils.ModelUtils.*;
@@ -329,11 +332,19 @@ public void testAddOne() {
329332
assertEquals(aspect.getKey().getUrn(), urn.toString());
330333
assertEquals(aspect.getKey().getAspect(), aspectName);
331334
assertEquals(aspect.getKey().getVersion(), 0);
332-
assertEquals(aspect.getCreatedOn(), new Timestamp(_now));
333335
assertEquals(aspect.getCreatedBy(), "urn:li:test:actor");
334336
if (_schemaConfig != SchemaConfig.NEW_SCHEMA_ONLY) {
335337
// didn't even implement this in the new schema since the createdfor column is not being read by anyone. so skipping this check.
336338
assertEquals(aspect.getCreatedFor(), "urn:li:test:impersonator");
339+
assertEquals(aspect.getCreatedOn(), new Timestamp(_now));
340+
} else {
341+
Instant instant = Instant.ofEpochMilli(_now);
342+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(ZoneOffset.UTC);
343+
344+
String utcString = formatter.format(instant);
345+
346+
// Compare using local datetime
347+
assertEquals(aspect.getCreatedOn().toString(), utcString.replaceAll("\\.0+$", ".0"));
337348
}
338349

339350
AspectFoo actual = RecordUtils.toRecordTemplate(AspectFoo.class, aspect.getMetadata());
@@ -4027,22 +4038,28 @@ private <ASPECT extends RecordTemplate> void addMetadataEntityTable(Urn urn, Cla
40274038
if (version != 0) {
40284039
return;
40294040
}
4041+
String createdOnString = Instant.ofEpochMilli(createdOn)
4042+
.atZone(ZoneOffset.UTC)
4043+
.format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT));
40304044
String aspectName = aspectClass.getCanonicalName();
40314045
String columnName = SQLSchemaUtils.getAspectColumnName(urn.getEntityType(), aspectName);
40324046
String template = "insert into metadata_entity_%s (urn, %s, lastmodifiedon, lastmodifiedby, createdfor) value"
40334047
+ "('%s', '%s', '%s', '%s', '%s') ON DUPLICATE KEY UPDATE %s = '%s';";
40344048
String query = String.format(template, urn.getEntityType(), columnName, urn, createAuditedAspect(metadata, aspectClass, createdOn, createdBy, createdFor),
4035-
new Timestamp(createdOn), createdBy, createdFor, columnName, createAuditedAspect(metadata, aspectClass, createdOn, createdBy, createdFor));
4049+
createdOnString, createdBy, createdFor, columnName, createAuditedAspect(metadata, aspectClass, createdOn, createdBy, createdFor));
40364050
_server.createSqlUpdate(query).execute();
40374051
}
40384052

40394053
private <ASPECT extends RecordTemplate> String createAuditedAspect(RecordTemplate metadata, Class<ASPECT> aspectClass,
40404054
long createdOn, String createdBy, String createdFor) {
4055+
String createdOnString = Instant.ofEpochMilli(createdOn)
4056+
.atZone(ZoneOffset.UTC)
4057+
.format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT));
40414058
return metadata == null ? DELETED_VALUE : EbeanLocalAccess.toJsonString(new AuditedAspect()
40424059
.setAspect(RecordUtils.toJsonString(metadata))
40434060
.setCanonicalName(aspectClass.getCanonicalName())
40444061
.setLastmodifiedby(createdBy)
4045-
.setLastmodifiedon(new Timestamp(createdOn).toString())
4062+
.setLastmodifiedon(createdOnString)
40464063
.setCreatedfor(createdFor, SetMode.IGNORE_NULL));
40474064
}
40484065

0 commit comments

Comments
 (0)