CAMEL-20371: Asynchrnous Camel health checks
diff --git a/core/camel-spring-boot/src/main/docs/spring-boot.adoc b/core/camel-spring-boot/src/main/docs/spring-boot.adoc
index e29685e..684bbc1 100644
--- a/core/camel-spring-boot/src/main/docs/spring-boot.adoc
+++ b/core/camel-spring-boot/src/main/docs/spring-boot.adoc
@@ -417,6 +417,24 @@
 
 For more details about `GraalVM Native Image Support` in Spring Boot please refer to https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
 
+== Camel Asynchronous Health Checks
+
+Camel health checks can be executed asynchronously via a Task Scheduler so that the result can be cached and the actual health check is executed in background every few seconds. Asynchronous Camel health checks are disabled by default but can be enabled with the following property:
+
+[source,properties]
+----
+camel.health.async-camel-health-check=true
+----
+
+moreover the Camel health check task scheduler can be customized with the following properties:
+
+[source,properties]
+----
+camel.health.healthCheckPoolSize=5
+camel.health.healthCheckFrequency=10
+camel.health.healthCheckThreadNamePrefix=CamelHealthTaskScheduler
+----
+
 == Camel Readiness and Liveness State Indicators
 
 Camel specific Readiness and Liveness checks can be added to a Spring Boot 3 application including respectively in the
diff --git a/core/camel-spring-boot/src/main/docs/spring-boot.json b/core/camel-spring-boot/src/main/docs/spring-boot.json
index 878e51e..8ddf6bb 100644
--- a/core/camel-spring-boot/src/main/docs/spring-boot.json
+++ b/core/camel-spring-boot/src/main/docs/spring-boot.json
@@ -514,6 +514,13 @@
       "defaultValue": false
     },
     {
+      "name": "camel.health.async-camel-health-check",
+      "type": "java.lang.Boolean",
+      "description": "Whether Camel Health Checks are executed asynchronously <p> disabled by default",
+      "sourceType": "org.apache.camel.spring.boot.actuate.health.CamelHealthCheckConfigurationProperties",
+      "defaultValue": false
+    },
+    {
       "name": "camel.health.consumers-enabled",
       "type": "java.lang.Boolean",
       "description": "Whether consumers health check is enabled. <p> Is default enabled",
@@ -539,6 +546,27 @@
       "defaultValue": "default"
     },
     {
+      "name": "camel.health.health-check-frequency",
+      "type": "java.lang.Integer",
+      "description": "Camel's HealthCheck frequency in seconds",
+      "sourceType": "org.apache.camel.spring.boot.actuate.health.CamelHealthCheckConfigurationProperties",
+      "defaultValue": 10
+    },
+    {
+      "name": "camel.health.health-check-pool-size",
+      "type": "java.lang.Integer",
+      "description": "Camel HealthCheck pool size",
+      "sourceType": "org.apache.camel.spring.boot.actuate.health.CamelHealthCheckConfigurationProperties",
+      "defaultValue": 5
+    },
+    {
+      "name": "camel.health.health-check-thread-name-prefix",
+      "type": "java.lang.String",
+      "description": "Camel HealthCheck thread name prefix",
+      "sourceType": "org.apache.camel.spring.boot.actuate.health.CamelHealthCheckConfigurationProperties",
+      "defaultValue": "CamelHealthTaskScheduler"
+    },
+    {
       "name": "camel.health.initial-state",
       "type": "java.lang.String",
       "description": "The initial state of health-checks (readiness). There are the following states: UP, DOWN, UNKNOWN. By default, the state is DOWN, is regarded as being pessimistic\/careful. This means that the overall health checks may report as DOWN during startup and then only if everything is up and running flip to being UP. Setting the initial state to UP, is regarded as being optimistic. This means that the overall health checks may report as UP during startup and then if a consumer or other service is in fact un-healthy, then the health-checks can flip being DOWN. Setting the state to UNKNOWN means that some health-check would be reported in unknown state, especially during early bootstrap where a consumer may not be fully initialized or validated a connection to a remote system. This option allows to pre-configure the state for different modes.",
diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/AsyncHealthIndicatorAutoConfiguration.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/AsyncHealthIndicatorAutoConfiguration.java
new file mode 100644
index 0000000..0a58d9e
--- /dev/null
+++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/AsyncHealthIndicatorAutoConfiguration.java
@@ -0,0 +1,126 @@
+package org.apache.camel.spring.boot.actuate.health;
+
+import org.apache.camel.spring.boot.actuate.health.liveness.CamelLivenessStateHealthIndicator;
+import org.apache.camel.spring.boot.actuate.health.readiness.CamelReadinessStateHealthIndicator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthContributorRegistry;
+import org.springframework.boot.actuate.health.HealthIndicator;
+import org.springframework.boot.actuate.health.NamedContributor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+
+/**
+ * Configuration class that replace synchronous Camel Health Checks with asynchronous ones.
+ *
+ * This implementation is based on https://github.com/spring-projects/spring-boot/issues/2652 that most probably
+ * will be added in spring boot 3.2.x as a new feature in the future.
+ *
+ * TODO: To be refactored once async health contributors feature will be added in spring boot.
+ *
+ */
+@Configuration
+@ConditionalOnProperty(prefix = "camel.health", name = "async-camel-health-check", havingValue = "true")
+public class AsyncHealthIndicatorAutoConfiguration implements InitializingBean {
+	private static final Logger log = LoggerFactory.getLogger(AsyncHealthIndicatorAutoConfiguration.class);
+
+	private HealthContributorRegistry healthContributorRegistry;
+	private TaskScheduler taskScheduler;
+	private CamelHealthCheckConfigurationProperties config;
+
+	public AsyncHealthIndicatorAutoConfiguration(HealthContributorRegistry healthContributorRegistry,
+												 CamelHealthCheckConfigurationProperties config) {
+		this.healthContributorRegistry = healthContributorRegistry;
+		this.config = config;
+
+		ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
+		threadPoolTaskScheduler.setPoolSize(config.getHealthCheckPoolSize());
+		threadPoolTaskScheduler.setThreadNamePrefix(config.getHealthCheckThreadNamePrefix());
+		threadPoolTaskScheduler.initialize();
+		taskScheduler = threadPoolTaskScheduler;
+	}
+
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		for (NamedContributor<?> namedContributor : healthContributorRegistry) {
+			final String name = namedContributor.getName();
+			final Object contributor = namedContributor.getContributor();
+			if (contributor instanceof CamelHealthCheckIndicator
+				|| contributor instanceof CamelLivenessStateHealthIndicator
+				|| contributor instanceof CamelReadinessStateHealthIndicator) {
+				HealthIndicator camelHealthCheckIndicator = (HealthIndicator) contributor;
+				healthContributorRegistry.unregisterContributor(name);
+				log.debug(
+						"Wrapping " + contributor.getClass().getSimpleName() + " for async health scheduling");
+				WrappedHealthIndicator wrappedHealthIndicator =
+						new WrappedHealthIndicator(camelHealthCheckIndicator);
+				healthContributorRegistry.registerContributor(name, wrappedHealthIndicator);
+				taskScheduler.scheduleWithFixedDelay(
+						wrappedHealthIndicator, Duration.ofSeconds(config.getHealthCheckFrequency()));
+			}
+		}
+	}
+
+	/**
+	 * Health Check Indicator that executes Health Checks within a Task Scheduler
+	 */
+	private static class WrappedHealthIndicator implements HealthIndicator, Runnable {
+		private static final String LAST_CHECKED_KEY = "lastChecked";
+		private static final String LAST_DURATION_KEY = "lastDuration";
+
+		private HealthIndicator wrappedHealthIndicator;
+
+		private Health lastHealth;
+
+		public WrappedHealthIndicator(HealthIndicator wrappedHealthIndicator) {
+			this.wrappedHealthIndicator = wrappedHealthIndicator;
+		}
+
+		@Override
+		public Health health() {
+			Health lastHealth = getLastHealth();
+			if (lastHealth == null) {
+				setLastHealth(getAndWrapHealth());
+				lastHealth = getLastHealth();
+			}
+
+			return lastHealth;
+		}
+
+		private Health getAndWrapHealth() {
+			ZonedDateTime startTime = ZonedDateTime.now();
+			Health baseHealth = getWrappedHealthIndicator().health();
+			ZonedDateTime endTime = ZonedDateTime.now();
+			Duration duration = Duration.between(startTime, endTime);
+			return Health.status(baseHealth.getStatus())
+					.withDetails(baseHealth.getDetails())
+					.withDetail(LAST_CHECKED_KEY, startTime)
+					.withDetail(LAST_DURATION_KEY, duration)
+					.build();
+		}
+
+		@Override
+		public void run() {
+			setLastHealth(getAndWrapHealth());
+		}
+
+		public HealthIndicator getWrappedHealthIndicator() {
+			return wrappedHealthIndicator;
+		}
+
+		public Health getLastHealth() {
+			return lastHealth;
+		}
+
+		public void setLastHealth(Health lastHealth) {
+			this.lastHealth = lastHealth;
+		}
+	}
+}
diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/CamelHealthCheckConfigurationProperties.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/CamelHealthCheckConfigurationProperties.java
index d4c7f6c..fd45283 100644
--- a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/CamelHealthCheckConfigurationProperties.java
+++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/health/CamelHealthCheckConfigurationProperties.java
@@ -64,6 +64,28 @@
     private String excludePattern;
 
     /**
+     * Camel's HealthCheck frequency in seconds
+     */
+    private int healthCheckFrequency = 10;
+
+    /**
+     * Camel HealthCheck pool size
+     */
+    private int healthCheckPoolSize = 5;
+
+    /**
+     * Camel HealthCheck thread name prefix
+     */
+    private String healthCheckThreadNamePrefix = "CamelHealthTaskScheduler";
+
+    /**
+     * Whether Camel Health Checks are executed asynchronously
+     * <p>
+     * disabled by default
+     */
+    private boolean asyncCamelHealthCheck = false;
+
+    /**
      * Sets the level of details to exposure as result of invoking health checks. There are the following levels: full,
      * default, oneline
      *
@@ -158,6 +180,38 @@
     public void setInitialState(String initialState) {
         this.initialState = initialState;
     }
+
+    public int getHealthCheckFrequency() {
+        return healthCheckFrequency;
+    }
+
+    public void setHealthCheckFrequency(int healthCheckFrequency) {
+        this.healthCheckFrequency = healthCheckFrequency;
+    }
+
+    public int getHealthCheckPoolSize() {
+        return healthCheckPoolSize;
+    }
+
+    public void setHealthCheckPoolSize(int healthCheckPoolSize) {
+        this.healthCheckPoolSize = healthCheckPoolSize;
+    }
+
+    public String getHealthCheckThreadNamePrefix() {
+        return healthCheckThreadNamePrefix;
+    }
+
+    public void setHealthCheckThreadNamePrefix(String healthCheckThreadNamePrefix) {
+        this.healthCheckThreadNamePrefix = healthCheckThreadNamePrefix;
+    }
+
+    public boolean isAsyncCamelHealthCheck() {
+        return asyncCamelHealthCheck;
+    }
+
+    public void setAsyncCamelHealthCheck(boolean asyncCamelHealthCheck) {
+        this.asyncCamelHealthCheck = asyncCamelHealthCheck;
+    }
 }
 
 
diff --git a/core/camel-spring-boot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/core/camel-spring-boot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 13a8efb..cf103fe 100644
--- a/core/camel-spring-boot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/core/camel-spring-boot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -19,6 +19,7 @@
 org.apache.camel.spring.boot.actuate.console.CamelDevConsoleAutoConfiguration
 org.apache.camel.spring.boot.actuate.endpoint.CamelRouteControllerEndpointAutoConfiguration
 org.apache.camel.spring.boot.actuate.endpoint.CamelRoutesEndpointAutoConfiguration
+org.apache.camel.spring.boot.actuate.health.AsyncHealthIndicatorAutoConfiguration
 org.apache.camel.spring.boot.actuate.health.CamelHealthCheckAutoConfiguration
 org.apache.camel.spring.boot.actuate.health.CamelAvailabilityCheckAutoConfiguration
 org.apache.camel.spring.boot.actuate.info.CamelInfoAutoConfiguration