From bfc0e7e5508cd121415efe6bb23928898dc1cb8b Mon Sep 17 00:00:00 2001 From: Stanislav Trailov Date: Tue, 16 Dec 2025 14:06:18 +0200 Subject: [PATCH] Support for action cancellation in ddi controller sdk (#2846) * Support for action cancellation in ddi controller sdk Signed-off-by: strailov * minor refactor Signed-off-by: strailov * add comment for cancel action Signed-off-by: strailov * minor refactor Signed-off-by: strailov --------- Signed-off-by: strailov --- .../hawkbit/sdk/device/DdiController.java | 81 ++++++++++++++----- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java index 6c40529eb..83b8b7dbd 100644 --- a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java +++ b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java @@ -24,12 +24,16 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; +import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback; import org.eclipse.hawkbit.ddi.json.model.DdiChunk; import org.eclipse.hawkbit.ddi.json.model.DdiConfigData; import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback; import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase; import org.eclipse.hawkbit.ddi.json.model.DdiDeployment; import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase; +import org.eclipse.hawkbit.ddi.json.model.DdiProgress; +import org.eclipse.hawkbit.ddi.json.model.DdiResult; +import org.eclipse.hawkbit.ddi.json.model.DdiStatus; import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode; import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi; import org.eclipse.hawkbit.sdk.Certificate; @@ -55,6 +59,7 @@ public class DdiController { private static final String DEPLOYMENT_BASE_LINK = "deploymentBase"; private static final String CONFIRMATION_BASE_LINK = "confirmationBase"; + private static final String CANCEL_ACTION_LINK = "cancelAction"; private final Tenant tenant; private final Controller controller; @@ -147,6 +152,19 @@ public class DdiController { } } + public void sendCancelFeedback(long actionId) { + try { + log.info(LOG_PREFIX + "Sending cancelation feedback for action with id : {}", getTenantId(), getControllerId(), actionId); + getDdiApi().postCancelActionFeedback(new DdiActionFeedback( + new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, new DdiResult(DdiResult.FinalResult.SUCCESS, + new DdiProgress(100, 100)), 200, List.of("Successfully cancelled by the controller.")) + ), getTenantId(), getControllerId(), actionId); + } catch (Exception ex) { + log.error(LOG_PREFIX + "Failed to send cancelation feedback {}", getTenantId(), getControllerId(), + actionId, ex); + } + } + private void poll() { log.debug(LOG_PREFIX + " Polling ...", getTenantId(), getControllerId()); Optional.ofNullable(executorService).ifPresent(executor -> @@ -165,31 +183,21 @@ public class DdiController { .ifPresentOrElse(actionWithDeployment -> { final long actionId = actionWithDeployment.getKey(); if (currentActionId == null) { - if (lastActionId != null && lastActionId == actionId) { - log.info(LOG_PREFIX + "Still receive the last action {}", - getTenantId(), getControllerId(), actionId); - return; - } - - log.info(LOG_PREFIX + "Process action {}", getTenantId(), getControllerId(), - actionId); - final DdiDeployment deployment = actionWithDeployment.getValue().getDeployment(); - final DdiDeployment.HandlingType updateType = deployment.getUpdate(); - final List modules = deployment.getChunks(); - - currentActionId = actionId; - executor.submit(updateHandler.getUpdateProcessor(this, updateType, modules)); + processAction(actionId, actionWithDeployment, executor); } else if (currentActionId != actionId) { - // TODO - cancel and start new one? + // currentActionId had failed to be processed and new one had been initiated + // try cancel current and process new one log.info(LOG_PREFIX + "Action {} is canceled while in process (new {})!", getTenantId(), getControllerId(), currentActionId, actionId); + cancelActionByCancellationLink(controllerBase, currentActionId); + currentActionId = null; + // then process the new one + poll(); } // else same action - already processing }, () -> { - if (currentActionId != null) { - // TODO - cancel current? - log.info(LOG_PREFIX + "Action {} is canceled while in process (not returned)!", getTenantId(), - getControllerId(), getCurrentActionId()); - } + cancelActionByCancellationLink(controllerBase, -1); + // reset current action id - on next poll should be available deploymentBase + currentActionId = null; }); executor.schedule(this::poll, getPollMillis(controllerBase), TimeUnit.MILLISECONDS); } @@ -198,6 +206,23 @@ public class DdiController { executor.schedule(this::poll, DEFAULT_POLL_MS, TimeUnit.MILLISECONDS))); } + private void processAction(final long actionId, final Map.Entry actionWithDeployment, final ScheduledExecutorService executor) { + if (lastActionId != null && lastActionId == actionId) { + log.info(LOG_PREFIX + "Still receive the last action {}", + getTenantId(), getControllerId(), actionId); + return; + } + + log.info(LOG_PREFIX + "Process action {}", getTenantId(), getControllerId(), + actionId); + final DdiDeployment deployment = actionWithDeployment.getValue().getDeployment(); + final DdiDeployment.HandlingType updateType = deployment.getUpdate(); + final List modules = deployment.getChunks(); + + currentActionId = actionId; + executor.submit(updateHandler.getUpdateProcessor(this, updateType, modules)); + } + private Optional getControllerBase() { log.trace(LOG_PREFIX + "Polling ...", getTenantId(), getControllerId()); final ResponseEntity poll; @@ -216,6 +241,16 @@ public class DdiController { return Optional.ofNullable(poll.getBody()); } + private void cancelActionByCancellationLink(DdiControllerBase controllerBase, long actionToBeCanceled) { + getRequiredLink(controllerBase, CANCEL_ACTION_LINK).ifPresentOrElse(link -> { + // action is in CANCELING state - send cancel feedback + final long actionId = actionToBeCanceled == -1 ? getActionIdFromCancellationLink(link) : actionToBeCanceled; + log.info(LOG_PREFIX + "Cancelling current action {}", getTenantId(), getControllerId(), actionId); + sendCancelFeedback(actionId); + }, () -> log.info(LOG_PREFIX + "Action {} is canceled while in process (not returned)!", getTenantId(), getControllerId(), getCurrentActionId()) + ); + } + private Optional getRequiredLink(final DdiControllerBase controllerBase, final String nameOfTheLink) { final Optional link = controllerBase != null ? controllerBase.getLink(nameOfTheLink) : Optional.empty(); link.ifPresentOrElse( @@ -262,4 +297,10 @@ public class DdiController { final String href = link.getHref(); return Long.parseLong(href.substring(href.lastIndexOf('/') + 1, href.indexOf('?'))); } + + private long getActionIdFromCancellationLink(final Link link) { + final String href = link.getHref(); + final String[] split = href.split("/"); + return Long.parseLong(split[split.length - 1]); + } } \ No newline at end of file