Skip to content

Commit 12513e1

Browse files
server: Update gson date format for serializing/deserializing Date in MS stats (#11506)
* Update gson date format for serializing/deserializing Date in MS stats (across multiple management servers) * review * review comments, and unit tests * added unit test with different date format * Use separate Gson for MS stats serialization/deserialization
1 parent 393b5d2 commit 12513e1

File tree

4 files changed

+122
-7
lines changed

4 files changed

+122
-7
lines changed

server/src/main/java/com/cloud/server/ManagementServerHostStatsEntry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public void setManagementServerRunId(long managementServerRunId) {
105105
}
106106

107107
@Override
108-
public Date getCollectionTime(){
108+
public Date getCollectionTime() {
109109
return collectionTime;
110110
}
111111

server/src/main/java/com/cloud/server/StatsCollector.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
import javax.inject.Inject;
4848

49+
import com.cloud.utils.DateUtil;
4950
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
5051
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
5152
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider;
@@ -170,10 +171,10 @@
170171
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
171172
import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
172173
import com.google.gson.Gson;
174+
import com.google.gson.GsonBuilder;
173175
import com.google.gson.JsonElement;
174176
import com.google.gson.JsonObject;
175177
import com.google.gson.JsonParseException;
176-
import com.google.gson.reflect.TypeToken;
177178
import com.sun.management.OperatingSystemMXBean;
178179

179180
/**
@@ -294,6 +295,9 @@ public String toString() {
294295
private static StatsCollector s_instance = null;
295296

296297
private static Gson gson = new Gson();
298+
private static Gson msStatsGson = new GsonBuilder()
299+
.setDateFormat(DateUtil.ZONED_DATETIME_FORMAT)
300+
.create();
297301

298302
private ScheduledExecutorService _executor = null;
299303
@Inject
@@ -739,7 +743,6 @@ private void getDynamicDataFromDB() {
739743
dbStats.put(uptime, (Long.valueOf(stats.get(uptime))));
740744
}
741745

742-
743746
@Override
744747
protected Point createInfluxDbPoint(Object metricsObject) {
745748
return null;
@@ -759,7 +762,7 @@ protected void runInContext() {
759762
hostStatsEntry = getDataFrom(mshost);
760763
managementServerHostStats.put(mshost.getUuid(), hostStatsEntry);
761764
// send to other hosts
762-
clusterManager.publishStatus(gson.toJson(hostStatsEntry));
765+
clusterManager.publishStatus(msStatsGson.toJson(hostStatsEntry));
763766
} catch (Throwable t) {
764767
// pokemon catch to make sure the thread stays running
765768
logger.error("Error trying to retrieve management server host statistics", t);
@@ -1158,9 +1161,9 @@ public String newStatus(ClusterServicePdu pdu) {
11581161
logger.debug(String.format("StatusUpdate from %s, json: %s", pdu.getSourcePeer(), pdu.getJsonPackage()));
11591162
}
11601163

1161-
ManagementServerHostStatsEntry hostStatsEntry = null;
1164+
ManagementServerHostStatsEntry hostStatsEntry;
11621165
try {
1163-
hostStatsEntry = gson.fromJson(pdu.getJsonPackage(),new TypeToken<ManagementServerHostStatsEntry>(){}.getType());
1166+
hostStatsEntry = msStatsGson.fromJson(pdu.getJsonPackage(), ManagementServerHostStatsEntry.class);
11641167
managementServerHostStats.put(hostStatsEntry.getManagementServerHostUuid(), hostStatsEntry);
11651168

11661169
// Update peer state to Up in mshost_peer

server/src/test/java/com/cloud/server/StatsCollectorTest.java

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import static org.mockito.Mockito.when;
2222

23+
import java.lang.reflect.Field;
2324
import java.net.URI;
2425
import java.net.URISyntaxException;
26+
import java.text.SimpleDateFormat;
2527
import java.util.ArrayList;
2628
import java.util.Arrays;
2729
import java.util.Date;
@@ -33,6 +35,8 @@
3335
import java.util.concurrent.ConcurrentHashMap;
3436
import java.util.concurrent.TimeUnit;
3537

38+
import com.cloud.utils.DateUtil;
39+
import com.google.gson.JsonSyntaxException;
3640
import org.apache.cloudstack.framework.config.ConfigKey;
3741
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
3842
import org.apache.commons.collections.CollectionUtils;
@@ -116,6 +120,8 @@ public class StatsCollectorTest {
116120

117121
private static Gson gson = new Gson();
118122

123+
private Gson msStatsGson;
124+
119125
private MockedStatic<InfluxDBFactory> influxDBFactoryMocked;
120126

121127
private AutoCloseable closeable;
@@ -125,6 +131,9 @@ public void setUp() throws Exception {
125131
closeable = MockitoAnnotations.openMocks(this);
126132
statsCollector.vmStatsDao = vmStatsDaoMock;
127133
statsCollector.volumeStatsDao = volumeStatsDao;
134+
Field msStatsGsonField = StatsCollector.class.getDeclaredField("msStatsGson");
135+
msStatsGsonField.setAccessible(true);
136+
msStatsGson = (Gson) msStatsGsonField.get(null);
128137
}
129138

130139
@After
@@ -612,4 +621,107 @@ public void testPoolNeedsIopsStatsUpdating_NullIops() {
612621
Mockito.verify(mockPool, Mockito.never()).setCapacityIops(Mockito.anyLong());
613622
Mockito.verify(mockPool, Mockito.never()).setUsedIops(Mockito.anyLong());
614623
}
624+
625+
@Test
626+
public void testGsonDateFormatSerialization() {
627+
Date now = new Date();
628+
TestClass testObj = new TestClass("TestString", 999, now);
629+
String json = msStatsGson.toJson(testObj);
630+
631+
Assert.assertTrue(json.contains("TestString"));
632+
Assert.assertTrue(json.contains("999"));
633+
String expectedDate = new SimpleDateFormat(DateUtil.ZONED_DATETIME_FORMAT).format(now);
634+
Assert.assertTrue(json.contains(expectedDate));
635+
}
636+
637+
@Test
638+
public void testGsonDateFormatDeserializationWithSameDateFormat() throws Exception {
639+
String json = "{\"str\":\"TestString\",\"num\":999,\"date\":\"2025-08-22T15:39:43+0000\"}";
640+
TestClass testObj = msStatsGson.fromJson(json, TestClass.class);
641+
642+
Assert.assertEquals("TestString", testObj.getStr());
643+
Assert.assertEquals(999, testObj.getNum());
644+
Date expectedDate = new SimpleDateFormat(DateUtil.ZONED_DATETIME_FORMAT).parse("2025-08-22T15:39:43+0000");
645+
Assert.assertEquals(expectedDate, testObj.getDate());
646+
}
647+
648+
@Test (expected = JsonSyntaxException.class)
649+
public void testGsonDateFormatDeserializationWithDifferentDateFormat() throws Exception {
650+
String json = "{\"str\":\"TestString\",\"num\":999,\"date\":\"22/08/2025T15:39:43+0000\"}";
651+
msStatsGson.fromJson(json, TestClass.class);
652+
/* Deserialization throws the below exception:
653+
com.google.gson.JsonSyntaxException: 22/08/2025T15:39:43+0000
654+
at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserializeToDate(DefaultTypeAdapters.java:376)
655+
at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserialize(DefaultTypeAdapters.java:351)
656+
at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserialize(DefaultTypeAdapters.java:307)
657+
at com.google.gson.JsonDeserializationVisitor.invokeCustomDeserializer(JsonDeserializationVisitor.java:92)
658+
at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler(JsonObjectDeserializationVisitor.java:117)
659+
at com.google.gson.ReflectingFieldNavigator.visitFieldsReflectively(ReflectingFieldNavigator.java:63)
660+
at com.google.gson.ObjectNavigator.accept(ObjectNavigator.java:120)
661+
at com.google.gson.JsonDeserializationContextDefault.fromJsonObject(JsonDeserializationContextDefault.java:76)
662+
at com.google.gson.JsonDeserializationContextDefault.deserialize(JsonDeserializationContextDefault.java:54)
663+
at com.google.gson.Gson.fromJson(Gson.java:551)
664+
at com.google.gson.Gson.fromJson(Gson.java:498)
665+
at com.google.gson.Gson.fromJson(Gson.java:467)
666+
at com.google.gson.Gson.fromJson(Gson.java:417)
667+
at com.google.gson.Gson.fromJson(Gson.java:389)
668+
at com.cloud.serializer.GsonHelperTest.testGsonDateFormatDeserializationWithDifferentDateFormat(GsonHelperTest.java:113)
669+
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
670+
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
671+
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
672+
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
673+
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
674+
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
675+
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
676+
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
677+
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
678+
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
679+
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
680+
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
681+
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
682+
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
683+
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
684+
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
685+
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
686+
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
687+
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
688+
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
689+
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
690+
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
691+
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
692+
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
693+
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
694+
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
695+
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:231)
696+
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
697+
Caused by: java.text.ParseException: Unparseable date: "22/08/2025T15:39:43+0000"
698+
at java.base/java.text.DateFormat.parse(DateFormat.java:395)
699+
at com.google.gson.DefaultTypeAdapters$DefaultDateTypeAdapter.deserializeToDate(DefaultTypeAdapters.java:374)
700+
... 42 more
701+
*/
702+
}
703+
704+
private static class TestClass {
705+
private String str;
706+
private int num;
707+
private Date date;
708+
709+
public TestClass(String str, int num, Date date) {
710+
this.str = str;
711+
this.num = num;
712+
this.date = date;
713+
}
714+
715+
public String getStr() {
716+
return str;
717+
}
718+
719+
public int getNum() {
720+
return num;
721+
}
722+
723+
public Date getDate() {
724+
return date;
725+
}
726+
}
615727
}

utils/src/main/java/com/cloud/utils/DateUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public class DateUtil {
4848

4949
public static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
5050
public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss";
51-
private static final String ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
51+
public static final String ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
5252
private static final DateFormat ZONED_DATETIME_SIMPLE_FORMATTER = new SimpleDateFormat(ZONED_DATETIME_FORMAT);
5353

5454
private static final DateTimeFormatter[] parseFormats = new DateTimeFormatter[]{

0 commit comments

Comments
 (0)