Service Registration Architecture
This document describes the capability registration workflow for the Wanaku service discovery system. It is intended as a reference for implementing similar service discovery patterns.
Architecture Overview
The service registration system uses a layered design:
┌─────────────────────────────────────────────────────────┐
│ REST API Layer │
│ (DiscoveryResource.java) │
├─────────────────────────────────────────────────────────┤
│ Bean Layer │
│ (DiscoveryBean.java) │
├─────────────────────────────────────────────────────────┤
│ Registry Interface │
│ (ServiceRegistry.java) │
├─────────────────────────────────────────────────────────┤
│ Persistence Layer │
│ (InfinispanServiceRegistry.java) │
│ ┌─────────────────────┬─────────────────────────┐ │
│ │CapabilitiesRepository│ ServiceRecordRepository│ │
│ └─────────────────────┴─────────────────────────┘ │
└─────────────────────────────────────────────────────────┘Key characteristics:
- Service targets represent downstream capability endpoints
- Reactive messaging distributes events to subscribers
- gRPC transport for service-to-service communication
- Dual-repository pattern separates service metadata from activity history
Core Data Types
ServiceTarget
Service endpoint descriptor stored in the registry.
Package: ai.wanaku.capabilities.sdk.api.types.providers
| Field | Type | Description |
|---|---|---|
id | String | Unique identifier (assigned by registry) |
serviceName | String | Logical name of the service |
host | String | Host address |
port | int | Port number |
serviceType | String | Type category (see ServiceType) |
serviceSubType | String | Engine type: jvm, interpreted, camel |
languageName | String | Language: java, python, yaml, xml |
languageType | String | compiled, interpreted |
languageSubType | String | jvm, cpython, etc. |
Key Methods:
String toAddress() // Returns "host:port"
static ServiceTarget newEmptyTarget(String serviceName, String host,
int port, String serviceType, String serviceSubType)ServiceType
Enumeration of service categories.
Package: ai.wanaku.capabilities.sdk.api.types.providers
| Value | String | Int |
|---|---|---|
RESOURCE_PROVIDER | "resource-provider" | 1 |
TOOL_INVOKER | "tool-invoker" | 2 |
MULTI_CAPABILITY | "multi-capability" | 3 |
CODE_EXECUTION_ENGINE | "code-execution-engine" | 4 |
ServiceState
Point-in-time health snapshot.
Package: ai.wanaku.capabilities.sdk.api.types.discovery
| Field | Type | Description |
|---|---|---|
timestamp | Instant | When state was recorded |
healthy | boolean | Health status |
reason | String | Message (used when unhealthy) |
Factory Methods:
static ServiceState newHealthy() // Healthy with "HEALTHY" message
static ServiceState newUnhealthy(String reason) // Unhealthy with reason
static ServiceState newMissingInAction() // "MISSING_IN_ACTION"
static ServiceState newInactive() // "AUTO_DEREGISTRATION"ActivityRecord
Service lifecycle tracking with state history.
Package: ai.wanaku.capabilities.sdk.api.types.discovery
| Field | Type | Description |
|---|---|---|
id | String | Service identifier |
lastSeen | Instant | Last activity timestamp |
active | boolean | Current active status |
states | List<ServiceState> | State history (circular buffer) |
Discovery API Endpoints
Base Path: /api/v1/management/discovery
POST /register
Register a service with the router. Returns the service target with assigned ID.
Request: ServiceTarget (JSON) Response: WanakuResponse<ServiceTarget>
RestResponse<WanakuResponse<ServiceTarget>> register(ServiceTarget serviceTarget);POST /deregister
Remove a service from the registry.
Request: ServiceTarget (JSON) Response: 200 OK
Response deregister(ServiceTarget serviceTarget);POST /update/
Update service state (report health status).
Path Parameter: id - Service identifier Request: ServiceState (JSON) Response: 200 OK
Response updateState(@PathParam("id") String id, ServiceState serviceState);POST /ping
Send heartbeat to indicate service is still active.
Request: Service ID as plain text Response: 200 OK
Response ping(String id);Registry Layer
ServiceRegistry Interface
Package: ai.wanaku.core.mcp.providers
Defines the contract for service registration and discovery operations.
public interface ServiceRegistry {
// Registration
ServiceTarget register(ServiceTarget serviceTarget);
void deregister(ServiceTarget serviceTarget);
void update(ServiceTarget serviceTarget);
// Query
List<ServiceTarget> getServiceByName(String serviceName, String serviceType);
List<ServiceTarget> getCodeExecutionService(String serviceType,
String serviceSubType, String serviceName);
List<ServiceTarget> getEntries();
List<ServiceTarget> getEntries(String serviceType);
// State Management
ActivityRecord getStates(String id);
void updateLastState(String id, ServiceState state);
void ping(String id);
}InfinispanServiceRegistry
Package: ai.wanaku.core.persistence.infinispan.discovery
Implementation using dual-repository pattern:
InfinispanCapabilitiesRepository- storesServiceTargetentitiesInfinispanServiceRecordRepository- storesActivityRecordentities
Key Behaviors:
- Registration: Delegates to capabilities repository, generates UUID for ID
- Multi-Capability Matching: Queries include services with type "multi-capability"
- Circular Buffer: States list capped at configurable max (default 10), removes oldest 50% when exceeded
// Configuration
@ConfigProperty(name = "wanaku.persistence.infinispan.max-state-count",
defaultValue = "10")
int maxStateCount;Repository Queries
Capabilities Repository:
// Find by name and type (includes multi-capability)
findByService(serviceName, serviceType)
// WHERE serviceName = :serviceName
// AND (serviceType = :serviceType OR serviceType = 'multi-capability')
// List by type (includes multi-capability)
listCapable(serviceType)
// WHERE serviceType = :serviceType OR serviceType = 'multi-capability'
// Find code execution engine (exact match)
findCodeExecutionService(serviceType, serviceSubType, languageName)
// WHERE serviceType = :serviceType
// AND serviceSubType = :serviceSubType
// AND languageName = :languageNameRegistration Workflow (Service Side)
RegistrationManager Interface
Package: ai.wanaku.capabilities.sdk.api.discovery
Defines the contract for managing service lifecycle registration.
public interface RegistrationManager {
void register();
void deregister();
void ping();
void lastAsFail(String reason);
void lastAsSuccessful();
void addCallBack(DiscoveryCallback callback);
}DefaultRegistrationManager
Package: ai.wanaku.core.capabilities.discovery
Quarkus CDI implementation with retry logic and persistent identity.
Key Features:
- Restores service ID from persistent storage on startup
- Uses
ReentrantLockfor thread-safe registration - Retry logic with configurable wait time
- Callback pattern for lifecycle notifications
Registration Flow:
┌──────────────────┐
│ register() called│
└────────┬─────────┘
│
▼
┌──────────────────┐ Yes ┌─────────────┐
│ Already registered?├─────────►│ Call ping() │
└────────┬─────────┘ └─────────────┘
│ No
▼
┌──────────────────┐
│ Acquire lock │
│ (1s timeout) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ tryRegistering() │◄─────────┐
└────────┬─────────┘ │
│ │ Retry
▼ │
┌──────────────────┐ Fail │
│ Call REST API ├──────────┘
└────────┬─────────┘
│ Success
▼
┌──────────────────┐
│ Save ID to disk │
│ Set registered │
│ Invoke callbacks │
└──────────────────┘DiscoveryCallback Interface
Package: ai.wanaku.capabilities.sdk.api.discovery
Callback interface for receiving registration lifecycle events.
public interface DiscoveryCallback {
void onPing(RegistrationManager manager, ServiceTarget target, int status);
void onRegistration(RegistrationManager manager, ServiceTarget target);
void onDeregistration(RegistrationManager manager, ServiceTarget target, int status);
}Usage:
registrationManager.addCallBack(new DiscoveryCallback() {
@Override
public void onRegistration(RegistrationManager manager, ServiceTarget target) {
LOG.info("Registered with ID: " + target.getId());
}
@Override
public void onPing(RegistrationManager manager, ServiceTarget target, int status) {
if (status != 200) {
LOG.warn("Ping failed with status: " + status);
}
}
@Override
public void onDeregistration(RegistrationManager manager, ServiceTarget target, int status) {
LOG.info("Deregistered");
}
});Service Resolution (Router Side)
ServiceResolver Interface
Package: ai.wanaku.backend.service.support
Defines contract for resolving services from the registry.
public interface ServiceResolver {
ServiceTarget resolve(String serviceName, String serviceType);
ServiceTarget resolveCodeExecution(String serviceType, String serviceSubType,
String languageName);
}FirstAvailable Strategy
Package: ai.wanaku.backend.service.support
Simple strategy that returns the first matching service from the registry.
public class FirstAvailable implements ServiceResolver {
private final ServiceRegistry serviceRegistry;
public FirstAvailable(ServiceRegistry serviceRegistry) {
this.serviceRegistry = serviceRegistry;
}
@Override
public ServiceTarget resolve(String serviceName, String serviceType) {
List<ServiceTarget> services = serviceRegistry.getServiceByName(
serviceName, serviceType);
if (services != null && !services.isEmpty()) {
return services.getFirst();
}
return null;
}
@Override
public ServiceTarget resolveCodeExecution(String serviceType,
String serviceSubType, String languageName) {
List<ServiceTarget> services = serviceRegistry.getCodeExecutionService(
serviceType, serviceSubType, languageName);
if (services != null && !services.isEmpty()) {
return services.getFirst();
}
return null;
}
}Provider Pattern
Bridges are exposed via CDI producers:
@ApplicationScoped
public class ToolsProvider {
@Inject
ServiceResolver serviceResolver;
@Inject
WanakuBridgeTransport transport;
@Inject
@Channel("tool-call-event")
@OnOverflow(OnOverflow.Strategy.DROP)
MutinyEmitter<ToolCallEvent> toolCallEventEmitter;
@Produces
ToolsBridge getToolsBridge() {
return new InvokerBridge(serviceResolver, transport, new EventNotifier(toolCallEventEmitter));
}
}
@ApplicationScoped
public class ResourcesProvider {
@Produces
ProvisionerBridge getProvisionerBridge() {
return new ProvisionerBridge(new FirstAvailable(serviceRegistry), new GrpcTransport());
}
@Produces
ResourceBridge getResourceBridge() {
ProvisionerBridge provisioner = new ProvisionerBridge(resolver, transport);
return new ResourceAcquirerBridge(provisioner, transport);
}
}Event System
ServiceTargetEvent
Package: ai.wanaku.backend.common
Event object emitted when service registration state changes.
| Field | Type | Description |
|---|---|---|
eventType | EventType | Type of event |
id | String | Service identifier |
serviceTarget | ServiceTarget | Full service descriptor (register/deregister) |
serviceState | ServiceState | Health state (update events) |
Factory Methods:
static ServiceTargetEvent register(ServiceTarget serviceTarget)
static ServiceTargetEvent deregister(ServiceTarget serviceTarget)
static ServiceTargetEvent update(String id, ServiceState serviceState)
static ServiceTargetEvent ping(String id)EventType Enum
public enum EventType {
REGISTER("register"),
DEREGISTER("deregister"),
UPDATE("update"),
PING("ping");
}Reactive Messaging
Events are published to the service-target-event channel:
@Inject
@Channel("service-target-event")
@OnOverflow(OnOverflow.Strategy.DROP)
MutinyEmitter<ServiceTargetEvent> serviceTargetEventEmitter;
private void emitEvent(ServiceTargetEvent event) {
if (serviceTargetEventEmitter.hasRequests()) {
serviceTargetEventEmitter.send(event);
}
}SSE Notifications
Events are exposed via Server-Sent Events at /api/v1/capabilities/notifications:
@Path("/notifications")
@Produces(MediaType.SERVER_SENT_EVENTS)
@GET
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<OutboundSseEvent> targetsEventStream(@Context Sse sse) {
return serviceTargetEvents.map(event -> sse.newEventBuilder()
.name(event.getEventType().name())
.id(event.getId())
.data(event)
.build());
}Code Examples
Creating a ServiceTarget
ServiceTarget target = new ServiceTarget();
target.setServiceName("my-tool-provider");
target.setHost("localhost");
target.setPort(8080);
target.setServiceType(ServiceType.TOOL_INVOKER.asValue());
// Or using factory method
ServiceTarget target = ServiceTarget.newEmptyTarget(
"my-tool-provider",
"localhost",
8080,
ServiceType.TOOL_INVOKER.asValue(),
null // serviceSubType
);Registration Lifecycle
// Create the discovery service client (Quarkus REST Client)
DiscoveryService discoveryService = RestClientBuilder.newBuilder()
.baseUri(URI.create("http://router:8080"))
.build(DiscoveryService.class);
// Register
RestResponse<WanakuResponse<ServiceTarget>> response =
discoveryService.register(target);
String assignedId = response.getEntity().getData().getId();
// Periodic heartbeat
scheduledExecutor.scheduleAtFixedRate(() -> {
discoveryService.ping(assignedId);
}, 30, 30, TimeUnit.SECONDS);
// Report state changes
discoveryService.updateState(assignedId, ServiceState.newHealthy());
discoveryService.updateState(assignedId,
ServiceState.newUnhealthy("Database connection lost"));
// Deregister on shutdown
discoveryService.deregister(target);Service Resolution
@Inject
ServiceRegistry serviceRegistry;
// Resolve a tool invoker
ServiceResolver resolver = new FirstAvailable(serviceRegistry);
ServiceTarget toolService = resolver.resolve(
"my-tool",
ServiceType.TOOL_INVOKER.asValue()
);
if (toolService != null) {
String address = toolService.toAddress(); // "host:port"
// Connect to service...
}
// Resolve code execution engine
ServiceTarget engine = resolver.resolveCodeExecution(
ServiceType.CODE_EXECUTION_ENGINE.asValue(),
"camel", // engine type
"yaml" // language
);Event Handling
// Subscribe to service events
@Inject
@Channel("service-target-event")
Multi<ServiceTargetEvent> events;
void subscribeToEvents() {
events.subscribe().with(event -> {
switch (event.getEventType()) {
case REGISTER -> handleRegistration(event.getServiceTarget());
case DEREGISTER -> handleDeregistration(event.getServiceTarget());
case UPDATE -> handleStateUpdate(event.getId(), event.getServiceState());
case PING -> handlePing(event.getId());
}
});
}File Locations
| Component | Path |
|---|---|
| Discovery REST API | apps/wanaku-router-backend/.../discovery/DiscoveryResource.java |
| Discovery Bean | apps/wanaku-router-backend/.../discovery/DiscoveryBean.java |
| ServiceRegistry Interface | core/core-mcp/.../providers/ServiceRegistry.java |
| Infinispan Implementation | core/core-persistence/core-persistence-infinispan/.../discovery/ |
| ServiceTarget Model | capabilities-api/.../types/providers/ServiceTarget.java |
| State Models | capabilities-api/.../types/discovery/ |
| Registration Manager | capabilities-quarkus-sdk/wanaku-capabilities-base/.../discovery/DefaultRegistrationManager.java |
| Service Resolver | apps/wanaku-router-backend/.../support/ServiceResolver.java |
| Event Types | apps/wanaku-router-backend/.../common/EventType.java |
| SSE Endpoint | apps/wanaku-router-backend/.../capabilities/CapabilitiesResource.java |