From 27813aca30544953869c36f02ed47508667dd2d0 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:52:12 +0300 Subject: [PATCH 1/7] Add service timeout parameters and resolution model Introduce the vocabulary for configurable service operation timeouts: - SupportedParameters: resource-level (create-service-timeout, bind-service-timeout, create-service-key-timeout) and global-level (services-*) parameter names - Variables: the CLI-override process variables and the bind global fallback - CloudServiceInstanceExtended: resource-level timeout fields, populated by ServicesCloudModelBuilder from the service's mtad parameters - DurationUtil: parse and validate raw timeout values into Duration - Constants: fixed ceilings for delete-service, delete-service-key and unbind-service operations --- .../domain/CloudServiceInstanceExtended.java | 12 +++ .../multiapps/controller/core/Messages.java | 1 + .../core/cf/v2/ServicesCloudModelBuilder.java | 21 +++++ .../core/model/SupportedParameters.java | 14 ++- .../controller/core/util/DurationUtil.java | 37 ++++++++ .../core/util/DurationUtilTest.java | 89 +++++++++++++++++++ .../controller/process/Constants.java | 5 ++ .../process/variables/Variables.java | 20 +++++ 8 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtilTest.java diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/CloudServiceInstanceExtended.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/CloudServiceInstanceExtended.java index 3e0c375695..d765de5bf8 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/CloudServiceInstanceExtended.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/CloudServiceInstanceExtended.java @@ -1,5 +1,7 @@ package org.cloudfoundry.multiapps.controller.client.lib.domain; +import java.time.Duration; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.cloudfoundry.multiapps.common.Nullable; @@ -53,4 +55,14 @@ public boolean shouldSkipSyslogUrlUpdate() { @Nullable public abstract Boolean shouldFailOnTagsUpdateFailure(); + @Nullable + public abstract Duration getCreateServiceTimeout(); + + @Nullable + public abstract Duration getBindServiceTimeout(); + + @Nullable + public abstract Duration getCreateServiceKeyTimeout(); + + } diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 16e8fada28..887f6b0c92 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -309,6 +309,7 @@ public final class Messages { public static final String API_INFO_AUDIT_LOG_CONFIG = "Api info"; public static final String IGNORING_NAMESPACE_PARAMETERS = "Ignoring parameter \"{0}\" , as the MTA is not deployed with namespace!"; public static final String NAMESPACE_PARSING_ERROR_MESSAGE = "Cannot parse \"{0}\" flag - expected a boolean format."; + public static final String PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1 = "Parameter \"{0}\" must be positive integer value up to {1}!"; private Messages() { } diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java index 90abef06be..b2f95f3c17 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ServicesCloudModelBuilder.java @@ -1,12 +1,14 @@ package org.cloudfoundry.multiapps.controller.core.cf.v2; import java.text.MessageFormat; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.stream.Collectors; + import org.cloudfoundry.client.v3.serviceinstances.ServiceInstanceType; import org.cloudfoundry.multiapps.common.ContentException; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; @@ -14,6 +16,7 @@ import org.cloudfoundry.multiapps.controller.core.Messages; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.util.CloudModelBuilderUtil; +import org.cloudfoundry.multiapps.controller.core.util.DurationUtil; import org.cloudfoundry.multiapps.controller.core.util.NameUtil; import org.cloudfoundry.multiapps.controller.core.util.SpecialResourceTypesRequiredParametersUtil; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; @@ -88,6 +91,9 @@ protected CloudServiceInstanceExtended createManagedService(Resource resource, C commonServiceParameters.failOnServiceParametersUpdateFailure()) .shouldFailOnPlanUpdateFailure(commonServiceParameters.failOnServicePlanUpdateFailure()) .shouldFailOnTagsUpdateFailure(commonServiceParameters.failOnServiceTagsUpdateFailure()) + .createServiceTimeout(extractTimeoutValue(parameters, SupportedParameters.CREATE_SERVICE_TIMEOUT)) + .bindServiceTimeout(extractTimeoutValue(parameters, SupportedParameters.BIND_SERVICE_TIMEOUT)) + .createServiceKeyTimeout(extractTimeoutValue(parameters, SupportedParameters.CREATE_SERVICE_KEY_TIMEOUT)) .v3Metadata(ServiceMetadataBuilder.build(deploymentDescriptor, namespace, resource)) .build(); } @@ -120,11 +126,15 @@ protected CloudServiceInstanceExtended createUserProvidedService(Resource resour commonServiceParameters.failOnServiceParametersUpdateFailure()) .shouldFailOnPlanUpdateFailure(commonServiceParameters.failOnServicePlanUpdateFailure()) .shouldFailOnTagsUpdateFailure(commonServiceParameters.failOnServiceTagsUpdateFailure()) + .createServiceTimeout(extractTimeoutValue(parameters, SupportedParameters.CREATE_SERVICE_TIMEOUT)) + .bindServiceTimeout(extractTimeoutValue(parameters, SupportedParameters.BIND_SERVICE_TIMEOUT)) + .createServiceKeyTimeout(extractTimeoutValue(parameters, SupportedParameters.CREATE_SERVICE_KEY_TIMEOUT)) .v3Metadata(ServiceMetadataBuilder.build(deploymentDescriptor, namespace, resource)) .build(); } protected CloudServiceInstanceExtended createExistingService(Resource resource, CommonServiceParameters commonServiceParameters) { + Map parameters = resource.getParameters(); return ImmutableCloudServiceInstanceExtended.builder() .name(commonServiceParameters.getServiceName()) .resourceName(resource.getName()) @@ -137,6 +147,9 @@ protected CloudServiceInstanceExtended createExistingService(Resource resource, commonServiceParameters.failOnServiceParametersUpdateFailure()) .shouldFailOnPlanUpdateFailure(commonServiceParameters.failOnServicePlanUpdateFailure()) .shouldFailOnTagsUpdateFailure(commonServiceParameters.failOnServiceTagsUpdateFailure()) + .createServiceTimeout(extractTimeoutValue(parameters, SupportedParameters.CREATE_SERVICE_TIMEOUT)) + .bindServiceTimeout(extractTimeoutValue(parameters, SupportedParameters.BIND_SERVICE_TIMEOUT)) + .createServiceKeyTimeout(extractTimeoutValue(parameters, SupportedParameters.CREATE_SERVICE_KEY_TIMEOUT)) .v3Metadata(ServiceMetadataBuilder.build(deploymentDescriptor, namespace, resource)) .build(); } @@ -215,4 +228,12 @@ private Boolean failOnServicePlanUpdateFailure() { } + private Duration extractTimeoutValue(Map parameters, String timeoutParameterName) { + if (parameters == null) { + return null; + } + return DurationUtil.parseDurationSafely(parameters.get(timeoutParameterName)) + .orElse(null); + } + } diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java index 08792a9542..664e713075 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java @@ -188,6 +188,13 @@ public class SupportedParameters { @Deprecated public static final String DEPRECATED_CONFIG_MTA_PROVIDES_DEPENDENCY = "mta-provides-dependency"; + public static final String CREATE_SERVICE_TIMEOUT = "create-service-timeout"; + public static final String SERVICES_CREATE_SERVICE_TIMEOUT = "services-create-service-timeout"; + public static final String BIND_SERVICE_TIMEOUT = "bind-service-timeout"; + public static final String SERVICES_BIND_SERVICE_TIMEOUT = "services-bind-service-timeout"; + public static final String CREATE_SERVICE_KEY_TIMEOUT = "create-service-key-timeout"; + public static final String SERVICES_CREATE_SERVICE_KEY_TIMEOUT = "services-create-service-key-timeout"; + public static final Set MODULE_PARAMETERS = Set.of(APP_NAME, APPLY_NAMESPACE, BUILDPACK, BUILDPACKS, LIFECYCLE, COMMAND, CREATE_SERVICE_BROKER, DEFAULT_APP_NAME, DEFAULT_HOST, DEFAULT_INSTANCES, DEFAULT_LIVE_APP_NAME, DEFAULT_LIVE_DOMAIN, DEFAULT_LIVE_HOST, @@ -217,10 +224,13 @@ public class SupportedParameters { SKIP_SERVICE_UPDATES, TYPE, PROVIDER_ID, PROVIDER_NID, TARGET, SERVICE_CONFIG_PATH, FILTER, MANAGED, VERSION, PATH, MEMORY, FAIL_ON_SERVICE_UPDATE, SERVICE_PROVIDER, SERVICE_VERSION, LOG_LEVEL, - DESTINATION, CLS_ORG_NAME, CLS_SPACE_NAME); + DESTINATION, CLS_ORG_NAME, CLS_SPACE_NAME, + CREATE_SERVICE_TIMEOUT, BIND_SERVICE_TIMEOUT, CREATE_SERVICE_KEY_TIMEOUT); public static final Set GLOBAL_PARAMETERS = Set.of(KEEP_EXISTING_ROUTES, APPS_UPLOAD_TIMEOUT, APPS_TASK_EXECUTION_TIMEOUT, APPS_START_TIMEOUT, APPS_STAGE_TIMEOUT, APPLY_NAMESPACE, - ENABLE_PARALLEL_DEPLOYMENTS, DEPLOY_MODE, BG_DEPENDENCY_AWARE_STOP_ORDER); + ENABLE_PARALLEL_DEPLOYMENTS, DEPLOY_MODE, BG_DEPENDENCY_AWARE_STOP_ORDER, + SERVICES_CREATE_SERVICE_TIMEOUT, + SERVICES_BIND_SERVICE_TIMEOUT, SERVICES_CREATE_SERVICE_KEY_TIMEOUT); public static final Set DEPENDENCY_PARAMETERS = Set.of(BINDING_NAME, ENV_VAR_NAME, VISIBILITY, USE_LIVE_ROUTES, SERVICE_BINDING_CONFIG, DELETE_SERVICE_KEY_AFTER_DEPLOYMENT); diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java new file mode 100644 index 0000000000..eab7b1b0c8 --- /dev/null +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java @@ -0,0 +1,37 @@ +package org.cloudfoundry.multiapps.controller.core.util; + +import java.time.Duration; +import java.util.Optional; + +import org.cloudfoundry.multiapps.common.ContentException; +import org.cloudfoundry.multiapps.controller.core.Messages; + +public final class DurationUtil { + + private DurationUtil() { + // utility class + } + + public static Duration parseDuration(Object timeout, String parameterName, int maxAllowedValue) { + if (timeout == null) { + return null; + } + if (!(timeout instanceof Number number)) { + throw new ContentException(Messages.PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1, parameterName, maxAllowedValue); + } + int value = number.intValue(); + if (value < 0 || value > maxAllowedValue) { + throw new ContentException(Messages.PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1, parameterName, maxAllowedValue); + } + return Duration.ofSeconds(value); + } + + public static Optional parseDurationSafely(Object timeout) { + if (timeout == null || !(timeout instanceof Number number)) { + return Optional.empty(); + } + long seconds = number.longValue(); + return seconds > 0 ? Optional.of(Duration.ofSeconds(seconds)) : Optional.empty(); + } +} + diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtilTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtilTest.java new file mode 100644 index 0000000000..f611451941 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtilTest.java @@ -0,0 +1,89 @@ +package org.cloudfoundry.multiapps.controller.core.util; + +import java.time.Duration; +import java.util.Optional; + +import org.cloudfoundry.multiapps.common.ContentException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DurationUtilTest { + + private static final String PARAM_NAME = "test-timeout"; + private static final int MAX_VALUE = 3600; + + @Test + void testParseDurationWithValidValue() { + Duration result = DurationUtil.parseDuration(300, PARAM_NAME, MAX_VALUE); + assertEquals(Duration.ofSeconds(300), result); + } + + @Test + void testParseDurationWithZero() { + Duration result = DurationUtil.parseDuration(0, PARAM_NAME, MAX_VALUE); + assertEquals(Duration.ZERO, result); + } + + @Test + void testParseDurationWithMaxValue() { + Duration result = DurationUtil.parseDuration(MAX_VALUE, PARAM_NAME, MAX_VALUE); + assertEquals(Duration.ofSeconds(MAX_VALUE), result); + } + + @Test + void testParseDurationWithNull() { + Duration result = DurationUtil.parseDuration(null, PARAM_NAME, MAX_VALUE); + assertNull(result); + } + + @Test + void testParseDurationWithNegativeValue() { + assertThrows(ContentException.class, () -> DurationUtil.parseDuration(-1, PARAM_NAME, MAX_VALUE)); + } + + @Test + void testParseDurationWithValueExceedingMax() { + assertThrows(ContentException.class, () -> DurationUtil.parseDuration(MAX_VALUE + 1, PARAM_NAME, MAX_VALUE)); + } + + @Test + void testParseDurationWithNonNumber() { + assertThrows(ContentException.class, () -> DurationUtil.parseDuration("invalid", PARAM_NAME, MAX_VALUE)); + } + + @Test + void testParseDurationSafelyWithValidValue() { + Optional result = DurationUtil.parseDurationSafely(300); + assertTrue(result.isPresent()); + assertEquals(Duration.ofSeconds(300), result.get()); + } + + @Test + void testParseDurationSafelyWithNull() { + Optional result = DurationUtil.parseDurationSafely(null); + assertTrue(result.isEmpty()); + } + + @Test + void testParseDurationSafelyWithZero() { + Optional result = DurationUtil.parseDurationSafely(0); + assertTrue(result.isEmpty()); + } + + @Test + void testParseDurationSafelyWithNegative() { + Optional result = DurationUtil.parseDurationSafely(-5); + assertTrue(result.isEmpty()); + } + + @Test + void testParseDurationSafelyWithNonNumber() { + Optional result = DurationUtil.parseDurationSafely("invalid"); + assertTrue(result.isEmpty()); + } +} + diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Constants.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Constants.java index 7ff3d2c76a..eee2f6669b 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Constants.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Constants.java @@ -1,5 +1,6 @@ package org.cloudfoundry.multiapps.controller.process; +import java.time.Duration; import java.util.regex.Pattern; public class Constants { @@ -46,6 +47,10 @@ public class Constants { public static final Pattern STANDARD_INT_PATTERN = Pattern.compile("[+-]?[0-9]+"); + public static final Duration DEFAULT_DELETE_SERVICE_TIMEOUT = Duration.ofMinutes(60); + public static final Duration DEFAULT_DELETE_SERVICE_KEY_TIMEOUT = Duration.ofMinutes(60); + public static final Duration DEFAULT_UNBIND_SERVICE_TIMEOUT = Duration.ofMinutes(60); + protected Constants() { } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java index efe9f0f026..52a21943e4 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java @@ -987,4 +987,24 @@ public Serializer> getSerializer() { .name("parentProcessInstanceId") .defaultValue(null) .build(); + + Variable CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE = ImmutableSimpleVariable. builder() + .name("servicesCreateServiceTimeout") + .defaultValue(Duration.ofHours(1)) + .build(); + + Variable BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE = ImmutableSimpleVariable. builder() + .name("servicesBindServiceTimeout") + .defaultValue(Duration.ofHours(1)) + .build(); + + Variable CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE = ImmutableSimpleVariable. builder() + .name("servicesCreateServiceKeyTimeout") + .defaultValue(Duration.ofHours(1)) + .build(); + + Variable BIND_SERVICE_TIMEOUT_GLOBAL_FALLBACK = ImmutableSimpleVariable. builder() + .name("servicesBindServiceTimeoutGlobalFallback") + .build(); + } From 366b97d09f1130f2203412d812950e54be48aace Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:53:09 +0300 Subject: [PATCH 2/7] Resolve async step timeouts from CLI, resource and global sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the timeout resolution engine: - TimeoutType: declares, per operation, its parameter names, CLI process variable, maximum value, and (for services) how to read the timeout off the service instance - TimeoutValueResolver: resolves a timeout by priority — CLI process variable, then resource/service-object value, then global descriptor parameter, then the default. Bind additionally falls back to a pre-resolved global variable because its subprocess has no descriptor - TimeoutStepStateManager: tracks per-step elapsed time with correct reset semantics across retries and poll cycles --- .../controller/process/Messages.java | 7 +- .../process/util/TimeoutStepStateManager.java | 57 +++++ .../controller/process/util/TimeoutType.java | 108 +++++++-- .../process/util/TimeoutValueResolver.java | 155 ++++++++++++ .../util/TimeoutStepStateManagerTest.java | 225 ++++++++++++++++++ .../util/TimeoutValueResolverTest.java | 153 ++++++++++++ 6 files changed, 687 insertions(+), 18 deletions(-) create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManager.java create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolver.java create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManagerTest.java create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolverTest.java diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java index 86ef2d9b56..6750cda01a 100755 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java @@ -101,6 +101,7 @@ public class Messages { public static final String ERROR_WHILE_DELETING_SERVICE_INSTANCE_METADATA_0 = "Error while deleting service instance metadata \"{0}\""; public static final String ERROR_WHILE_DELETING_SERVICE_KEY_0 = "Error while deleting service key \"{0}\""; public static final String ERROR_WHILE_CALCULATING_SERVICE_KEYS_FOR_WAITING = "Error while calculating service keys for waiting"; + public static final String SERVICE_KEY_NOT_FOUND = "Service key \"{0}\" for service \"{1}\" was not found and will be skipped"; public static final String COULD_NOT_GET_APP_LOGS = "Could not get application recent logs: {0}"; public static final String ERROR_DURING_INCREMENTAL_INSTANCE_UPDATE_OF_MODULE_0 = "Error during incremental instance update of module \"{0}\""; public static final String ERROR_DURING_POLL_OF_INCREMENTAL_INSTANCE_UPDATE_OF_MODULE_0 = "Error during poll of incremental instance update of module \"{0}\""; @@ -556,8 +557,7 @@ public class Messages { public static final String REMAINING_SERVICES_TO_POLL = "Remaining services to poll: {0}"; public static final String LAST_OPERATION_FOR_SERVICE = "Last operation for service \"{0}\": {1}"; public static final String DELETING_ERROR_TYPE_O_FOR_PROCESS_1 = "Deleting error type \"{0}\" for process \"{1}\""; - public static final String TIMEOUT_MESSAGE = "Application {0} timeout is set by \"{1}\" parameter to {2} seconds"; - public static final String TIMEOUT_DEFAULT_VALUE_MESSAGE = "Application {0} timeout is default one: {1} seconds"; + public static final String OPERATION_TIMEOUT_MESSAGE = "{0} timeout is set by \"{1}\" parameter to {2} seconds"; public static final String PROVIDED_EXTENSION_DESCRIPTORS = "Provided extension descriptors: {0}"; public static final String CREATED_SUBSCRIPTION = "Created subscription with ID: {0}"; public static final String UPDATING_SUBSCRIBER_0 = "Updating subscriber: {0} "; @@ -741,6 +741,7 @@ public class Messages { public static final String WILL_ONLY_REMOVE_SERVICE_INSTANCE_METADATA_BECAUSE_THE_SERVICE_TYPE_IS_EXISTING = "Will only remove service instance metadata, because the service type is \"existing\""; public static final String DETERMINING_DELETE_ACTIONS_FOR_SERVICE_INSTANCE_0 = "Determining delete actions for service instance \"{0}\""; public static final String CANNOT_RETRIEVE_OPTIONAL_SERVICE_BINDING_FOR_SERVICE_INSTANCE_0 = "Cannot retrieve optional service binding for service instance \"{0}\""; + public static final String CANNOT_RETRIEVE_SERVICE_BINDING_FOR_SERVICE_INSTANCE_0_NOT_FOUND = "Service instance \"{0}\" was not found; skipping service binding check."; public static final String SERVICE_KEYS_SCHEDULED_FOR_RECREATION_MODIFICATION_0 = "Service keys scheduled for recreation due to modification: \"{0}\""; public static final String SERVICE_KEYS_SCHEDULED_FOR_RECREATION_STATE_0 = "Service keys scheduled for recreation due to state: \"{0}\""; public static final String SERVICE_KEYS_SCHEDULED_FOR_CREATION_0 = "Service keys scheduled for creation: \"{0}\""; @@ -835,6 +836,8 @@ public class Messages { public static final String INVALID_BOOLEAN_VALUE = "Invalid boolean value: must be 'true' or 'false'"; public static final String DISABLE_AUTOSCALER_LABEL_CONTENT = "Disabled_by_MTA_operation_{0}"; + public static final String DEPLOYMENT_DESCRIPTOR_MISSING_GLOBAL_PARAMETER_0_NOT_APPLIED_FOR_1 = "Deployment descriptor is missing; global parameter {0} cannot be applied for {1}"; + protected Messages() { } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManager.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManager.java new file mode 100644 index 0000000000..e40d8fb368 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManager.java @@ -0,0 +1,57 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.time.Duration; + +import org.cloudfoundry.multiapps.controller.process.Constants; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.steps.StepPhase; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; + +public class TimeoutStepStateManager { + + public boolean hasTimedOut(ProcessContext context, String stepName, Duration timeout) { + long stepStartTime = getStepStartTime(context, stepName); + long currentTime = System.currentTimeMillis(); + return (currentTime - stepStartTime) >= timeout.toMillis(); + } + + public long getStepStartTime(ProcessContext context, String stepName) { + Long stepStartTime = (Long) context.getExecution() + .getVariable(getStepStartTimeVariable(stepName)); + if (shouldResetTimeout(stepStartTime, context)) { + stepStartTime = System.currentTimeMillis(); + context.getExecution() + .setVariable(getStepStartTimeVariable(stepName), stepStartTime); + clearResetFlag(context); + } + return stepStartTime; + } + + private boolean shouldResetTimeout(Long stepStartTime, ProcessContext context) { + return isTimeoutNotInitialized(stepStartTime) || isInRetry(context) || mustResetTimeout(context); + } + + private boolean isTimeoutNotInitialized(Long stepStartTime) { + return stepStartTime == null; + } + + private boolean isInRetry(ProcessContext context) { + return context.getVariable(Variables.STEP_PHASE) == StepPhase.RETRY; + } + + private boolean mustResetTimeout(ProcessContext context) { + return context.getVariable(Variables.MUST_RESET_TIMEOUT); + } + + private void clearResetFlag(ProcessContext context) { + if (mustResetTimeout(context)) { + context.setVariable(Variables.MUST_RESET_TIMEOUT, false); + } + } + + private String getStepStartTimeVariable(String stepName) { + return Constants.VAR_STEP_START_TIME + stepName; + } + +} + diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java index 6c43828fdf..630ae9ecd5 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java @@ -1,45 +1,121 @@ package org.cloudfoundry.multiapps.controller.process.util; +import java.time.Duration; + +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.process.variables.Variable; import org.cloudfoundry.multiapps.controller.process.variables.Variables; -import java.time.Duration; - public enum TimeoutType { + UPLOAD(new TimeoutParameterNames(SupportedParameters.UPLOAD_TIMEOUT, null, SupportedParameters.APPS_UPLOAD_TIMEOUT), + Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE, + null, + 10800, + null), + + STAGE(new TimeoutParameterNames(SupportedParameters.STAGE_TIMEOUT, null, SupportedParameters.APPS_STAGE_TIMEOUT), + Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE, + null, + 10800, + null), + + START(new TimeoutParameterNames(SupportedParameters.START_TIMEOUT, null, SupportedParameters.APPS_START_TIMEOUT), + Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE, + null, + 10800, + null), + + TASK(new TimeoutParameterNames(SupportedParameters.TASK_EXECUTION_TIMEOUT, null, SupportedParameters.APPS_TASK_EXECUTION_TIMEOUT), + Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE, + null, + 86400, + null), - UPLOAD(SupportedParameters.UPLOAD_TIMEOUT, SupportedParameters.APPS_UPLOAD_TIMEOUT, Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE, - 10800), STAGE(SupportedParameters.STAGE_TIMEOUT, SupportedParameters.APPS_STAGE_TIMEOUT, - Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE, 10800), START(SupportedParameters.START_TIMEOUT, - SupportedParameters.APPS_START_TIMEOUT, Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE, - 10800), TASK(SupportedParameters.TASK_EXECUTION_TIMEOUT, SupportedParameters.APPS_TASK_EXECUTION_TIMEOUT, - Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE, 86400); + CREATE_SERVICE( + new TimeoutParameterNames(null, SupportedParameters.CREATE_SERVICE_TIMEOUT, SupportedParameters.SERVICES_CREATE_SERVICE_TIMEOUT), + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE, + null, + 7200, + CloudServiceInstanceExtended::getCreateServiceTimeout), - private final String moduleLevelParamName; - private final String globalLevelParamName; + BIND_SERVICE( + new TimeoutParameterNames(null, SupportedParameters.BIND_SERVICE_TIMEOUT, SupportedParameters.SERVICES_BIND_SERVICE_TIMEOUT), + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE, + Variables.BIND_SERVICE_TIMEOUT_GLOBAL_FALLBACK, + 7200, + CloudServiceInstanceExtended::getBindServiceTimeout), + + CREATE_SERVICE_KEY(new TimeoutParameterNames(null, SupportedParameters.CREATE_SERVICE_KEY_TIMEOUT, + SupportedParameters.SERVICES_CREATE_SERVICE_KEY_TIMEOUT), + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE, + null, + 7200, + CloudServiceInstanceExtended::getCreateServiceKeyTimeout); + + @FunctionalInterface + public interface ServiceTimeoutGetter { + Duration getServiceTimeout(CloudServiceInstanceExtended service); + } + + private final TimeoutParameterNames parameterNames; private final Variable processVariable; + private final Variable globalFallbackVariable; private final Integer maxAllowedValue; + private final ServiceTimeoutGetter serviceTimeoutGetter; - TimeoutType(String moduleLevelParamName, String globalLevelParamName, Variable processVariable, Integer maxAllowedValue) { - this.moduleLevelParamName = moduleLevelParamName; - this.globalLevelParamName = globalLevelParamName; + TimeoutType(TimeoutParameterNames parameterNames, + Variable processVariable, + Variable globalFallbackVariable, + Integer maxAllowedValue, + ServiceTimeoutGetter serviceTimeoutGetter) { + this.parameterNames = parameterNames; this.processVariable = processVariable; + this.globalFallbackVariable = globalFallbackVariable; this.maxAllowedValue = maxAllowedValue; + this.serviceTimeoutGetter = serviceTimeoutGetter; } public String getModuleLevelParamName() { - return moduleLevelParamName; + return parameterNames.moduleLevelParamName(); + } + + public String getResourceLevelParamName() { + return parameterNames.resourceLevelParamName(); } - public String getProcessVariableAndGlobalLevelParamName() { - return globalLevelParamName; + public String getGlobalLevelParamName() { + return parameterNames.globalLevelParamName(); } public Variable getProcessVariable() { return processVariable; } + public Variable getGlobalFallbackVariable() { + return globalFallbackVariable; + } + public Integer getMaxAllowedValue() { return maxAllowedValue; } + + public ServiceTimeoutGetter getServiceTimeoutGetter() { + return serviceTimeoutGetter; + } + + public boolean isModuleScoped() { + return getModuleLevelParamName() != null; + } + + public boolean isServiceScoped() { + return !isModuleScoped(); + } + + public String getEntityLevelParamName() { + return isModuleScoped() ? getModuleLevelParamName() : getResourceLevelParamName(); + } + + public record TimeoutParameterNames(String moduleLevelParamName, String resourceLevelParamName, String globalLevelParamName) { + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolver.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolver.java new file mode 100644 index 0000000000..620aa28deb --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolver.java @@ -0,0 +1,155 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.core.helpers.ApplicationAttributes; +import org.cloudfoundry.multiapps.controller.core.util.DurationUtil; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variable; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; + +@Named +public class TimeoutValueResolver { + + public record TimeoutResolution(Duration timeout, String parameterName) { + } + + public TimeoutResolution resolveTimeout(ProcessContext context, TimeoutType timeoutType, StepLogger logger) { + return timeoutType.isModuleScoped() + ? resolveModuleTimeout(context, timeoutType, logger) + : resolveServiceTimeout(context, timeoutType, logger); + } + + private TimeoutResolution resolveModuleTimeout(ProcessContext context, TimeoutType timeoutType, StepLogger logger) { + return resolveProcessVariableTimeout(context, timeoutType) + .or(() -> extractTimeoutFromModuleDescriptorParameters(context, timeoutType)) + .or(() -> extractTimeoutFromAppAttributes(context, timeoutType)) + .or(() -> extractTimeoutFromDescriptorParameters(context, timeoutType, logger)) + .orElseGet(() -> new TimeoutResolution(timeoutType.getProcessVariable().getDefaultValue(), timeoutType.getGlobalLevelParamName())); + } + + private TimeoutResolution resolveServiceTimeout(ProcessContext context, TimeoutType timeoutType, StepLogger logger) { + return resolveProcessVariableTimeout(context, timeoutType) + .or(() -> extractTimeoutFromServiceObject(context, timeoutType)) + .or(() -> extractTimeoutFromGlobalFallback(context, timeoutType)) + .or(() -> extractTimeoutFromDescriptorParameters(context, timeoutType, logger)) + .orElseGet(() -> new TimeoutResolution(timeoutType.getProcessVariable().getDefaultValue(), timeoutType.getGlobalLevelParamName())); + } + + private Optional resolveProcessVariableTimeout(ProcessContext context, TimeoutType timeoutType) { + return Optional.ofNullable(context.getVariableIfSet(timeoutType.getProcessVariable())) + .map(pv -> new TimeoutResolution(pv, timeoutType.getGlobalLevelParamName())); + } + + private Optional extractTimeoutFromModuleDescriptorParameters(ProcessContext context, TimeoutType timeoutType) { + String paramName = timeoutType.getModuleLevelParamName(); + DeploymentDescriptor descriptor = getDeploymentDescriptor(context); + if (paramName == null || descriptor == null) { + return Optional.empty(); + } + String appName = Optional.ofNullable(context.getVariableIfSet(Variables.APP_TO_PROCESS)) + .map(CloudApplicationExtended::getName) + .orElse(null); + + return findModuleByAppName(descriptor, appName) + .flatMap(module -> toResolution(getParameter(module.getParameters(), paramName), paramName, timeoutType)); + } + + private Optional extractTimeoutFromAppAttributes(ProcessContext context, TimeoutType timeoutType) { + String paramName = timeoutType.getModuleLevelParamName(); + if (paramName == null) { + return Optional.empty(); + } + return Optional.ofNullable(context.getVariable(Variables.APP_TO_PROCESS)) + .map(app -> ApplicationAttributes.fromApplication(app, app.getEnv())) + .map(attrs -> attrs.get(paramName, Number.class)) + .flatMap(value -> toResolution(value, timeoutType.getGlobalLevelParamName(), timeoutType)); + } + + private Optional extractTimeoutFromGlobalFallback(ProcessContext context, TimeoutType timeoutType) { + Variable fallbackVar = timeoutType.getGlobalFallbackVariable(); + if (fallbackVar == null) { + return Optional.empty(); + } + return Optional.ofNullable(context.getVariableIfSet(fallbackVar)) + .map(d -> new TimeoutResolution(d, timeoutType.getGlobalLevelParamName())); + } + + private Optional extractTimeoutFromServiceObject(ProcessContext context, TimeoutType timeoutType) { + return resolveServiceInstance(context, timeoutType) + .flatMap(service -> Optional.ofNullable(timeoutType.getServiceTimeoutGetter()) + .map(getter -> getter.getServiceTimeout(service))) + .map(timeout -> new TimeoutResolution(timeout, timeoutType.getEntityLevelParamName())); + } + + private Optional resolveServiceInstance(ProcessContext context, TimeoutType timeoutType) { + if (timeoutType != TimeoutType.BIND_SERVICE) { + return Optional.ofNullable(context.getVariableIfSet(Variables.SERVICE_TO_PROCESS)); + } + String serviceName = context.getVariableIfSet(Variables.SERVICE_TO_UNBIND_BIND); + List services = context.getVariableIfSet(Variables.SERVICES_TO_BIND); + if (serviceName == null || services == null) { + return Optional.empty(); + } + return services.stream() + .filter(s -> serviceName.equals(s.getName())) + .findFirst(); + } + + private Optional extractTimeoutFromDescriptorParameters(ProcessContext context, TimeoutType timeoutType, + StepLogger logger) { + String paramName = timeoutType.getGlobalLevelParamName(); + DeploymentDescriptor descriptor = getDeploymentDescriptor(context); + if (descriptor == null) { + if (timeoutType.isServiceScoped()) { + logger.debug(Messages.DEPLOYMENT_DESCRIPTOR_MISSING_GLOBAL_PARAMETER_0_NOT_APPLIED_FOR_1, paramName, timeoutType); + } + return Optional.empty(); + } + return toResolution(getParameter(descriptor.getParameters(), paramName), paramName, timeoutType); + } + + private Optional toResolution(Object timeoutValue, String parameterName, TimeoutType timeoutType) { + return Optional.ofNullable(toDuration(timeoutValue, parameterName, timeoutType.getMaxAllowedValue())) + .map(duration -> new TimeoutResolution(duration, parameterName)); + } + + private Object getParameter(Map parameters, String parameterName) { + return parameters != null ? parameters.get(parameterName) : null; + } + + public Duration toDuration(Object timeout, String timeoutParameterName, Integer maxAllowedValue) { + return DurationUtil.parseDuration(timeout, timeoutParameterName, maxAllowedValue); + } + + private Optional findModuleByAppName(DeploymentDescriptor descriptor, String appName) { + if (descriptor == null || appName == null) { + return Optional.empty(); + } + return descriptor.getModules() + .stream() + .filter(m -> appName.equals(m.getName())) + .findFirst(); + } + + private DeploymentDescriptor getDeploymentDescriptor(ProcessContext context) { + return Stream.of(Variables.DEPLOYMENT_DESCRIPTOR, + Variables.DEPLOYMENT_DESCRIPTOR_WITH_SYSTEM_PARAMETERS, + Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR) + .map(context::getVariable) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManagerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManagerTest.java new file mode 100644 index 0000000000..77a3d05dae --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutStepStateManagerTest.java @@ -0,0 +1,225 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.time.Duration; + +import org.cloudfoundry.multiapps.controller.process.Constants; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.steps.StepPhase; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TimeoutStepStateManagerTest { + + private TimeoutStepStateManager timeoutStepStateManager; + + @Mock + private ProcessContext context; + + @Mock + private DelegateExecution execution; + + @BeforeEach + void setUp() { + try (var closeable = MockitoAnnotations.openMocks(this)) { + } catch (Exception e) { + throw new RuntimeException(e); + } + timeoutStepStateManager = new TimeoutStepStateManager(); + when(context.getExecution()).thenReturn(execution); + } + + @Test + void testHasTimedOutReturnsFalseWhenWithinTimeout() { + String stepName = "testStep"; + Duration timeout = Duration.ofSeconds(10); + long now = System.currentTimeMillis(); + long stepStartTime = now - 5000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(stepStartTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + boolean hasTimedOut = timeoutStepStateManager.hasTimedOut(context, stepName, timeout); + + assertFalse(hasTimedOut); + } + + @Test + void testHasTimedOutReturnsTrueWhenTimeoutExceeded() { + String stepName = "testStep"; + Duration timeout = Duration.ofSeconds(5); + long now = System.currentTimeMillis(); + long stepStartTime = now - 10000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(stepStartTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + boolean hasTimedOut = timeoutStepStateManager.hasTimedOut(context, stepName, timeout); + + assertTrue(hasTimedOut); + } + + @Test + void testHasTimedOutReturnsFalseAtExactTimeout() { + String stepName = "testStep"; + Duration timeout = Duration.ofSeconds(10); + long now = System.currentTimeMillis(); + long stepStartTime = now - 10000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(stepStartTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + boolean hasTimedOut = timeoutStepStateManager.hasTimedOut(context, stepName, timeout); + + assertTrue(hasTimedOut); + } + + @Test + void testGetStepStartTimeInitializesTimeWhenNotSet() { + String stepName = "testStep"; + long now = System.currentTimeMillis(); + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(null); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + long stepStartTime = timeoutStepStateManager.getStepStartTime(context, stepName); + + assertTrue(stepStartTime >= now); + assertTrue(stepStartTime <= System.currentTimeMillis()); + verify(execution).setVariable(Constants.VAR_STEP_START_TIME + stepName, stepStartTime); + } + + @Test + void testGetStepStartTimeReturnsExistingTimeWhenInitialized() { + String stepName = "testStep"; + long existingTime = System.currentTimeMillis() - 5000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(existingTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + long stepStartTime = timeoutStepStateManager.getStepStartTime(context, stepName); + + assertEquals(existingTime, stepStartTime); + } + + @Test + void testGetStepStartTimeResetsTimeoutOnRetry() { + String stepName = "testStep"; + long oldTime = System.currentTimeMillis() - 100000; + long now = System.currentTimeMillis(); + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(oldTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.RETRY); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + long stepStartTime = timeoutStepStateManager.getStepStartTime(context, stepName); + + assertTrue(stepStartTime >= now); + assertTrue(stepStartTime <= System.currentTimeMillis()); + verify(execution).setVariable(Constants.VAR_STEP_START_TIME + stepName, stepStartTime); + } + + @Test + void testGetStepStartTimeResetsTimeoutWhenMustResetFlagIsSet() { + String stepName = "testStep"; + long oldTime = System.currentTimeMillis() - 100000; + long now = System.currentTimeMillis(); + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(oldTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(true); + + long stepStartTime = timeoutStepStateManager.getStepStartTime(context, stepName); + + assertTrue(stepStartTime >= now); + assertTrue(stepStartTime <= System.currentTimeMillis()); + verify(execution).setVariable(Constants.VAR_STEP_START_TIME + stepName, stepStartTime); + verify(context).setVariable(Variables.MUST_RESET_TIMEOUT, false); + } + + @Test + void testGetStepStartTimeDoesNotResetWhenNotNeeded() { + String stepName = "testStep"; + long existingTime = System.currentTimeMillis() - 5000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(existingTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + long stepStartTime = timeoutStepStateManager.getStepStartTime(context, stepName); + + assertEquals(existingTime, stepStartTime); + } + + @Test + void testMultipleStepsHaveSeparateTimeoutStates() { + String step1 = "step1"; + String step2 = "step2"; + long time1 = System.currentTimeMillis() - 5000; + long time2 = System.currentTimeMillis() - 10000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + step1)).thenReturn(time1); + when(execution.getVariable(Constants.VAR_STEP_START_TIME + step2)).thenReturn(time2); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + long step1StartTime = timeoutStepStateManager.getStepStartTime(context, step1); + long step2StartTime = timeoutStepStateManager.getStepStartTime(context, step2); + + assertEquals(time1, step1StartTime); + assertEquals(time2, step2StartTime); + } + + @Test + void testHasTimedOutWithLargeTimeout() { + String stepName = "testStep"; + Duration timeout = Duration.ofHours(1); + long now = System.currentTimeMillis(); + long stepStartTime = now - 60000; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(stepStartTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + boolean hasTimedOut = timeoutStepStateManager.hasTimedOut(context, stepName, timeout); + + assertFalse(hasTimedOut); + } + + @Test + void testHasTimedOutWithSmallTimeout() { + String stepName = "testStep"; + Duration timeout = Duration.ofMillis(100); + long now = System.currentTimeMillis(); + long stepStartTime = now - 200; + + when(execution.getVariable(Constants.VAR_STEP_START_TIME + stepName)).thenReturn(stepStartTime); + when(context.getVariable(Variables.STEP_PHASE)).thenReturn(StepPhase.DONE); + when(context.getVariable(Variables.MUST_RESET_TIMEOUT)).thenReturn(false); + + boolean hasTimedOut = timeoutStepStateManager.hasTimedOut(context, stepName, timeout); + + assertTrue(hasTimedOut); + } + +} + + + + + + diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolverTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolverTest.java new file mode 100644 index 0000000000..618c999df1 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutValueResolverTest.java @@ -0,0 +1,153 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.time.Duration; + +import org.cloudfoundry.multiapps.common.ContentException; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +class TimeoutValueResolverTest { + + private TimeoutValueResolver timeoutValueResolver; + + @Mock + private ProcessContext context; + + @Mock + private StepLogger stepLogger; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + timeoutValueResolver = new TimeoutValueResolver(); + } + + @Test + void testResolveTimeoutFromProcessVariable() { + Duration expectedTimeout = Duration.ofSeconds(600); + when(context.getVariableIfSet(Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)) + .thenReturn(expectedTimeout); + when(context.getVariable(Variables.APP_TO_PROCESS)).thenReturn(null); + when(context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR)).thenReturn(null); + + TimeoutValueResolver.TimeoutResolution resolution = + timeoutValueResolver.resolveTimeout(context, TimeoutType.UPLOAD, stepLogger); + + assertNotNull(resolution); + assertEquals(expectedTimeout, resolution.timeout()); + } + + @Test + void testResolveTimeoutFromAppAttributes() { + var app = ImmutableCloudApplicationExtended.builder() + .name("test-app") + .build(); + + when(context.getVariableIfSet(Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)).thenReturn(null); + when(context.getVariable(Variables.APP_TO_PROCESS)).thenReturn(app); + when(context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR)).thenReturn(null); + + TimeoutValueResolver.TimeoutResolution resolution = + timeoutValueResolver.resolveTimeout(context, TimeoutType.START, stepLogger); + + assertNotNull(resolution); + assertNotNull(resolution.timeout()); + } + + @Test + void testResolveTimeoutReturnsDefaultWhenNoTimeoutFound() { + when(context.getVariableIfSet(Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)).thenReturn(null); + when(context.getVariable(Variables.APP_TO_PROCESS)).thenReturn(null); + when(context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR)).thenReturn(null); + when(context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR_WITH_SYSTEM_PARAMETERS)).thenReturn(null); + when(context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR)).thenReturn(null); + + TimeoutValueResolver.TimeoutResolution resolution = + timeoutValueResolver.resolveTimeout(context, TimeoutType.UPLOAD, stepLogger); + + assertNotNull(resolution); + assertEquals(Duration.ofSeconds(3600), resolution.timeout()); + } + + @Test + void testToDurationWithValidPositiveValue() { + Duration duration = timeoutValueResolver.toDuration(300, "testParam", 3600); + + assertEquals(Duration.ofSeconds(300), duration); + } + + @Test + void testToDurationWithZero() { + Duration duration = timeoutValueResolver.toDuration(0, "testParam", 3600); + + assertEquals(Duration.ZERO, duration); + } + + @Test + void testToDurationWithMaxValue() { + Duration duration = timeoutValueResolver.toDuration(3600, "testParam", 3600); + + assertEquals(Duration.ofSeconds(3600), duration); + } + + @Test + void testToDurationThrowsExceptionForNegativeValue() { + assertThrows(ContentException.class, () -> + timeoutValueResolver.toDuration(-1, "testParam", 3600) + ); + } + + @Test + void testToDurationThrowsExceptionForValueExceedingMax() { + assertThrows(ContentException.class, () -> + timeoutValueResolver.toDuration(3601, "testParam", 3600) + ); + } + + @Test + void testToDurationThrowsExceptionForNonNumberValue() { + assertThrows(ContentException.class, () -> + timeoutValueResolver.toDuration("invalid", "testParam", 3600) + ); + } + + @Test + void testToDurationReturnsNullForNullValue() { + Duration duration = timeoutValueResolver.toDuration(null, "testParam", 3600); + + assertNull(duration); + } + + @Test + void testToDurationWithDoubleValue() { + Duration duration = timeoutValueResolver.toDuration(300.5, "testParam", 3600); + + assertEquals(Duration.ofSeconds(300), duration); + } + + @Test + void testToDurationWithLongValue() { + Duration duration = timeoutValueResolver.toDuration(300L, "testParam", 3600); + + assertEquals(Duration.ofSeconds(300), duration); + } + +} + + + + + + From 2006142a902e904ef9043e070785f5ebfa5eac05 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:53:43 +0300 Subject: [PATCH 3/7] Enforce resolved timeouts in service and app lifecycle steps Wire the async steps to the resolver: - TimeoutAsyncFlowableStep: resolve and log the timeout, then enforce it via the step state manager on every execution - Create/Bind/CheckServiceBinding/CheckServiceKey steps resolve their configurable timeout from the resolver - Delete/DeleteServiceKey/Unbind steps adopt a fixed safety ceiling (previously unbounded) - ProcessDescriptorStep pre-resolves the global bind timeout into a process variable for the binding subprocess, which has no descriptor --- .../steps/BindServiceToApplicationStep.java | 8 +- .../CheckServiceBindingOperationStep.java | 9 +- .../steps/CheckServiceKeyOperationStep.java | 9 +- .../process/steps/CreateServiceKeyStep.java | 8 +- .../process/steps/DeleteServiceKeyStep.java | 8 +- .../process/steps/DeleteServiceStep.java | 8 +- .../process/steps/ProcessDescriptorStep.java | 20 +++ .../controller/process/steps/ServiceStep.java | 9 +- .../steps/TimeoutAsyncFlowableStep.java | 144 +++++------------- .../UnbindServiceFromApplicationStep.java | 8 +- .../steps/ProcessDescriptorStepTest.java | 10 +- .../process/steps/SyncFlowableStepTest.java | 7 +- .../process/steps/UploadAppStepTest.java | 4 +- 13 files changed, 133 insertions(+), 119 deletions(-) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BindServiceToApplicationStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BindServiceToApplicationStep.java index b5ffe14295..509ae68b45 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BindServiceToApplicationStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BindServiceToApplicationStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; @@ -13,13 +14,14 @@ import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.DefaultApplicationServicesUpdateCallback; import org.cloudfoundry.multiapps.controller.process.util.ServiceBindingPollingFactory; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @Named("bindServiceToApplicationStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class BindServiceToApplicationStep extends AsyncFlowableStep { +public class BindServiceToApplicationStep extends TimeoutAsyncFlowableStep { @Override protected StepPhase executeAsyncStep(ProcessContext context) throws Exception { @@ -71,4 +73,8 @@ protected String getStepErrorMessage(ProcessContext context) { .getName()); } + @Override + public Duration getTimeout(ProcessContext context) { + return calculateTimeout(context, TimeoutType.BIND_SERVICE); + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceBindingOperationStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceBindingOperationStep.java index 2972d915ad..10a9e69a52 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceBindingOperationStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceBindingOperationStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -13,13 +14,14 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @Named("checkServiceBindingOperationStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class CheckServiceBindingOperationStep extends AsyncFlowableStep { +public class CheckServiceBindingOperationStep extends TimeoutAsyncFlowableStep { @Override protected StepPhase executeAsyncStep(ProcessContext context) throws Exception { @@ -108,4 +110,9 @@ protected String getStepErrorMessage(ProcessContext context) { return MessageFormat.format(Messages.ERROR_WHILE_CHECKING_SERVICE_BINDING_OPERATIONS_BETWEEN_APP_0_AND_SERVICE_INSTANCE_1, app.getName(), serviceInstanceName); } + + @Override + public Duration getTimeout(ProcessContext context) { + return calculateTimeout(context, TimeoutType.BIND_SERVICE); + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceKeyOperationStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceKeyOperationStep.java index de465222b5..33d18db162 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceKeyOperationStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CheckServiceKeyOperationStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.List; import jakarta.inject.Named; @@ -8,13 +9,14 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @Named("checkServiceKeyOperationStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class CheckServiceKeyOperationStep extends AsyncFlowableStep { +public class CheckServiceKeyOperationStep extends TimeoutAsyncFlowableStep { @Override protected StepPhase executeAsyncStep(ProcessContext context) throws Exception { @@ -51,4 +53,9 @@ protected String getStepErrorMessage(ProcessContext context) { CloudServiceKey serviceKeyToProcess = context.getVariable(Variables.SERVICE_KEY_TO_PROCESS); return MessageFormat.format(Messages.ERROR_WHILE_CHECKING_SERVICE_KEY_OPERATION_0, serviceKeyToProcess.getName()); } + + @Override + public Duration getTimeout(ProcessContext context) { + return calculateTimeout(context, TimeoutType.CREATE_SERVICE_KEY); + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceKeyStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceKeyStep.java index 69d45976e7..4f6c949ff8 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceKeyStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CreateServiceKeyStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -13,6 +14,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ServiceKeyPollingFactory; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @@ -20,7 +22,7 @@ @Named("createServiceKeyStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class CreateServiceKeyStep extends AsyncFlowableStep { +public class CreateServiceKeyStep extends TimeoutAsyncFlowableStep { @Override protected StepPhase executeAsyncStep(ProcessContext context) throws Exception { @@ -81,4 +83,8 @@ protected String getStepErrorMessage(ProcessContext context) { return MessageFormat.format(Messages.ERROR_WHILE_CREATING_SERVICE_KEY_0, serviceKeyToCreate.getName()); } + @Override + public Duration getTimeout(ProcessContext context) { + return calculateTimeout(context, TimeoutType.CREATE_SERVICE_KEY); + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceKeyStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceKeyStep.java index 9740690372..d9ec486f91 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceKeyStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceKeyStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -11,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ServiceKeyPollingFactory; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -20,7 +22,7 @@ @Named("deleteServiceKeyStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class DeleteServiceKeyStep extends AsyncFlowableStep { +public class DeleteServiceKeyStep extends TimeoutAsyncFlowableStep { @Override protected StepPhase executeAsyncStep(ProcessContext context) throws Exception { @@ -94,4 +96,8 @@ protected String getStepErrorMessage(ProcessContext context) { return MessageFormat.format(Messages.ERROR_WHILE_DELETING_SERVICE_KEY_0, serviceKeyToDelete.getName()); } + @Override + public Duration getTimeout(ProcessContext context) { + return Constants.DEFAULT_DELETE_SERVICE_KEY_TIMEOUT; + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceStep.java index 9835a9ced2..ce827cc8be 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/DeleteServiceStep.java @@ -1,5 +1,6 @@ package org.cloudfoundry.multiapps.controller.process.steps; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; @@ -11,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceOperation; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ExceptionMessageTailMapper; import org.cloudfoundry.multiapps.controller.process.util.ExceptionMessageTailMapper.CloudComponents; @@ -23,7 +25,7 @@ @Named("deleteServiceStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class DeleteServiceStep extends AsyncFlowableStep { +public class DeleteServiceStep extends TimeoutAsyncFlowableStep { private final ServiceOperationGetter serviceOperationGetter; private final ServiceProgressReporter serviceProgressReporter; @@ -79,4 +81,8 @@ protected List getAsyncStepExecutions(ProcessContext context) { return Collections.singletonList(new PollServiceDeleteOperationsExecution(serviceOperationGetter, serviceProgressReporter)); } + @Override + public Duration getTimeout(ProcessContext context) { + return Constants.DEFAULT_DELETE_SERVICE_TIMEOUT; + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java index b82438bf32..6d5b131a67 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java @@ -1,12 +1,14 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; + import jakarta.inject.Inject; import jakarta.inject.Named; import org.apache.commons.collections4.CollectionUtils; @@ -23,6 +25,8 @@ import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.security.util.SecureLoggingUtil; import org.cloudfoundry.multiapps.controller.process.util.NamespaceGlobalParameters; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutValueResolver; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.Module; @@ -35,6 +39,8 @@ public class ProcessDescriptorStep extends SyncFlowableStep { private ConfigurationEntryService configurationEntryService; private ModuleToDeployHelper moduleToDeployHelper; + @Inject + TimeoutValueResolver timeoutValueResolver; @Inject public ProcessDescriptorStep(ConfigurationEntryService configurationEntryService, ModuleToDeployHelper moduleToDeployHelper) { @@ -58,6 +64,7 @@ protected StepPhase executeStep(ProcessContext context) { setDynamicResolvableParametersIfAbsent(context, resolver); context.setVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); + preResolveServiceTimeoutsFromDescriptor(context, descriptor); // Set MTA modules in the context List modulesForDeployment = context.getVariable(Variables.MODULES_FOR_DEPLOYMENT); List invalidModulesSpecifiedForDeployment = findInvalidModulesSpecifiedForDeployment(descriptor, modulesForDeployment); @@ -126,6 +133,19 @@ private MtaDescriptorPropertiesResolverContext buildMtaDescriptorPropertiesResol .build(); } + // Bind runs in a subprocess that does not receive the deployment descriptor, so the global-level + // services-bind-service-timeout is pre-resolved here into a process variable the resolver can read there. + private void preResolveServiceTimeoutsFromDescriptor(ProcessContext context, DeploymentDescriptor descriptor) { + TimeoutType timeoutType = TimeoutType.BIND_SERVICE; + Duration timeout = timeoutValueResolver.toDuration( + descriptor.getParameters() != null ? descriptor.getParameters().get(timeoutType.getGlobalLevelParamName()) : null, + timeoutType.getGlobalLevelParamName(), + timeoutType.getMaxAllowedValue()); + if (timeout != null) { + context.setVariable(timeoutType.getGlobalFallbackVariable(), timeout); + } + } + private void setDynamicResolvableParametersIfAbsent(ProcessContext context, MtaDescriptorPropertiesResolver resolver) { if (context.getVariable(Variables.DYNAMIC_RESOLVABLE_PARAMETERS) .isEmpty()) { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ServiceStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ServiceStep.java index cfb562278e..cb9aa6a1a7 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ServiceStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ServiceStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -16,10 +17,11 @@ import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ServiceOperationGetter; import org.cloudfoundry.multiapps.controller.process.util.ServiceProgressReporter; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.springframework.http.HttpStatus; -public abstract class ServiceStep extends AsyncFlowableStep { +public abstract class ServiceStep extends TimeoutAsyncFlowableStep { @Inject private ServiceOperationGetter serviceOperationGetter; @@ -73,6 +75,11 @@ protected void processServiceActionFailure(ProcessContext context, CloudServiceI throw new CloudControllerException(e.getStatusCode(), e.getStatusText(), detailedMessage); } + @Override + public Duration getTimeout(ProcessContext context) { + return calculateTimeout(context, TimeoutType.CREATE_SERVICE); + } + protected abstract ServiceOperation.Type getOperationType(); protected ServiceOperationGetter getServiceOperationGetter() { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java index a052583ba8..d55a9510c7 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java @@ -3,133 +3,65 @@ import java.text.MessageFormat; import java.time.Duration; -import org.cloudfoundry.multiapps.common.ContentException; +import jakarta.inject.Inject; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; -import org.cloudfoundry.multiapps.controller.core.helpers.ApplicationAttributes; -import org.cloudfoundry.multiapps.controller.process.Constants; +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutStepStateManager; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutValueResolver; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutValueResolver.TimeoutResolution; import org.cloudfoundry.multiapps.controller.process.variables.Variables; -import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; public abstract class TimeoutAsyncFlowableStep extends AsyncFlowableStep { - private static final String DEFAULT_TIMEOUT = "default"; + + private final TimeoutStepStateManager timeoutStepStateManager = new TimeoutStepStateManager(); + + @Inject + protected TimeoutValueResolver timeoutValueResolver; @Override public StepPhase executeStep(ProcessContext context) throws Exception { - boolean hasTimedOut = hasTimedOut(context); - if (hasTimedOut) { - throw new SLException(MessageFormat.format(Messages.EXECUTION_OF_STEP_HAS_TIMED_OUT, getStepName())); + if (timeoutStepStateManager.hasTimedOut(context, getClass().getSimpleName(), getTimeout(context))) { + throw new SLException(MessageFormat.format(Messages.EXECUTION_OF_STEP_HAS_TIMED_OUT, getClass().getSimpleName())); } return super.executeStep(context); } - private boolean hasTimedOut(ProcessContext context) { - long stepStartTime = getStepStartTime(context); - long currentTime = System.currentTimeMillis(); - return (currentTime - stepStartTime) >= getTimeout(context).toMillis(); - } - - private long getStepStartTime(ProcessContext context) { - Long stepStartTime = (Long) context.getExecution() - .getVariable(getStepStartTimeVariable()); - if (stepStartTime == null || isInRetry(context)) { - stepStartTime = System.currentTimeMillis(); - context.getExecution() - .setVariable(getStepStartTimeVariable(), stepStartTime); - } - if (context.getVariable(Variables.MUST_RESET_TIMEOUT)) { - stepStartTime = System.currentTimeMillis(); - context.getExecution() - .setVariable(getStepStartTimeVariable(), stepStartTime); - context.setVariable(Variables.MUST_RESET_TIMEOUT, false); - } - return stepStartTime; - } - - private boolean isInRetry(ProcessContext context) { - return context.getVariable(Variables.STEP_PHASE) == StepPhase.RETRY; - } - - private String getStepStartTimeVariable() { - return Constants.VAR_STEP_START_TIME + getStepName(); - } - - private String getStepName() { - return getClass().getSimpleName(); - } - - public abstract Duration getTimeout(ProcessContext context); - protected Duration calculateTimeout(ProcessContext context, TimeoutType timeoutType) { - Duration timeoutFinal = context.getVariableIfSet(timeoutType.getProcessVariable()); - String timeoutParameterName = timeoutType.getProcessVariableAndGlobalLevelParamName(); - - if (timeoutFinal == null) { - timeoutFinal = extractTimeoutFromAppAttributes(context, timeoutType); - if (timeoutFinal != null) { - timeoutParameterName = timeoutType.getModuleLevelParamName(); - } - } - - if (timeoutFinal == null) { - timeoutFinal = extractTimeoutFromDescriptorParameters(context, timeoutType); - } - - if (timeoutFinal == null) { - timeoutFinal = Duration.ofSeconds(timeoutType.getProcessVariable() - .getDefaultValue() - .getSeconds()); - timeoutParameterName = DEFAULT_TIMEOUT; - } - - logTimeout(timeoutType.getModuleLevelParamName(), timeoutParameterName, timeoutFinal.toSeconds()); - return timeoutFinal; + TimeoutResolution resolution = timeoutValueResolver.resolveTimeout(context, timeoutType, getStepLogger()); + logTimeout(context, timeoutType, resolution); + return resolution.timeout(); } - private Duration extractTimeoutFromAppAttributes(ProcessContext context, TimeoutType timeoutType) { - CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); - ApplicationAttributes appAttributes = ApplicationAttributes.fromApplication(app, app.getEnv()); - Number timeout = appAttributes.get(timeoutType.getModuleLevelParamName(), Number.class); - if (timeout != null && (timeout.intValue() < 0 || timeout.intValue() > timeoutType.getMaxAllowedValue())) { - throw new ContentException(Messages.PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1, - timeoutType.getModuleLevelParamName(), - timeoutType.getMaxAllowedValue()); - } - return timeout != null ? Duration.ofSeconds(timeout.intValue()) : null; + private void logTimeout(ProcessContext context, TimeoutType timeoutType, TimeoutResolution resolution) { + String operation = resolveOperationName(context, timeoutType); + long seconds = resolution.timeout().toSeconds(); + getStepLogger().info(Messages.OPERATION_TIMEOUT_MESSAGE, operation, resolution.parameterName(), seconds); } - private Duration extractTimeoutFromDescriptorParameters(ProcessContext context, TimeoutType timeoutType) { - DeploymentDescriptor descriptor = context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR); - Object timeoutGlobalLevelValue = descriptor.getParameters() - .get(timeoutType.getProcessVariableAndGlobalLevelParamName()); - validateTimeoutGlobalValue(timeoutGlobalLevelValue, timeoutType); - return timeoutGlobalLevelValue != null ? Duration.ofSeconds((Integer) timeoutGlobalLevelValue) : null; - } - - private void validateTimeoutGlobalValue(Object timeout, TimeoutType timeoutType) { - if (timeout != null && (timeout instanceof String || ((Number) timeout).intValue() < 0 - || ((Number) timeout).intValue() > timeoutType.getMaxAllowedValue())) { - throw new ContentException(Messages.PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1, - timeoutType.getProcessVariableAndGlobalLevelParamName(), - timeoutType.getMaxAllowedValue()); - } - } - - private void logTimeout(String moduleLevelParameterName, String timeoutParameterName, Number timeout) { - String timeoutTypeName = processString(moduleLevelParameterName); - if (timeoutParameterName.equals(DEFAULT_TIMEOUT)) { - getStepLogger().debug(Messages.TIMEOUT_DEFAULT_VALUE_MESSAGE, timeoutTypeName, timeout); - } else { - getStepLogger().debug(Messages.TIMEOUT_MESSAGE, timeoutTypeName, timeoutParameterName, timeout); - } + private String resolveOperationName(ProcessContext context, TimeoutType timeoutType) { + return switch (timeoutType) { + case UPLOAD, STAGE, START, TASK -> { + CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); + yield app != null ? MessageFormat.format("Application {0}", app.getName()) : "Application"; + } + case BIND_SERVICE -> { + String service = context.getVariable(Variables.SERVICE_TO_UNBIND_BIND); + yield service != null ? MessageFormat.format("Service binding for {0}", service) : "Service binding"; + } + case CREATE_SERVICE -> formatServiceOperation("Service creation", context); + case CREATE_SERVICE_KEY -> formatServiceOperation("Service key creation", context); + }; } - private String processString(String input) { - return input.replace("-", " ") - .replace("timeout", "") - .trim(); + private String formatServiceOperation(String operation, ProcessContext context) { + CloudServiceInstanceExtended service = context.getVariable(Variables.SERVICE_TO_PROCESS); + return service != null && service.getResourceName() != null + ? MessageFormat.format("{0} for {1}", operation, service.getResourceName()) + : operation; } + public abstract Duration getTimeout(ProcessContext context); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UnbindServiceFromApplicationStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UnbindServiceFromApplicationStep.java index ababdea3c3..7fd9bce05b 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UnbindServiceFromApplicationStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UnbindServiceFromApplicationStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.function.Supplier; @@ -12,6 +13,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBinding; import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.DeletingServiceBindingOperationCallback; import org.cloudfoundry.multiapps.controller.process.util.ServiceBindingPollingFactory; @@ -22,7 +24,7 @@ @Named("unbindServiceFromApplicationStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class UnbindServiceFromApplicationStep extends AsyncFlowableStep { +public class UnbindServiceFromApplicationStep extends TimeoutAsyncFlowableStep { @Override protected StepPhase executeAsyncStep(ProcessContext context) throws Exception { @@ -104,4 +106,8 @@ private ServiceBindingOperationCallback getServiceBindingOperationCallback(Proce return new DeletingServiceBindingOperationCallback(context, controllerClient); } + @Override + public Duration getTimeout(ProcessContext context) { + return Constants.DEFAULT_UNBIND_SERVICE_TIMEOUT; + } } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStepTest.java index d01a933f11..7045cfe3ad 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStepTest.java @@ -15,6 +15,8 @@ import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.test.DescriptorTestUtil; import org.cloudfoundry.multiapps.controller.process.Constants; + +import org.cloudfoundry.multiapps.controller.process.util.TimeoutValueResolver; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.Module; @@ -58,6 +60,8 @@ protected MtaDescriptorPropertiesResolver getMtaDescriptorPropertiesResolver(Pro private MtaDescriptorPropertiesResolver resolver; @Mock private ModuleToDeployHelper moduleToDeployHelper; + @Mock + private TimeoutValueResolver timeoutValueResolver; @BeforeEach public void setUp() throws Exception { @@ -153,7 +157,9 @@ private void prepareContextForSkipDeployParameters(boolean skipDeploy1, boolean @Override protected ProcessDescriptorStep createStep() { - return new ProcessDescriptorStepMock(moduleToDeployHelper); + ProcessDescriptorStep step = new ProcessDescriptorStepMock(moduleToDeployHelper); + step.timeoutValueResolver = timeoutValueResolver; + return step; } -} \ No newline at end of file +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java index 59ceec125e..2cf001e7db 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java @@ -33,6 +33,7 @@ import org.cloudfoundry.multiapps.controller.process.util.ProcessHelper; import org.cloudfoundry.multiapps.controller.process.util.StepLogger; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; +import org.cloudfoundry.multiapps.controller.process.util.TimeoutValueResolver; import org.cloudfoundry.multiapps.controller.process.variables.Variable; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; @@ -112,6 +113,7 @@ public abstract class SyncFlowableStepTest { protected ProcessHelper processHelper; @Mock protected OperationLogsExporter operationLogsExporter; + protected TimeoutValueResolver timeoutValueResolver = new TimeoutValueResolver(); protected ProcessContext context; @InjectMocks @@ -123,6 +125,9 @@ public abstract class SyncFlowableStepTest { public void initMocks() throws Exception { MockitoAnnotations.openMocks(this) .close(); + if (step instanceof TimeoutAsyncFlowableStep timeoutStep) { + timeoutStep.timeoutValueResolver = timeoutValueResolver; + } this.stepLogger = Mockito.spy( new StepLogger(execution, progressMessageService, processLoggerProvider, LOGGER, operationLogsExporter)); when(stepLoggerFactory.create(any(), any(), any(), any(), any())).thenReturn(stepLogger); @@ -239,7 +244,7 @@ protected void setUpContext(Integer timeoutProcessVariable, Integer timeoutModul } context.setVariable(Variables.APP_TO_PROCESS, app); - when(timeoutType.getProcessVariableAndGlobalLevelParamName()).thenReturn(SupportedParameters.APPS_UPLOAD_TIMEOUT); + when(timeoutType.getGlobalLevelParamName()).thenReturn(SupportedParameters.APPS_UPLOAD_TIMEOUT); Map timeoutGlobalLevelParameters = new HashMap<>(); timeoutGlobalLevelParameters.put(timeoutGlobalParameter, timeoutGlobalLevel); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStepTest.java index 31a95ac90c..89ed87ef16 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStepTest.java @@ -1,7 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.steps; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.time.Duration; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; @@ -15,6 +13,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class UploadAppStepTest { @Nested From 5346dd0024fc2dba8285ba1ef84f08a9558f85ca Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:53:54 +0300 Subject: [PATCH 4/7] Register service timeout parameters in deploy metadata Register the create-service, bind-service and create-service-key timeout parameters (with their integer conversion) in the deploy, blue-green, CTS and rollback metadata so they are accepted and converted to process variables. Update metadata tests accordingly. --- .../metadata/BlueGreenDeployMetadata.java | 18 ++++++++++++++++++ .../process/metadata/CtsDeployMetadata.java | 18 ++++++++++++++++++ .../process/metadata/DeployMetadata.java | 18 ++++++++++++++++++ .../process/metadata/RollbackMtaMetadata.java | 18 ++++++++++++++++++ .../metadata/BlueGreenDeployMetadataTest.java | 19 +++++++++++-------- .../metadata/CtsDeployMetadataTest.java | 5 ++++- .../process/metadata/DeployMetadataTest.java | 5 ++++- .../metadata/RollbackMtaMetadataTest.java | 5 ++++- .../metadata/UndeployMetadataTest.java | 2 +- 9 files changed, 96 insertions(+), 12 deletions(-) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java index b996d7c06a..08daccb6dc 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java @@ -130,6 +130,24 @@ public static OperationMetadata getMetadata() { .customConverter(new TimeoutParameterConverter( Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE)) .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE)) + .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.IS_SECURITY_ENABLED.getName()) .type(ParameterType.BOOLEAN) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadata.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadata.java index ff2131283c..49945708a6 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadata.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadata.java @@ -135,6 +135,24 @@ public static OperationMetadata getMetadata() { .customConverter(new TimeoutParameterConverter( Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)) .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE)) + .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadata.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadata.java index 9e51a153ec..65f554ade1 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadata.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadata.java @@ -124,6 +124,24 @@ public static OperationMetadata getMetadata() { .customConverter(new TimeoutParameterConverter( Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)) .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE)) + .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadata.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadata.java index c9abbd7e8e..42886d95ca 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadata.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadata.java @@ -64,6 +64,24 @@ public static OperationMetadata getMetadata() { .customConverter(new TimeoutParameterConverter( Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)) .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE)) + .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName()) + .type(ParameterType.INTEGER) + .customConverter(new TimeoutParameterConverter( + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE)) + .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java index c2d88499d2..353a67953e 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java @@ -44,21 +44,24 @@ protected String[] getParametersIds() { Variables.ABORT_ON_ERROR.getName(), Variables.MODULES_FOR_DEPLOYMENT.getName(), Variables.RESOURCES_FOR_DEPLOYMENT.getName(), - Variables.NO_CONFIRM.getName(), - Variables.KEEP_ORIGINAL_APP_NAMES_AFTER_DEPLOY.getName(), - Variables.SKIP_IDLE_START.getName(), - Variables.SHOULD_APPLY_INCREMENTAL_INSTANCES_UPDATE.getName(), Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName(), - Variables.SKIP_APP_DIGEST_CALCULATION.getName(), - Variables.SHOULD_BACKUP_PREVIOUS_VERSION.getName(), - Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName(), + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.IS_SECURITY_ENABLED.getName(), Variables.DISPOSABLE_USER_PROVIDED_SERVICE_NAME.getName(), Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED.getName(), - Variables.API_REQUEST_PATH.getName() + Variables.API_REQUEST_PATH.getName(), + Variables.NO_CONFIRM.getName(), + Variables.KEEP_ORIGINAL_APP_NAMES_AFTER_DEPLOY.getName(), + Variables.SKIP_IDLE_START.getName(), + Variables.SHOULD_APPLY_INCREMENTAL_INSTANCES_UPDATE.getName(), + Variables.SKIP_APP_DIGEST_CALCULATION.getName(), + Variables.SHOULD_BACKUP_PREVIOUS_VERSION.getName(), + Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName() // @formatter:on }; } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadataTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadataTest.java index 7109b0b423..646f26d5fc 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadataTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/CtsDeployMetadataTest.java @@ -61,7 +61,10 @@ protected String[] getParametersIds() { Variables.IS_SECURITY_ENABLED.getName(), Variables.DISPOSABLE_USER_PROVIDED_SERVICE_NAME.getName(), Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED.getName(), - Variables.API_REQUEST_PATH.getName() + Variables.API_REQUEST_PATH.getName(), + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName() // @formatter:on }; } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadataTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadataTest.java index 1e35d8dfff..4a38c8ed09 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadataTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/DeployMetadataTest.java @@ -52,7 +52,10 @@ protected String[] getParametersIds() { Variables.IS_SECURITY_ENABLED.getName(), Variables.DISPOSABLE_USER_PROVIDED_SERVICE_NAME.getName(), Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED.getName(), - Variables.API_REQUEST_PATH.getName() + Variables.API_REQUEST_PATH.getName(), + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName() // @formatter:on }; } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadataTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadataTest.java index 7363be5780..9e2eef2739 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadataTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/RollbackMtaMetadataTest.java @@ -28,7 +28,10 @@ protected String[] getParametersIds() { Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.PROCESS_USER_PROVIDED_SERVICES.getName(), Variables.IS_SECURITY_ENABLED.getName(), - Variables.DISPOSABLE_USER_PROVIDED_SERVICE_NAME.getName(), Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED.getName() }; + Variables.DISPOSABLE_USER_PROVIDED_SERVICE_NAME.getName(), Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED.getName(), + Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE.getName(), + Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE.getName() }; } } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/UndeployMetadataTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/UndeployMetadataTest.java index 0927c1cd45..fad4fdad37 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/UndeployMetadataTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/UndeployMetadataTest.java @@ -32,7 +32,7 @@ protected String[] getParametersIds() { Variables.MTA_NAMESPACE.getName(), Variables.NO_RESTART_SUBSCRIBED_APPS.getName(), Variables.NO_FAIL_ON_MISSING_PERMISSIONS.getName(), - Variables.ABORT_ON_ERROR.getName(), + Variables.ABORT_ON_ERROR.getName() // @formatter:on }; } From 7f1c66182b5684fef3422f0a402f6aa6284206d1 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:54:02 +0300 Subject: [PATCH 5/7] Forward service timeout variables into deploy subprocesses --- .../cloudfoundry/multiapps/controller/process/deploy-app.bpmn | 4 ++++ .../controller/process/process-batches-sequentially.bpmn | 3 +++ .../multiapps/controller/process/rollback-mta.bpmn | 3 +++ .../multiapps/controller/process/xs2-bg-deploy.bpmn | 3 +++ .../cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn | 3 +++ 5 files changed, 16 insertions(+) diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn index 74aa96ee69..33c7c2efd3 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn @@ -190,6 +190,8 @@ + + @@ -226,6 +228,8 @@ + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn index 898fb6a75d..99595e469f 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn @@ -41,6 +41,9 @@ + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/rollback-mta.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/rollback-mta.bpmn index 510c4dd246..17b05ca40a 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/rollback-mta.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/rollback-mta.bpmn @@ -102,6 +102,9 @@ + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn index 6c002fa97b..7c6554472d 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn @@ -167,6 +167,9 @@ + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn index a2610d963b..155e13d5cc 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn @@ -200,6 +200,9 @@ + + + From f6d8338e89aec2ef574a6293fda9fefd84dfaee2 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:21:40 +0300 Subject: [PATCH 6/7] Log per-operation timeout without emitting a progress message --- .../controller/process/steps/TimeoutAsyncFlowableStep.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java index d55a9510c7..c0b481a01c 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java @@ -38,7 +38,7 @@ protected Duration calculateTimeout(ProcessContext context, TimeoutType timeoutT private void logTimeout(ProcessContext context, TimeoutType timeoutType, TimeoutResolution resolution) { String operation = resolveOperationName(context, timeoutType); long seconds = resolution.timeout().toSeconds(); - getStepLogger().info(Messages.OPERATION_TIMEOUT_MESSAGE, operation, resolution.parameterName(), seconds); + getStepLogger().infoWithoutProgressMessage(Messages.OPERATION_TIMEOUT_MESSAGE, operation, resolution.parameterName(), seconds); } private String resolveOperationName(ProcessContext context, TimeoutType timeoutType) { From 8ef2b647bc494f1b4676e7c2493c2dd91c81c5e0 Mon Sep 17 00:00:00 2001 From: vkalapov <87693906+vkalapov@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:59:01 +0300 Subject: [PATCH 7/7] Address review comments: extract constants and messages, rename method --- .../multiapps/controller/core/Messages.java | 1 + .../controller/core/util/DurationUtil.java | 2 +- .../controller/process/Messages.java | 7 +++++++ .../process/steps/ProcessDescriptorStep.java | 4 ++-- .../steps/TimeoutAsyncFlowableStep.java | 12 ++++++----- .../controller/process/util/TimeoutType.java | 20 ++++++++++++------- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 887f6b0c92..96a700d808 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -310,6 +310,7 @@ public final class Messages { public static final String IGNORING_NAMESPACE_PARAMETERS = "Ignoring parameter \"{0}\" , as the MTA is not deployed with namespace!"; public static final String NAMESPACE_PARSING_ERROR_MESSAGE = "Cannot parse \"{0}\" flag - expected a boolean format."; public static final String PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1 = "Parameter \"{0}\" must be positive integer value up to {1}!"; + public static final String PARAMETER_0_MUST_BE_A_NUMBER = "Parameter \"{0}\" must be a number!"; private Messages() { } diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java index eab7b1b0c8..f1b250de4d 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/util/DurationUtil.java @@ -17,7 +17,7 @@ public static Duration parseDuration(Object timeout, String parameterName, int m return null; } if (!(timeout instanceof Number number)) { - throw new ContentException(Messages.PARAMETER_0_MUST_BE_POSITIVE_WITH_MAX_VALUE_1, parameterName, maxAllowedValue); + throw new ContentException(Messages.PARAMETER_0_MUST_BE_A_NUMBER, parameterName); } int value = number.intValue(); if (value < 0 || value > maxAllowedValue) { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java index 6750cda01a..048a74ec98 100755 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java @@ -558,6 +558,13 @@ public class Messages { public static final String LAST_OPERATION_FOR_SERVICE = "Last operation for service \"{0}\": {1}"; public static final String DELETING_ERROR_TYPE_O_FOR_PROCESS_1 = "Deleting error type \"{0}\" for process \"{1}\""; public static final String OPERATION_TIMEOUT_MESSAGE = "{0} timeout is set by \"{1}\" parameter to {2} seconds"; + public static final String TIMEOUT_OPERATION_APPLICATION_NAMED = "Application {0}"; + public static final String TIMEOUT_OPERATION_APPLICATION = "Application"; + public static final String TIMEOUT_OPERATION_SERVICE_BINDING_NAMED = "Service binding for {0}"; + public static final String TIMEOUT_OPERATION_SERVICE_BINDING = "Service binding"; + public static final String TIMEOUT_OPERATION_SERVICE_CREATION = "Service creation"; + public static final String TIMEOUT_OPERATION_SERVICE_KEY_CREATION = "Service key creation"; + public static final String TIMEOUT_OPERATION_FOR_RESOURCE = "{0} for {1}"; public static final String PROVIDED_EXTENSION_DESCRIPTORS = "Provided extension descriptors: {0}"; public static final String CREATED_SUBSCRIPTION = "Created subscription with ID: {0}"; public static final String UPDATING_SUBSCRIBER_0 = "Updating subscriber: {0} "; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java index 6d5b131a67..10b76adf92 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessDescriptorStep.java @@ -64,7 +64,7 @@ protected StepPhase executeStep(ProcessContext context) { setDynamicResolvableParametersIfAbsent(context, resolver); context.setVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); - preResolveServiceTimeoutsFromDescriptor(context, descriptor); + preResolveBindServiceTimeoutFromDescriptor(context, descriptor); // Set MTA modules in the context List modulesForDeployment = context.getVariable(Variables.MODULES_FOR_DEPLOYMENT); List invalidModulesSpecifiedForDeployment = findInvalidModulesSpecifiedForDeployment(descriptor, modulesForDeployment); @@ -135,7 +135,7 @@ private MtaDescriptorPropertiesResolverContext buildMtaDescriptorPropertiesResol // Bind runs in a subprocess that does not receive the deployment descriptor, so the global-level // services-bind-service-timeout is pre-resolved here into a process variable the resolver can read there. - private void preResolveServiceTimeoutsFromDescriptor(ProcessContext context, DeploymentDescriptor descriptor) { + private void preResolveBindServiceTimeoutFromDescriptor(ProcessContext context, DeploymentDescriptor descriptor) { TimeoutType timeoutType = TimeoutType.BIND_SERVICE; Duration timeout = timeoutValueResolver.toDuration( descriptor.getParameters() != null ? descriptor.getParameters().get(timeoutType.getGlobalLevelParamName()) : null, diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java index c0b481a01c..da0758c793 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/TimeoutAsyncFlowableStep.java @@ -45,21 +45,23 @@ private String resolveOperationName(ProcessContext context, TimeoutType timeoutT return switch (timeoutType) { case UPLOAD, STAGE, START, TASK -> { CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); - yield app != null ? MessageFormat.format("Application {0}", app.getName()) : "Application"; + yield app != null ? MessageFormat.format(Messages.TIMEOUT_OPERATION_APPLICATION_NAMED, app.getName()) + : Messages.TIMEOUT_OPERATION_APPLICATION; } case BIND_SERVICE -> { String service = context.getVariable(Variables.SERVICE_TO_UNBIND_BIND); - yield service != null ? MessageFormat.format("Service binding for {0}", service) : "Service binding"; + yield service != null ? MessageFormat.format(Messages.TIMEOUT_OPERATION_SERVICE_BINDING_NAMED, service) + : Messages.TIMEOUT_OPERATION_SERVICE_BINDING; } - case CREATE_SERVICE -> formatServiceOperation("Service creation", context); - case CREATE_SERVICE_KEY -> formatServiceOperation("Service key creation", context); + case CREATE_SERVICE -> formatServiceOperation(Messages.TIMEOUT_OPERATION_SERVICE_CREATION, context); + case CREATE_SERVICE_KEY -> formatServiceOperation(Messages.TIMEOUT_OPERATION_SERVICE_KEY_CREATION, context); }; } private String formatServiceOperation(String operation, ProcessContext context) { CloudServiceInstanceExtended service = context.getVariable(Variables.SERVICE_TO_PROCESS); return service != null && service.getResourceName() != null - ? MessageFormat.format("{0} for {1}", operation, service.getResourceName()) + ? MessageFormat.format(Messages.TIMEOUT_OPERATION_FOR_RESOURCE, operation, service.getResourceName()) : operation; } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java index 630ae9ecd5..9a37a5eb5b 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/TimeoutType.java @@ -11,48 +11,54 @@ public enum TimeoutType { UPLOAD(new TimeoutParameterNames(SupportedParameters.UPLOAD_TIMEOUT, null, SupportedParameters.APPS_UPLOAD_TIMEOUT), Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE, null, - 10800, + Limits.MAX_APP_TIMEOUT_SECONDS, null), STAGE(new TimeoutParameterNames(SupportedParameters.STAGE_TIMEOUT, null, SupportedParameters.APPS_STAGE_TIMEOUT), Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE, null, - 10800, + Limits.MAX_APP_TIMEOUT_SECONDS, null), START(new TimeoutParameterNames(SupportedParameters.START_TIMEOUT, null, SupportedParameters.APPS_START_TIMEOUT), Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE, null, - 10800, + Limits.MAX_APP_TIMEOUT_SECONDS, null), TASK(new TimeoutParameterNames(SupportedParameters.TASK_EXECUTION_TIMEOUT, null, SupportedParameters.APPS_TASK_EXECUTION_TIMEOUT), Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE, null, - 86400, + Limits.MAX_TASK_TIMEOUT_SECONDS, null), CREATE_SERVICE( new TimeoutParameterNames(null, SupportedParameters.CREATE_SERVICE_TIMEOUT, SupportedParameters.SERVICES_CREATE_SERVICE_TIMEOUT), Variables.CREATE_SERVICE_TIMEOUT_PROCESS_VARIABLE, null, - 7200, + Limits.MAX_SERVICE_TIMEOUT_SECONDS, CloudServiceInstanceExtended::getCreateServiceTimeout), BIND_SERVICE( new TimeoutParameterNames(null, SupportedParameters.BIND_SERVICE_TIMEOUT, SupportedParameters.SERVICES_BIND_SERVICE_TIMEOUT), Variables.BIND_SERVICE_TIMEOUT_PROCESS_VARIABLE, Variables.BIND_SERVICE_TIMEOUT_GLOBAL_FALLBACK, - 7200, + Limits.MAX_SERVICE_TIMEOUT_SECONDS, CloudServiceInstanceExtended::getBindServiceTimeout), CREATE_SERVICE_KEY(new TimeoutParameterNames(null, SupportedParameters.CREATE_SERVICE_KEY_TIMEOUT, SupportedParameters.SERVICES_CREATE_SERVICE_KEY_TIMEOUT), Variables.CREATE_SERVICE_KEY_TIMEOUT_PROCESS_VARIABLE, null, - 7200, + Limits.MAX_SERVICE_TIMEOUT_SECONDS, CloudServiceInstanceExtended::getCreateServiceKeyTimeout); + private static final class Limits { + private static final int MAX_APP_TIMEOUT_SECONDS = 10800; + private static final int MAX_TASK_TIMEOUT_SECONDS = 86400; + private static final int MAX_SERVICE_TIMEOUT_SECONDS = 7200; + } + @FunctionalInterface public interface ServiceTimeoutGetter { Duration getServiceTimeout(CloudServiceInstanceExtended service);