getEnvironmentsByRepositoryId(Long repositoryId) {
/**
* Locks the environment with the specified ID.
*
- * This method attempts to lock the environment by setting its locked status to true. If the
- * environment is already locked, it returns an empty Optional. If the environment is successfully
+ *
This method attempts to lock the environment by setting its locked status to
+ * true. If the
+ * environment is already locked, it returns an empty Optional. If the
+ * environment is successfully
* locked, it returns an Optional containing the locked environment.
*
*
This method is transactional and handles optimistic locking failures.
@@ -82,10 +87,9 @@ public List getEnvironmentsByRepositoryId(Long repositoryId) {
public Optional lockEnvironment(Long id) {
final String currentUserName = authService.getPreferredUsername();
- Environment environment =
- environmentRepository
- .findById(id)
- .orElseThrow(() -> new EntityNotFoundException("Environment not found with ID: " + id));
+ Environment environment = environmentRepository
+ .findById(id)
+ .orElseThrow(() -> new EntityNotFoundException("Environment not found with ID: " + id));
if (!environment.isEnabled()) {
throw new IllegalStateException("Environment is disabled");
@@ -132,10 +136,9 @@ public Optional lockEnvironment(Long id) {
public EnvironmentDto unlockEnvironment(Long id) {
final String currentUserName = authService.getPreferredUsername();
- Environment environment =
- environmentRepository
- .findById(id)
- .orElseThrow(() -> new EntityNotFoundException("Environment not found with ID: " + id));
+ Environment environment = environmentRepository
+ .findById(id)
+ .orElseThrow(() -> new EntityNotFoundException("Environment not found with ID: " + id));
if (!environment.isLocked()) {
throw new IllegalStateException("Environment is not locked");
@@ -172,13 +175,16 @@ public EnvironmentDto unlockEnvironment(Long id) {
/**
* Updates the environment with the specified ID.
*
- * This method updates the environment with the specified ID using the provided EnvironmentDto.
+ *
This method updates the environment with the specified ID using the provided
+ * EnvironmentDto.
*
* @param id the ID of the environment to update
- * @param environmentDto the EnvironmentDto containing the updated environment information
+ * @param environmentDto the EnvironmentDto containing the updated environment
+ * information
* @return an Optional containing the updated environment if successful,
- * or an empty Optional if no environment is found with the specified ID
- * @throws EnvironmentException if the environment is locked and cannot be disabled
+ * or an empty Optional if no environment is found with the specified ID
+ * @throws EnvironmentException if the environment is locked and cannot be
+ * disabled
*/
public Optional updateEnvironment(Long id, EnvironmentDto environmentDto)
throws EnvironmentException {
@@ -209,6 +215,13 @@ public Optional updateEnvironment(Long id, EnvironmentDto enviro
if (environmentDto.serverUrl() != null) {
environment.setServerUrl(environmentDto.serverUrl());
}
+ if (environmentDto.statusCheckType() != null) {
+ environment.setStatusCheckType(environmentDto.statusCheckType());
+ environment.setStatusUrl(environmentDto.statusUrl());
+ } else {
+ environment.setStatusCheckType(null);
+ environment.setStatusUrl(null);
+ }
environmentRepository.save(environment);
return EnvironmentDto.fromEnvironment(environment);
@@ -217,8 +230,8 @@ public Optional updateEnvironment(Long id, EnvironmentDto enviro
public EnvironmentLockHistoryDto getUsersCurrentLock() {
final String currentUserName = authService.getPreferredUsername();
- Optional lockHistory =
- lockHistoryRepository.findLatestLockForEnabledEnvironment(currentUserName);
+ Optional lockHistory = lockHistoryRepository
+ .findLatestLockForEnabledEnvironment(currentUserName);
return lockHistory.map(EnvironmentLockHistoryDto::fromEnvironmentLockHistory).orElse(null);
}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/ArtemisInfoCheck.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/ArtemisInfoCheck.java
new file mode 100644
index 000000000..559376b5c
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/ArtemisInfoCheck.java
@@ -0,0 +1,60 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import de.tum.cit.aet.helios.environment.Environment;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+@RequiredArgsConstructor
+public class ArtemisInfoCheck implements StatusCheckStrategy {
+ private final RestTemplate restTemplate;
+
+ @Override
+ public StatusCheckResult check(Environment environment) {
+ final String url = environment.getStatusUrl();
+ final Map metadata = new HashMap<>();
+
+ try {
+ ResponseEntity response = restTemplate.getForEntity(
+ url,
+ ArtemisInfo.class);
+
+ ArtemisInfo artemisInfo = response.getBody();
+
+ if (artemisInfo != null) {
+ ArtemisInfo.BuildInfo build = artemisInfo.build();
+
+ metadata.put("artifact", build.artifact());
+ metadata.put("name", build.name());
+ metadata.put("version", build.version());
+ metadata.put("group", build.group());
+ metadata.put("buildTime", build.time());
+ }
+
+ return new StatusCheckResult(
+ response.getStatusCode().is2xxSuccessful(),
+ response.getStatusCode().value(),
+ metadata);
+
+ } catch (Exception e) {
+ return new StatusCheckResult(false, 503, Map.of());
+ }
+ }
+
+ public record ArtemisInfo(@JsonProperty("build") BuildInfo build) {
+ public record BuildInfo(
+ String artifact,
+ String name,
+ @JsonProperty("time") Instant time,
+ String version,
+ String group) {
+ }
+ }
+
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatus.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatus.java
new file mode 100644
index 000000000..41d007a1a
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatus.java
@@ -0,0 +1,50 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import de.tum.cit.aet.helios.environment.Environment;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.Getter;
+import lombok.Setter;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+
+@Entity
+@Getter
+@Setter
+public class EnvironmentStatus {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "environment_id")
+ private Environment environment;
+
+ @Column(nullable = false)
+ private boolean success;
+
+ @Column(name = "http_status_code", nullable = false)
+ private Integer httpStatusCode;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "check_type", length = 20, nullable = false)
+ private StatusCheckType checkType;
+
+ @Column(name = "check_timestamp", nullable = false)
+ private Instant checkTimestamp;
+
+ @JdbcTypeCode(SqlTypes.JSON)
+ @Column(columnDefinition = "jsonb")
+ private Map metadata = new HashMap<>();
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusConfig.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusConfig.java
new file mode 100644
index 000000000..3dccc6a80
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusConfig.java
@@ -0,0 +1,75 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import java.time.Duration;
+import java.util.Map;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+@EnableAsync
+@EnableScheduling
+@EnableConfigurationProperties(EnvironmentStatusConfig.StatusCheckTaskExecutorConfig.class)
+public class EnvironmentStatusConfig {
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplateBuilder()
+ .connectTimeout(Duration.ofSeconds(5))
+ .readTimeout(Duration.ofSeconds(5))
+ .build();
+ }
+
+ @Bean
+ public Map checkStrategies(
+ HttpStatusCheck httpStatusCheck,
+ ArtemisInfoCheck artemisInfoCheck) {
+ return Map.of(
+ StatusCheckType.HTTP_STATUS, httpStatusCheck,
+ StatusCheckType.ARTEMIS_INFO, artemisInfoCheck);
+ }
+
+ /**
+ * Creates a TaskExecutor bean named "statusCheckTaskExecutor".
+ * This executor is used to manage and execute status check tasks concurrently.
+ * The core and maximum pool sizes are configured based on the available processors
+ * to optimize performance but can be overridden in the application configuration.
+ *
+ * @param config the configuration for the StatusCheckTaskExecutor
+ * @return a configured ThreadPoolTaskExecutor instance
+ */
+ @Bean("statusCheckTaskExecutor")
+ public TaskExecutor taskExecutor(
+ StatusCheckTaskExecutorConfig config) {
+ int availableProcessors = Runtime.getRuntime().availableProcessors();
+
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(config.getCorePoolSize(availableProcessors));
+ executor.setMaxPoolSize(config.getMaxPoolSize(availableProcessors));
+ executor.setThreadNamePrefix("status-check-");
+ return executor;
+ }
+
+ @ConfigurationProperties(prefix = "status-check.executor")
+ @Data
+ public static class StatusCheckTaskExecutorConfig {
+ private Integer corePoolSize;
+ private Integer maxPoolSize;
+
+ // Dynamic defaults based on CPU cores
+ public int getCorePoolSize(int availableProcessors) {
+ return corePoolSize != null ? corePoolSize : availableProcessors * 2;
+ }
+
+ public int getMaxPoolSize(int availableProcessors) {
+ return maxPoolSize != null ? maxPoolSize : availableProcessors * 4;
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusRepository.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusRepository.java
new file mode 100644
index 000000000..88518f10a
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/EnvironmentStatusRepository.java
@@ -0,0 +1,14 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+public interface EnvironmentStatusRepository extends JpaRepository {
+ @Modifying
+ @Query("DELETE FROM EnvironmentStatus es "
+ + "WHERE es.environment.id = :environmentId AND es.id NOT IN ("
+ + "SELECT es2.id FROM EnvironmentStatus es2 WHERE es2.environment.id = :environmentId "
+ + "ORDER BY es2.checkTimestamp DESC LIMIT :keepCount)")
+ void deleteAllButLatestByEnvironmentId(Long environmentId, int keepCount);
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/HttpStatusCheck.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/HttpStatusCheck.java
new file mode 100644
index 000000000..e0a8e2880
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/HttpStatusCheck.java
@@ -0,0 +1,31 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import de.tum.cit.aet.helios.environment.Environment;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+@RequiredArgsConstructor
+public class HttpStatusCheck implements StatusCheckStrategy {
+ private final RestTemplate restTemplate;
+
+ @Override
+ public StatusCheckResult check(Environment environment) {
+ try {
+ ResponseEntity response = restTemplate.getForEntity(
+ environment.getStatusUrl(),
+ Void.class);
+
+ return new StatusCheckResult(
+ response.getStatusCode().is2xxSuccessful(),
+ response.getStatusCode().value(),
+ Map.of());
+ } catch (Exception e) {
+ return new StatusCheckResult(false, 503, Map.of());
+ }
+ }
+
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckResult.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckResult.java
new file mode 100644
index 000000000..1e683d798
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckResult.java
@@ -0,0 +1,9 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import java.util.Map;
+
+public record StatusCheckResult(
+ boolean success,
+ int httpStatusCode,
+ Map metadata) {
+}
\ No newline at end of file
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckScheduler.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckScheduler.java
new file mode 100644
index 000000000..d6aea8816
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckScheduler.java
@@ -0,0 +1,43 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import de.tum.cit.aet.helios.environment.Environment;
+import de.tum.cit.aet.helios.environment.EnvironmentRepository;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class StatusCheckScheduler {
+ private final EnvironmentRepository environmentRepository;
+ private final StatusCheckService statusCheckService;
+
+ /*
+ * Runs status checks for all environments with a status check type configured
+ * at a fixed interval.
+ *
+ * The interval is configurable via the status-check.interval property.
+ * Defaults to 120 seconds.
+ */
+ @Scheduled(fixedRateString = "${status-check.interval:120s}")
+ public void runScheduledChecks() {
+ log.info("Starting scheduled status checks.");
+
+ List environments = environmentRepository.findByStatusCheckTypeIsNotNull();
+
+ log.info("Found {} environments with status check type configured.", environments.size());
+
+ List> futures = environments.stream()
+ .map(env -> statusCheckService.performStatusCheck(env))
+ .toList();
+
+ // Wait for all status checks to complete
+ futures.forEach(CompletableFuture::join);
+
+ log.info("Scheduled status checks completed.");
+ }
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckService.java
new file mode 100644
index 000000000..c726b9ee7
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckService.java
@@ -0,0 +1,90 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import de.tum.cit.aet.helios.environment.Environment;
+import jakarta.transaction.Transactional;
+import java.time.Instant;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+@Transactional
+public class StatusCheckService {
+ private final Map checkStrategies;
+ private final EnvironmentStatusRepository statusRepository;
+
+ /**
+ * The number of status entries to keep for each environment
+ * before deleting the oldest entries.
+ */
+ @Value("${status-check.keep-count:10}")
+ private int keepCount;
+
+ /**
+ * Performs a status check on the given environment asynchronously as the status
+ * check may take a while to complete.
+ *
+ * The type of status check to be performed is determined by the environment's
+ * configuration.
+ * Saves the result of the status check after completion.
+ *
+ * @param environment the environment on which to perform the status check
+ */
+ @Async("statusCheckTaskExecutor")
+ public CompletableFuture performStatusCheck(Environment environment) {
+ // We need to return a CompletableFuture to allow the caller to wait for the
+ // completion of the status check, even though we don't need to return any value.
+ final CompletableFuture returnFuture = CompletableFuture.completedFuture(null);
+
+ final StatusCheckType checkType = environment.getStatusCheckType();
+
+ log.debug("Starting status check for environment {} (ID: {}) with type {}",
+ environment.getName(), environment.getId(), checkType);
+
+ if (checkType == null) {
+ log.warn("Skipping environment {} - no check type configured", environment.getId());
+ return returnFuture;
+ }
+
+ final StatusCheckStrategy strategy = checkStrategies.get(checkType);
+
+ if (strategy == null) {
+ log.error("No strategy found for check type {} in environment {}",
+ checkType, environment.getId());
+ return returnFuture;
+ }
+
+ final StatusCheckResult result = strategy.check(environment);
+ log.debug("Check completed for environment {} - success: {}, code: {}",
+ environment.getId(), result.success(), result.httpStatusCode());
+
+ saveStatusResult(environment, result);
+
+ return returnFuture;
+ }
+
+ private void saveStatusResult(Environment environment, StatusCheckResult result) {
+ EnvironmentStatus status = new EnvironmentStatus();
+
+ status.setEnvironment(environment);
+ status.setCheckType(environment.getStatusCheckType());
+ status.setSuccess(result.success());
+ status.setHttpStatusCode(result.httpStatusCode());
+ status.setCheckTimestamp(Instant.now());
+ status.setMetadata(result.metadata());
+
+ statusRepository.save(status);
+
+ // To prevent the status table from growing indefinitely, delete all but the
+ // oldest keepCount entries for the environment
+ statusRepository.deleteAllButLatestByEnvironmentId(environment.getId(), this.keepCount);
+
+ log.debug("Persisted status entry for environment {}", environment.getId());
+ }
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckStrategy.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckStrategy.java
new file mode 100644
index 000000000..4eb9b9447
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckStrategy.java
@@ -0,0 +1,7 @@
+package de.tum.cit.aet.helios.environment.status;
+
+import de.tum.cit.aet.helios.environment.Environment;
+
+public interface StatusCheckStrategy {
+ StatusCheckResult check(Environment environment);
+}
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckType.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckType.java
new file mode 100644
index 000000000..f2993e1df
--- /dev/null
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/status/StatusCheckType.java
@@ -0,0 +1,6 @@
+package de.tum.cit.aet.helios.environment.status;
+
+public enum StatusCheckType {
+ HTTP_STATUS, // Simple HTTP status check
+ ARTEMIS_INFO, // Checks the /management/info endpoint of Artemis
+}
diff --git a/server/application-server/src/main/resources/db/migration/V2__status_checks.sql b/server/application-server/src/main/resources/db/migration/V2__status_checks.sql
new file mode 100644
index 000000000..3e5465904
--- /dev/null
+++ b/server/application-server/src/main/resources/db/migration/V2__status_checks.sql
@@ -0,0 +1,20 @@
+-- Add new columns to environment table
+ALTER TABLE public.environment
+ ADD COLUMN status_url VARCHAR(255),
+ ADD COLUMN status_check_type VARCHAR(20);
+
+-- Create environment_status table
+CREATE TABLE public.environment_status (
+ id BIGSERIAL PRIMARY KEY,
+ environment_id BIGINT NOT NULL REFERENCES public.environment(id) ON DELETE CASCADE,
+ success BOOLEAN NOT NULL,
+ http_status_code INT NOT NULL,
+ check_type VARCHAR(20) NOT NULL,
+ check_timestamp TIMESTAMP NOT NULL,
+ metadata JSONB
+);
+
+-- Create indexes for common query patterns
+CREATE INDEX idx_env_status_env ON public.environment_status(environment_id);
+CREATE INDEX idx_env_status_order ON public.environment_status(environment_id, check_timestamp DESC);
+CREATE INDEX idx_env_status_type ON public.environment_status(check_type);
\ No newline at end of file