Add timestamp to Actions (#2113)

* Add timestamp to Actions

Signed-off-by: Vasil Ilchev <Vasil.Ilchev@bosch.com>

* Add Timestamp to All Actions Feedback DDI/DMF

* After review

* Removed Action timestamp as we have timestamp in each ActionStatus so use that instead

* Unify to use everywhere System.currentTimeMillis()

* Add constructor w/o timestamp to DmfActionUpdateStatus

---------

Signed-off-by: Vasil Ilchev <Vasil.Ilchev@bosch.com>
Co-authored-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2024-12-04 09:53:10 +02:00
committed by GitHub
parent 58e427d1ea
commit b4215a9d76
17 changed files with 96 additions and 64 deletions

View File

@@ -31,7 +31,7 @@ import lombok.Data;
*
* <p>
* The answer header would look like: {
* "time": "20140511T121314",
* "timestamp": "1733218554123",
* "status": {
* "execution": "closed",
* "result": {
@@ -47,8 +47,8 @@ import lombok.Data;
@JsonIgnoreProperties(ignoreUnknown = true)
public class DdiActionFeedback {
@Schema(description = "Timestamp of the action", example = "2023-08-03T12:31:41.890992967Z")
private final String time;
@Schema(description = "Timestamp of the action in milliseconds since epoch", example = "1627997501890")
private final Long timestamp;
@NotNull
@Valid
@@ -57,14 +57,19 @@ public class DdiActionFeedback {
/**
* Constructs an action-feedback
*
* @param time time of feedback
* @param timestamp time of feedback
* @param status status to be appended to the action
*/
@JsonCreator
public DdiActionFeedback(
@JsonProperty(value = "time") final String time,
@JsonProperty(value = "status", required = true) final DdiStatus status) {
this.time = time;
@JsonProperty(value = "status", required = true) final DdiStatus status,
@JsonProperty(value = "timestamp") final Long timestamp) {
this.status = status;
this.timestamp = timestamp != null ? timestamp : System.currentTimeMillis();
}
public DdiActionFeedback(final DdiStatus status) {
this(status, null);
}
}

View File

@@ -39,7 +39,7 @@ class DdiActionFeedbackTest {
void shouldSerializeAndDeserializeObjectWithoutOptionalValues() throws IOException {
// Setup
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, null, null, Collections.emptyList());
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(null, ddiStatus);
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(ddiStatus);
// Test
final String serializedDdiActionFeedback = mapper.writeValueAsString(ddiActionFeedback);
@@ -53,17 +53,16 @@ class DdiActionFeedbackTest {
@Description("Verify the correct serialization and deserialization of the model with all values provided")
void shouldSerializeAndDeserializeObjectWithOptionalValues() throws IOException {
// Setup
final String time = Instant.now().toString();
final Long timestamp = System.currentTimeMillis();
final DdiResult ddiResult = new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(10, 10));
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, ddiResult, 200, Collections.singletonList("myMessage"));
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(time, ddiStatus);
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(ddiStatus, timestamp);
// Test
final String serializedDdiActionFeedback = mapper.writeValueAsString(ddiActionFeedback);
final DdiActionFeedback deserializedDdiActionFeedback = mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class);
assertThat(serializedDdiActionFeedback).contains(time);
assertThat(deserializedDdiActionFeedback.getTime()).isEqualTo(time);
assertThat(deserializedDdiActionFeedback.getTimestamp()).isEqualTo(timestamp);
assertThat(deserializedDdiActionFeedback.getStatus()).hasToString(ddiStatus.toString());
}
@@ -71,28 +70,38 @@ class DdiActionFeedbackTest {
@Description("Verify that deserialization fails for known properties with a wrong datatype")
void shouldFailForObjectWithWrongDataTypes() throws IOException {
// Setup
final String serializedDdiActionFeedback = "{\"time\":\"20190809T121314\",\"status\":{\"execution\": [closed],\"result\":null,\"details\":[]}}";
final String serializedDdiActionFeedback = """
{
"timestamp" : "1627997501890",
"status" : {
"execution" : "[closed]",
"result" : null,
"details" : []
}
}
""";
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
() -> mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class));
}
@Test
@Description("Verify that deserialization works if optional fields are not parsed")
void shouldConvertItWithoutOptionalFieldTime() throws JsonProcessingException {
void shouldConvertItWithoutOptionalFieldTimestamp() throws JsonProcessingException {
// Setup
final String serializedDdiActionFeedback = "{\n" + //
" \"status\" : {\n" + //
" \"result\" : {\n" + //
" \"finished\" : \"none\"\n" + //
" },\n" + //
" \"execution\" : \"download\",\n" + //
" \"details\" : [ \"Some message\" ]\n" + //
" }\n" + //
"}";//
final String serializedDdiActionFeedback = """
{
"status" : {
"result" : {
"finished" : "none"
},
"execution" : "download",
"details" : [ "Some message" ]
}
}
""";
assertThat(mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class)).satisfies(deserializedDdiActionFeedback -> {
assertThat(deserializedDdiActionFeedback.getTime()).isNull();
assertThat(deserializedDdiActionFeedback.getTimestamp()).isNotNull();
assertThat(deserializedDdiActionFeedback.getStatus()).isNotNull();
assertThat(deserializedDdiActionFeedback.getStatus().getResult()).isNotNull();
assertThat(deserializedDdiActionFeedback.getStatus().getResult().getFinished()).isEqualTo(DdiResult.FinalResult.NONE);

View File

@@ -503,7 +503,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
private static ActionStatusCreate generateActionCancelStatus(
final DdiActionFeedback feedback, final Target target, final Long actionId, final EntityFactory entityFactory) {
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId).occurredAt(feedback.getTimestamp());
final List<String> messages = new ArrayList<>();
final Status status;
switch (feedback.getStatus().getExecution()) {
@@ -616,7 +616,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
}
private ActionStatusCreate generateUpdateStatus(final DdiActionFeedback feedback, final String controllerId, final Long actionId) {
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId).occurredAt(feedback.getTimestamp());
final List<String> messages = new ArrayList<>();
if (!CollectionUtils.isEmpty(feedback.getStatus().getDetails())) {

View File

@@ -295,7 +295,7 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
final DdiStatus.ExecutionStatus executionStatus, final DdiResult ddiResult,
final List<String> messages) throws JsonProcessingException {
final DdiStatus ddiStatus = new DdiStatus(executionStatus, ddiResult, null, messages);
return OBJECT_MAPPER.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
return OBJECT_MAPPER.writeValueAsString(new DdiActionFeedback(ddiStatus));
}
protected String getJsonActionFeedback(
@@ -308,7 +308,7 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
final DdiStatus.ExecutionStatus executionStatus,
final DdiResult.FinalResult finalResult, final Integer code, final List<String> messages) throws JsonProcessingException {
final DdiStatus ddiStatus = new DdiStatus(executionStatus, new DdiResult(finalResult, new DdiProgress(2, 5)), code, messages);
return OBJECT_MAPPER.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
return OBJECT_MAPPER.writeValueAsString(new DdiActionFeedback(ddiStatus));
}
protected String getJsonConfirmationFeedback(

View File

@@ -330,7 +330,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt
protected void createAndSendActionStatusUpdateMessage(final String target, final long actionId,
final DmfActionStatus status) {
final DmfActionUpdateStatus dmfActionUpdateStatus = new DmfActionUpdateStatus(actionId, status);
final DmfActionUpdateStatus dmfActionUpdateStatus = new DmfActionUpdateStatus(actionId, status, System.currentTimeMillis());
final Message eventMessage = createUpdateActionEventMessage(dmfActionUpdateStatus);
eventMessage.getMessageProperties().getHeaders().put(MessageHeaderKey.THING_ID, target);

View File

@@ -34,6 +34,7 @@ public class DmfActionUpdateStatus {
private final Long actionId;
private final DmfActionStatus actionStatus;
private final long timestamp;
@JsonProperty
private Long softwareModuleId;
@@ -46,9 +47,14 @@ public class DmfActionUpdateStatus {
private Integer code;
public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) final Long actionId,
@JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus) {
@JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus, @JsonProperty(value = "timestamp") final Long timestamp) {
this.actionId = actionId;
this.actionStatus = actionStatus;
this.timestamp = timestamp != null ? timestamp : System.currentTimeMillis();
}
public DmfActionUpdateStatus(final Long actionId, final DmfActionStatus actionStatus) {
this(actionId, actionStatus, null);
}
@JsonIgnore

View File

@@ -131,4 +131,5 @@ public class MgmtAction extends MgmtBaseEntity {
@JsonProperty
@Schema(description = "If created by external system this field contains the external reference for the action")
private String externalRef;
}

View File

@@ -43,6 +43,10 @@ public class MgmtActionStatus {
@Schema(example = "1691065929524")
private Long reportedAt;
@JsonProperty
@Schema(example = "1691065929524")
private Long timestamp;
@JsonProperty
@Schema(example = "200")
private Integer code;

View File

@@ -354,6 +354,7 @@ public final class MgmtTargetMapper {
result.setMessages(messages);
result.setReportedAt(actionStatus.getCreatedAt());
result.setTimestamp(actionStatus.getOccurredAt());
result.setStatusId(actionStatus.getId());
result.setType(actionStatus.getStatus().name().toLowerCase());
actionStatus.getCode().ifPresent(result::setCode);

View File

@@ -37,7 +37,7 @@ public final class TimestampCalculator {
* @return <em>overdue_ts</em> in milliseconds since Unix epoch as long value
*/
public static long calculateOverdueTimestamp() {
return Instant.now().toEpochMilli() - getDurationForKey(TenantConfigurationKey.POLLING_TIME_INTERVAL).toMillis()
return System.currentTimeMillis() - getDurationForKey(TenantConfigurationKey.POLLING_TIME_INTERVAL).toMillis()
- getDurationForKey(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL).toMillis();
}

View File

@@ -10,7 +10,6 @@
package org.eclipse.hawkbit.repository.rsql;
import java.io.Serial;
import java.time.Instant;
import org.apache.commons.lang3.text.StrLookup;
import org.apache.commons.lang3.text.StrSubstitutor;
@@ -54,7 +53,7 @@ public class VirtualPropertyResolver extends StrLookup<String> implements Virtua
String resolved = null;
if ("now_ts".equalsIgnoreCase(rhs)) {
resolved = String.valueOf(Instant.now().toEpochMilli());
resolved = String.valueOf(System.currentTimeMillis());
} else if ("overdue_ts".equalsIgnoreCase(rhs)) {
resolved = String.valueOf(TimestampCalculator.calculateOverdueTimestamp());
}

View File

@@ -88,7 +88,7 @@ public class JpaActionManagement {
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
}
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action) {
protected void onActionStatusUpdate(final JpaActionStatus newActionStatus, final JpaAction action) {
// can be overwritten to intercept the persistence of the action status
}
@@ -165,7 +165,7 @@ public class JpaActionManagement {
assertActionStatusMessageQuota(actionStatus);
actionStatus.setAction(action);
onActionStatusUpdate(actionStatus.getStatus(), action);
onActionStatusUpdate(actionStatus, action);
actionStatusRepository.save(actionStatus);

View File

@@ -163,8 +163,8 @@ public class JpaConfirmationManagement extends JpaActionManagement implements Co
}
@Override
protected void onActionStatusUpdate(final Status updatedActionStatus, final JpaAction action) {
if (updatedActionStatus == Status.RUNNING && action.isActive()) {
protected void onActionStatusUpdate(final JpaActionStatus newActionStatus, final JpaAction action) {
if (newActionStatus.getStatus() == Status.RUNNING && action.isActive()) {
action.setStatus(Status.RUNNING);
}
}

View File

@@ -177,7 +177,9 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
}
@Override
protected void onActionStatusUpdate(final Action.Status updatedActionStatus, final JpaAction action) {
protected void onActionStatusUpdate(final JpaActionStatus newActionStatus, final JpaAction action) {
final Action.Status updatedActionStatus = newActionStatus.getStatus();
final long occurredAt = newActionStatus.getOccurredAt();
switch (updatedActionStatus) {
case ERROR: {
final JpaTarget target = (JpaTarget) action.getTarget();
@@ -186,7 +188,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
break;
}
case FINISHED: {
handleFinishedAndStoreInTargetStatus(action).ifPresent(this::requestControllerAttributes);
handleFinishedAndStoreInTargetStatus(occurredAt, action).ifPresent(this::requestControllerAttributes);
break;
}
case DOWNLOADED: {
@@ -865,33 +867,35 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
* @return a present controllerId in case the attributes needs to be
* requested.
*/
private Optional<String> handleFinishedAndStoreInTargetStatus(final JpaAction action) {
private Optional<String> handleFinishedAndStoreInTargetStatus(final long occurredAt, final JpaAction action) {
final JpaTarget target = (JpaTarget) action.getTarget();
action.setActive(false);
action.setStatus(Status.FINISHED);
final JpaDistributionSet ds = (JpaDistributionSet) entityManager.merge(action.getDistributionSet());
if (target.getInstallationDate() == null || target.getInstallationDate() < occurredAt) {
final JpaDistributionSet ds = (JpaDistributionSet) entityManager.merge(action.getDistributionSet());
target.setInstalledDistributionSet(ds);
target.setInstallationDate(System.currentTimeMillis());
target.setInstalledDistributionSet(ds);
target.setInstallationDate(occurredAt);
// Target reported an installation of a DOWNLOAD_ONLY assignment, the
// assigned DS has to be adapted
// because the currently assigned DS can be unequal to the currently
// installed DS (the downloadOnly DS)
if (isDownloadOnly(action)) {
target.setAssignedDistributionSet((JpaDistributionSet) action.getDistributionSet());
// Target reported an installation of a DOWNLOAD_ONLY assignment, the
// assigned DS has to be adapted
// because the currently assigned DS can be unequal to the currently
// installed DS (the downloadOnly DS)
if (isDownloadOnly(action)) {
target.setAssignedDistributionSet((JpaDistributionSet) action.getDistributionSet());
}
// check if the assigned set is equal to the installed set (not
// necessarily the case as another update might be pending already).
if (target.getAssignedDistributionSet() != null
&& target.getAssignedDistributionSet().getId().equals(target.getInstalledDistributionSet().getId())) {
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
}
targetRepository.save(target);
entityManager.detach(ds);
}
// check if the assigned set is equal to the installed set (not
// necessarily the case as another update might be pending already).
if (target.getAssignedDistributionSet() != null
&& target.getAssignedDistributionSet().getId().equals(target.getInstalledDistributionSet().getId())) {
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
}
targetRepository.save(target);
entityManager.detach(ds);
return Optional.of(target.getControllerId());
}

View File

@@ -18,6 +18,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
@@ -51,6 +53,7 @@ import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.springframework.data.annotation.CreatedDate;
/**
* JPA implementation of {@link Action}.
@@ -283,6 +286,7 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
return Optional.ofNullable(lastActionStatusCode);
}
@Override
public Optional<ZonedDateTime> getMaintenanceWindowStartTime() {
return MaintenanceScheduleHelper.getNextMaintenanceWindow(maintenanceWindowSchedule, maintenanceWindowDuration,

View File

@@ -89,7 +89,7 @@ class TargetManagementSearchTest extends AbstractJpaIntegrationTest {
final DistributionSet installedSet = testdataFactory.createDistributionSet("another");
final Long lastTargetQueryNotOverdue = Instant.now().toEpochMilli();
final Long lastTargetQueryNotOverdue = System.currentTimeMillis();
final Long lastTargetQueryAlwaysOverdue = 0L;
final String targetDsAIdPref = "targ-A";
@@ -276,7 +276,7 @@ class TargetManagementSearchTest extends AbstractJpaIntegrationTest {
void targetSearchWithOverdueFilterAndOrderByDistributionSet() {
final Long lastTargetQueryAlwaysOverdue = 0L;
final long lastTargetQueryNotOverdue = Instant.now().toEpochMilli();
final long lastTargetQueryNotOverdue = System.currentTimeMillis();
final Long[] overdueMix = { lastTargetQueryAlwaysOverdue, lastTargetQueryNotOverdue,
lastTargetQueryAlwaysOverdue, null, lastTargetQueryAlwaysOverdue };

View File

@@ -18,8 +18,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
public record UpdateStatus(Status status, List<String> messages) {
DdiActionFeedback feedback() {
return new DdiActionFeedback(null,
new DdiStatus(status.executionStatus, new DdiResult(status.finalResult, null), status.code, messages));
return new DdiActionFeedback(new DdiStatus(status.executionStatus, new DdiResult(status.finalResult, null), status.code, messages));
}
/**