Skip to content

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:

text
┌─────────────────────────────────────────────────────────┐
│                    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

FieldTypeDescription
idStringUnique identifier (assigned by registry)
serviceNameStringLogical name of the service
hostStringHost address
portintPort number
serviceTypeStringType category (see ServiceType)
serviceSubTypeStringEngine type: jvm, interpreted, camel
languageNameStringLanguage: java, python, yaml, xml
languageTypeStringcompiled, interpreted
languageSubTypeStringjvm, cpython, etc.

Key Methods:

java
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

ValueStringInt
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

FieldTypeDescription
timestampInstantWhen state was recorded
healthybooleanHealth status
reasonStringMessage (used when unhealthy)

Factory Methods:

java
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

FieldTypeDescription
idStringService identifier
lastSeenInstantLast activity timestamp
activebooleanCurrent active status
statesList<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>

java
RestResponse<WanakuResponse<ServiceTarget>> register(ServiceTarget serviceTarget);

POST /deregister

Remove a service from the registry.

Request: ServiceTarget (JSON) Response: 200 OK

java
Response deregister(ServiceTarget serviceTarget);

POST /update/

Update service state (report health status).

Path Parameter: id - Service identifier Request: ServiceState (JSON) Response: 200 OK

java
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

java
Response ping(String id);

Registry Layer

ServiceRegistry Interface

Package: ai.wanaku.core.mcp.providers

Defines the contract for service registration and discovery operations.

java
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 - stores ServiceTarget entities
  • InfinispanServiceRecordRepository - stores ActivityRecord entities

Key Behaviors:

  1. Registration: Delegates to capabilities repository, generates UUID for ID
  2. Multi-Capability Matching: Queries include services with type "multi-capability"
  3. Circular Buffer: States list capped at configurable max (default 10), removes oldest 50% when exceeded
java
// Configuration
@ConfigProperty(name = "wanaku.persistence.infinispan.max-state-count",
    defaultValue = "10")
int maxStateCount;

Repository Queries

Capabilities Repository:

java
// 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 = :languageName

Registration Workflow (Service Side)

RegistrationManager Interface

Package: ai.wanaku.capabilities.sdk.api.discovery

Defines the contract for managing service lifecycle registration.

java
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 ReentrantLock for thread-safe registration
  • Retry logic with configurable wait time
  • Callback pattern for lifecycle notifications

Registration Flow:

text
┌──────────────────┐
│ 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.

java
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:

java
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.

java
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.

java
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:

java
@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.

FieldTypeDescription
eventTypeEventTypeType of event
idStringService identifier
serviceTargetServiceTargetFull service descriptor (register/deregister)
serviceStateServiceStateHealth state (update events)

Factory Methods:

java
static ServiceTargetEvent register(ServiceTarget serviceTarget)
static ServiceTargetEvent deregister(ServiceTarget serviceTarget)
static ServiceTargetEvent update(String id, ServiceState serviceState)
static ServiceTargetEvent ping(String id)

EventType Enum

java
public enum EventType {
    REGISTER("register"),
    DEREGISTER("deregister"),
    UPDATE("update"),
    PING("ping");
}

Reactive Messaging

Events are published to the service-target-event channel:

java
@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:

java
@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

java
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

java
// 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

java
@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

java
// 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

ComponentPath
Discovery REST APIapps/wanaku-router-backend/.../discovery/DiscoveryResource.java
Discovery Beanapps/wanaku-router-backend/.../discovery/DiscoveryBean.java
ServiceRegistry Interfacecore/core-mcp/.../providers/ServiceRegistry.java
Infinispan Implementationcore/core-persistence/core-persistence-infinispan/.../discovery/
ServiceTarget Modelcapabilities-api/.../types/providers/ServiceTarget.java
State Modelscapabilities-api/.../types/discovery/
Registration Managercapabilities-quarkus-sdk/wanaku-capabilities-base/.../discovery/DefaultRegistrationManager.java
Service Resolverapps/wanaku-router-backend/.../support/ServiceResolver.java
Event Typesapps/wanaku-router-backend/.../common/EventType.java
SSE Endpointapps/wanaku-router-backend/.../capabilities/CapabilitiesResource.java