diff --git a/genie-client/src/test/groovy/com/netflix/genie/client/GenieClientUtilsSpec.groovy b/genie-client/src/test/groovy/com/netflix/genie/client/GenieClientUtilsSpec.groovy index 581b11c0fc8..a2f7359e4b0 100644 --- a/genie-client/src/test/groovy/com/netflix/genie/client/GenieClientUtilsSpec.groovy +++ b/genie-client/src/test/groovy/com/netflix/genie/client/GenieClientUtilsSpec.groovy @@ -50,13 +50,13 @@ class GenieClientUtilsSpec extends Specification { noExceptionThrown() results == expectedResults where: - body | expectedResults - "{}" | [] - "[]" | [] - "{\"_embedded\": []}" | [] - "{\"_embedded\": {\"notRight\": []}}" | [] - "{\"_embedded\": {\"jobSearchResultList\": {}}}" | [] - "{\"_embedded\": {\"jobSearchResultList\": [{\"id\":\"1234\",\"name\":\"testJob\",\"user\":\"tgianos\",\"status\":\"SUCCEEDED\",\"started\":\"1970-01-01T00:00:50Z\",\"finished\":\"1970-01-01T00:00:52Z\",\"clusterName\":null,\"commandName\":null,\"runtime\":\"PT2S\"}]}}" | [new JobSearchResult("1234", "testJob", "tgianos", JobStatus.SUCCEEDED, Instant.ofEpochSecond(50), Instant.ofEpochSecond(52), null, null)] + body | expectedResults + "{}" | [] + "[]" | [] + "{\"_embedded\": []}" | [] + "{\"_embedded\": {\"notRight\": []}}" | [] + "{\"_embedded\": {\"jobSearchResultList\": {}}}" | [] + "{\"_embedded\": {\"jobSearchResultList\": [{\"id\":\"1234\",\"name\":\"testJob\",\"user\":\"tgianos\",\"status\":\"SUCCEEDED\",\"started\":\"1970-01-01T00:00:50Z\",\"finished\":\"1970-01-01T00:00:52.001Z\",\"clusterName\":null,\"commandName\":null,\"runtime\":\"PT2.001S\"}]}}" | [new JobSearchResult("1234", "testJob", "tgianos", JobStatus.SUCCEEDED, Instant.ofEpochSecond(50), Instant.ofEpochMilli(52001), null, null)] } def "Unsuccessful search result request throws client exception"() { diff --git a/genie-common/src/main/java/com/netflix/genie/common/dto/BaseDTO.java b/genie-common/src/main/java/com/netflix/genie/common/dto/BaseDTO.java index a5035f3f56f..b256ee72af9 100644 --- a/genie-common/src/main/java/com/netflix/genie/common/dto/BaseDTO.java +++ b/genie-common/src/main/java/com/netflix/genie/common/dto/BaseDTO.java @@ -18,7 +18,9 @@ package com.netflix.genie.common.dto; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.netflix.genie.common.external.util.GenieObjectMapper; +import com.netflix.genie.common.util.JsonUtils; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -41,7 +43,9 @@ public abstract class BaseDTO implements Serializable { @Size(max = 255, message = "Max length for the ID is 255 characters") private final String id; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant created; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant updated; /** diff --git a/genie-common/src/main/java/com/netflix/genie/common/dto/Job.java b/genie-common/src/main/java/com/netflix/genie/common/dto/Job.java index 18819ac0d8c..6ffa671e430 100644 --- a/genie-common/src/main/java/com/netflix/genie/common/dto/Job.java +++ b/genie-common/src/main/java/com/netflix/genie/common/dto/Job.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.netflix.genie.common.util.JsonUtils; import com.netflix.genie.common.util.TimeUtils; import lombok.Getter; import org.apache.commons.lang3.StringUtils; @@ -50,7 +51,9 @@ public class Job extends CommonDTO { private final JobStatus status; @Size(max = 255, message = "Max length of the status message is 255 characters") private final String statusMsg; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant started; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant finished; @Size(max = 1024, message = "Max character length is 1024 characters for the archive location") private final String archiveLocation; diff --git a/genie-common/src/main/java/com/netflix/genie/common/dto/JobExecution.java b/genie-common/src/main/java/com/netflix/genie/common/dto/JobExecution.java index 3b20d3d2db8..9ef7db1e788 100644 --- a/genie-common/src/main/java/com/netflix/genie/common/dto/JobExecution.java +++ b/genie-common/src/main/java/com/netflix/genie/common/dto/JobExecution.java @@ -20,7 +20,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.netflix.genie.common.external.dtos.v4.ArchiveStatus; +import com.netflix.genie.common.util.JsonUtils; import lombok.Getter; import javax.annotation.Nullable; @@ -64,6 +66,7 @@ public class JobExecution extends BaseDTO { message = "The delay between checks must be at least 1 millisecond. Probably should be much more than that" ) private final Long checkDelay; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant timeout; private final Integer exitCode; @Min( diff --git a/genie-common/src/main/java/com/netflix/genie/common/dto/search/JobSearchResult.java b/genie-common/src/main/java/com/netflix/genie/common/dto/search/JobSearchResult.java index 5cbb23421ea..3729021ce0d 100644 --- a/genie-common/src/main/java/com/netflix/genie/common/dto/search/JobSearchResult.java +++ b/genie-common/src/main/java/com/netflix/genie/common/dto/search/JobSearchResult.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer; import com.netflix.genie.common.dto.JobStatus; +import com.netflix.genie.common.util.JsonUtils; import com.netflix.genie.common.util.TimeUtils; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -48,7 +49,9 @@ public class JobSearchResult extends BaseSearchResult { private static final long serialVersionUID = -3886685874572773514L; private final JobStatus status; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant started; + @JsonSerialize(using = JsonUtils.OptionalInstantMillisecondSerializer.class) private final Instant finished; private final String clusterName; private final String commandName; diff --git a/genie-common/src/main/java/com/netflix/genie/common/util/JsonUtils.java b/genie-common/src/main/java/com/netflix/genie/common/util/JsonUtils.java index ef5dae99288..3e63d8bfa48 100644 --- a/genie-common/src/main/java/com/netflix/genie/common/util/JsonUtils.java +++ b/genie-common/src/main/java/com/netflix/genie/common/util/JsonUtils.java @@ -17,8 +17,11 @@ */ package com.netflix.genie.common.util; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; import com.netflix.genie.common.exceptions.GenieException; import com.netflix.genie.common.exceptions.GenieServerException; import com.netflix.genie.common.external.util.GenieObjectMapper; @@ -29,8 +32,11 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; /** @@ -125,4 +131,50 @@ public static String joinArguments(final List commandArgs) { .collect(Collectors.joining(StringUtils.SPACE)); } } + + /** + * Truncate instants to millisecond precision during ISO 8601 serialization to string for backwards compatibility. + * + * @author tgianos + * @since 4.1.2 + */ + public static class InstantMillisecondSerializer extends JsonSerializer { + + /** + * {@inheritDoc} + */ + @Override + public void serialize( + final Instant value, + final JsonGenerator gen, + final SerializerProvider serializers + ) throws IOException { + gen.writeString(value.truncatedTo(ChronoUnit.MILLIS).toString()); + } + } + + /** + * Truncate instants to millisecond precision during ISO 8601 serialization to string for backwards compatibility. + * + * @author tgianos + * @since 4.1.2 + */ + public static class OptionalInstantMillisecondSerializer extends JsonSerializer> { + + /** + * {@inheritDoc} + */ + @Override + public void serialize( + final Optional value, + final JsonGenerator gen, + final SerializerProvider serializers + ) throws IOException { + if (value.isPresent()) { + gen.writeString(value.get().truncatedTo(ChronoUnit.MILLIS).toString()); + } else { + gen.writeNull(); + } + } + } } diff --git a/genie-common/src/test/groovy/com/netflix/genie/common/util/JsonUtilsSpec.groovy b/genie-common/src/test/groovy/com/netflix/genie/common/util/JsonUtilsSpec.groovy index 0635ccc42f1..24a3b109a16 100644 --- a/genie-common/src/test/groovy/com/netflix/genie/common/util/JsonUtilsSpec.groovy +++ b/genie-common/src/test/groovy/com/netflix/genie/common/util/JsonUtilsSpec.groovy @@ -17,9 +17,13 @@ */ package com.netflix.genie.common.util +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider import spock.lang.Specification import spock.lang.Unroll +import java.time.Instant + /** * Specifications for {@link JsonUtils}. * @@ -59,4 +63,50 @@ class JsonUtilsSpec extends Specification { ["arg1", "arg2", "arg3", "arg4", "arg5", "'"] | "'arg1' 'arg2' 'arg3' 'arg4' 'arg5' '''" ["foo/bar", "--hello", "\$world"] | "'foo/bar' '--hello' '\$world'" } + + @Unroll + def "Can serialize instant #instant to millisecond precision string #instantString"() { + def generator = Mock(JsonGenerator) + def serializer = new JsonUtils.InstantMillisecondSerializer() + + when: + serializer.serialize(instant, generator, Mock(SerializerProvider)) + + then: + 1 * generator.writeString(instantString) + + where: + instant | instantString + Instant.ofEpochMilli(52) | "1970-01-01T00:00:00.052Z" + Instant.ofEpochMilli(52000) | "1970-01-01T00:00:52Z" + Instant.ofEpochMilli(52000).plusNanos(7) | "1970-01-01T00:00:52Z" + Instant.ofEpochMilli(52001) | "1970-01-01T00:00:52.001Z" + Instant.ofEpochMilli(52001).plusNanos(1) | "1970-01-01T00:00:52.001Z" + } + + @Unroll + def "Can serialize optional instant #instant to millisecond precision string #instantString"() { + def generator = Mock(JsonGenerator) + def serializer = new JsonUtils.OptionalInstantMillisecondSerializer() + + when: + serializer.serialize((Optional) instant, generator, Mock(SerializerProvider)) + + then: + if (instant.isPresent()) { + 1 * generator.writeString(instantString) + } + else { + 1 * generator.writeNull() + } + + where: + instant | instantString + Optional.of(Instant.ofEpochMilli(52)) | "1970-01-01T00:00:00.052Z" + Optional.of(Instant.ofEpochMilli(52000)) | "1970-01-01T00:00:52Z" + Optional.of(Instant.ofEpochMilli(52000).plusNanos(7)) | "1970-01-01T00:00:52Z" + Optional.of(Instant.ofEpochMilli(52001)) | "1970-01-01T00:00:52.001Z" + Optional.of(Instant.ofEpochMilli(52001).plusNanos(1)) | "1970-01-01T00:00:52.001Z" + Optional.empty() | "blah" + } }