From e07c6ed93ede926a35357a5bc2c6b3be10489b07 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Fri, 29 May 2026 18:15:07 +0300 Subject: [PATCH 1/2] Plumb liveness health-check-interval module parameter Adds a new MTA module-level parameter `health-check-interval` (Integer, seconds) that is parsed from the descriptor, propagated through the Staging immutable, sent on the outbound CF Data builder in buildHealthCheck, read back via RawCloudProcess into CloudProcess, included in HealthCheckInfo equality so the staging attribute updater detects re-stage conditions, and logged by AdditionalModuleParametersReporter. Mirrors the existing readiness-health-check-interval pipeline in every layer. JIRA:LMCROSSITXSADEPLOY-3316 --- .../facade/adapters/RawCloudProcess.java | 3 +++ .../client/facade/domain/CloudProcess.java | 3 +++ .../client/facade/domain/Staging.java | 6 ++++++ .../rest/CloudControllerRestClientImpl.java | 1 + .../client/lib/domain/HealthCheckInfo.java | 17 +++++++++++---- .../core/model/SupportedParameters.java | 4 +++- .../core/parser/StagingParametersParser.java | 3 +++ .../AdditionalModuleParametersReporter.java | 21 +++++++++++++++++++ 8 files changed, 53 insertions(+), 5 deletions(-) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java index 4bd4b83775..931f528b75 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java @@ -23,11 +23,13 @@ public CloudProcess derive() { Integer healthCheckTimeout = null; String healthCheckHttpEndpoint = null; Integer healthCheckInvocationTimeout = null; + Integer healthCheckInterval = null; if (healthCheck.getData() != null) { Data healthCheckData = healthCheck.getData(); healthCheckTimeout = healthCheckData.getTimeout(); healthCheckInvocationTimeout = healthCheckData.getInvocationTimeout(); healthCheckHttpEndpoint = healthCheckData.getEndpoint(); + healthCheckInterval = healthCheckData.getInterval(); } Integer readinessHealthCheckInvocationTimeout = null; String readinessHealthCheckHttpEndpoint = null; @@ -49,6 +51,7 @@ public CloudProcess derive() { .healthCheckHttpEndpoint(healthCheckHttpEndpoint) .healthCheckTimeout(healthCheckTimeout) .healthCheckInvocationTimeout(healthCheckInvocationTimeout) + .healthCheckInterval(healthCheckInterval) .readinessHealthCheckType(readinessHealthCheckType.getType() .getValue()) .readinessHealthCheckHttpEndpoint(readinessHealthCheckHttpEndpoint) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java index 86df6003a7..ef141ae3fe 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java @@ -29,6 +29,9 @@ public abstract class CloudProcess extends CloudEntity implements Derivable> parametersList) { Integer healthCheckInvocationTimeout = (Integer) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_INVOCATION_TIMEOUT, null); + Integer healthCheckInterval = (Integer) PropertiesUtil.getPropertyValue(parametersList, + SupportedParameters.HEALTH_CHECK_INTERVAL, null); String healthCheckType = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_TYPE, null); String healthCheckHttpEndpoint = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT, @@ -67,6 +69,7 @@ public Staging parse(List> parametersList) { .stackName(stackName) .healthCheckTimeout(healthCheckTimeout) .invocationTimeout(healthCheckInvocationTimeout) + .healthCheckInterval(healthCheckInterval) .healthCheckType(healthCheckType) .healthCheckHttpEndpoint(healthCheckHttpEndpoint) .readinessHealthCheckType(readinessHealthCheckType) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java index 1d2cff63be..02700b5d59 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java @@ -40,6 +40,18 @@ public void reportUsageOfAdditionalParameters(Module module) { readinessHealthCheckInvocationTimeout, readinessHealthCheckInterval, buildpacks.toString(), module.getType()); } + String healthCheckType = (String) module.getParameters() + .get(SupportedParameters.HEALTH_CHECK_TYPE); + String healthCheckHttpEndpoint = (String) module.getParameters() + .get(SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT); + Integer healthCheckInvocationTimeout = (Integer) module.getParameters() + .get(SupportedParameters.HEALTH_CHECK_INVOCATION_TIMEOUT); + Integer healthCheckInterval = (Integer) module.getParameters() + .get(SupportedParameters.HEALTH_CHECK_INTERVAL); + if (healthCheckType != null) { + reportUsageOfHealthCheckParameters(mtaId, correlationId, healthCheckType, healthCheckHttpEndpoint, healthCheckInvocationTimeout, + healthCheckInterval, buildpacks.toString(), module.getType()); + } } // this method is being observed by Dynatrace, be careful if you change it @@ -51,4 +63,13 @@ private void reportUsageOfReadinessHealthCheckParameters(String mtaId, String co mtaId, correlationId, readinessHealthCheckType, readinessHealthCheckHttpEndpoint, readinessHealthCheckInvocationTimeout, readinessHealthCheckInterval, buildpacks, moduleType)); } + + // this method is being observed by Dynatrace, be careful if you change it + private void reportUsageOfHealthCheckParameters(String mtaId, String correlationId, String healthCheckType, + String healthCheckHttpEndpoint, Integer healthCheckInvocationTimeout, + Integer healthCheckInterval, String buildpacks, String moduleType) { + LOGGER.info(MessageFormat.format("MTA with ID \"{0}\" associated with operation ID \"{1}\" uses health check parameters: type=\"{2}\", httpEndpoint=\"{3}\", invocationTimeout=\"{4}\", interval=\"{5}\", buildpacks=\"{6}\", moduleType=\"{7}\"", + mtaId, correlationId, healthCheckType, healthCheckHttpEndpoint, healthCheckInvocationTimeout, + healthCheckInterval, buildpacks, moduleType)); + } } From 28c8af509485cceca3b46917e58f8ab2c2082c11 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Fri, 29 May 2026 18:26:56 +0300 Subject: [PATCH 2/2] Add tests for health-check-interval parameter plumbing Covers HealthCheckInfo equality semantics for the new healthCheckInterval field, StagingParametersParser parsing of the health-check-interval key, and AdditionalModuleParametersReporter behaviour for the new branch. JIRA:LMCROSSITXSADEPLOY-3316 --- .../lib/domain/HealthCheckInfoTest.java | 153 ++++++++++++++++++ .../parser/StagingParametersParserTest.java | 19 +++ ...dditionalModuleParametersReporterTest.java | 124 ++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java new file mode 100644 index 0000000000..ac6a3d44bc --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java @@ -0,0 +1,153 @@ +package org.cloudfoundry.multiapps.controller.client.lib.domain; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableStaging; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class HealthCheckInfoTest { + + private static final String HTTP = "http"; + private static final String PORT = "port"; + private static final String ENDPOINT = "/health"; + private static final Integer TIMEOUT = 60; + private static final Integer INVOCATION_TIMEOUT = 5; + private static final Integer INTERVAL = 30; + + @Test + void testFromStagingPopulatesInterval() { + Staging staging = ImmutableStaging.builder() + .healthCheckType(HTTP) + .healthCheckTimeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .healthCheckHttpEndpoint(ENDPOINT) + .healthCheckInterval(INTERVAL) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertEquals(HTTP, info.getType()); + assertEquals(TIMEOUT, info.getTimeout()); + assertEquals(INVOCATION_TIMEOUT, info.getInvocationTimeout()); + assertEquals(ENDPOINT, info.getHttpEndpoint()); + assertEquals(INTERVAL, info.getInterval()); + } + + @Test + void testFromStagingDefaultsTypeToPortWhenMissing() { + Staging staging = ImmutableStaging.builder() + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertEquals(PORT, info.getType()); + assertNull(info.getTimeout()); + assertNull(info.getInvocationTimeout()); + assertNull(info.getHttpEndpoint()); + assertNull(info.getInterval()); + } + + @Test + void testFromStagingPropagatesNullInterval() { + Staging staging = ImmutableStaging.builder() + .healthCheckType(HTTP) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertNull(info.getInterval()); + } + + @Test + void testFromProcessPopulatesInterval() { + CloudProcess process = ImmutableCloudProcess.builder() + .command("") + .diskInMb(0) + .instances(1) + .memoryInMb(256) + .healthCheckType(HealthCheckType.HTTP) + .healthCheckTimeout(TIMEOUT) + .healthCheckInvocationTimeout(INVOCATION_TIMEOUT) + .healthCheckHttpEndpoint(ENDPOINT) + .healthCheckInterval(INTERVAL) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromProcess(process); + + assertEquals(HealthCheckType.HTTP.toString(), info.getType()); + assertEquals(TIMEOUT, info.getTimeout()); + assertEquals(INVOCATION_TIMEOUT, info.getInvocationTimeout()); + assertEquals(ENDPOINT, info.getHttpEndpoint()); + assertEquals(INTERVAL, info.getInterval()); + } + + @Test + void testFromProcessPropagatesNullInterval() { + CloudProcess process = ImmutableCloudProcess.builder() + .command("") + .diskInMb(0) + .instances(1) + .memoryInMb(256) + .healthCheckType(HealthCheckType.PORT) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromProcess(process); + + assertNull(info.getInterval()); + } + + @Test + void testEqualsWhenAllFieldsMatchIncludingInterval() { + HealthCheckInfo a = HealthCheckInfo.fromStaging(buildStagingWithInterval(INTERVAL)); + HealthCheckInfo b = HealthCheckInfo.fromStaging(buildStagingWithInterval(INTERVAL)); + + assertEquals(a, b); + } + + private static Staging buildStagingWithInterval(Integer interval) { + return ImmutableStaging.builder() + .healthCheckType(HTTP) + .healthCheckTimeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .healthCheckHttpEndpoint(ENDPOINT) + .healthCheckInterval(interval) + .build(); + } + + @Test + void testEqualsWhenIntervalDiffers() { + HealthCheckInfo a = HealthCheckInfo.fromStaging(buildStagingWithInterval(30)); + HealthCheckInfo b = HealthCheckInfo.fromStaging(buildStagingWithInterval(60)); + + assertNotEquals(a, b); + } + + @Test + void testEqualsWhenOneIntervalIsNull() { + HealthCheckInfo a = HealthCheckInfo.fromStaging(buildStagingWithInterval(INTERVAL)); + HealthCheckInfo b = HealthCheckInfo.fromStaging(buildStagingWithInterval(null)); + + assertNotEquals(a, b); + } + + @Test + void testEqualsIsReflexive() { + HealthCheckInfo info = HealthCheckInfo.fromStaging(buildStagingWithInterval(INTERVAL)); + + assertEquals(info, info); + } + + @Test + void testEqualsWithDifferentClass() { + HealthCheckInfo info = HealthCheckInfo.fromStaging(buildStagingWithInterval(INTERVAL)); + + assertNotEquals(info, "not a HealthCheckInfo"); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java index 98677fc307..10176c6b64 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java @@ -21,6 +21,7 @@ import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACK; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACKS; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.DOCKER; +import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.HEALTH_CHECK_INTERVAL; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.LIFECYCLE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -138,6 +139,24 @@ void testValidateWithAllParametersMissing() { assertNull(staging.getDockerInfo()); } + @Test + void testHealthCheckIntervalIsParsedWhenProvided() { + parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 45)); + + Staging staging = parser.parse(parametersList); + + assertNotNull(staging); + assertEquals(Integer.valueOf(45), staging.getHealthCheckInterval()); + } + + @Test + void testHealthCheckIntervalDefaultsToNullWhenMissing() { + Staging staging = parser.parse(parametersList); + + assertNotNull(staging); + assertNull(staging.getHealthCheckInterval()); + } + private static Map mapOf(String key, Object value) { return Collections.singletonMap(key, value); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java new file mode 100644 index 0000000000..fd9e6d4aa3 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java @@ -0,0 +1,124 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.HashMap; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.AfterEach; +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.assertDoesNotThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class AdditionalModuleParametersReporterTest { + + private static final String MTA_ID = "test-mta"; + private static final String CORRELATION_ID = "corr-1"; + private static final String MODULE_TYPE = "java.tomee"; + private static final String HTTP = "http"; + private static final String ENDPOINT = "/health"; + private static final Integer INVOCATION_TIMEOUT = 5; + private static final Integer INTERVAL = 30; + + @Mock + private ProcessContext context; + + private AdditionalModuleParametersReporter reporter; + private AutoCloseable mockitoCloseable; + + @BeforeEach + void setUp() { + mockitoCloseable = MockitoAnnotations.openMocks(this); + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3() + .setId(MTA_ID); + when(context.getRequiredVariable(Variables.DEPLOYMENT_DESCRIPTOR)).thenReturn(descriptor); + when(context.getRequiredVariable(Variables.CORRELATION_ID)).thenReturn(CORRELATION_ID); + reporter = new AdditionalModuleParametersReporter(context); + } + + @AfterEach + void tearDown() throws Exception { + mockitoCloseable.close(); + } + + @Test + void testReportEntersHealthCheckBranchWhenHealthCheckTypeIsPresent() { + Module module = buildModuleWithHealthCheck(HTTP, INTERVAL); + + assertDoesNotThrow(() -> reporter.reportUsageOfAdditionalParameters(module)); + + verify(context).getRequiredVariable(Variables.DEPLOYMENT_DESCRIPTOR); + verify(context).getRequiredVariable(Variables.CORRELATION_ID); + } + + private static Module buildModuleWithHealthCheck(String type, Integer interval) { + Map params = new HashMap<>(); + params.put(SupportedParameters.HEALTH_CHECK_TYPE, type); + params.put(SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT, ENDPOINT); + params.put(SupportedParameters.HEALTH_CHECK_INVOCATION_TIMEOUT, INVOCATION_TIMEOUT); + if (interval != null) { + params.put(SupportedParameters.HEALTH_CHECK_INTERVAL, interval); + } + return Module.createV3() + .setName("m") + .setType(MODULE_TYPE) + .setParameters(params); + } + + @Test + void testReportToleratesNullHealthCheckInterval() { + Module module = buildModuleWithHealthCheck(HTTP, null); + + assertDoesNotThrow(() -> reporter.reportUsageOfAdditionalParameters(module)); + } + + @Test + void testReportSkipsHealthCheckBranchWhenHealthCheckTypeIsMissing() { + Map params = new HashMap<>(); + params.put(SupportedParameters.HEALTH_CHECK_INTERVAL, INTERVAL); + Module module = Module.createV3() + .setName("m") + .setType(MODULE_TYPE) + .setParameters(params); + + assertDoesNotThrow(() -> reporter.reportUsageOfAdditionalParameters(module)); + } + + @Test + void testReportHandlesBothHealthCheckBranchesWithoutThrowing() { + Map params = new HashMap<>(); + params.put(SupportedParameters.HEALTH_CHECK_TYPE, HTTP); + params.put(SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT, ENDPOINT); + params.put(SupportedParameters.HEALTH_CHECK_INVOCATION_TIMEOUT, INVOCATION_TIMEOUT); + params.put(SupportedParameters.HEALTH_CHECK_INTERVAL, INTERVAL); + params.put(SupportedParameters.READINESS_HEALTH_CHECK_TYPE, HTTP); + params.put(SupportedParameters.READINESS_HEALTH_CHECK_HTTP_ENDPOINT, ENDPOINT); + params.put(SupportedParameters.READINESS_HEALTH_CHECK_INVOCATION_TIMEOUT, INVOCATION_TIMEOUT); + params.put(SupportedParameters.READINESS_HEALTH_CHECK_INTERVAL, INTERVAL); + Module module = Module.createV3() + .setName("m") + .setType(MODULE_TYPE) + .setParameters(params); + + assertDoesNotThrow(() -> reporter.reportUsageOfAdditionalParameters(module)); + } + + @Test + void testReportToleratesEmptyParameterMap() { + Module module = Module.createV3() + .setName("m") + .setType(MODULE_TYPE) + .setParameters(new HashMap<>()); + + assertDoesNotThrow(() -> reporter.reportUsageOfAdditionalParameters(module)); + } + +}