From 7dac565059e9b5cd8ba3a532cfc1cdf8dc193458 Mon Sep 17 00:00:00 2001 From: AlanJager Date: Sat, 24 Jan 2026 09:39:18 +0800 Subject: [PATCH] [longjob]: Introduce suspend long job message APIImpact Resolves: ZSTAC-81237 Change-Id: I6f7262716c7075637373646b726a796c73737563 Signed-off-by: AlanJager --- .gitconfig/hooks/commit-msg | 2 +- .gitconfig/hooks/prepare-commit-msg | 2 +- conf/serviceConfig/longjob.xml | 3 + .../longjob/APISuspendLongJobEvent.java | 34 ++++++ .../APISuspendLongJobEventDoc_zh_cn.groovy | 32 ++++++ .../header/longjob/APISuspendLongJobMsg.java | 38 +++++++ .../APISuspendLongJobMsgDoc_zh_cn.groovy | 58 ++++++++++ .../org/zstack/header/longjob/LongJob.java | 1 + .../org/zstack/longjob/LongJobFactory.java | 1 + .../zstack/longjob/LongJobFactoryImpl.java | 10 ++ .../zstack/longjob/LongJobManagerImpl.java | 76 +++++++++++++ .../java/org/zstack/longjob/LongJobUtils.java | 22 ++++ .../org/zstack/sdk/SuspendLongJobAction.java | 101 ++++++++++++++++++ .../org/zstack/sdk/SuspendLongJobResult.java | 14 +++ test/src/test/resources/zstack.properties | 1 + .../java/org/zstack/testlib/ApiHelper.groovy | 27 +++++ .../CloudOperationsErrorCode.java | 8 ++ 17 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEvent.java create mode 100644 header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsg.java create mode 100644 header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy create mode 100644 sdk/src/main/java/org/zstack/sdk/SuspendLongJobAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SuspendLongJobResult.java diff --git a/.gitconfig/hooks/commit-msg b/.gitconfig/hooks/commit-msg index 26e2a97ba80..2cf131bd448 100755 --- a/.gitconfig/hooks/commit-msg +++ b/.gitconfig/hooks/commit-msg @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # NOTE(weiw): although we could use shell script, but python script may # be easier to port to Windows/MacOS/Linux diff --git a/.gitconfig/hooks/prepare-commit-msg b/.gitconfig/hooks/prepare-commit-msg index 28656de1be6..9d49e7ff602 100755 --- a/.gitconfig/hooks/prepare-commit-msg +++ b/.gitconfig/hooks/prepare-commit-msg @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # An example hook script to prepare the commit log message. # Called by "git commit" with the name of the file that has the diff --git a/conf/serviceConfig/longjob.xml b/conf/serviceConfig/longjob.xml index c5546538205..f059ee67d11 100644 --- a/conf/serviceConfig/longjob.xml +++ b/conf/serviceConfig/longjob.xml @@ -25,6 +25,9 @@ org.zstack.header.longjob.APIResumeLongJobMsg + + org.zstack.header.longjob.APISuspendLongJobMsg + org.zstack.header.longjob.APICleanLongJobMsg diff --git a/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEvent.java b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEvent.java new file mode 100644 index 00000000000..3fe347a8545 --- /dev/null +++ b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEvent.java @@ -0,0 +1,34 @@ +package org.zstack.header.longjob; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +@RestResponse(allTo = "inventory") +public class APISuspendLongJobEvent extends APIEvent { + private LongJobInventory inventory; + + public LongJobInventory getInventory() { + return inventory; + } + + public void setInventory(LongJobInventory inventory) { + this.inventory = inventory; + } + + public APISuspendLongJobEvent() { + super(); + } + + public APISuspendLongJobEvent(String apiId) { + super(apiId); + } + + public static APISuspendLongJobEvent __example__() { + APISuspendLongJobEvent event = new APISuspendLongJobEvent(); + LongJobInventory inv = new LongJobInventory(); + inv.setUuid(uuid()); + inv.setState(LongJobState.Suspended); + event.setInventory(inv); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..320b286ad34 --- /dev/null +++ b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.longjob + +import org.zstack.header.longjob.LongJobInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "在这里输入结构的名称" + + ref { + name "inventory" + path "org.zstack.header.longjob.APISuspendLongJobEvent.inventory" + desc "null" + type "LongJobInventory" + since "5.5.0" + clz LongJobInventory.class + } + field { + name "success" + desc "" + type "boolean" + since "5.5.0" + } + ref { + name "error" + path "org.zstack.header.longjob.APISuspendLongJobEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false + type "ErrorCode" + since "5.5.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsg.java b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsg.java new file mode 100644 index 00000000000..c84ae0b659e --- /dev/null +++ b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsg.java @@ -0,0 +1,38 @@ +package org.zstack.header.longjob; + +import org.springframework.http.HttpMethod; +import org.zstack.header.identity.Action; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; + +@Action(category = LongJobConstants.ACTION_CATEGORY) +@RestRequest( + path = "/longjobs/{uuid}/actions", + isAction = true, + method = HttpMethod.PUT, + responseClass = APISuspendLongJobEvent.class +) +public class APISuspendLongJobMsg extends APIMessage implements LongJobMessage { + @APIParam(resourceType = LongJobVO.class, checkAccount = true) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public static APISuspendLongJobMsg __example__() { + APISuspendLongJobMsg msg = new APISuspendLongJobMsg(); + msg.setUuid(uuid()); + return msg; + } + + @Override + public String getLongJobUuid() { + return uuid; + } +} diff --git a/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..acb75528476 --- /dev/null +++ b/header/src/main/java/org/zstack/header/longjob/APISuspendLongJobMsgDoc_zh_cn.groovy @@ -0,0 +1,58 @@ +package org.zstack.header.longjob + +import org.zstack.header.longjob.APISuspendLongJobEvent + +doc { + title "SuspendLongJob" + + category "longjob" + + desc """在这里填写API描述""" + + rest { + request { + url "PUT /v1/longjobs/{uuid}/actions" + + header (Authorization: 'OAuth the-session-uuid') + + clz APISuspendLongJobMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "suspendLongJob" + desc "资源的UUID,唯一标示该资源" + location "url" + type "String" + optional false + since "5.5.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.5.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.5.0" + } + } + } + + response { + clz APISuspendLongJobEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/longjob/LongJob.java b/header/src/main/java/org/zstack/header/longjob/LongJob.java index c83d03242cf..bde854845d7 100644 --- a/header/src/main/java/org/zstack/header/longjob/LongJob.java +++ b/header/src/main/java/org/zstack/header/longjob/LongJob.java @@ -10,6 +10,7 @@ public interface LongJob { void start(LongJobVO job, ReturnValueCompletion completion); default void cancel(LongJobVO job, ReturnValueCompletion completion) {} + default void suspend(LongJobVO job, ReturnValueCompletion completion) {} default void resume(LongJobVO job, ReturnValueCompletion completion) {} default void clean(LongJobVO job, NoErrorCompletion completion) {} default Class getAuditType() { diff --git a/longjob/src/main/java/org/zstack/longjob/LongJobFactory.java b/longjob/src/main/java/org/zstack/longjob/LongJobFactory.java index df808d6c4f5..5176db4a6e8 100644 --- a/longjob/src/main/java/org/zstack/longjob/LongJobFactory.java +++ b/longjob/src/main/java/org/zstack/longjob/LongJobFactory.java @@ -11,6 +11,7 @@ public interface LongJobFactory { LongJob getLongJob(String jobName); TreeMap getFullJobName(); boolean supportCancel(String jobName); + boolean supportSuspend(String jobName); boolean supportResume(String jobName); boolean supportClean(String jobName); } diff --git a/longjob/src/main/java/org/zstack/longjob/LongJobFactoryImpl.java b/longjob/src/main/java/org/zstack/longjob/LongJobFactoryImpl.java index 15dc68c76a3..bf73119e693 100755 --- a/longjob/src/main/java/org/zstack/longjob/LongJobFactoryImpl.java +++ b/longjob/src/main/java/org/zstack/longjob/LongJobFactoryImpl.java @@ -30,6 +30,7 @@ public class LongJobFactoryImpl implements LongJobFactory, Component { private TreeMap fullJobName = new TreeMap<>(); private Set notSupportCancelJobType = new HashSet<>(); + private Set notSupportSuspendJobType = new HashSet<>(); private Set notSupportResumeJobType = new HashSet<>(); private Set notSupportCleanJobType = new HashSet<>(); @@ -80,6 +81,11 @@ public boolean supportCancel(String jobName) { return !notSupportCancelJobType.contains(jobName); } + @Override + public boolean supportSuspend(String jobName) { + return !notSupportSuspendJobType.contains(jobName); + } + @Override public boolean supportResume(String jobName) { return !notSupportResumeJobType.contains(jobName); @@ -96,6 +102,10 @@ private void checkBehaviorSupported(String jobName, LongJob job) { notSupportCancelJobType.add(jobName); } + if (method.getName().equals("suspend") && method.isDefault()) { + notSupportSuspendJobType.add(jobName); + } + if (method.getName().equals("resume") && method.isDefault()) { notSupportResumeJobType.add(jobName); } diff --git a/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java b/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java index c74b43909c2..549d2c5d043 100755 --- a/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java +++ b/longjob/src/main/java/org/zstack/longjob/LongJobManagerImpl.java @@ -127,6 +127,8 @@ private void handleApiMessage(APIMessage msg) { handle((APIRerunLongJobMsg) msg); } else if (msg instanceof APIResumeLongJobMsg) { handle((APIResumeLongJobMsg) msg); + } else if (msg instanceof APISuspendLongJobMsg) { + handle((APISuspendLongJobMsg) msg); } else if (msg instanceof APICleanLongJobMsg) { handle((APICleanLongJobMsg) msg); } else { @@ -424,6 +426,80 @@ public String getName() { }); } + private void handle(APISuspendLongJobMsg msg) { + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return "longjob-" + msg.getUuid(); + } + + @Override + public void run(SyncTaskChain chain) { + final APISuspendLongJobEvent evt = new APISuspendLongJobEvent(msg.getId()); + suspendLongJob(msg.getUuid(), new ReturnValueCompletion(chain) { + @Override + public void success(LongJobVO vo) { + evt.setInventory(LongJobInventory.valueOf(vo)); + bus.publish(evt); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + evt.setError(errorCode); + bus.publish(evt); + chain.next(); + } + }); + } + + @Override + public String getName() { + return String.format("suspend-longjob-%s", msg.getUuid()); + } + }); + } + + private void suspendLongJob(String uuid, ReturnValueCompletion completion) { + Tuple t = Q.New(LongJobVO.class).eq(LongJobVO_.uuid, uuid).select(LongJobVO_.state, LongJobVO_.jobName).findTuple(); + LongJobState currentState = t.get(0, LongJobState.class); + String jobName = t.get(1, String.class); + + if (currentState == LongJobState.Suspended) { + LongJobVO vo = dbf.findByUuid(uuid, LongJobVO.class); + completion.success(vo); + return; + } + + if (currentState != LongJobState.Running) { + completion.fail(err(ORG_ZSTACK_LONGJOB_10002, LongJobErrors.NOT_SUPPORTED, "can only suspend running jobs, current state: %s", currentState)); + return; + } + + if (!longJobFactory.supportSuspend(jobName)) { + completion.fail(err(ORG_ZSTACK_LONGJOB_10002, LongJobErrors.NOT_SUPPORTED, "job type %s does not support suspend", jobName)); + return; + } + + LongJobVO vo = dbf.findByUuid(uuid, LongJobVO.class); + LongJob job = longJobFactory.getLongJob(vo.getJobName()); + + job.suspend(vo, new ReturnValueCompletion(completion) { + @Override + public void success(Boolean suspended) { + LongJobVO updatedVo = changeState(uuid, LongJobStateEvent.suspend); + logger.info(String.format("longjob [uuid:%s, name:%s] has been suspended", vo.getUuid(), vo.getName())); + completion.success(updatedVo); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.error(String.format("failed to suspend longjob [uuid:%s, name:%s]: %s", vo.getUuid(), vo.getName(), errorCode)); + completion.fail(errorCode); + } + }); + } + private void handle(ResumeLongJobMsg msg) { thdf.chainSubmit(new ChainTask(msg) { @Override diff --git a/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java b/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java index 89691955a14..a9d66ef3dd2 100644 --- a/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java +++ b/longjob/src/main/java/org/zstack/longjob/LongJobUtils.java @@ -162,11 +162,33 @@ static LongJobStateEvent getEventOnError(ErrorCode errorCode) { return LongJobStateEvent.suspend; } else if (errorCode.isError(LongJobErrors.CANCELED)) { return LongJobStateEvent.canceled; + } else if (isRecoverableError(errorCode)) { + return LongJobStateEvent.suspend; } else { return LongJobStateEvent.fail; } } + /** + * Check if an error is marked as recoverable in its opaque field. + * Any business module can mark an error as recoverable by setting + * "longJobRecoverable" to true in the error code's opaque field. + * This allows the long job framework to automatically suspend instead of fail + * for recoverable errors, enabling automatic retry after service restart. + * + * @param errorCode the error code to check + * @return true if the error is marked as recoverable, false otherwise + */ + private static boolean isRecoverableError(ErrorCode errorCode) { + // Check if error code has recoverable flag in opaque + // Any business module can set this flag to indicate the error is recoverable + Object recoverable = errorCode.getFromOpaque("longJobRecoverable"); + if (recoverable instanceof Boolean && (Boolean) recoverable) { + return true; + } + return false; + } + private static void setExecuteTimeIfNeed(LongJobVO job) { if (job.getExecuteTime() == null) { long time = (System.currentTimeMillis() - job.getCreateDate().getTime()) / 1000; diff --git a/sdk/src/main/java/org/zstack/sdk/SuspendLongJobAction.java b/sdk/src/main/java/org/zstack/sdk/SuspendLongJobAction.java new file mode 100644 index 00000000000..24d56735197 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SuspendLongJobAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class SuspendLongJobAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.SuspendLongJobResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.SuspendLongJobResult value = res.getResult(org.zstack.sdk.SuspendLongJobResult.class); + ret.value = value == null ? new org.zstack.sdk.SuspendLongJobResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/longjobs/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "suspendLongJob"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/SuspendLongJobResult.java b/sdk/src/main/java/org/zstack/sdk/SuspendLongJobResult.java new file mode 100644 index 00000000000..0eeb24b9bd9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SuspendLongJobResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.LongJobInventory; + +public class SuspendLongJobResult { + public LongJobInventory inventory; + public void setInventory(LongJobInventory inventory) { + this.inventory = inventory; + } + public LongJobInventory getInventory() { + return this.inventory; + } + +} diff --git a/test/src/test/resources/zstack.properties b/test/src/test/resources/zstack.properties index b8ee3650b69..f7f4da51bf3 100755 --- a/test/src/test/resources/zstack.properties +++ b/test/src/test/resources/zstack.properties @@ -84,3 +84,4 @@ InfluxDB.metadata.version=v2 chain.task.qos=true identity.init.type= + diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index a77924c67b7..91ff3f209ba 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -40941,6 +40941,33 @@ abstract class ApiHelper { } + def suspendLongJob(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SuspendLongJobAction.class) Closure c) { + def a = new org.zstack.sdk.SuspendLongJobAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + def syncAINginxConfiguration(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SyncAINginxConfigurationAction.class) Closure c) { def a = new org.zstack.sdk.SyncAINginxConfigurationAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 1913ee8a618..2c4e3cd04b3 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -7506,6 +7506,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_MESSAGE_10002 = "ORG_ZSTACK_AI_MESSAGE_10002"; + public static final String ORG_ZSTACK_AI_MESSAGE_10003 = "ORG_ZSTACK_AI_MESSAGE_10003"; + public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10000 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10000"; public static final String ORG_ZSTACK_BAREMETAL2_INSTANCE_10001 = "ORG_ZSTACK_BAREMETAL2_INSTANCE_10001"; @@ -14786,6 +14788,12 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_AI_10130 = "ORG_ZSTACK_AI_10130"; + public static final String ORG_ZSTACK_AI_10131 = "ORG_ZSTACK_AI_10131"; + + public static final String ORG_ZSTACK_AI_10132 = "ORG_ZSTACK_AI_10132"; + + public static final String ORG_ZSTACK_AI_10133 = "ORG_ZSTACK_AI_10133"; + public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000"; public static final String ORG_ZSTACK_CORE_CLOUDBUS_10001 = "ORG_ZSTACK_CORE_CLOUDBUS_10001";