Skip to content

Commit f59c13d

Browse files
committed
Add HealthStatus enum and integrate with ActivityRecord and ServiceState
Add enum-based health status model (PENDING, HEALTHY, UNHEALTHY, DOWN) for richer service health tracking. Update ActivityRecord and ServiceState to use HealthStatus alongside the existing boolean fields for backward compatibility.
1 parent d3c33f9 commit f59c13d

File tree

7 files changed

+226
-19
lines changed

7 files changed

+226
-19
lines changed

capabilities-api/src/main/java/ai/wanaku/capabilities/sdk/api/types/discovery/ActivityRecord.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
public class ActivityRecord implements WanakuEntity<String> {
1717
private String id;
1818
private Instant lastSeen;
19-
private boolean active;
19+
private HealthStatus healthStatus = HealthStatus.PENDING;
2020
private List<ServiceState> states = new ArrayList<>();
2121

2222
/**
@@ -64,20 +64,32 @@ public void setLastSeen(Instant lastSeen) {
6464

6565
/**
6666
* Checks whether the service is currently active.
67+
* <p>
68+
* A service is considered active if its health status is {@link HealthStatus#HEALTHY}
69+
* or {@link HealthStatus#PENDING}.
6770
*
6871
* @return {@code true} if the service is active, {@code false} otherwise
6972
*/
7073
public boolean isActive() {
71-
return active;
74+
return healthStatus == HealthStatus.HEALTHY || healthStatus == HealthStatus.PENDING;
75+
}
76+
77+
/**
78+
* Gets the health status of this service.
79+
*
80+
* @return the current health status
81+
*/
82+
public HealthStatus getHealthStatus() {
83+
return healthStatus;
7284
}
7385

7486
/**
75-
* Sets the active status of the service.
87+
* Sets the health status of this service.
7688
*
77-
* @param active {@code true} to mark the service as active, {@code false} otherwise
89+
* @param healthStatus the health status to set
7890
*/
79-
public void setActive(boolean active) {
80-
this.active = active;
91+
public void setHealthStatus(HealthStatus healthStatus) {
92+
this.healthStatus = healthStatus;
8193
}
8294

8395
/**
@@ -107,23 +119,23 @@ public boolean equals(Object o) {
107119
return false;
108120
}
109121
ActivityRecord that = (ActivityRecord) o;
110-
return active == that.active
122+
return healthStatus == that.healthStatus
111123
&& Objects.equals(id, that.id)
112124
&& Objects.equals(lastSeen, that.lastSeen)
113125
&& Objects.equals(states, that.states);
114126
}
115127

116128
@Override
117129
public int hashCode() {
118-
return Objects.hash(id, lastSeen, active, states);
130+
return Objects.hash(id, lastSeen, healthStatus, states);
119131
}
120132

121133
@Override
122134
public String toString() {
123135
return "ActivityRecord{" + "id='"
124136
+ id + '\'' + ", lastSeen="
125-
+ lastSeen + ", active="
126-
+ active + ", states="
137+
+ lastSeen + ", healthStatus="
138+
+ healthStatus + ", states="
127139
+ states + '}';
128140
}
129141
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ai.wanaku.capabilities.sdk.api.types.discovery;
2+
3+
/**
4+
* Represents the health status of a service in the Wanaku system.
5+
*/
6+
public enum HealthStatus {
7+
PENDING("pending"),
8+
HEALTHY("healthy"),
9+
UNHEALTHY("unhealthy"),
10+
DOWN("down");
11+
12+
private final String value;
13+
14+
HealthStatus(String value) {
15+
this.value = value;
16+
}
17+
18+
/**
19+
* Returns the lowercase string representation of this health status.
20+
*
21+
* @return the status value as a lowercase string
22+
*/
23+
public String asValue() {
24+
return value;
25+
}
26+
27+
/**
28+
* Converts a string value to a HealthStatus enum constant.
29+
*
30+
* @param value the string value to convert
31+
* @return the matching HealthStatus, or {@link #PENDING} if no match is found or value is null
32+
*/
33+
public static HealthStatus fromValue(String value) {
34+
if (value == null) {
35+
return PENDING;
36+
}
37+
for (HealthStatus status : values()) {
38+
if (status.value.equals(value)) {
39+
return status;
40+
}
41+
}
42+
return PENDING;
43+
}
44+
}

capabilities-api/src/main/java/ai/wanaku/capabilities/sdk/api/types/discovery/ServiceState.java

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
public class ServiceState {
1010
private Instant timestamp;
1111
private boolean healthy;
12+
private HealthStatus healthStatus;
1213
private String reason;
1314

1415
/**
@@ -25,6 +26,21 @@ public ServiceState() {}
2526
public ServiceState(Instant timestamp, boolean healthy, String reason) {
2627
this.timestamp = timestamp;
2728
this.healthy = healthy;
29+
this.healthStatus = healthy ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY;
30+
this.reason = reason;
31+
}
32+
33+
/**
34+
* Saves the current state of the service with an explicit health status
35+
* @param timestamp the current timestamp
36+
* @param healthy whether it is healthy (true for healthy, false otherwise)
37+
* @param healthStatus the detailed health status
38+
* @param reason Optional state message (ignored if healthy)
39+
*/
40+
public ServiceState(Instant timestamp, boolean healthy, HealthStatus healthStatus, String reason) {
41+
this.timestamp = timestamp;
42+
this.healthy = healthy;
43+
this.healthStatus = healthStatus;
2844
this.reason = reason;
2945
}
3046

@@ -64,6 +80,24 @@ public void setHealthy(boolean healthy) {
6480
this.healthy = healthy;
6581
}
6682

83+
/**
84+
* Gets the detailed health status.
85+
*
86+
* @return the health status enum value
87+
*/
88+
public HealthStatus getHealthStatus() {
89+
return healthStatus;
90+
}
91+
92+
/**
93+
* Sets the detailed health status.
94+
*
95+
* @param healthStatus the health status to set
96+
*/
97+
public void setHealthStatus(HealthStatus healthStatus) {
98+
this.healthStatus = healthStatus;
99+
}
100+
67101
/**
68102
* Gets the reason for the current service state.
69103
*
@@ -89,18 +123,20 @@ public boolean equals(Object o) {
89123
}
90124
ServiceState that = (ServiceState) o;
91125
return healthy == that.healthy
126+
&& healthStatus == that.healthStatus
92127
&& Objects.equals(timestamp, that.timestamp)
93128
&& Objects.equals(reason, that.reason);
94129
}
95130

96131
@Override
97132
public int hashCode() {
98-
return Objects.hash(timestamp, healthy, reason);
133+
return Objects.hash(timestamp, healthy, healthStatus, reason);
99134
}
100135

101136
@Override
102137
public String toString() {
103-
return "ServiceState{" + "timestamp=" + timestamp + ", healthy=" + healthy + ", reason='" + reason + '\'' + '}';
138+
return "ServiceState{" + "timestamp=" + timestamp + ", healthy=" + healthy + ", healthStatus=" + healthStatus
139+
+ ", reason='" + reason + '\'' + '}';
104140
}
105141

106142
/**
@@ -109,7 +145,7 @@ public String toString() {
109145
* @return a new healthy ServiceState instance
110146
*/
111147
public static ServiceState newHealthy() {
112-
return new ServiceState(Instant.now(), true, StandardMessages.HEALTHY);
148+
return new ServiceState(Instant.now(), true, HealthStatus.HEALTHY, StandardMessages.HEALTHY);
113149
}
114150

115151
/**
@@ -119,7 +155,7 @@ public static ServiceState newHealthy() {
119155
* @return a new unhealthy ServiceState instance
120156
*/
121157
public static ServiceState newUnhealthy(String reason) {
122-
return new ServiceState(Instant.now(), false, reason);
158+
return new ServiceState(Instant.now(), false, HealthStatus.UNHEALTHY, reason);
123159
}
124160

125161
/**
@@ -128,7 +164,7 @@ public static ServiceState newUnhealthy(String reason) {
128164
* @return a new ServiceState instance with missing-in-action status
129165
*/
130166
public static ServiceState newMissingInAction() {
131-
return new ServiceState(Instant.now(), false, StandardMessages.MISSING_IN_ACTION);
167+
return new ServiceState(Instant.now(), false, HealthStatus.DOWN, StandardMessages.MISSING_IN_ACTION);
132168
}
133169

134170
/**
@@ -137,6 +173,25 @@ public static ServiceState newMissingInAction() {
137173
* @return a new inactive ServiceState instance
138174
*/
139175
public static ServiceState newInactive() {
140-
return new ServiceState(Instant.now(), true, StandardMessages.AUTO_DEREGISTRATION);
176+
return new ServiceState(Instant.now(), false, HealthStatus.DOWN, StandardMessages.AUTO_DEREGISTRATION);
177+
}
178+
179+
/**
180+
* Creates a new pending service state with the current timestamp.
181+
*
182+
* @return a new pending ServiceState instance
183+
*/
184+
public static ServiceState newPending() {
185+
return new ServiceState(Instant.now(), false, HealthStatus.PENDING, "pending health check");
186+
}
187+
188+
/**
189+
* Creates a new down service state with the current timestamp and the specified reason.
190+
*
191+
* @param reason the reason the service is down
192+
* @return a new down ServiceState instance
193+
*/
194+
public static ServiceState newDown(String reason) {
195+
return new ServiceState(Instant.now(), false, HealthStatus.DOWN, reason);
141196
}
142197
}

capabilities-discovery/src/main/java/ai/wanaku/capabilities/sdk/discovery/ZeroDepRegistrationManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,8 @@ private void runCallBack(Consumer<DiscoveryCallback> registrationManagerConsumer
330330
public void addCallBack(DiscoveryCallback callback) {
331331
callbacks.add(callback);
332332
}
333+
334+
public ServiceTarget getTarget() {
335+
return target;
336+
}
333337
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
syntax = "proto3";
2+
3+
option java_multiple_files = true;
4+
option java_package = "ai.wanaku.core.exchange";
5+
option java_outer_classname = "HealthProbeExchange";
6+
7+
package health;
8+
9+
// The health probe service definition.
10+
service HealthProbe {
11+
// Gets the runtime status of the capability
12+
rpc GetStatus (HealthProbeRequest) returns (HealthProbeReply) {}
13+
}
14+
15+
// The health probe request message
16+
message HealthProbeRequest {
17+
string id = 1;
18+
}
19+
20+
// The runtime status of the capability
21+
enum RuntimeStatus {
22+
STOPPED = 0;
23+
STOPPING = 1;
24+
STARTING = 2;
25+
STARTED = 3;
26+
}
27+
28+
// The health probe response message
29+
message HealthProbeReply {
30+
RuntimeStatus status = 1;
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package ai.wanaku.capabilities.sdk.runtime.camel.grpc;
2+
3+
import java.util.Objects;
4+
import org.apache.camel.CamelContext;
5+
import org.apache.camel.ServiceStatus;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import io.grpc.Status;
9+
import io.grpc.stub.StreamObserver;
10+
import ai.wanaku.capabilities.sdk.api.exceptions.WanakuException;
11+
import ai.wanaku.capabilities.sdk.api.types.providers.ServiceTarget;
12+
import ai.wanaku.core.exchange.HealthProbeGrpc;
13+
import ai.wanaku.core.exchange.HealthProbeReply;
14+
import ai.wanaku.core.exchange.HealthProbeRequest;
15+
import ai.wanaku.core.exchange.RuntimeStatus;
16+
17+
public class CamelHealthProbe extends HealthProbeGrpc.HealthProbeImplBase {
18+
private static final Logger LOG = LoggerFactory.getLogger(CamelHealthProbe.class);
19+
20+
private final CamelContext camelContext;
21+
private final ServiceTarget target;
22+
23+
public CamelHealthProbe(CamelContext camelContext, ServiceTarget target) {
24+
this.camelContext = camelContext;
25+
this.target = Objects.requireNonNull(target);
26+
27+
if (target.getId().isEmpty()) {
28+
throw new WanakuException("Invalid capability service ID");
29+
}
30+
}
31+
32+
public RuntimeStatus getStatus(ServiceStatus serviceStatus) {
33+
switch (serviceStatus) {
34+
case Initializing, Initialized, Starting -> {
35+
return RuntimeStatus.STARTING;
36+
}
37+
case Started -> {
38+
return RuntimeStatus.STARTED;
39+
}
40+
default -> {
41+
return RuntimeStatus.STOPPED;
42+
}
43+
}
44+
}
45+
46+
@Override
47+
public void getStatus(HealthProbeRequest request, StreamObserver<HealthProbeReply> responseObserver) {
48+
if (!target.getId().equals(request.getId())) {
49+
LOG.error("Requested capability ID ({}) doesn't match exiting one {}", request.getId(), target.getId());
50+
responseObserver.onError(Status.INVALID_ARGUMENT
51+
.withDescription("Invalid request id " + request.getId())
52+
.asRuntimeException());
53+
} else {
54+
RuntimeStatus status = getStatus(camelContext.getStatus());
55+
responseObserver.onNext(
56+
HealthProbeReply.newBuilder().setStatus(status).build());
57+
responseObserver.onCompleted();
58+
}
59+
}
60+
}

capabilities-runtimes/capabilities-runtimes-camel/capabilities-runtimes-camel-plugin/src/main/java/ai/wanaku/capabilities/sdk/runtime/camel/plugin/CamelIntegrationPlugin.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import io.grpc.InsecureServerCredentials;
1515
import io.grpc.Server;
1616
import io.grpc.ServerBuilder;
17-
import ai.wanaku.capabilities.sdk.api.discovery.RegistrationManager;
1817
import ai.wanaku.capabilities.sdk.api.types.providers.ServiceTarget;
1918
import ai.wanaku.capabilities.sdk.api.types.providers.ServiceType;
2019
import ai.wanaku.capabilities.sdk.common.ServicesHelper;
@@ -32,6 +31,7 @@
3231
import ai.wanaku.capabilities.sdk.runtime.camel.downloader.ResourceListBuilder;
3332
import ai.wanaku.capabilities.sdk.runtime.camel.downloader.ResourceRefs;
3433
import ai.wanaku.capabilities.sdk.runtime.camel.downloader.ResourceType;
34+
import ai.wanaku.capabilities.sdk.runtime.camel.grpc.CamelHealthProbe;
3535
import ai.wanaku.capabilities.sdk.runtime.camel.grpc.CamelResource;
3636
import ai.wanaku.capabilities.sdk.runtime.camel.grpc.CamelTool;
3737
import ai.wanaku.capabilities.sdk.runtime.camel.grpc.ProvisionBase;
@@ -57,7 +57,7 @@ public class CamelIntegrationPlugin implements ContextServicePlugin {
5757
private static final Logger LOG = LoggerFactory.getLogger(CamelIntegrationPlugin.class);
5858

5959
private Server grpcServer;
60-
private RegistrationManager registrationManager;
60+
private ZeroDepRegistrationManager registrationManager;
6161
private PluginConfiguration config;
6262

6363
@Override
@@ -117,6 +117,7 @@ public void load(CamelContext camelContext) {
117117
.addService(new CamelTool(camelContext, mcpSpec))
118118
.addService(new CamelResource(camelContext, mcpSpec))
119119
.addService(new ProvisionBase(config.getServiceName()))
120+
.addService(new CamelHealthProbe(camelContext, registrationManager.getTarget()))
120121
.build();
121122

122123
grpcServer.start();
@@ -156,7 +157,7 @@ private ServiceTarget newServiceTarget() {
156157
config.getServiceName(), address, config.getGrpcPort(), ServiceType.MULTI_CAPABILITY.asValue());
157158
}
158159

159-
private RegistrationManager newRegistrationManager(
160+
private ZeroDepRegistrationManager newRegistrationManager(
160161
ServiceTarget serviceTarget,
161162
ResourceDownloaderCallback resourcesDownloaderCallback,
162163
ServiceConfig serviceConfig) {

0 commit comments

Comments
 (0)