You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/03/26 22:10:24 UTC
(airavata) branch master updated: AIRAVATA-3981: Add integration health check — ServiceRegistry, monitoring endpoints, Tiltfile, test suite (#596)
This is an automated email from the ASF dual-hosted git repository.
yasith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/master by this push:
new f1872d30d5 AIRAVATA-3981: Add integration health check — ServiceRegistry, monitoring endpoints, Tiltfile, test suite (#596)
f1872d30d5 is described below
commit f1872d30d5bc34e7e8a7998db200c9f46299b502
Author: Yasith Jayawardana <ya...@gatech.edu>
AuthorDate: Thu Mar 26 17:10:18 2026 -0500
AIRAVATA-3981: Add integration health check — ServiceRegistry, monitoring endpoints, Tiltfile, test suite (#596)
* feat: add ServiceRegistry and ServiceStatus for background service tracking
* feat: add /health/services and /admin/restart endpoints to MonitoringServer
* refactor: use ServiceRegistry in AiravataServer for background service lifecycle
* feat: add Tiltfile for dev orchestration and integration testing
* feat: add integration-tests Maven module scaffold
* feat: add ThriftServiceHealthTest for all 9 multiplexed services
* feat: add Database, RabbitMQ, and Kafka integration health tests
* feat: add Keycloak and Prometheus integration health tests
* feat: add ServiceHealthEndpoint and BackgroundServiceHealth integration tests
* chore: final cleanup — gitignore docs/superpowers, fix health endpoint format
- Wrap /health/services response in {"services": {...}} per spec
- Update ServiceHealthEndpointTest and BackgroundServiceHealthTest to
unwrap the "services" key before asserting on service entries
- Add docs/superpowers/ to .gitignore and remove already-committed files
from git tracking (local copies preserved)
* style: apply spotless formatting to MonitoringServer and integration tests
* fix: ServiceRegistry uses IServer.getStatus() for health, add actuator to Spring Boot services
- ServiceRegistry now checks IServer.getStatus() == STARTED in addition to
Thread.isAlive(), fixing false DOWN for services like PostWorkflowManager
that spawn sub-threads and let their main thread exit
- Add spring-boot-starter-actuator to all 4 Spring Boot modules with
/actuator/health endpoint exposed
- Update Tiltfile to use spring-boot:run for Spring Boot services
- All 8/8 background services + 4/4 Spring Boot services report UP
* docs: update README for thrift-server split, Tiltfile, health monitoring
- Update architecture diagram to show airavata-thrift-server + airavata-api split
- Replace scripts/start.sh quick start with tilt up
- Add health monitoring endpoints table (/health/services, /admin/restart)
- Add integration tests section
- Update Spring Boot services table with ports and gRPC
- Replace ide-integration IDE setup with Tilt-based workflow
- Update workspace CLAUDE.md to include airavata project
---
.gitignore | 2 +
README.md | 102 +-
Tiltfile | 112 ++
airavata-api/pom.xml | 18 +
.../patform/monitoring/MonitoringServer.java | 124 ++-
.../patform/monitoring/ServiceRegistry.java | 153 +++
.../airavata/patform/monitoring/ServiceStatus.java | 62 ++
airavata-thrift-server/pom.xml | 19 +-
.../apache/airavata/api/server/AiravataServer.java | 40 +-
.../api/server/mapper/WorkspaceMapper.java | 40 +-
...2026-03-26-airavata-thrift-server-extraction.md | 1090 --------------------
...-26-airavata-thrift-server-extraction-design.md | 190 ----
integration-tests/pom.xml | 99 ++
.../integration/BackgroundServiceHealthTest.java | 120 +++
.../airavata/integration/DatabaseHealthTest.java | 79 ++
.../airavata/integration/KafkaHealthTest.java | 57 +
.../airavata/integration/KeycloakHealthTest.java | 132 +++
.../airavata/integration/PrometheusHealthTest.java | 84 ++
.../airavata/integration/RabbitMQHealthTest.java | 66 ++
.../integration/ServiceHealthEndpointTest.java | 122 +++
.../integration/ThriftServiceHealthTest.java | 115 +++
modules/agent-framework/agent-service/pom.xml | 4 +
.../src/main/resources/application.yml | 6 +
modules/file-server/pom.xml | 4 +
.../src/main/resources/application.properties | 2 +
modules/ide-integration/.gitignore | 2 -
modules/ide-integration/README.md | 294 ------
modules/ide-integration/pom.xml | 61 --
.../airavata/ide/integration/APIServerStarter.java | 34 -
.../airavata/ide/integration/JobEngineStarter.java | 72 --
.../ide/integration/JobMonitorStarter.java | 33 -
.../research-framework/research-service/pom.xml | 4 +
.../src/main/resources/application.yml | 6 +
modules/restproxy/pom.xml | 4 +
.../src/main/resources/application.properties | 2 +
pom.xml | 7 +-
36 files changed, 1498 insertions(+), 1863 deletions(-)
diff --git a/.gitignore b/.gitignore
index 6a9a2b896a..a9c37a7d2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,3 +66,5 @@ keystores/airavata.p12
keystores/aes.p12
modules/*/distribution/
modules/*/*/distribution/
+
+docs/superpowers/
diff --git a/README.md b/README.md
index 978e948b4b..f099284164 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ Airavata is composed of a consolidated JVM server plus three Spring Boot microse
```mermaid
graph TB
- subgraph "Docker Infrastructure"
+ subgraph "Docker Infrastructure (compose.yml)"
DB[(MariaDB<br/>:13306)]
RMQ[RabbitMQ<br/>:5672]
ZK[ZooKeeper<br/>:2181]
@@ -67,34 +67,38 @@ graph TB
KC[Keycloak<br/>:18080]
end
- subgraph "AiravataServer (single JVM, :8930)"
+ subgraph "AiravataServer (single JVM)"
direction TB
- MUX["TMultiplexedProcessor<br/>9 Thrift services"]
- BG["Background Services<br/>12 IServer workers"]
+ MUX["TMultiplexedProcessor :8930<br/>9 Thrift services<br/>(airavata-thrift-server module)"]
+ SVC["Service Layer<br/>(airavata-api module)"]
+ BG["Background Services<br/>8 IServer workers<br/>(ServiceRegistry)"]
+ MON["MonitoringServer :9097<br/>/metrics /health/services<br/>/admin/restart"]
end
- subgraph "Spring Boot Modules"
- AS[Agent Service<br/>:18880]
+ subgraph "Spring Boot Services"
+ RS[Research Service<br/>:18889 / gRPC :19908]
+ AS[Agent Service<br/>:18880 / gRPC :19900]
FS[File Server<br/>:8050]
- RS[Research Service<br/>:18889]
RP[REST Proxy<br/>:8082]
end
SDK[Python SDK] -->|TMultiplexedProtocol| MUX
Portal[Web Portal] -->|REST| RP
- MUX --> DB
- MUX --> RMQ
+ MUX --> SVC
+ SVC --> DB
+ SVC --> RMQ
BG --> ZK
BG --> KFK
BG --> RMQ
AS --> DB
RS --> DB
- MUX -.->|auth| KC
+ SVC -.->|auth| KC
+ MON -.->|tracks| BG
```
-### 1. Airavata Server `(apache-airavata-api-server)`
-> Entry point: `org.apache.airavata.api.server.AiravataServer`
-> Started via: `./scripts/start.sh`
+### 1. Airavata Server `(airavata-thrift-server + airavata-api)`
+> Entry point: `org.apache.airavata.api.server.AiravataServer` (in `airavata-thrift-server` module)
+> Started via: `tilt up` or manually with `java -cp ...`
`AiravataServer` is the single JVM that hosts all Thrift services and background workers. On startup it:
@@ -219,50 +223,76 @@ The Airavata Research Service is the backend for the **research catalog** in Air
| **Apache Maven** | 3.8+ | `mvn -v` |
| **Docker Engine** | 20.10+ | `docker -v` |
| **Docker Compose** | 2.0+ | `docker compose version` |
+| **Tilt** | 0.33+ | `tilt version` |
-### Quick Start
+### Quick Start (Tilt)
-First, clone the repository and start the infrastructure (MariaDB, RabbitMQ, ZooKeeper, Kafka, Keycloak):
+Tilt orchestrates the full development stack — infrastructure, build, and all services:
```bash
git clone https://github.com/apache/airavata.git
cd airavata
-docker compose up -d
+tilt up
```
-Next, build Airavata and set up the database:
+This starts:
+- **Infrastructure**: MariaDB, RabbitMQ, ZooKeeper, Kafka, Keycloak (via `compose.yml`)
+- **Airavata Thrift Server**: All 9 services on port 8930, monitoring on 9097
+- **Spring Boot Services**: Research Service (18889), Agent Service (18880), File Server (8050), REST Proxy (8082)
-```bash
-./scripts/setup.sh
-```
+Open the Tilt UI at `http://localhost:10350` to monitor all resources. Integration tests can be triggered manually from the Tilt UI.
-Finally, start the server:
+### Manual Start
```bash
-./scripts/start.sh
+docker compose up -d # Infrastructure
+mvn clean package -DskipTests -T4 # Build
+java -cp "airavata-thrift-server/target/classes:airavata-api/target/classes:airavata-api/target/dependency/*" \
+ -Dairavata.config.dir=airavata-api/src/main/resources \
+ org.apache.airavata.api.server.AiravataServer # Thrift server
+mvn spring-boot:run -pl modules/research-framework/research-service # Research Service
+mvn spring-boot:run -pl modules/agent-framework/agent-service # Agent Service
+mvn spring-boot:run -pl modules/file-server # File Server
+mvn spring-boot:run -pl modules/restproxy # REST Proxy
```
-The Airavata Server will be available on port **8930** (Thrift, multiplexed). All 9 Thrift services are served on this single port.
+### Health Monitoring
+
+The monitoring server on port **9097** exposes:
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/metrics` | GET | Prometheus metrics |
+| `/health/services` | GET | JSON status of all background services (UP/DOWN/DISABLED) |
+| `/admin/restart/{name}` | POST | Restart a background service by name |
+
+Spring Boot services expose `/actuator/health` on their respective ports.
### Configuration
The main configuration file is `airavata-api/src/main/resources/airavata-server.properties`. Key settings:
* `apiserver.port` — Thrift port (default: `8930`)
-* `spring.datasource.url` — JDBC URL for the unified `airavata` database
+* `airavata.jdbc.url` — JDBC URL for the unified `airavata` database
* `email.based.monitoring.enabled` — Enable/disable email-based job monitor
* `api.server.monitoring.enabled` — Enable/disable Prometheus metrics endpoint
-### Spring Boot Microservices
+### Integration Tests
+
+```bash
+mvn test -pl integration-tests -Dgroups=integration
+```
-The following services are separate Spring Boot applications, packaged independently:
+Tests verify: all 9 thrift services, database tables, RabbitMQ exchanges, Kafka broker, Keycloak realm, Prometheus metrics, and the `/health/services` endpoint.
-| Service | Entry point | Purpose |
-|---|---|---|
-| **Agent Service** | `AgentServiceApplication` | Backend for interactive jobs via gRPC |
-| **File Server** | `FileServerApplication` | SFTP wrapper for storage nodes |
-| **Research Service** | `ResearchServiceApplication` | Research catalog API |
-| **REST Proxy** | *(restproxy module)* | REST-to-Thrift proxy for the API |
+### Spring Boot Microservices
+
+| Service | Port | gRPC | Purpose |
+|---|---|---|---|
+| **Research Service** | 18889 | 19908 | Research catalog API |
+| **Agent Service** | 18880 | 19900 | Backend for interactive jobs |
+| **File Server** | 8050 | — | SFTP wrapper for storage nodes |
+| **REST Proxy** | 8082 | — | REST-to-Kafka bridge |
## 🤝 Contributing
@@ -282,13 +312,7 @@ We welcome contributions from the community! Here's how you can help:
### Setting up your IDE
-The easiest way to setup a development environment is to follow the instructions in the [ide-integration README](./modules/ide-integration/README.md).Those instructions will guide you on setting up a development environment with IntelliJ IDEA.
-
-### Additional Tools
-
-* `org.apache.airavata.sharing.registry.migrator.airavata.AiravataDataMigrator`
-* `modules/deployment-scripts`
-* `modules/load-client`
+Use `tilt up` to start the full stack, then attach your IDE debugger to the running services. For the Airavata Thrift Server, use remote JVM debug (add `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005` to the `serve_cmd` in the Tiltfile). For Spring Boot services, use `mvn spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=..."` directly.
## 💬 Community & Support
diff --git a/Tiltfile b/Tiltfile
new file mode 100644
index 0000000000..ab6859953f
--- /dev/null
+++ b/Tiltfile
@@ -0,0 +1,112 @@
+# Airavata Development Tiltfile
+# Usage: tilt up
+
+# --- Infrastructure (from compose.yml) ---
+docker_compose('./compose.yml')
+
+# --- Build ---
+local_resource(
+ 'build',
+ cmd='mvn package -DskipTests -T4 -am -q',
+ deps=[
+ 'airavata-api/src',
+ 'airavata-api/pom.xml',
+ 'airavata-thrift-server/src',
+ 'airavata-thrift-server/pom.xml',
+ 'modules/file-server/src',
+ 'modules/file-server/pom.xml',
+ 'modules/agent-framework/agent-service/src',
+ 'modules/agent-framework/agent-service/pom.xml',
+ 'modules/research-framework/research-service/src',
+ 'modules/research-framework/research-service/pom.xml',
+ 'modules/restproxy/src',
+ 'modules/restproxy/pom.xml',
+ ],
+ labels=['build'],
+)
+
+# --- Airavata Thrift Server (port 8930 thrift, 9097 monitoring) ---
+local_resource(
+ 'airavata-server',
+ serve_cmd=' '.join([
+ 'java',
+ '-cp', 'airavata-thrift-server/target/classes:airavata-api/target/classes:airavata-api/target/dependency/*',
+ '-Dlog4j.configurationFile=airavata-api/src/main/resources/log4j2.xml',
+ '-Dairavata.config.dir=airavata-api/src/main/resources',
+ 'org.apache.airavata.api.server.AiravataServer',
+ ]),
+ readiness_probe=probe(
+ http_get=http_get_action(port=9097, path='/health/services'),
+ period_secs=5,
+ timeout_secs=5,
+ initial_delay_secs=15,
+ ),
+ resource_deps=['build', 'db', 'rabbitmq', 'zookeeper', 'kafka', 'keycloak'],
+ labels=['airavata'],
+)
+
+# --- Research Service (port 18889 HTTP, 19908 gRPC) ---
+local_resource(
+ 'research-service',
+ serve_cmd='mvn spring-boot:run -pl modules/research-framework/research-service -q',
+ readiness_probe=probe(
+ http_get=http_get_action(port=18889, path='/actuator/health'),
+ period_secs=5,
+ timeout_secs=5,
+ initial_delay_secs=20,
+ ),
+ resource_deps=['build', 'airavata-server', 'db', 'keycloak'],
+ labels=['airavata'],
+)
+
+# --- Agent Service (port 18880 HTTP, 19900 gRPC) ---
+local_resource(
+ 'agent-service',
+ serve_cmd='mvn spring-boot:run -pl modules/agent-framework/agent-service -q',
+ readiness_probe=probe(
+ http_get=http_get_action(port=18880, path='/actuator/health'),
+ period_secs=5,
+ timeout_secs=5,
+ initial_delay_secs=20,
+ ),
+ resource_deps=['build', 'airavata-server', 'db'],
+ labels=['airavata'],
+)
+
+# --- File Server (port 8050) ---
+local_resource(
+ 'file-server',
+ serve_cmd='mvn spring-boot:run -pl modules/file-server -q',
+ readiness_probe=probe(
+ http_get=http_get_action(port=8050, path='/actuator/health'),
+ period_secs=5,
+ timeout_secs=5,
+ initial_delay_secs=15,
+ ),
+ resource_deps=['build', 'airavata-server'],
+ labels=['airavata'],
+)
+
+# --- REST Proxy (port 8082) ---
+local_resource(
+ 'rest-proxy',
+ serve_cmd='mvn spring-boot:run -pl modules/restproxy -q',
+ readiness_probe=probe(
+ http_get=http_get_action(port=8082, path='/actuator/health'),
+ period_secs=5,
+ timeout_secs=5,
+ initial_delay_secs=15,
+ ),
+ resource_deps=['build', 'airavata-server', 'kafka'],
+ labels=['airavata'],
+)
+
+# --- Integration Tests (manual trigger) ---
+local_resource(
+ 'integration-tests',
+ cmd='mvn test -pl integration-tests -Dgroups=integration -am -q',
+ resource_deps=['airavata-server'],
+ auto_init=False,
+ trigger_mode=TRIGGER_MODE_MANUAL,
+ labels=['tests'],
+)
diff --git a/airavata-api/pom.xml b/airavata-api/pom.xml
index d9a8a88957..363c46a43a 100644
--- a/airavata-api/pom.xml
+++ b/airavata-api/pom.xml
@@ -570,6 +570,24 @@ under the License.
</executions>
</plugin>
+ <!-- Copy runtime dependencies for classpath mode (Tiltfile) -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy-dependencies</id>
+ <phase>package</phase>
+ <goals>
+ <goal>copy-dependencies</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/dependency</outputDirectory>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
<!-- Package distribution (docker) preparation -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
diff --git a/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/MonitoringServer.java b/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/MonitoringServer.java
index 23d90111b7..ce92273fdd 100644
--- a/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/MonitoringServer.java
+++ b/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/MonitoringServer.java
@@ -19,8 +19,17 @@
*/
package org.apache.airavata.patform.monitoring;
-import io.prometheus.client.exporter.HTTPServer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.exporter.common.TextFormat;
import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
import org.apache.airavata.common.utils.IServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,9 +39,11 @@ public class MonitoringServer implements IServer {
private static final Logger logger = LoggerFactory.getLogger(MonitoringServer.class);
private static final String SERVER_NAME = "Monitoring Server";
- private String host;
- private int port;
- private HTTPServer httpServer;
+ private final String host;
+ private final int port;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private HttpServer httpServer;
+ private ServiceRegistry serviceRegistry;
private ServerStatus status = ServerStatus.STOPPED;
public MonitoringServer(String host, int port) {
@@ -40,6 +51,10 @@ public class MonitoringServer implements IServer {
this.port = port;
}
+ public void setServiceRegistry(ServiceRegistry serviceRegistry) {
+ this.serviceRegistry = serviceRegistry;
+ }
+
@Override
public String getName() {
return SERVER_NAME;
@@ -49,10 +64,18 @@ public class MonitoringServer implements IServer {
public void run() {
setStatus(ServerStatus.STARTING);
try {
- logger.info("Starting the monitoring server");
- httpServer = new HTTPServer(host, port, true);
+ logger.info("Starting the monitoring server on {}:{}", host, port);
+ httpServer = HttpServer.create(new InetSocketAddress(host, port), 0);
+
+ httpServer.createContext("/metrics", this::handleMetrics);
+ httpServer.createContext("/health/services", this::handleHealthServices);
+ httpServer.createContext("/admin/restart/", this::handleAdminRestart);
+
+ httpServer.start();
setStatus(ServerStatus.STARTED);
- // HTTPServer is non-blocking; park this thread until interrupted
+ logger.info("Monitoring server started on {}:{}", host, port);
+
+ // Park thread until interrupted
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(Long.MAX_VALUE);
@@ -61,7 +84,7 @@ public class MonitoringServer implements IServer {
}
}
} catch (IOException e) {
- logger.error("Failed to start the monitoring server on host {} na port {}", host, port, e);
+ logger.error("Failed to start the monitoring server on host {} and port {}", host, port, e);
setStatus(ServerStatus.FAILED);
}
}
@@ -70,8 +93,8 @@ public class MonitoringServer implements IServer {
public void stop() {
setStatus(ServerStatus.STOPPING);
if (httpServer != null) {
- logger.info("Stopping the monitor server");
- httpServer.stop();
+ logger.info("Stopping the monitoring server");
+ httpServer.stop(0);
}
setStatus(ServerStatus.STOPPED);
}
@@ -85,4 +108,85 @@ public class MonitoringServer implements IServer {
status = stat;
status.updateTime();
}
+
+ private void handleMetrics(HttpExchange exchange) throws IOException {
+ if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed");
+ return;
+ }
+ try {
+ StringWriter writer = new StringWriter();
+ TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples());
+ byte[] body = writer.toString().getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", TextFormat.CONTENT_TYPE_004);
+ exchange.sendResponseHeaders(200, body.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(body);
+ }
+ } catch (Exception e) {
+ logger.error("Error serving /metrics", e);
+ sendResponse(exchange, 500, "Internal Server Error");
+ }
+ }
+
+ private void handleHealthServices(HttpExchange exchange) throws IOException {
+ if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed");
+ return;
+ }
+ try {
+ Map<String, ServiceStatus> statuses = serviceRegistry != null ? serviceRegistry.getStatuses() : Map.of();
+ sendJson(exchange, 200, Map.of("services", statuses));
+ } catch (Exception e) {
+ logger.error("Error serving /health/services", e);
+ sendResponse(exchange, 500, "Internal Server Error");
+ }
+ }
+
+ private void handleAdminRestart(HttpExchange exchange) throws IOException {
+ if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed");
+ return;
+ }
+ String path = exchange.getRequestURI().getPath();
+ // path is /admin/restart/{name}
+ String prefix = "/admin/restart/";
+ if (!path.startsWith(prefix) || path.length() <= prefix.length()) {
+ sendJson(exchange, 400, Map.of("error", "Missing service name in path"));
+ return;
+ }
+ String name = path.substring(prefix.length());
+ if (serviceRegistry == null) {
+ sendJson(exchange, 500, Map.of("error", "ServiceRegistry not configured"));
+ return;
+ }
+ try {
+ serviceRegistry.restart(name);
+ sendJson(exchange, 200, Map.of("status", "restarted", "service", name));
+ } catch (IllegalArgumentException e) {
+ sendJson(exchange, 404, Map.of("error", e.getMessage()));
+ } catch (IllegalStateException e) {
+ sendJson(exchange, 400, Map.of("error", e.getMessage()));
+ } catch (Exception e) {
+ logger.error("Error restarting service '{}'", name, e);
+ sendJson(exchange, 500, Map.of("error", e.getMessage()));
+ }
+ }
+
+ private void sendJson(HttpExchange exchange, int statusCode, Object body) throws IOException {
+ byte[] bytes = objectMapper.writeValueAsBytes(body);
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(statusCode, bytes.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(bytes);
+ }
+ }
+
+ private void sendResponse(HttpExchange exchange, int statusCode, String body) throws IOException {
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(statusCode, bytes.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(bytes);
+ }
+ }
}
diff --git a/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/ServiceRegistry.java b/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/ServiceRegistry.java
new file mode 100644
index 0000000000..61b6ac81ed
--- /dev/null
+++ b/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/ServiceRegistry.java
@@ -0,0 +1,153 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.patform.monitoring;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.airavata.common.utils.IServer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tracks background {@link IServer} instances and their threads, exposing
+ * lifecycle operations (status query, restart, stop-all) and error recording.
+ */
+public class ServiceRegistry {
+
+ private static final Logger logger = LoggerFactory.getLogger(ServiceRegistry.class);
+
+ private static class Entry {
+ final IServer server;
+ Thread thread;
+ final Supplier<IServer> factory;
+ final long startedAt;
+ String lastError;
+
+ Entry(IServer server, Thread thread, Supplier<IServer> factory) {
+ this.server = server;
+ this.thread = thread;
+ this.factory = factory;
+ this.startedAt = System.currentTimeMillis();
+ }
+ }
+
+ private final Map<String, Entry> entries = new LinkedHashMap<>();
+
+ /**
+ * Registers a service with an optional restart factory.
+ *
+ * @param label human-readable service identifier
+ * @param service the IServer instance
+ * @param thread the thread running the service
+ * @param factory optional supplier to create a fresh IServer for restart; may be null
+ */
+ public synchronized void register(String label, IServer service, Thread thread, Supplier<IServer> factory) {
+ entries.put(label, new Entry(service, thread, factory));
+ }
+
+ /**
+ * Registers a service without a restart factory.
+ */
+ public synchronized void register(String label, IServer service, Thread thread) {
+ register(label, service, thread, null);
+ }
+
+ /**
+ * Returns a snapshot of statuses keyed by label.
+ * A service is "UP" if its IServer status is STARTED (preferred) or its thread is alive.
+ * Some services (e.g. PostWorkflowManager) spawn sub-threads and let their main thread
+ * exit — IServer.getStatus() remains STARTED in that case.
+ */
+ public synchronized Map<String, ServiceStatus> getStatuses() {
+ Map<String, ServiceStatus> result = new LinkedHashMap<>();
+ long now = System.currentTimeMillis();
+ for (Map.Entry<String, Entry> e : entries.entrySet()) {
+ Entry entry = e.getValue();
+ boolean up = entry.server.getStatus() == IServer.ServerStatus.STARTED || entry.thread.isAlive();
+ String status = up ? "UP" : "DOWN";
+ long uptimeMs = up ? now - entry.startedAt : 0L;
+ result.put(e.getKey(), new ServiceStatus(status, uptimeMs, entry.lastError));
+ }
+ return result;
+ }
+
+ /**
+ * Attempts to restart the named service using its registered factory.
+ *
+ * @param name the service label
+ * @throws IllegalArgumentException if no service with that name exists
+ * @throws IllegalStateException if the service has no restart factory
+ * @throws Exception if the new service thread fails to start
+ */
+ public synchronized void restart(String name) throws Exception {
+ Entry entry = entries.get(name);
+ if (entry == null) {
+ throw new IllegalArgumentException("No service registered with name: " + name);
+ }
+ if (entry.factory == null) {
+ throw new IllegalStateException("Service '" + name + "' has no restart factory");
+ }
+ // Stop the old instance
+ try {
+ entry.server.stop();
+ } catch (Exception e) {
+ logger.warn("Error stopping '{}' before restart: {}", name, e.getMessage());
+ }
+ entry.thread.interrupt();
+
+ // Start a fresh instance
+ IServer newServer = entry.factory.get();
+ Thread newThread = new Thread(newServer, "airavata-" + name);
+ newThread.setDaemon(true);
+ newThread.start();
+
+ Entry newEntry = new Entry(newServer, newThread, entry.factory);
+ entries.put(name, newEntry);
+ logger.info("Service '{}' restarted", name);
+ }
+
+ /**
+ * Records an error message against the named service.
+ * Silently ignores unknown names.
+ */
+ public synchronized void recordError(String name, String errorMessage) {
+ Entry entry = entries.get(name);
+ if (entry != null) {
+ entry.lastError = errorMessage;
+ }
+ }
+
+ /**
+ * Stops all registered services and interrupts their threads.
+ */
+ public synchronized void stopAll() {
+ for (Map.Entry<String, Entry> e : entries.entrySet()) {
+ String label = e.getKey();
+ Entry entry = e.getValue();
+ try {
+ entry.server.stop();
+ } catch (Exception ex) {
+ logger.warn("Error stopping '{}': {}", label, ex.getMessage());
+ }
+ entry.thread.interrupt();
+ }
+ }
+}
diff --git a/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/ServiceStatus.java b/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/ServiceStatus.java
new file mode 100644
index 0000000000..9d1b48854a
--- /dev/null
+++ b/airavata-api/src/main/java/org/apache/airavata/patform/monitoring/ServiceStatus.java
@@ -0,0 +1,62 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.patform.monitoring;
+
+/**
+ * DTO representing the runtime status of a single background service.
+ */
+public class ServiceStatus {
+
+ private String status;
+ private long uptimeMs;
+ private String lastError;
+
+ public ServiceStatus() {}
+
+ public ServiceStatus(String status, long uptimeMs, String lastError) {
+ this.status = status;
+ this.uptimeMs = uptimeMs;
+ this.lastError = lastError;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public long getUptimeMs() {
+ return uptimeMs;
+ }
+
+ public void setUptimeMs(long uptimeMs) {
+ this.uptimeMs = uptimeMs;
+ }
+
+ public String getLastError() {
+ return lastError;
+ }
+
+ public void setLastError(String lastError) {
+ this.lastError = lastError;
+ }
+}
diff --git a/airavata-thrift-server/pom.xml b/airavata-thrift-server/pom.xml
index a77569bb70..294e6ed356 100644
--- a/airavata-thrift-server/pom.xml
+++ b/airavata-thrift-server/pom.xml
@@ -1,4 +1,21 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
diff --git a/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java b/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java
index 4d4acf38cf..b3ad7a80f1 100644
--- a/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java
+++ b/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java
@@ -23,6 +23,7 @@ import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.function.Supplier;
import org.apache.airavata.api.Airavata;
import org.apache.airavata.api.server.handler.AiravataServerHandler;
import org.apache.airavata.api.server.util.Constants;
@@ -51,6 +52,7 @@ import org.apache.airavata.monitor.realtime.RealtimeMonitor;
import org.apache.airavata.orchestrator.cpi.OrchestratorService;
import org.apache.airavata.orchestrator.server.OrchestratorServerHandler;
import org.apache.airavata.patform.monitoring.MonitoringServer;
+import org.apache.airavata.patform.monitoring.ServiceRegistry;
import org.apache.airavata.registry.api.RegistryService;
import org.apache.airavata.registry.api.service.handler.RegistryServerHandler;
import org.apache.airavata.registry.core.utils.AppCatalogDBInitConfig;
@@ -101,8 +103,7 @@ public class AiravataServer {
private static final Logger logger = LoggerFactory.getLogger(AiravataServer.class);
private TServer server;
- private final List<IServer> backgroundServices = new ArrayList<>();
- private final List<Thread> serviceThreads = new ArrayList<>();
+ private final ServiceRegistry serviceRegistry = new ServiceRegistry();
private final List<DBInitConfig> dbInitConfigs = Arrays.asList(
new ExpCatalogDBInitConfig(),
@@ -239,10 +240,13 @@ public class AiravataServer {
if (ServerSettings.getBooleanSetting("api.server.monitoring.enabled")) {
String monHost = ServerSettings.getSetting("api.server.monitoring.host", "localhost");
int monPort = Integer.parseInt(ServerSettings.getSetting("api.server.monitoring.port", "9097"));
- registerAndStart(new MonitoringServer(monHost, monPort), "monitoring_server");
+ MonitoringServer monitoringServer = new MonitoringServer(monHost, monPort);
+ monitoringServer.setServiceRegistry(serviceRegistry);
+ registerAndStart(monitoringServer, "monitoring_server");
}
} catch (Exception e) {
logger.warn(" monitoring_server: config error — {}", e.getMessage());
+ serviceRegistry.recordError("monitoring_server", e.getMessage());
}
// Cluster status monitoring — polls compute resource queue status
@@ -252,6 +256,7 @@ public class AiravataServer {
}
} catch (Exception e) {
logger.warn(" cluster_status_monitor: config error — {}", e.getMessage());
+ serviceRegistry.recordError("cluster_status_monitor", e.getMessage());
}
// Data interpreter — metadata analysis for submitted jobs
@@ -261,6 +266,7 @@ public class AiravataServer {
}
} catch (Exception e) {
logger.warn(" data_interpreter: config error — {}", e.getMessage());
+ serviceRegistry.recordError("data_interpreter", e.getMessage());
}
// Process rescheduler — retries/reschedules failed processes
@@ -270,6 +276,7 @@ public class AiravataServer {
}
} catch (Exception e) {
logger.warn(" process_rescheduler: config error — {}", e.getMessage());
+ serviceRegistry.recordError("process_rescheduler", e.getMessage());
}
// Execution engine services — controller must initialize the cluster
@@ -279,6 +286,7 @@ public class AiravataServer {
waitForHelixCluster();
} catch (Exception e) {
logger.warn(" helix_controller: config error — {}", e.getMessage());
+ serviceRegistry.recordError("helix_controller", e.getMessage());
}
try {
ArrayList<Class<? extends AbstractTask>> taskClasses = new ArrayList<>();
@@ -288,21 +296,25 @@ public class AiravataServer {
registerAndStart(new GlobalParticipant(taskClasses, null), "helix_participant");
} catch (Exception e) {
logger.warn(" helix_participant: config error — {}", e.getMessage());
+ serviceRegistry.recordError("helix_participant", e.getMessage());
}
try {
registerAndStart(new PreWorkflowManager(), "pre_workflow_manager");
} catch (Exception e) {
logger.warn(" pre_workflow_manager: config error — {}", e.getMessage());
+ serviceRegistry.recordError("pre_workflow_manager", e.getMessage());
}
try {
registerAndStart(new PostWorkflowManager(), "post_workflow_manager");
} catch (Exception e) {
logger.warn(" post_workflow_manager: config error — {}", e.getMessage());
+ serviceRegistry.recordError("post_workflow_manager", e.getMessage());
}
try {
registerAndStart(new ParserWorkflowManager(), "parser_workflow_manager");
} catch (Exception e) {
logger.warn(" parser_workflow_manager: config error — {}", e.getMessage());
+ serviceRegistry.recordError("parser_workflow_manager", e.getMessage());
}
// Job monitors
@@ -312,14 +324,16 @@ public class AiravataServer {
}
} catch (Exception e) {
logger.warn(" email_monitor: config error — {}", e.getMessage());
+ serviceRegistry.recordError("email_monitor", e.getMessage());
}
try {
registerAndStart(new RealtimeMonitor(), "realtime_monitor");
} catch (Exception e) {
logger.warn(" realtime_monitor: config error — {}", e.getMessage());
+ serviceRegistry.recordError("realtime_monitor", e.getMessage());
}
- logger.info("Background services initialization complete ({} running)", backgroundServices.size());
+ logger.info("Background services initialization complete");
}
private void waitForHelixCluster() {
@@ -347,11 +361,14 @@ public class AiravataServer {
}
private void registerAndStart(IServer service, String label) {
+ registerAndStart(service, label, null);
+ }
+
+ private void registerAndStart(IServer service, String label, Supplier<IServer> factory) {
Thread t = new Thread(service, "airavata-" + label);
t.setDaemon(true);
t.start();
- backgroundServices.add(service);
- serviceThreads.add(t);
+ serviceRegistry.register(label, service, t, factory);
logger.info(" {}: started", label);
}
@@ -359,16 +376,7 @@ public class AiravataServer {
if (server != null && server.isServing()) {
server.stop();
}
- for (IServer service : backgroundServices) {
- try {
- service.stop();
- } catch (Exception e) {
- logger.warn("Error stopping {}: {}", service.getName(), e.getMessage());
- }
- }
- for (Thread t : serviceThreads) {
- t.interrupt();
- }
+ serviceRegistry.stopAll();
}
public static void main(String[] args) {
diff --git a/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper/WorkspaceMapper.java b/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper/WorkspaceMapper.java
index 44dad11244..aff29a9c30 100644
--- a/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper/WorkspaceMapper.java
+++ b/airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper/WorkspaceMapper.java
@@ -1,21 +1,22 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
package org.apache.airavata.api.server.mapper;
import java.util.List;
@@ -78,8 +79,7 @@ public interface WorkspaceMapper {
*/
@AfterMapping
default void fillRepeatedFields(
- org.apache.airavata.model.workspace.Project thrift,
- @MappingTarget Project.Builder builder) {
+ org.apache.airavata.model.workspace.Project thrift, @MappingTarget Project.Builder builder) {
List<String> users = thrift.getSharedUsers();
if (users != null) {
builder.addAllSharedUsers(users);
diff --git a/docs/superpowers/plans/2026-03-26-airavata-thrift-server-extraction.md b/docs/superpowers/plans/2026-03-26-airavata-thrift-server-extraction.md
deleted file mode 100644
index 837d304414..0000000000
--- a/docs/superpowers/plans/2026-03-26-airavata-thrift-server-extraction.md
+++ /dev/null
@@ -1,1090 +0,0 @@
-# Airavata Thrift Server Extraction Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Extract all thrift server concerns from `airavata-api` into a new `airavata-thrift-server` Maven module, introduce proto DTOs in `airavata-api`, and set up MapStruct mapping at the thrift handler boundary.
-
-**Architecture:** `airavata-thrift-server` depends on `airavata-api`. Thrift IDL lives in `airavata-thrift-server/src/main/thrift/`. Server bootstrap (AiravataServer), the main API handler (AiravataServerHandler), profile handlers, OrchestratorServerHandler, ThriftAdapter, and Constants move to `airavata-thrift-server`. Data-access handlers (RegistryServerHandler, SharingRegistryServerHandler, CredentialStoreServerHandler) stay in `airavata-api` because services depend on them directly — [...]
-
-**Tech Stack:** Maven, Apache Thrift 0.22.0, Protocol Buffers 4.30.1, MapStruct 1.6.3, protobuf-maven-plugin 3.10.2
-
-**Spec:** `docs/superpowers/specs/2026-03-26-airavata-thrift-server-extraction-design.md`
-
----
-
-## File Structure
-
-### New files (airavata-thrift-server)
-
-| File | Responsibility |
-|------|---------------|
-| `airavata-thrift-server/pom.xml` | Module POM — depends on airavata-api, libthrift, MapStruct |
-| `airavata-thrift-server/src/main/thrift/**` | Moved from `thrift-interface-descriptions/` — all IDL files |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java` | Moved — thrift server bootstrap |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/util/Constants.java` | Moved — server config constants |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java` | Moved — main API thrift handler |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java` | Moved — thrift error translation |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/UserProfileServiceHandler.java` | Moved |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/TenantProfileServiceHandler.java` | Moved |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/IamAdminServicesHandler.java` | Moved |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/GroupManagerServiceHandler.java` | Moved |
-| `airavata-thrift-server/src/main/java/org/apache/airavata/orchestrator/server/OrchestratorServerHandler.java` | Moved |
-
-### New files (airavata-api — proto)
-
-| File | Responsibility |
-|------|---------------|
-| `airavata-api/src/main/proto/org/apache/airavata/model/workspace/workspace.proto` | Proto mirror of workspace_model.thrift |
-| `airavata-api/src/main/proto/org/apache/airavata/model/experiment/experiment.proto` | Proto mirror of experiment_model.thrift |
-| `airavata-api/src/main/proto/org/apache/airavata/model/status/status.proto` | Proto mirror of status_models.thrift |
-| ... (one proto per thrift data-model file — 28 total) | Mechanical 1:1 translation |
-| `airavata-api/src/main/proto/org/apache/airavata/model/security/security.proto` | Proto mirror of security_model.thrift |
-| `airavata-api/src/main/proto/org/apache/airavata/model/error/errors.proto` | Proto mirror of airavata_errors.thrift |
-| `airavata-api/src/main/proto/org/apache/airavata/model/commons/commons.proto` | Proto mirror of airavata_commons.thrift |
-
-### Modified files
-
-| File | Change |
-|------|--------|
-| `pom.xml` (root) | Add `airavata-thrift-server` module before `airavata-api` in module order |
-| `airavata-api/pom.xml` | Update thrift compiler path, add protobuf-java + protobuf-maven-plugin, add MapStruct deps |
-
-### Deleted files (from airavata-api)
-
-| File | Reason |
-|------|--------|
-| `airavata-api/src/main/java/org/apache/airavata/api/server/AiravataServer.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/api/server/util/Constants.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/UserProfileServiceHandler.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/TenantProfileServiceHandler.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/IamAdminServicesHandler.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/GroupManagerServiceHandler.java` | Moved |
-| `airavata-api/src/main/java/org/apache/airavata/orchestrator/server/OrchestratorServerHandler.java` | Moved |
-| `thrift-interface-descriptions/` | Moved to airavata-thrift-server/src/main/thrift/ |
-
----
-
-### Task 1: Create git worktree
-
-**Files:**
-- None (git operation)
-
-- [ ] **Step 1: Create worktree branch**
-
-```bash
-cd /Users/yasith/code/artisan/airavata
-git worktree add ../airavata-thrift-extraction -b feat/thrift-server-extraction
-```
-
-- [ ] **Step 2: Verify worktree**
-
-```bash
-cd /Users/yasith/code/artisan/airavata-thrift-extraction
-git branch --show-current
-```
-
-Expected: `feat/thrift-server-extraction`
-
-All subsequent tasks run inside `/Users/yasith/code/artisan/airavata-thrift-extraction/`.
-
----
-
-### Task 2: Create airavata-thrift-server Maven module
-
-**Files:**
-- Create: `airavata-thrift-server/pom.xml`
-- Modify: `pom.xml` (root)
-
-- [ ] **Step 1: Create module directory**
-
-```bash
-mkdir -p airavata-thrift-server/src/main/java
-```
-
-- [ ] **Step 2: Write airavata-thrift-server/pom.xml**
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.apache.airavata</groupId>
- <artifactId>airavata</artifactId>
- <version>0.21-SNAPSHOT</version>
- <relativePath>../pom.xml</relativePath>
- </parent>
-
- <artifactId>airavata-thrift-server</artifactId>
- <packaging>jar</packaging>
- <name>Airavata Thrift Server</name>
-
- <dependencies>
- <dependency>
- <groupId>org.apache.airavata</groupId>
- <artifactId>airavata-api</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.thrift</groupId>
- <artifactId>libthrift</artifactId>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </dependency>
- </dependencies>
-</project>
-```
-
-- [ ] **Step 3: Add module to root pom.xml**
-
-In `pom.xml`, add `airavata-thrift-server` AFTER `airavata-api` in the `<modules>` block (it depends on airavata-api):
-
-```xml
- <modules>
- <module>airavata-api</module>
- <module>airavata-thrift-server</module>
- <module>modules/file-server</module>
- <module>modules/agent-framework/agent-service</module>
- <module>modules/research-framework/research-service</module>
- <module>modules/restproxy</module>
- <module>modules/ide-integration</module>
- </modules>
-```
-
-- [ ] **Step 4: Verify module resolves**
-
-```bash
-mvn validate -pl airavata-thrift-server -am --quiet
-```
-
-Expected: BUILD SUCCESS (no compilation yet, just POM validation)
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add airavata-thrift-server/pom.xml pom.xml
-git commit -m "feat: add airavata-thrift-server Maven module scaffold"
-```
-
----
-
-### Task 3: Move thrift IDL files
-
-**Files:**
-- Move: `thrift-interface-descriptions/` → `airavata-thrift-server/src/main/thrift/`
-- Modify: `airavata-api/pom.xml` (update thrift compiler path)
-
-- [ ] **Step 1: Move the directory**
-
-```bash
-git mv thrift-interface-descriptions airavata-thrift-server/src/main/thrift
-```
-
-- [ ] **Step 2: Update thrift compiler path in airavata-api/pom.xml**
-
-In `airavata-api/pom.xml`, find the `maven-antrun-plugin` execution `generate-thrift-sources` and change the thrift input path from:
-
-```xml
-<arg value="${project.basedir}/../thrift-interface-descriptions/stubs_java.thrift" />
-```
-
-to:
-
-```xml
-<arg value="${project.basedir}/../airavata-thrift-server/src/main/thrift/stubs_java.thrift" />
-```
-
-- [ ] **Step 3: Verify thrift compilation still works**
-
-```bash
-mvn generate-sources -pl airavata-api --quiet
-ls airavata-api/target/generated-sources/thrift/org/apache/airavata/api/Airavata.java
-```
-
-Expected: file exists (thrift compiler found the moved IDL and generated code)
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add -A
-git commit -m "refactor: move thrift IDL to airavata-thrift-server/src/main/thrift"
-```
-
----
-
-### Task 4: Move server bootstrap to airavata-thrift-server
-
-**Files:**
-- Move: `airavata-api/src/main/java/org/apache/airavata/api/server/AiravataServer.java` → `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java`
-- Move: `airavata-api/src/main/java/org/apache/airavata/api/server/util/Constants.java` → `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/util/Constants.java`
-
-- [ ] **Step 1: Create target directories**
-
-```bash
-mkdir -p airavata-thrift-server/src/main/java/org/apache/airavata/api/server/util
-```
-
-- [ ] **Step 2: Move AiravataServer.java**
-
-```bash
-git mv airavata-api/src/main/java/org/apache/airavata/api/server/AiravataServer.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/api/server/AiravataServer.java
-```
-
-- [ ] **Step 3: Move Constants.java**
-
-```bash
-git mv airavata-api/src/main/java/org/apache/airavata/api/server/util/Constants.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/api/server/util/Constants.java
-```
-
-Note: `Constants.java` contains `API_SERVER_PORT`, `API_SERVER_HOST`, `API_SERVER_MIN_THREADS` — used only by `AiravataServer.java`. The `ThriftAdapter.java` imports `org.apache.airavata.common.utils.Constants` (a different class in commons), NOT this one.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add -A
-git commit -m "refactor: move AiravataServer and Constants to airavata-thrift-server"
-```
-
----
-
-### Task 5: Move AiravataServerHandler and ThriftAdapter
-
-**Files:**
-- Move: `airavata-api/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java` → `airavata-thrift-server/...`
-- Move: `airavata-api/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java` → `airavata-thrift-server/...`
-
-- [ ] **Step 1: Create target directory**
-
-```bash
-mkdir -p airavata-thrift-server/src/main/java/org/apache/airavata/api/server/handler
-```
-
-- [ ] **Step 2: Move both files**
-
-```bash
-git mv airavata-api/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/api/server/handler/AiravataServerHandler.java
-
-git mv airavata-api/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/api/server/handler/ThriftAdapter.java
-```
-
-No import changes needed — packages stay the same, and `airavata-thrift-server` depends on `airavata-api` which provides all service classes and handler classes they reference.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add -A
-git commit -m "refactor: move AiravataServerHandler and ThriftAdapter to airavata-thrift-server"
-```
-
----
-
-### Task 6: Move profile handlers and OrchestratorServerHandler
-
-**Files:**
-- Move: `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/UserProfileServiceHandler.java`
-- Move: `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/TenantProfileServiceHandler.java`
-- Move: `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/IamAdminServicesHandler.java`
-- Move: `airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/GroupManagerServiceHandler.java`
-- Move: `airavata-api/src/main/java/org/apache/airavata/orchestrator/server/OrchestratorServerHandler.java`
-
-- [ ] **Step 1: Create target directories**
-
-```bash
-mkdir -p airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers
-mkdir -p airavata-thrift-server/src/main/java/org/apache/airavata/orchestrator/server
-```
-
-- [ ] **Step 2: Move profile handlers**
-
-```bash
-git mv airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/UserProfileServiceHandler.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/UserProfileServiceHandler.java
-
-git mv airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/TenantProfileServiceHandler.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/TenantProfileServiceHandler.java
-
-git mv airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/IamAdminServicesHandler.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/IamAdminServicesHandler.java
-
-git mv airavata-api/src/main/java/org/apache/airavata/service/profile/handlers/GroupManagerServiceHandler.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/service/profile/handlers/GroupManagerServiceHandler.java
-```
-
-- [ ] **Step 3: Move OrchestratorServerHandler**
-
-```bash
-git mv airavata-api/src/main/java/org/apache/airavata/orchestrator/server/OrchestratorServerHandler.java \
- airavata-thrift-server/src/main/java/org/apache/airavata/orchestrator/server/OrchestratorServerHandler.java
-```
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add -A
-git commit -m "refactor: move profile handlers and OrchestratorServerHandler to airavata-thrift-server"
-```
-
----
-
-### Task 7: Verify full build compiles
-
-**Files:**
-- Possibly modify: `airavata-thrift-server/pom.xml` (if missing deps surface)
-
-- [ ] **Step 1: Build airavata-api (should still compile — only lost handler classes it no longer owns)**
-
-```bash
-mvn compile -pl airavata-api --quiet 2>&1 | tail -5
-```
-
-Expected: BUILD SUCCESS. If any code in `airavata-api` still imports the moved handler classes, identify the files and update imports or remove dead references.
-
-- [ ] **Step 2: Build airavata-thrift-server**
-
-```bash
-mvn compile -pl airavata-thrift-server -am 2>&1 | tail -20
-```
-
-Expected: BUILD SUCCESS. If compilation fails due to missing dependencies, add them to `airavata-thrift-server/pom.xml`. Likely candidates:
-- Profile service CPI constants (thrift generated — already available via airavata-api dependency)
-- Helix/ZooKeeper classes used by OrchestratorServerHandler
-- Messaging classes used by AiravataServerHandler
-- Monitoring/background service classes used by AiravataServer
-
-For each missing dependency, add it to `airavata-thrift-server/pom.xml`:
-
-```xml
-<!-- Add as needed based on compilation errors -->
-<dependency>
- <groupId>org.apache.helix</groupId>
- <artifactId>helix-core</artifactId>
-</dependency>
-<dependency>
- <groupId>io.prometheus</groupId>
- <artifactId>simpleclient</artifactId>
-</dependency>
-<!-- etc. -->
-```
-
-- [ ] **Step 3: Fix any compilation errors**
-
-Iterate: read error → add dependency or fix import → rebuild. Continue until BUILD SUCCESS for both modules.
-
-- [ ] **Step 4: Run tests**
-
-```bash
-mvn test -pl airavata-api --quiet 2>&1 | tail -5
-```
-
-Expected: Tests pass (no behavioral changes, only file moves).
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add -A
-git commit -m "fix: resolve airavata-thrift-server compilation dependencies"
-```
-
----
-
-### Task 8: Create proto files — workspace model (representative example)
-
-**Files:**
-- Create: `airavata-api/src/main/proto/org/apache/airavata/model/workspace/workspace.proto`
-
-This task demonstrates the proto translation pattern. All subsequent proto files follow the same rules.
-
-**Translation rules (thrift → proto):**
-- `namespace java X` → `option java_package = "X"; option java_outer_classname = "XProtos";`
-- `struct` → `message`
-- `enum` → `enum` (add `UNKNOWN = 0` sentinel as proto3 requires 0-value)
-- `required/optional` qualifiers are dropped (proto3 has no required/optional)
-- Field numbers match thrift field IDs
-- `string` → `string`, `i32` → `int32`, `i64` → `int64`, `bool` → `bool`, `double` → `double`, `binary` → `bytes`
-- `list<T>` → `repeated T`
-- `map<K,V>` → `map<K,V>`
-- `set<T>` → `repeated T` (proto has no set; enforce uniqueness in application code)
-- Cross-file references: use `import` and fully qualified message names
-
-- [ ] **Step 1: Create directory**
-
-```bash
-mkdir -p airavata-api/src/main/proto/org/apache/airavata/model/workspace
-```
-
-- [ ] **Step 2: Write workspace.proto**
-
-```protobuf
-syntax = "proto3";
-
-package org.apache.airavata.model.workspace;
-
-option java_package = "org.apache.airavata.model.workspace";
-option java_outer_classname = "WorkspaceProtos";
-option java_multiple_files = true;
-
-import "org/apache/airavata/model/commons/commons.proto";
-
-message Group {
- string group_name = 1;
- string description = 2;
-}
-
-message Project {
- string project_id = 1;
- string owner = 2;
- string gateway_id = 3;
- string name = 4;
- string description = 5;
- int64 creation_time = 6;
- repeated string shared_users = 7;
- repeated string shared_groups = 8;
-}
-
-message User {
- string airavata_internal_user_id = 1;
- string user_name = 2;
- string gateway_id = 3;
- string first_name = 4;
- string last_name = 5;
- string email = 6;
-}
-
-enum GatewayApprovalStatus {
- GATEWAY_APPROVAL_STATUS_UNKNOWN = 0;
- REQUESTED = 1;
- APPROVED = 2;
- ACTIVE = 3;
- DEACTIVATED = 4;
- CANCELLED = 5;
- DENIED = 6;
- CREATED = 7;
- DEPLOYED = 8;
-}
-
-message Gateway {
- string airavata_internal_gateway_id = 1;
- string gateway_id = 2;
- GatewayApprovalStatus gateway_approval_status = 3;
- string gateway_name = 4;
- string domain = 5;
- string email_address = 6;
- string gateway_acronym = 7;
- string gateway_url = 8;
- string gateway_public_abstract = 9;
- string review_proposal_description = 10;
- string gateway_admin_first_name = 11;
- string gateway_admin_last_name = 12;
- string gateway_admin_email = 13;
- string identity_server_user_name = 14;
- string identity_server_password_token = 15;
- string declined_reason = 16;
- string oauth_client_id = 17;
- string oauth_client_secret = 18;
- int64 request_creation_time = 19;
- string requester_username = 20;
-}
-
-message GatewayUsageReportingCommand {
- string gateway_id = 1;
- string compute_resource_id = 2;
- string command = 3;
-}
-
-enum NotificationPriority {
- NOTIFICATION_PRIORITY_UNKNOWN = 0;
- LOW = 1;
- NORMAL = 2;
- HIGH = 3;
-}
-
-message Notification {
- string notification_id = 1;
- string gateway_id = 2;
- string title = 3;
- string notification_message = 4;
- int64 creation_time = 5;
- int64 published_time = 6;
- int64 expiration_time = 7;
- NotificationPriority priority = 8;
-}
-```
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add airavata-api/src/main/proto/
-git commit -m "feat: add workspace proto definitions"
-```
-
----
-
-### Task 9: Create proto files — commons and security
-
-**Files:**
-- Create: `airavata-api/src/main/proto/org/apache/airavata/model/commons/commons.proto`
-- Create: `airavata-api/src/main/proto/org/apache/airavata/model/security/security.proto`
-
-These are foundational protos referenced by many others.
-
-- [ ] **Step 1: Create directories**
-
-```bash
-mkdir -p airavata-api/src/main/proto/org/apache/airavata/model/commons
-mkdir -p airavata-api/src/main/proto/org/apache/airavata/model/security
-```
-
-- [ ] **Step 2: Write commons.proto**
-
-Read `airavata-thrift-server/src/main/thrift/airavata-apis/airavata_commons.thrift` to get the exact constants and types. Translate to:
-
-```protobuf
-syntax = "proto3";
-
-package org.apache.airavata.model.commons;
-
-option java_package = "org.apache.airavata.model.commons";
-option java_outer_classname = "CommonsProtos";
-option java_multiple_files = true;
-
-// DEFAULT_ID is a compile-time constant in thrift; in proto it becomes a convention.
-// Consumers should use "DO_NOT_SET_AT_CLIENTS" as the default for ID fields.
-
-message ValidationResults {
- bool validation_state = 1;
- repeated ValidationResult validation_result_list = 2;
-}
-
-message ValidationResult {
- bool result = 1;
- string error_details = 2;
-}
-```
-
-- [ ] **Step 3: Write security.proto**
-
-Read `airavata-thrift-server/src/main/thrift/airavata-apis/security_model.thrift` and translate:
-
-```protobuf
-syntax = "proto3";
-
-package org.apache.airavata.model.security;
-
-option java_package = "org.apache.airavata.model.security";
-option java_outer_classname = "SecurityProtos";
-option java_multiple_files = true;
-
-message AuthzToken {
- string access_token = 1;
- map<string, string> claims_map = 2;
-}
-```
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add airavata-api/src/main/proto/
-git commit -m "feat: add commons and security proto definitions"
-```
-
----
-
-### Task 10: Create proto files — status and experiment models
-
-**Files:**
-- Create: `airavata-api/src/main/proto/org/apache/airavata/model/status/status.proto`
-- Create: `airavata-api/src/main/proto/org/apache/airavata/model/experiment/experiment.proto`
-
-- [ ] **Step 1: Create directories**
-
-```bash
-mkdir -p airavata-api/src/main/proto/org/apache/airavata/model/status
-mkdir -p airavata-api/src/main/proto/org/apache/airavata/model/experiment
-```
-
-- [ ] **Step 2: Write status.proto**
-
-Read `airavata-thrift-server/src/main/thrift/data-models/status_models.thrift` and translate all enums/structs. Example structure:
-
-```protobuf
-syntax = "proto3";
-
-package org.apache.airavata.model.status;
-
-option java_package = "org.apache.airavata.model.status";
-option java_outer_classname = "StatusProtos";
-option java_multiple_files = true;
-
-enum ExperimentState {
- EXPERIMENT_STATE_UNKNOWN = 0;
- CREATED = 1;
- VALIDATED = 2;
- SCHEDULED = 3;
- LAUNCHED = 4;
- EXECUTING = 5;
- CANCELING = 6;
- CANCELED = 7;
- COMPLETED = 8;
- FAILED = 9;
-}
-
-message ExperimentStatus {
- ExperimentState state = 1;
- int64 time_of_state_change = 2;
- string reason = 3;
- int64 status_id = 4;
-}
-
-enum JobState {
- JOB_STATE_UNKNOWN = 0;
- SUBMITTED = 1;
- QUEUED = 2;
- ACTIVE = 3;
- COMPLETE = 4;
- CANCELED = 5;
- FAILED = 6;
- SUSPENDED = 7;
- NON_CRITICAL_FAIL = 8;
-}
-
-message JobStatus {
- JobState job_state = 1;
- int64 time_of_state_change = 2;
- string reason = 3;
- int64 status_id = 4;
-}
-
-enum ProcessState {
- PROCESS_STATE_UNKNOWN = 0;
- CREATED = 1;
- VALIDATED = 2;
- STARTED = 3;
- PRE_PROCESSING = 4;
- CONFIGURING_WORKSPACE = 5;
- INPUT_DATA_STAGING = 6;
- EXECUTING = 7;
- MONITORING = 8;
- OUTPUT_DATA_STAGING = 9;
- POST_PROCESSING = 10;
- COMPLETED = 11;
- FAILED = 12;
- CANCELLING = 13;
- CANCELED = 14;
-}
-
-message ProcessStatus {
- ProcessState state = 1;
- int64 time_of_state_change = 2;
- string reason = 3;
- int64 status_id = 4;
-}
-
-enum TaskState {
- TASK_STATE_UNKNOWN = 0;
- CREATED = 1;
- EXECUTING = 2;
- COMPLETED = 3;
- FAILED = 4;
- CANCELED = 5;
-}
-
-message TaskStatus {
- TaskState state = 1;
- int64 time_of_state_change = 2;
- string reason = 3;
- int64 status_id = 4;
-}
-
-message QueueStatusModel {
- string host_name = 1;
- string queue_name = 2;
- bool queue_up = 3;
- int32 running_jobs = 4;
- int32 queued_jobs = 5;
- int64 time = 6;
-}
-```
-
-- [ ] **Step 3: Write experiment.proto**
-
-Read `airavata-thrift-server/src/main/thrift/data-models/experiment_model.thrift` and translate. Use imports for cross-references:
-
-```protobuf
-syntax = "proto3";
-
-package org.apache.airavata.model.experiment;
-
-option java_package = "org.apache.airavata.model.experiment";
-option java_outer_classname = "ExperimentProtos";
-option java_multiple_files = true;
-
-import "org/apache/airavata/model/commons/commons.proto";
-import "org/apache/airavata/model/status/status.proto";
-// import other model protos as needed for cross-references
-
-enum ExperimentType {
- EXPERIMENT_TYPE_UNKNOWN = 0;
- SINGLE_APPLICATION = 1;
- WORKFLOW = 2;
-}
-
-enum ExperimentSearchFields {
- EXPERIMENT_SEARCH_FIELDS_UNKNOWN = 0;
- EXPERIMENT_NAME = 1;
- EXPERIMENT_DESC = 2;
- APPLICATION_ID = 3;
- FROM_DATE = 4;
- TO_DATE = 5;
- STATUS = 6;
- PROJECT_ID = 7;
- USER_NAME = 8;
- JOB_ID = 9;
-}
-
-enum ProjectSearchFields {
- PROJECT_SEARCH_FIELDS_UNKNOWN = 0;
- PROJECT_NAME = 1;
- PROJECT_DESCRIPTION = 2;
-}
-
-// Continue translating all structs from experiment_model.thrift:
-// UserConfigurationDataModel, ExperimentModel, ExperimentSummaryModel, ExperimentStatistics
-// Follow the same field-number and naming conventions as workspace.proto
-```
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add airavata-api/src/main/proto/
-git commit -m "feat: add status and experiment proto definitions"
-```
-
----
-
-### Task 11: Create remaining proto files
-
-**Files:**
-- Create: one `.proto` file per thrift data-model file (approximately 25 more)
-
-This is mechanical translation work. For each thrift file in `airavata-thrift-server/src/main/thrift/data-models/`, create a corresponding proto file following the exact pattern from Tasks 8-10.
-
-- [ ] **Step 1: Create all remaining proto files**
-
-Complete checklist of thrift files to translate (check off each):
-
-| Thrift File | Proto Output Path |
-|-------------|------------------|
-| `account_provisioning_model.thrift` | `org/apache/airavata/model/appcatalog/accountprovisioning/account_provisioning.proto` |
-| `application_deployment_model.thrift` | `org/apache/airavata/model/appcatalog/appdeployment/app_deployment.proto` |
-| `application_interface_model.thrift` | `org/apache/airavata/model/appcatalog/appinterface/app_interface.proto` |
-| `application_io_models.thrift` | `org/apache/airavata/model/application/io/application_io.proto` |
-| `compute_resource_model.thrift` | `org/apache/airavata/model/appcatalog/computeresource/compute_resource.proto` |
-| `credential_store_models.thrift` | `org/apache/airavata/model/credential/store/credential_store.proto` |
-| `data_movement_models.thrift` | `org/apache/airavata/model/data/movement/data_movement.proto` |
-| `gateway_groups_model.thrift` | `org/apache/airavata/model/appcatalog/gatewaygroups/gateway_groups.proto` |
-| `gateway_resource_profile_model.thrift` | `org/apache/airavata/model/appcatalog/gatewayprofile/gateway_profile.proto` |
-| `group_manager_model.thrift` | `org/apache/airavata/model/group/group_manager.proto` |
-| `group_resource_profile_model.thrift` | `org/apache/airavata/model/appcatalog/groupresourceprofile/group_resource_profile.proto` |
-| `job_model.thrift` | `org/apache/airavata/model/job/job.proto` |
-| `parallelism_model.thrift` | `org/apache/airavata/model/parallelism/parallelism.proto` |
-| `parser_model.thrift` | `org/apache/airavata/model/appcatalog/parser/parser.proto` |
-| `process_model.thrift` | `org/apache/airavata/model/process/process.proto` |
-| `replica_catalog_models.thrift` | `org/apache/airavata/model/data/replica/replica_catalog.proto` |
-| `scheduling_model.thrift` | `org/apache/airavata/model/scheduling/scheduling.proto` |
-| `sharing_models.thrift` | `org/apache/airavata/model/sharing/sharing.proto` |
-| `storage_resource_model.thrift` | `org/apache/airavata/model/appcatalog/storageresource/storage_resource.proto` |
-| `task_model.thrift` | `org/apache/airavata/model/task/task.proto` |
-| `tenant_profile_model.thrift` | `org/apache/airavata/model/tenant/tenant_profile.proto` |
-| `user_profile_model.thrift` | `org/apache/airavata/model/user/user_profile.proto` |
-| `user_resource_profile_model.thrift` | `org/apache/airavata/model/appcatalog/userresourceprofile/user_resource_profile.proto` |
-| `workflow_data_model.thrift` | `org/apache/airavata/model/workflow/data/workflow_data.proto` |
-| `workflow_model.thrift` | `org/apache/airavata/model/workflow/workflow.proto` |
-
-Also translate API-level thrift files:
-
-| Thrift File | Proto Output Path |
-|-------------|------------------|
-| `airavata_errors.thrift` | `org/apache/airavata/model/error/errors.proto` |
-| `db_event_model.thrift` | `org/apache/airavata/model/dbevent/db_event.proto` |
-| `messaging_events.thrift` | `org/apache/airavata/model/messaging/messaging_events.proto` |
-
-For each file:
-1. Read the thrift file
-2. Create the proto file following translation rules from Task 8
-3. Match field numbers to thrift field IDs
-4. Add `UNKNOWN = 0` sentinel to every enum
-5. Use `import` for cross-file references
-6. Set `java_package` matching the thrift `namespace java`
-
-- [ ] **Step 2: Commit**
-
-```bash
-git add airavata-api/src/main/proto/
-git commit -m "feat: add all remaining proto definitions mirroring thrift IDL"
-```
-
----
-
-### Task 12: Set up protoc compilation in airavata-api
-
-**Files:**
-- Modify: `airavata-api/pom.xml`
-
-- [ ] **Step 1: Add protobuf-java dependency**
-
-In `airavata-api/pom.xml` `<dependencies>` section, add:
-
-```xml
- <dependency>
- <groupId>com.google.protobuf</groupId>
- <artifactId>protobuf-java</artifactId>
- </dependency>
-```
-
-- [ ] **Step 2: Add protobuf-maven-plugin**
-
-In `airavata-api/pom.xml` `<build><plugins>` section, add the protobuf-maven-plugin (already in root POM's pluginManagement):
-
-```xml
- <plugin>
- <groupId>io.github.ascopes</groupId>
- <artifactId>protobuf-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>generate-proto-sources</id>
- <goals>
- <goal>generate</goal>
- </goals>
- <configuration>
- <sourceDirectories>
- <sourceDirectory>${project.basedir}/src/main/proto</sourceDirectory>
- </sourceDirectories>
- </configuration>
- </execution>
- </executions>
- </plugin>
-```
-
-- [ ] **Step 3: Verify proto compilation**
-
-```bash
-mvn generate-sources -pl airavata-api --quiet 2>&1 | tail -10
-ls airavata-api/target/generated-sources/protobuf/java/org/apache/airavata/model/workspace/
-```
-
-Expected: generated Java files for the proto messages (Project.java, Gateway.java, etc.)
-
-- [ ] **Step 4: Full compile**
-
-```bash
-mvn compile -pl airavata-api --quiet 2>&1 | tail -5
-```
-
-Expected: BUILD SUCCESS — both thrift generated code and proto generated code compile.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add airavata-api/pom.xml
-git commit -m "build: add protobuf-java dependency and protoc compilation to airavata-api"
-```
-
----
-
-### Task 13: Add MapStruct to airavata-thrift-server
-
-**Files:**
-- Modify: `airavata-thrift-server/pom.xml`
-- Modify: `pom.xml` (root — add MapStruct to dependency management)
-
-- [ ] **Step 1: Add MapStruct to root pom.xml dependency management**
-
-In `pom.xml` root, add to `<dependencyManagement><dependencies>`:
-
-```xml
- <dependency>
- <groupId>org.mapstruct</groupId>
- <artifactId>mapstruct</artifactId>
- <version>1.6.3</version>
- </dependency>
- <dependency>
- <groupId>org.mapstruct</groupId>
- <artifactId>mapstruct-processor</artifactId>
- <version>1.6.3</version>
- </dependency>
-```
-
-- [ ] **Step 2: Add MapStruct dependency to airavata-thrift-server/pom.xml**
-
-Add to `<dependencies>`:
-
-```xml
- <dependency>
- <groupId>org.mapstruct</groupId>
- <artifactId>mapstruct</artifactId>
- </dependency>
-```
-
-Add to `<build><plugins>`:
-
-```xml
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <annotationProcessorPaths>
- <path>
- <groupId>org.mapstruct</groupId>
- <artifactId>mapstruct-processor</artifactId>
- <version>1.6.3</version>
- </path>
- </annotationProcessorPaths>
- </configuration>
- </plugin>
- </plugins>
- </build>
-```
-
-- [ ] **Step 3: Verify build**
-
-```bash
-mvn compile -pl airavata-thrift-server -am --quiet 2>&1 | tail -5
-```
-
-Expected: BUILD SUCCESS
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add pom.xml airavata-thrift-server/pom.xml
-git commit -m "build: add MapStruct dependency to airavata-thrift-server"
-```
-
----
-
-### Task 14: Create representative MapStruct mapper
-
-**Files:**
-- Create: `airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper/WorkspaceMapper.java`
-
-This establishes the mapper pattern. Full mapper coverage is built incrementally as services are converted to proto types (follow-up work).
-
-- [ ] **Step 1: Create mapper directory**
-
-```bash
-mkdir -p airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper
-```
-
-- [ ] **Step 2: Write WorkspaceMapper.java**
-
-```java
-package org.apache.airavata.api.server.mapper;
-
-import org.apache.airavata.model.workspace.Gateway;
-import org.apache.airavata.model.workspace.Notification;
-import org.apache.airavata.model.workspace.Project;
-import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.factory.Mappers;
-
-/**
- * Maps between thrift-generated workspace types and proto-generated workspace types.
- *
- * <p>Thrift types use camelCase field accessors (getGatewayId()).
- * Proto types use snake_case with standard proto getters (getGatewayId()).
- * MapStruct handles the mapping automatically when accessor names match.
- */
-@Mapper
-public interface WorkspaceMapper {
- WorkspaceMapper INSTANCE = Mappers.getMapper(WorkspaceMapper.class);
-
- // --- Project ---
- org.apache.airavata.model.workspace.ProjectProtos.Project toProto(Project thrift);
- Project toThrift(org.apache.airavata.model.workspace.ProjectProtos.Project proto);
-
- // --- Gateway ---
- org.apache.airavata.model.workspace.WorkspaceProtos.Gateway toProtoGateway(Gateway thrift);
- Gateway toThriftGateway(org.apache.airavata.model.workspace.WorkspaceProtos.Gateway proto);
-}
-```
-
-Note: The exact proto class names depend on how protoc generates them (based on `java_multiple_files`, `java_outer_classname`). Adjust class references after verifying proto compilation output from Task 12.
-
-- [ ] **Step 3: Verify compilation**
-
-```bash
-mvn compile -pl airavata-thrift-server -am --quiet 2>&1 | tail -10
-```
-
-If proto class names don't match, check `airavata-api/target/generated-sources/` for exact class names and update mapper imports.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add airavata-thrift-server/src/main/java/org/apache/airavata/api/server/mapper/
-git commit -m "feat: add representative MapStruct mapper for workspace types"
-```
-
----
-
-### Task 15: Final build verification and cleanup
-
-**Files:**
-- None (verification only)
-
-- [ ] **Step 1: Full project build**
-
-```bash
-mvn clean compile -T4 2>&1 | tail -20
-```
-
-Expected: BUILD SUCCESS for all modules
-
-- [ ] **Step 2: Run tests**
-
-```bash
-mvn test -pl airavata-api,airavata-thrift-server 2>&1 | tail -20
-```
-
-Expected: All tests pass
-
-- [ ] **Step 3: Verify no remaining handler references in airavata-api**
-
-```bash
-grep -r "AiravataServer\b" airavata-api/src/main/java/ --include="*.java" | grep -v "import"
-grep -r "AiravataServerHandler\b" airavata-api/src/main/java/ --include="*.java" | grep -v "import"
-```
-
-Expected: No matches (these classes have moved). If there are stale references, remove them.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add -A
-git commit -m "chore: final cleanup after thrift server extraction"
-```
-
----
-
-## Deferred Work (Future Plans)
-
-These items are explicitly out of scope for this plan but are required for full thrift removal from `airavata-api`:
-
-1. **Extract data access from Registry/Sharing/Credential handlers** — Create non-thrift interfaces in `airavata-api`, refactor services to use them, move handler implementations to `airavata-thrift-server`
-2. **Convert service signatures to proto types** — Change all 14 service classes to accept/return proto types instead of thrift types
-3. **Move thrift code generation to airavata-thrift-server** — Once `airavata-api` no longer imports any thrift types
-4. **Remove libthrift dependency from airavata-api** — Final step after all thrift references are eliminated
-5. **Full MapStruct mapper coverage** — One mapper per domain (experiment, credential, sharing, etc.)
diff --git a/docs/superpowers/specs/2026-03-26-airavata-thrift-server-extraction-design.md b/docs/superpowers/specs/2026-03-26-airavata-thrift-server-extraction-design.md
deleted file mode 100644
index c792b0596e..0000000000
--- a/docs/superpowers/specs/2026-03-26-airavata-thrift-server-extraction-design.md
+++ /dev/null
@@ -1,190 +0,0 @@
-# Airavata Thrift Server Extraction Design
-
-**Date:** 2026-03-26
-**Status:** Draft
-
-## Goal
-
-Extract all thrift server concerns from `airavata-api` into a new `airavata-thrift-server` Maven module. Introduce protobuf DTOs as the canonical data types in the service layer, making `airavata-api` completely thrift-free. MapStruct mappers in `airavata-thrift-server` bridge proto and thrift types at the handler boundary.
-
-## Repository Layout (Post-Extraction)
-
-```
-airavata/
-├── airavata-thrift-server/ # NEW — all thrift concerns
-│ ├── pom.xml
-│ └── src/main/
-│ ├── java/
-│ │ └── org/apache/airavata/
-│ │ ├── server/ # AiravataServer.java (TMultiplexedProcessor bootstrap)
-│ │ ├── handler/ # All 9 thrift handlers
-│ │ └── mapper/ # MapStruct mappers (proto <-> thrift)
-│ └── thrift/ # Moved from thrift-interface-descriptions/
-│ ├── airavata-apis/
-│ ├── base-api/
-│ ├── data-models/
-│ ├── service-cpis/
-│ └── stubs_java.thrift (etc.)
-│
-├── airavata-api/ # SLIMMED — services + background processes
-│ ├── pom.xml
-│ └── src/main/
-│ ├── java/
-│ │ └── org/apache/airavata/
-│ │ ├── service/ # Business logic services
-│ │ ├── repository/ # JPA data access
-│ │ └── background/ # Helix, monitors, event managers, etc.
-│ └── proto/ # NEW — .proto files mirroring thrift IDL
-│ └── org/apache/airavata/
-│ ├── model/
-│ ├── api/
-│ ├── registry/
-│ ├── credential/
-│ ├── sharing/
-│ └── ...
-│
-├── modules/ # Unchanged
-└── pom.xml # Root — adds airavata-thrift-server module
-```
-
-## Dependency Graph
-
-```
-airavata-thrift-server
-├── depends on: airavata-api (service layer + proto generated classes)
-├── depends on: libthrift (Apache Thrift runtime)
-├── owns: thrift IDL compilation -> generated thrift Java classes
-├── owns: MapStruct mappers (proto <-> thrift)
-└── owns: TMultiplexedProcessor, TThreadPoolServer bootstrap
-
-airavata-api
-├── depends on: protobuf-java (for generated proto classes)
-├── owns: service layer (o.a.a.service.*)
-├── owns: repository / data access layer
-├── owns: background services (Helix, monitors, event managers)
-├── owns: proto definitions + protoc generation
-└── NO thrift dependency
-```
-
-## Thrift Services Moved (All 9)
-
-| Service Name | Handler Class |
-|---------------------|------------------------------------|
-| Airavata | AiravataServerHandler |
-| RegistryService | RegistryServerHandler |
-| SharingRegistry | SharingRegistryServerHandler |
-| CredentialStore | CredentialStoreServerHandler |
-| UserProfile | UserProfileServiceHandler |
-| TenantProfile | TenantProfileServiceHandler |
-| IamAdminServices | IamAdminServicesHandler |
-| GroupManager | GroupManagerServiceHandler |
-| Orchestrator | OrchestratorServerHandler |
-
-## What Stays in `airavata-api`
-
-### Services (refactored to proto types)
-All 14 business logic services: ExperimentService, ApplicationCatalogService, CredentialService, GatewayService, GatewayResourceProfileService, GroupResourceProfileService, NotificationService, ParserService, ProjectService, ResourceService, UserResourceProfileService, DataProductService, ResourceSharingService, SSHAccountService.
-
-### Data Access
-JPA entities, repositories, Hibernate config, database initialization (ExpCatalogDB, AppCatalogDB, ReplicaCatalogDB, WorkflowCatalogDB, CredentialStoreDB, UserProfileCatalogDB).
-
-### Background Services (retains its own main entry point)
-- DB Event Manager
-- Monitoring Server (Prometheus on port 9097)
-- Cluster Status Monitor
-- Data Interpreter
-- Process Rescheduler
-- Helix Controller/Participant
-- Workflow Managers (Pre/Post/Parser)
-- Job Monitors (Email + Real-time)
-
-### Removed from `airavata-api`
-- `AiravataServer.java`
-- All 9 thrift handler classes
-- Thrift IDL files
-- `libthrift` dependency
-- Thrift code generation Maven plugin config
-
-## Proto File Strategy
-
-Proto files mirror thrift IDL 1:1 — same fields, same nesting, same enums. Mechanical translation, not a redesign.
-
-### Conventions
-- Field numbers match thrift field IDs where possible
-- Proto uses `snake_case` per protobuf convention; MapStruct handles naming differences
-- No proto service definitions — only message types (no gRPC yet)
-- Package structure mirrors thrift: `org.apache.airavata.model.*`, `org.apache.airavata.api.*`, etc.
-- Generated via protoc Maven plugin in `airavata-api`
-
-### Example
-
-Thrift:
-```thrift
-struct ExperimentModel {
- 1: string experimentId,
- 2: string projectId,
- 3: string gatewayId,
- 4: ExperimentType experimentType,
- 5: string userName,
- 6: string experimentName,
- 7: i64 creationTime
-}
-```
-
-Proto:
-```protobuf
-message ExperimentModel {
- string experiment_id = 1;
- string project_id = 2;
- string gateway_id = 3;
- ExperimentType experiment_type = 4;
- string user_name = 5;
- string experiment_name = 6;
- int64 creation_time = 7;
-}
-```
-
-## Handler Conversion Pattern
-
-```
-Thrift Client
- -> TMultiplexedProcessor (airavata-thrift-server)
- -> ThriftHandler.method(ThriftRequest)
- -> MapStruct: ThriftRequest -> ProtoDTO
- -> Service.method(ProtoDTO) (airavata-api)
- -> returns ProtoResult
- -> MapStruct: ProtoResult -> ThriftResponse
- -> returns ThriftResponse
-```
-
-### Example — createExperiment
-
-```java
-// In airavata-thrift-server: AiravataServerHandler
-public String createExperiment(AuthzToken authzToken, String gatewayId, ExperimentModel experiment) {
- var protoExperiment = ExperimentMapper.INSTANCE.toProto(experiment);
- var protoAuthz = AuthzTokenMapper.INSTANCE.toProto(authzToken);
- var result = experimentService.createExperiment(protoAuthz, gatewayId, protoExperiment);
- return result;
-}
-```
-
-### MapStruct Mapper
-
-```java
-@Mapper
-public interface ExperimentMapper {
- ExperimentMapper INSTANCE = Mappers.getMapper(ExperimentMapper.class);
- ExperimentProto toProto(ExperimentModel thriftModel);
- ExperimentModel toThrift(ExperimentProto proto);
-}
-```
-
-## Build Order
-
-1. `airavata-api` builds first — compiles proto files, produces service classes + proto generated code
-2. `airavata-thrift-server` builds second — compiles thrift IDL, compiles handlers + mappers against airavata-api
-
-## Development
-
-Work is done in a git worktree off the main `airavata` repo for isolation. Thrift IDL content is unchanged (no SDK breakage). Thrift spec changes are a separate future effort.
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
new file mode 100644
index 0000000000..df6504700d
--- /dev/null
+++ b/integration-tests/pom.xml
@@ -0,0 +1,99 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.airavata</groupId>
+ <artifactId>airavata</artifactId>
+ <version>0.21-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>airavata-integration-tests</artifactId>
+ <packaging>jar</packaging>
+ <name>Airavata Integration Tests</name>
+
+ <dependencies>
+ <!-- Airavata API (thrift clients, model classes) -->
+ <dependency>
+ <groupId>org.apache.airavata</groupId>
+ <artifactId>airavata-api</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- Thrift runtime -->
+ <dependency>
+ <groupId>org.apache.thrift</groupId>
+ <artifactId>libthrift</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- MariaDB JDBC driver -->
+ <dependency>
+ <groupId>org.mariadb.jdbc</groupId>
+ <artifactId>mariadb-java-client</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- RabbitMQ AMQP client -->
+ <dependency>
+ <groupId>com.rabbitmq</groupId>
+ <artifactId>amqp-client</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- Kafka admin client -->
+ <dependency>
+ <groupId>org.apache.kafka</groupId>
+ <artifactId>kafka-clients</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- JSON parsing for health endpoint responses -->
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- JUnit 5 -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <excludedGroups>integration</excludedGroups>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/BackgroundServiceHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/BackgroundServiceHealthTest.java
new file mode 100644
index 0000000000..e6ad95a15a
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/BackgroundServiceHealthTest.java
@@ -0,0 +1,120 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Verifies that each expected background service is registered and reported
+ * by the {@code /health/services} endpoint.
+ *
+ * <p>The parameterized test asserts that every listed service name appears in
+ * the health response. Services are expected to match the labels used in
+ * {@code AiravataServer#startBackgroundServices()}.
+ */
+@Tag("integration")
+class BackgroundServiceHealthTest {
+
+ private static final String HOST = System.getProperty("airavata.monitoring.host", "localhost");
+ private static final int PORT = Integer.parseInt(System.getProperty("airavata.monitoring.port", "9097"));
+
+ private static final String HEALTH_URL = "http://" + HOST + ":" + PORT + "/health/services";
+
+ private static HttpClient httpClient;
+ private static ObjectMapper objectMapper;
+ private static JsonNode cachedRoot;
+
+ @BeforeAll
+ static void fetchHealthResponse() throws Exception {
+ httpClient =
+ HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+ objectMapper = new ObjectMapper();
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(HEALTH_URL))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+ HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ assertEquals(
+ 200, response.statusCode(), "Could not reach /health/services — got HTTP " + response.statusCode());
+ JsonNode body = objectMapper.readTree(response.body());
+ cachedRoot = body.get("services");
+ assertNotNull(cachedRoot, "Expected 'services' key in /health/services response");
+ }
+
+ static Stream<String> expectedServices() {
+ return Stream.of(
+ "db_event_manager",
+ "monitoring_server",
+ "helix_controller",
+ "helix_participant",
+ "pre_workflow_manager",
+ "post_workflow_manager",
+ "parser_workflow_manager",
+ "realtime_monitor");
+ }
+
+ @ParameterizedTest(name = "{0} is registered")
+ @MethodSource("expectedServices")
+ void serviceShouldBeRegistered(String serviceName) {
+ assertNotNull(cachedRoot, "/health/services response was not fetched");
+ assertTrue(
+ cachedRoot.has(serviceName),
+ "Expected service '" + serviceName + "' to be registered, "
+ + "but it was not found in the health response. "
+ + "Registered services: " + cachedRoot.fieldNames());
+ }
+
+ @ParameterizedTest(name = "{0} is UP with positive uptime")
+ @MethodSource("expectedServices")
+ void registeredServiceShouldBeUp(String serviceName) {
+ assertNotNull(cachedRoot, "/health/services response was not fetched");
+
+ JsonNode serviceNode = cachedRoot.get(serviceName);
+ assertNotNull(serviceNode, "Service '" + serviceName + "' is not in the health response");
+
+ JsonNode statusNode = serviceNode.get("status");
+ assertNotNull(statusNode, "Service '" + serviceName + "' has no 'status' field");
+ assertEquals(
+ "UP", statusNode.asText(), "Service '" + serviceName + "' is not UP — status: " + statusNode.asText());
+
+ JsonNode uptimeNode = serviceNode.get("uptimeMs");
+ assertNotNull(uptimeNode, "Service '" + serviceName + "' has no 'uptimeMs' field");
+ assertTrue(
+ uptimeNode.asLong() > 0,
+ "Service '" + serviceName + "' is UP but uptimeMs is not positive: " + uptimeNode.asLong());
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/DatabaseHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/DatabaseHealthTest.java
new file mode 100644
index 0000000000..23f80a290c
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/DatabaseHealthTest.java
@@ -0,0 +1,79 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Verifies JDBC connectivity to the Airavata database and the presence of
+ * core tables required for experiment and resource management.
+ */
+@Tag("integration")
+class DatabaseHealthTest {
+
+ private static final String HOST = System.getProperty("airavata.db.host", "localhost");
+ private static final int PORT = Integer.parseInt(System.getProperty("airavata.db.port", "13306"));
+ private static final String DATABASE = System.getProperty("airavata.db.name", "airavata");
+ private static final String USER = System.getProperty("airavata.db.user", "airavata");
+ private static final String PASSWORD = System.getProperty("airavata.db.password", "123456");
+
+ private static final String JDBC_URL = "jdbc:mariadb://" + HOST + ":" + PORT + "/" + DATABASE;
+
+ static Stream<String> requiredTables() {
+ return Stream.of(
+ "EXPERIMENT",
+ "APPLICATION_DEPLOYMENT",
+ "COMPUTE_RESOURCE",
+ "GATEWAY",
+ "USER_PROFILE",
+ "SHARING_ENTITY",
+ "CREDENTIAL");
+ }
+
+ @ParameterizedTest(name = "table {0} exists")
+ @MethodSource("requiredTables")
+ void requiredTableShouldExist(String tableName) throws Exception {
+ try (Connection connection = DriverManager.getConnection(JDBC_URL, USER, PASSWORD)) {
+ assertTrue(connection.isValid(5), "Database connection is not valid");
+
+ DatabaseMetaData meta = connection.getMetaData();
+ List<String> found = new ArrayList<>();
+ try (ResultSet rs = meta.getTables(DATABASE, null, tableName, new String[] {"TABLE"})) {
+ while (rs.next()) {
+ found.add(rs.getString("TABLE_NAME"));
+ }
+ }
+ assertTrue(
+ found.stream().anyMatch(t -> t.equalsIgnoreCase(tableName)),
+ "Expected table '" + tableName + "' was not found in database '" + DATABASE + "'");
+ }
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/KafkaHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/KafkaHealthTest.java
new file mode 100644
index 0000000000..a63d4b73b2
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/KafkaHealthTest.java
@@ -0,0 +1,57 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.util.Collection;
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+import org.apache.kafka.clients.admin.AdminClient;
+import org.apache.kafka.clients.admin.AdminClientConfig;
+import org.apache.kafka.clients.admin.DescribeClusterResult;
+import org.apache.kafka.common.Node;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that the Kafka cluster is reachable and has at least one broker node.
+ */
+@Tag("integration")
+class KafkaHealthTest {
+
+ private static final String BOOTSTRAP_SERVERS =
+ System.getProperty("airavata.kafka.bootstrap.servers", "localhost:9092");
+
+ @Test
+ void kafkaClusterShouldHaveAtLeastOneBroker() throws ExecutionException, InterruptedException {
+ Properties config = new Properties();
+ config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
+ config.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, "10000");
+ config.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "10000");
+
+ try (AdminClient adminClient = AdminClient.create(config)) {
+ DescribeClusterResult result = adminClient.describeCluster();
+ Collection<Node> nodes = result.nodes().get();
+
+ assertFalse(nodes.isEmpty(), "Kafka cluster reported no broker nodes");
+ }
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/KeycloakHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/KeycloakHealthTest.java
new file mode 100644
index 0000000000..5e99bb7083
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/KeycloakHealthTest.java
@@ -0,0 +1,132 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that Keycloak is reachable, the OIDC discovery document is valid,
+ * and that a client-credentials token can be obtained for the test client.
+ */
+@Tag("integration")
+class KeycloakHealthTest {
+
+ private static final String BASE_URL = System.getProperty("airavata.keycloak.url", "http://localhost:18080");
+ private static final String REALM = System.getProperty("airavata.keycloak.realm", "default");
+ private static final String CLIENT_ID = System.getProperty("airavata.keycloak.client.id", "cs-jupyterlab");
+ private static final String CLIENT_SECRET =
+ System.getProperty("airavata.keycloak.client.secret", "DxeMtfiWU1qkDEmaGHf13RDahCujzhy1");
+
+ private static final String OIDC_URL = BASE_URL + "/realms/" + REALM + "/.well-known/openid-configuration";
+
+ private static HttpClient httpClient;
+ private static ObjectMapper objectMapper;
+
+ @BeforeAll
+ static void setUp() {
+ httpClient =
+ HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ @Test
+ void oidcDiscoveryShouldReturn200WithTokenEndpoint() throws Exception {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(OIDC_URL))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+
+ HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(
+ 200,
+ response.statusCode(),
+ "Expected HTTP 200 from OIDC discovery endpoint, got " + response.statusCode());
+
+ JsonNode doc = objectMapper.readTree(response.body());
+ JsonNode tokenEndpoint = doc.get("token_endpoint");
+ assertNotNull(tokenEndpoint, "OIDC discovery document is missing 'token_endpoint'");
+ assertTrue(
+ tokenEndpoint.asText().startsWith("http"),
+ "token_endpoint is not a valid URL: " + tokenEndpoint.asText());
+ }
+
+ @Test
+ void clientCredentialsGrantShouldReturnAccessToken() throws Exception {
+ // First retrieve the token endpoint from the discovery document
+ HttpRequest discoveryRequest = HttpRequest.newBuilder()
+ .uri(URI.create(OIDC_URL))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+ HttpResponse<String> discoveryResponse =
+ httpClient.send(discoveryRequest, HttpResponse.BodyHandlers.ofString());
+ assertEquals(200, discoveryResponse.statusCode(), "Could not reach Keycloak OIDC discovery");
+
+ String tokenEndpoint = objectMapper
+ .readTree(discoveryResponse.body())
+ .get("token_endpoint")
+ .asText();
+
+ String form = "grant_type=" + encode("client_credentials")
+ + "&client_id=" + encode(CLIENT_ID)
+ + "&client_secret=" + encode(CLIENT_SECRET);
+
+ HttpRequest tokenRequest = HttpRequest.newBuilder()
+ .uri(URI.create(tokenEndpoint))
+ .timeout(Duration.ofSeconds(10))
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString(form))
+ .build();
+
+ HttpResponse<String> tokenResponse = httpClient.send(tokenRequest, HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(
+ 200,
+ tokenResponse.statusCode(),
+ "client_credentials grant failed with status " + tokenResponse.statusCode() + ": "
+ + tokenResponse.body());
+
+ JsonNode tokenDoc = objectMapper.readTree(tokenResponse.body());
+ JsonNode accessToken = tokenDoc.get("access_token");
+ assertNotNull(accessToken, "Token response is missing 'access_token'");
+ assertTrue(accessToken.asText().length() > 0, "access_token is empty");
+ }
+
+ private static String encode(String value) {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8);
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/PrometheusHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/PrometheusHealthTest.java
new file mode 100644
index 0000000000..2051b81439
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/PrometheusHealthTest.java
@@ -0,0 +1,84 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that the Prometheus metrics endpoint served by {@code MonitoringServer}
+ * is reachable and returns valid Prometheus text format output.
+ */
+@Tag("integration")
+class PrometheusHealthTest {
+
+ private static final String HOST = System.getProperty("airavata.monitoring.host", "localhost");
+ private static final int PORT = Integer.parseInt(System.getProperty("airavata.monitoring.port", "9097"));
+
+ private static final String METRICS_URL = "http://" + HOST + ":" + PORT + "/metrics";
+
+ private static HttpClient httpClient;
+
+ @BeforeAll
+ static void setUp() {
+ httpClient =
+ HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+ }
+
+ @Test
+ void metricsEndpointShouldReturn200() throws Exception {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(METRICS_URL))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+
+ HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(
+ 200, response.statusCode(), "Expected HTTP 200 from /metrics endpoint, got " + response.statusCode());
+ }
+
+ @Test
+ void metricsEndpointShouldReturnPrometheusTextFormat() throws Exception {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(METRICS_URL))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+
+ HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ String body = response.body();
+
+ assertTrue(
+ body.contains("# HELP") || body.contains("# TYPE"),
+ "Expected Prometheus text format with '# HELP' or '# TYPE' markers, but body was: "
+ + body.substring(0, Math.min(body.length(), 500)));
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/RabbitMQHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/RabbitMQHealthTest.java
new file mode 100644
index 0000000000..0cdbc56b54
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/RabbitMQHealthTest.java
@@ -0,0 +1,66 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.Connection;
+import com.rabbitmq.client.ConnectionFactory;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Verifies AMQP connectivity to RabbitMQ and that the core Airavata exchanges
+ * are already declared (passive declare — does not create them).
+ */
+@Tag("integration")
+class RabbitMQHealthTest {
+
+ private static final String HOST = System.getProperty("airavata.rabbitmq.host", "localhost");
+ private static final int PORT = Integer.parseInt(System.getProperty("airavata.rabbitmq.port", "5672"));
+ private static final String USERNAME = System.getProperty("airavata.rabbitmq.username", "airavata");
+ private static final String PASSWORD = System.getProperty("airavata.rabbitmq.password", "airavata");
+ private static final String VHOST = System.getProperty("airavata.rabbitmq.vhost", "/");
+
+ static Stream<String> exchanges() {
+ return Stream.of("experiment_exchange", "process_exchange", "status_exchange");
+ }
+
+ @ParameterizedTest(name = "exchange {0} is declared")
+ @MethodSource("exchanges")
+ void exchangeShouldBeDeclared(String exchangeName) throws Exception {
+ ConnectionFactory factory = new ConnectionFactory();
+ factory.setHost(HOST);
+ factory.setPort(PORT);
+ factory.setUsername(USERNAME);
+ factory.setPassword(PASSWORD);
+ factory.setVirtualHost(VHOST);
+
+ try (Connection connection = factory.newConnection();
+ Channel channel = connection.createChannel()) {
+ assertDoesNotThrow(
+ () -> channel.exchangeDeclarePassive(exchangeName),
+ "Exchange '" + exchangeName + "' is not declared in RabbitMQ");
+ }
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/ServiceHealthEndpointTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/ServiceHealthEndpointTest.java
new file mode 100644
index 0000000000..886627a6c5
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/ServiceHealthEndpointTest.java
@@ -0,0 +1,122 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Iterator;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies the {@code /health/services} endpoint on the MonitoringServer.
+ *
+ * <p>The endpoint returns a JSON object whose keys are service names and values are
+ * {@code ServiceStatus} objects with fields: {@code status} (UP/DOWN),
+ * {@code uptimeMs} (long), and {@code lastError} (string or null).
+ *
+ * <p>Every service that is UP must report a positive uptimeMs.
+ */
+@Tag("integration")
+class ServiceHealthEndpointTest {
+
+ private static final String HOST = System.getProperty("airavata.monitoring.host", "localhost");
+ private static final int PORT = Integer.parseInt(System.getProperty("airavata.monitoring.port", "9097"));
+
+ private static final String HEALTH_URL = "http://" + HOST + ":" + PORT + "/health/services";
+
+ private static HttpClient httpClient;
+ private static ObjectMapper objectMapper;
+
+ @BeforeAll
+ static void setUp() {
+ httpClient =
+ HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ @Test
+ void healthEndpointShouldReturn200() throws Exception {
+ HttpResponse<String> response = getServicesResponse();
+ assertEquals(
+ 200, response.statusCode(), "Expected HTTP 200 from /health/services, got " + response.statusCode());
+ }
+
+ @Test
+ void healthResponseShouldBeNonEmptyJsonObject() throws Exception {
+ HttpResponse<String> response = getServicesResponse();
+ assertEquals(200, response.statusCode());
+
+ JsonNode root = objectMapper.readTree(response.body());
+ assertNotNull(root, "Response body could not be parsed as JSON");
+ assertTrue(root.isObject(), "Expected JSON object at root, got: " + root.getNodeType());
+ JsonNode services = root.get("services");
+ assertNotNull(services, "Expected 'services' key in response, got: " + root);
+ assertFalse(services.isEmpty(), "Expected at least one service entry in the response");
+ }
+
+ @Test
+ void allUpServicesShouldHavePositiveUptime() throws Exception {
+ HttpResponse<String> response = getServicesResponse();
+ assertEquals(200, response.statusCode());
+
+ JsonNode root = objectMapper.readTree(response.body());
+ JsonNode services = root.get("services");
+ assertNotNull(services, "Expected 'services' key in response");
+ Iterator<Map.Entry<String, JsonNode>> fields = services.fields();
+ while (fields.hasNext()) {
+ Map.Entry<String, JsonNode> entry = fields.next();
+ String serviceName = entry.getKey();
+ JsonNode serviceNode = entry.getValue();
+
+ JsonNode statusNode = serviceNode.get("status");
+ assertNotNull(statusNode, "Service '" + serviceName + "' is missing 'status' field");
+
+ if ("UP".equals(statusNode.asText())) {
+ JsonNode uptimeNode = serviceNode.get("uptimeMs");
+ assertNotNull(uptimeNode, "UP service '" + serviceName + "' is missing 'uptimeMs' field");
+ assertTrue(
+ uptimeNode.asLong() > 0,
+ "UP service '" + serviceName + "' has non-positive uptimeMs: " + uptimeNode.asLong());
+ }
+ }
+ }
+
+ private HttpResponse<String> getServicesResponse() throws Exception {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(HEALTH_URL))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+ return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/airavata/integration/ThriftServiceHealthTest.java b/integration-tests/src/test/java/org/apache/airavata/integration/ThriftServiceHealthTest.java
new file mode 100644
index 0000000000..e76d45e266
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/airavata/integration/ThriftServiceHealthTest.java
@@ -0,0 +1,115 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.integration;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.stream.Stream;
+import org.apache.airavata.api.Airavata;
+import org.apache.airavata.credential.store.cpi.CredentialStoreService;
+import org.apache.airavata.orchestrator.cpi.OrchestratorService;
+import org.apache.airavata.registry.api.RegistryService;
+import org.apache.airavata.service.profile.groupmanager.cpi.GroupManagerService;
+import org.apache.airavata.service.profile.groupmanager.cpi.group_manager_cpiConstants;
+import org.apache.airavata.service.profile.iam.admin.services.cpi.IamAdminServices;
+import org.apache.airavata.service.profile.iam.admin.services.cpi.iam_admin_services_cpiConstants;
+import org.apache.airavata.service.profile.tenant.cpi.TenantProfileService;
+import org.apache.airavata.service.profile.tenant.cpi.profile_tenant_cpiConstants;
+import org.apache.airavata.service.profile.user.cpi.UserProfileService;
+import org.apache.airavata.service.profile.user.cpi.profile_user_cpiConstants;
+import org.apache.airavata.sharing.registry.service.cpi.SharingRegistryService;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.protocol.TMultiplexedProtocol;
+import org.apache.thrift.protocol.TProtocol;
+import org.apache.thrift.transport.TSocket;
+import org.apache.thrift.transport.TTransport;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Verifies that all 9 multiplexed Thrift services on port 8930 respond to
+ * getAPIVersion(). Each service is tested independently; a failure in one
+ * does not prevent the others from running.
+ */
+@Tag("integration")
+class ThriftServiceHealthTest {
+
+ private static final String HOST = System.getProperty("airavata.thrift.host", "localhost");
+ private static final int PORT = Integer.parseInt(System.getProperty("airavata.thrift.port", "8930"));
+
+ static Stream<String> serviceNames() {
+ return Stream.of(
+ "Airavata",
+ "RegistryService",
+ "SharingRegistry",
+ "CredentialStore",
+ profile_user_cpiConstants.USER_PROFILE_CPI_NAME,
+ profile_tenant_cpiConstants.TENANT_PROFILE_CPI_NAME,
+ iam_admin_services_cpiConstants.IAM_ADMIN_SERVICES_CPI_NAME,
+ group_manager_cpiConstants.GROUP_MANAGER_CPI_NAME,
+ "Orchestrator");
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("serviceNames")
+ void serviceShouldRespondToGetAPIVersion(String serviceName) throws Exception {
+ try (TTransport transport = new TSocket(HOST, PORT)) {
+ transport.open();
+ TProtocol base = new TBinaryProtocol(transport);
+ TMultiplexedProtocol multiplexed = new TMultiplexedProtocol(base, serviceName);
+
+ String version = callGetAPIVersion(serviceName, multiplexed);
+
+ assertNotNull(version, "getAPIVersion() returned null for service: " + serviceName);
+ }
+ }
+
+ /**
+ * Instantiates the appropriate generated client and calls getAPIVersion().
+ * Each generated client extends BaseAPI.Client which provides the method.
+ */
+ private String callGetAPIVersion(String serviceName, TProtocol protocol) throws Exception {
+ return assertDoesNotThrow(
+ () -> switch (serviceName) {
+ case "Airavata" -> new Airavata.Client(protocol).getAPIVersion();
+ case "RegistryService" -> new RegistryService.Client(protocol).getAPIVersion();
+ case "SharingRegistry" -> new SharingRegistryService.Client(protocol).getAPIVersion();
+ case "CredentialStore" -> new CredentialStoreService.Client(protocol).getAPIVersion();
+ case "Orchestrator" -> new OrchestratorService.Client(protocol).getAPIVersion();
+ default -> getProfileServiceVersion(serviceName, protocol);
+ },
+ "getAPIVersion() threw for service: " + serviceName);
+ }
+
+ private String getProfileServiceVersion(String serviceName, TProtocol protocol) throws Exception {
+ if (profile_user_cpiConstants.USER_PROFILE_CPI_NAME.equals(serviceName)) {
+ return new UserProfileService.Client(protocol).getAPIVersion();
+ } else if (profile_tenant_cpiConstants.TENANT_PROFILE_CPI_NAME.equals(serviceName)) {
+ return new TenantProfileService.Client(protocol).getAPIVersion();
+ } else if (iam_admin_services_cpiConstants.IAM_ADMIN_SERVICES_CPI_NAME.equals(serviceName)) {
+ return new IamAdminServices.Client(protocol).getAPIVersion();
+ } else if (group_manager_cpiConstants.GROUP_MANAGER_CPI_NAME.equals(serviceName)) {
+ return new GroupManagerService.Client(protocol).getAPIVersion();
+ }
+ throw new IllegalArgumentException("Unknown service: " + serviceName);
+ }
+}
diff --git a/modules/agent-framework/agent-service/pom.xml b/modules/agent-framework/agent-service/pom.xml
index 6d383f732d..53db664256 100644
--- a/modules/agent-framework/agent-service/pom.xml
+++ b/modules/agent-framework/agent-service/pom.xml
@@ -165,6 +165,10 @@ under the License.
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/modules/agent-framework/agent-service/src/main/resources/application.yml b/modules/agent-framework/agent-service/src/main/resources/application.yml
index fab385e8ea..45de80b923 100644
--- a/modules/agent-framework/agent-service/src/main/resources/application.yml
+++ b/modules/agent-framework/agent-service/src/main/resources/application.yml
@@ -56,3 +56,9 @@ airavata:
serverPort: 17000
serverToken: airavata
serverApiUrl: http://localhost:8000
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health
diff --git a/modules/file-server/pom.xml b/modules/file-server/pom.xml
index 9e2d80ec31..0276417a25 100644
--- a/modules/file-server/pom.xml
+++ b/modules/file-server/pom.xml
@@ -54,6 +54,10 @@ under the License.
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/modules/file-server/src/main/resources/application.properties b/modules/file-server/src/main/resources/application.properties
index 9edf9c74e5..44675bb7ba 100644
--- a/modules/file-server/src/main/resources/application.properties
+++ b/modules/file-server/src/main/resources/application.properties
@@ -6,3 +6,5 @@ airavata.server.host=localhost
airavata.server.port=8930
server.port=8050
+
+management.endpoints.web.exposure.include=health
diff --git a/modules/ide-integration/.gitignore b/modules/ide-integration/.gitignore
deleted file mode 100644
index 5bd7a3df36..0000000000
--- a/modules/ide-integration/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-database_data/
-src/main/containers/pga/airavata-php-gateway/vendor
diff --git a/modules/ide-integration/README.md b/modules/ide-integration/README.md
deleted file mode 100644
index 40ab393d7d..0000000000
--- a/modules/ide-integration/README.md
+++ /dev/null
@@ -1,294 +0,0 @@
-# Apache Airavata - IDE Integration Setup
-
-<div align="center">
- <h3>🚀 Complete Development Environment Setup for IntelliJ IDEA</h3>
- <p>Set up a full Airavata installation inside IntelliJ IDEA for seamless development</p>
-</div>
-
----
-
-## 📋 Prerequisites
-
-Before starting, ensure you have the following installed on your system:
-
-| Tool | Version | Purpose | Installation Link |
-|------|---------|---------|-------------------|
-| 🐳 **Docker & Docker Compose** | Latest | Container orchestration | [Get Docker](https://docs.docker.com/compose/) |
-| 💡 **IntelliJ IDEA** | Latest | IDE with Java 17+ | [Download IDEA](https://www.jetbrains.com/idea/download/) |
-| ☕ **Java JDK** | 17+ | Runtime environment | [OpenJDK 17](https://openjdk.org/projects/jdk/17/) |
-| 🔧 **Apache Maven** | 3.8+ | Build tool | [Install Maven](https://maven.apache.org/install.html) |
-| 📝 **Git** | Latest | Version control | [Install Git](https://git-scm.com/downloads) |
-| 🐍 **Python** | 3.8+ | Django portal | [Python.org](https://www.python.org/downloads/) |
-| 📦 **Node.js & npm** | Latest LTS | Frontend build tools | [Node.js](https://nodejs.org/) |
-
-## 🏗️ Development Environment Setup
-
-### 1️⃣ Clone and Prepare Repository
-
-```bash
-# Clone the main repository
-git clone https://github.com/apache/airavata.git
-cd airavata
-
-# Build the project (this may take a few minutes)
-mvn clean install -DskipTests
-```
-
-### 2️⃣ Open in IntelliJ IDEA
-
-1. **Launch IntelliJ IDEA**
-2. **Open Project** → Navigate to your cloned `airavata` directory
-3. **Navigate to:** `modules` → `ide-integration` module
-
-## 🐳 Backend Services Setup
-
-### 3️⃣ Configure Host Resolution
-
-Add the following entry to your system's hosts file:
-
-**Linux/macOS:** `/etc/hosts`
-**Windows:** `C:\Windows\System32\drivers\etc\hosts`
-
-```bash
-127.0.0.1 airavata.host
-```
-
-### 4️⃣ Start Backend Services
-
-Navigate to the containers directory and start all required services:
-
-```bash
-cd .devcontainer
-docker-compose up -d
-```
-
-**Services Started:**
-- 🗄️ **MySQL Database**
-- 🔐 **Keycloak** (Authentication)
-- 📨 **Apache Kafka** (Messaging)
-- 🐰 **RabbitMQ** (Message Queue)
-- 🔒 **SSHD Server** (Secure connections)
-
-### 5️⃣ Initialize Database
-
-Apply database migrations:
-
-```bash
-cd .devcontainer
-cat ./database_scripts/init/*-migrations.sql | docker exec -i containers-db-1 mysql -p123456
-```
-
-## 🖥️ Starting Airavata Components
-
-### 6️⃣ Start API Server
-
-1. **Navigate to:** `org.apache.airavata.ide.integration.APIServerStarter`
-2. **Right-click** in the editor
-3. **Select:** `Run 'APIServerStarter.main()'`
-
-> 💡 **JDK 17+ Note:** Add this JVM argument in your run configuration:
-> ```
-> --add-opens java.base/java.lang=ALL-UNNAMED
-> ```
-
-### 7️⃣ Start Job Execution Engine
-
-1. **Navigate to:** `org.apache.airavata.ide.integration.JobEngineStarter`
-2. **Right-click** and select **Run**
-
-**Components Started:**
-- 🔄 Helix Controller
-- 👥 Helix Participant
-- ⚙️ Pre Workflow Manager
-- 📋 Post Workflow Manager
-
-### 8️⃣ Start Job Monitoring
-
-#### **Setup Email Monitor (One-Time Setup)**
-
-1. **Create a Gmail Account** [https://accounts.google.com/signup](https://accounts.google.com/signup)
-
-2. **Enable 2-Step Verification** [https://myaccount.google.com/security](https://myaccount.google.com/security)
-
-3. **Go to App Passwords** [https://myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords)
- *(Make sure you're logged in and have already enabled 2-Step Verification.)*
-
-4. **Generate App Password:**
- - Enter the name **"Airavata"** and click **"Generate"**.
-
-5. **Copy the Generated App Password**
- - A 16 character password will appear **copy and save it immediately**, as it will not be shown again.
-
-5. **Update Configuration:**
- Edit `src/main/resources/airavata-server.properties`:
- ```properties
- email.based.monitor.address=your-email@gmail.com
- email.based.monitor.password=your-app-password
- ```
-
-6. **Start Monitor:**
- - Navigate to: `org.apache.airavata.ide.integration.JobMonitorStarter`
- - Right-click and **Run**
-
-## 🌐 User Portal Setup (Django)
-
-### 9️⃣ Django Portal Installation
-
-**You can create and launch experiments and manage credentials using this portal.**
-
-```bash
-# Navigate outside the Airavata directory
-cd ..
-
-# Clone the Django portal repository
-git clone https://github.com/apache/airavata-portals.git
-cd airavata-portals/airavata-django-portal
-
-# Create a virtual environment
-python3 -m venv venv
-
-# Activate the virtual environment
-source venv/bin/activate # For Windows: venv\Scripts\activate
-
-# Install Python dependencies
-pip install -r requirements.txt
-
-### 🔟 Configure Django Portal
-
-```bash
-# Create local settings
-cp django_airavata/settings_local.py.ide django_airavata/settings_local.py
-
-# Run database migrations
-python3 manage.py migrate
-
-# Build JavaScript components
-./build_js.sh
-
-# Load default CMS pages
-python3 manage.py load_default_gateway
-
-# Start development server
-python3 manage.py runserver
-```
-
-### 🌍 Access User Portal
-
-- **URL:** [http://localhost:8000/auth/login](http://localhost:8000/auth/login)
-- **Username:** `default-admin`
-- **Password:** `123456`
-
-## 🛠️ Admin Portal Setup (Optional)
-
-For registering compute resources and storage resources:
-
-### 1️⃣ Starting Super Admin Portal (PGA)
-
-**This portal is required when registering new compute or storage resources into the gateway.**
-
-```bash
-cd .devcontainer/pga
-docker-compose up -d
-```
-
-### 2️⃣ Configure Host Resolution
-
-**Get host machine IP:**
-
-**macOS:**
-```bash
-docker-compose exec pga getent hosts docker.for.mac.host.internal | awk '{ print $1 }'
-```
-
-**Windows:**
-```bash
-docker-compose exec pga getent hosts host.docker.internal
-```
-
-**Update container hosts:**
-*Replace <host-machine-ip> with the actual IP*
-```bash
-docker-compose exec pga /bin/sh -c "echo '<host-machine-ip> airavata.host' >> /etc/hosts"
-```
-
-### 3️⃣ Access Admin Portal
-
-- **URL:** [http://airavata.host:8008](http://airavata.host:8008)
-- **Username:** `default-admin`
-- **Password:** `123456`
-
-## 🛑 Cleanup & Troubleshooting
-
-### Stop All Services
-
-```bash
-# In each docker-compose directory, run:
-docker-compose down
-docker-compose rm -f
-
-# Remove unused containers and networks
-docker system prune
-```
-
-### 🔐 Certificate Renewal (If Expired)
-
-Only needed when Keycloak certificates expire:
-
-```bash
-cd modules/ide-integration/src/main/resources/keystores
-
-# Remove old keystore
-rm airavata.p12
-
-# Generate new keystore (airavata.p12)
-keytool -genkey -keyalg RSA -alias selfsigned -keystore airavata.p12 \
- -storetype pkcs12 -storepass airavata -validity 360 -keysize 2048 \
- -dname "CN=airavata.host,OU=airavata.host,O=airavata.host,L=airavata.host,ST=airavata.host,C=airavata.host"
-
-# Generate self-signed key-pair (for TLS)
-openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt \
- -subj "/CN=airavata.host/OU=airavata.host/O=airavata.host/L=airavata.host/ST=airavata.host/C=airavata.host" \
- -addext "subjectAltName=DNS:airavata.host"
-```
-
-## 📊 Service Status Overview
-
-| Service | Port | Status Check | Purpose |
-|---------|------|-------------|---------|
-| 🗄️ **MySQL** | 3306 | `docker ps` | Database |
-| 🔐 **Keycloak** | 8443 | [airavata.host:8443](http://airavata.host:8443) | Authentication |
-| 📨 **Kafka** | 9092 | Internal | Messaging |
-| 🐰 **RabbitMQ** | 5672 | Internal | Message Queue |
-| 🌐 **Django Portal** | 8000 | [localhost:8000](http://localhost:8000) | User Interface |
-| 🛠️ **PGA Admin** | 8008 | [airavata.host:8008](http://airavata.host:8008) | Admin Portal |
-
-## 🆘 Common Issues
-
-**Port Conflicts:**
-```bash
-# Check what's using a port
-lsof -i :8000
-netstat -tulpn | grep :8000
-```
-
-**Docker Issues:**
-```bash
-# Reset Docker
-docker system prune -a
-docker-compose down --volumes
-```
-
-**Build Failures:**
-```bash
-# Clean Maven cache
-mvn clean
-rm -rf ~/.m2/repository/org/apache/airavata
-```
-
----
-
-<div align="center">
- <strong>🎉 Happy Developing with Apache Airavata!</strong>
- <br>
- <em>Need help? Check our <a href="https://airavata.apache.org/mailing-list.html">mailing lists</a></em>
-</div>
\ No newline at end of file
diff --git a/modules/ide-integration/pom.xml b/modules/ide-integration/pom.xml
deleted file mode 100644
index 4da3035247..0000000000
--- a/modules/ide-integration/pom.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<!--
-Licensed to the Apache Software Foundation (ASF) under one
-or more contributor license agreements. See the NOTICE file
-distributed with this work for additional information
-regarding copyright ownership. The ASF licenses this file
-to you under the Apache License, Version 2.0 (the
-"License"); you may not use this file except in compliance
-with the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing,
-software distributed under the License is distributed on an
-"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, either express or implied. See the License for the
-specific language governing permissions and limitations
-under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <parent>
- <artifactId>airavata</artifactId>
- <groupId>org.apache.airavata</groupId>
- <version>0.21-SNAPSHOT</version>
- <relativePath>../../pom.xml</relativePath>
- </parent>
- <modelVersion>4.0.0</modelVersion>
-
- <artifactId>ide-integration</artifactId>
- <name>Airavata IDE Integration</name>
- <packaging>jar</packaging>
-
- <dependencies>
- <dependency>
- <groupId>org.apache.airavata</groupId>
- <artifactId>airavata-api</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.apache.airavata</groupId>
- <artifactId>airavata-thrift-server</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.mariadb.jdbc</groupId>
- <artifactId>mariadb-java-client</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <resources>
- <resource>
- <directory>../../keystores</directory>
- <targetPath>keystores</targetPath>
- <includes>
- <include>*.jks</include>
- </includes>
- </resource>
- </resources>
- </build>
-
-</project>
diff --git a/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/APIServerStarter.java b/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/APIServerStarter.java
deleted file mode 100644
index b27f84141f..0000000000
--- a/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/APIServerStarter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.ide.integration;
-
-import org.apache.airavata.api.server.AiravataServer;
-
-/**
- * IDE convenience launcher that starts the consolidated Airavata Thrift server.
- * The AiravataServer hosts all services (API, Registry, Orchestrator, Profile, etc.)
- * on a single multiplexed port and manages all background services internally.
- */
-public class APIServerStarter {
-
- public static void main(String[] args) throws Exception {
- new AiravataServer().start();
- }
-}
diff --git a/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/JobEngineStarter.java b/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/JobEngineStarter.java
deleted file mode 100644
index c9945f18c0..0000000000
--- a/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/JobEngineStarter.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.ide.integration;
-
-import java.util.ArrayList;
-import org.apache.airavata.common.utils.ServerSettings;
-import org.apache.airavata.helix.core.AbstractTask;
-import org.apache.airavata.helix.impl.controller.HelixController;
-import org.apache.airavata.helix.impl.participant.GlobalParticipant;
-import org.apache.airavata.helix.impl.workflow.PostWorkflowManager;
-import org.apache.airavata.helix.impl.workflow.PreWorkflowManager;
-import org.apache.helix.manager.zk.ZKHelixAdmin;
-import org.apache.helix.manager.zk.ZNRecordSerializer;
-import org.apache.helix.manager.zk.ZkClient;
-
-/**
- * IDE convenience launcher for the Airavata job execution engine components.
- * IServer implementations run via Runnable.run() — each is started in its own thread.
- */
-public class JobEngineStarter {
-
- public static void main(String args[]) throws Exception {
-
- ZkClient zkClient = new ZkClient(
- ServerSettings.getZookeeperConnection(),
- ZkClient.DEFAULT_SESSION_TIMEOUT,
- ZkClient.DEFAULT_CONNECTION_TIMEOUT,
- new ZNRecordSerializer());
- ZKHelixAdmin zkHelixAdmin = new ZKHelixAdmin(zkClient);
-
- zkHelixAdmin.addCluster(ServerSettings.getSetting("helix.cluster.name"), true);
-
- System.out.println("Starting Helix Controller .......");
- HelixController controller = new HelixController();
- new Thread(controller, "helix-controller").start();
-
- ArrayList<Class<? extends AbstractTask>> taskClasses = new ArrayList<>();
-
- for (String taskClassName : GlobalParticipant.TASK_CLASS_NAMES) {
- taskClasses.add(Class.forName(taskClassName).asSubclass(AbstractTask.class));
- }
-
- System.out.println("Starting Helix Participant .......");
- GlobalParticipant participant = new GlobalParticipant(taskClasses, null);
- new Thread(participant, "helix-participant").start();
-
- System.out.println("Starting Pre Workflow Manager .......");
- PreWorkflowManager preWorkflowManager = new PreWorkflowManager();
- new Thread(preWorkflowManager, "pre-workflow-manager").start();
-
- System.out.println("Starting Post Workflow Manager .......");
- PostWorkflowManager postWorkflowManager = new PostWorkflowManager();
- new Thread(postWorkflowManager, "post-workflow-manager").start();
- }
-}
diff --git a/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/JobMonitorStarter.java b/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/JobMonitorStarter.java
deleted file mode 100644
index a863b18440..0000000000
--- a/modules/ide-integration/src/main/java/org/apache/airavata/ide/integration/JobMonitorStarter.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
-*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements. See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership. The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License. You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied. See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-package org.apache.airavata.ide.integration;
-
-import org.apache.airavata.monitor.email.EmailBasedMonitor;
-
-/**
- * IDE convenience launcher for the email-based job monitor.
- * EmailBasedMonitor implements IServer (Runnable) — started in its own thread.
- */
-public class JobMonitorStarter {
- public static void main(String args[]) throws Exception {
- EmailBasedMonitor emailBasedMonitor = new EmailBasedMonitor();
- new Thread(emailBasedMonitor, "email-monitor").start();
- }
-}
diff --git a/modules/research-framework/research-service/pom.xml b/modules/research-framework/research-service/pom.xml
index 79deaf1ee8..1c799544a1 100644
--- a/modules/research-framework/research-service/pom.xml
+++ b/modules/research-framework/research-service/pom.xml
@@ -173,6 +173,10 @@ under the License.
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml
index 914a0e29f0..64657d99e0 100644
--- a/modules/research-framework/research-service/src/main/resources/application.yml
+++ b/modules/research-framework/research-service/src/main/resources/application.yml
@@ -68,3 +68,9 @@ springdoc:
use-pkce-with-authorization-code-grant: true
client-id: data-catalog-portal
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health
+
diff --git a/modules/restproxy/pom.xml b/modules/restproxy/pom.xml
index e941cb7afc..7a955a62de 100644
--- a/modules/restproxy/pom.xml
+++ b/modules/restproxy/pom.xml
@@ -51,6 +51,10 @@ under the License.
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/modules/restproxy/src/main/resources/application.properties b/modules/restproxy/src/main/resources/application.properties
index 0ce150e047..4c3005b8b6 100644
--- a/modules/restproxy/src/main/resources/application.properties
+++ b/modules/restproxy/src/main/resources/application.properties
@@ -1,2 +1,4 @@
restproxy.broker.url=localhost:9092
server.port=8082
+
+management.endpoints.web.exposure.include=health
diff --git a/pom.xml b/pom.xml
index 957f82c16a..0a13f3a6a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,11 +70,11 @@ under the License.
<modules>
<module>airavata-api</module>
<module>airavata-thrift-server</module>
+ <module>integration-tests</module>
<module>modules/file-server</module>
<module>modules/agent-framework/agent-service</module>
<module>modules/research-framework/research-service</module>
<module>modules/restproxy</module>
- <module>modules/ide-integration</module>
</modules>
<properties>
@@ -480,6 +480,11 @@ under the License.
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.5.3</version>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-actuator</artifactId>
+ <version>3.5.3</version>
+ </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>