Skip to content

Service Registry

CoSky's Service Registry manages the lifecycle of service instances within a microservice cluster. Backed by Redis and Lua scripts, it provides atomic, race-condition-free registration, deregistration, heartbeat renewal, and metadata management -- all operating within a multi-tenant namespace model.

AspectDetail
InterfaceServiceRegistry
Redis ImplementationRedisServiceRegistry
Storage EngineRedis Hash + Set + Lua scripts
Concurrency ModelReactive (Mono<Boolean>)
HeartbeatRenewInstanceService (scheduled keep-alive)
SerializationServiceInstanceCodec

ServiceRegistry Interface

The ServiceRegistry interface defines the contract for all registry operations.

MethodReturn TypeDescriptionSource
registerMono<Boolean>Registers a service instance with optional TTLServiceRegistry.kt:33
deregisterMono<Boolean>Removes a service instance from the registryServiceRegistry.kt:43
renewMono<Boolean>Renews/extends the TTL of an ephemeral instanceServiceRegistry.kt:41
setServiceMono<Boolean>Creates a named service entry in the namespace indexServiceRegistry.kt:24
removeServiceMono<Boolean>Removes a named service from the namespace indexServiceRegistry.kt:25
setMetadataMono<Boolean>Sets metadata key-value pairs on a service instanceServiceRegistry.kt:53

ServiceInstance Data Model

A ServiceInstance extends the base Instance interface and carries the following fields:

FieldTypeDefaultDescription
instanceIdString--Unique identifier: {serviceId}@{schema}#{host}#{port}
serviceIdString--Logical service name
schemaString--Protocol schema (http, https, etc.)
hostString--Host address
portInt--Port number
weightInt1Load balancer weight
isEphemeralBooleantrueEphemeral instances expire; persistent instances do not
ttlAtLongTTL_AT_FOREVER (-1)Absolute TTL expiry timestamp (epoch seconds)
metadataMap<String, String>emptyMap()Arbitrary key-value metadata attached to the instance

The isExpired property on ServiceInstance compares ttlAt against the current system time to determine whether an ephemeral instance has expired (ServiceInstance.kt:34).

ServiceInstanceCodec Serialization

ServiceInstanceCodec handles encoding and decoding of instance data to/from Redis hash fields. It uses a _ prefix for metadata keys and reserves __ for system metadata:

kotlin
// Encoding: metadata key "version" becomes "_version" in Redis
fun encodeMetadataKey(key: String): String = METADATA_PREFIX + key

The decode function (ServiceInstanceCodec.kt:57) parses the flat key-value list returned by Redis HGETALL into a ServiceInstance object.

Redis Implementation

RedisServiceRegistry

RedisServiceRegistry is the Redis-backed implementation of ServiceRegistry. It uses Lua scripts for atomic operations and maintains an in-memory map of registered ephemeral instances:

kotlin
class RedisServiceRegistry(
    private val registryProperties: RegistryProperties,
    private val redisTemplate: ReactiveStringRedisTemplate
) : ServiceRegistry

Key design points:

DiscoveryRedisScripts

DiscoveryRedisScripts loads all registry-related Lua scripts from the classpath:

ScriptResource FilePurpose
SCRIPT_REGISTRY_REGISTERregistry_register.luaAtomic instance registration
SCRIPT_REGISTRY_DEREGISTERregistry_deregister.luaAtomic instance removal
SCRIPT_REGISTRY_RENEWregistry_renew.luaTTL renewal with publish throttling
SCRIPT_REGISTRY_SET_METADATAregistry_set_metadata.luaSet instance metadata fields
SCRIPT_REGISTRY_SET_SERVICEregistry_set_service.luaCreate service in namespace index
SCRIPT_REGISTRY_REMOVE_SERVICEregistry_remove_service.luaRemove service from namespace index

RegistryProperties

RegistryProperties configures the default instance TTL:

PropertyTypeDefaultDescription
instanceTtlDuration1 minuteTime-to-live for ephemeral instances

RenewInstanceService (Heartbeat)

RenewInstanceService provides the keep-alive mechanism for ephemeral instances. It runs on a dedicated scheduler (CoSky-Renew) and periodically renews all registered ephemeral instances:

PropertyDefaultDescription
initialDelay1 secondDelay before first renewal cycle
period10 secondsInterval between renewal cycles

The renewal period must be less than RegistryProperties.instanceTtl to prevent premature expiration. The service iterates all registeredEphemeralInstances and calls renew on each one via the ServiceRegistry (RenewInstanceService.kt:70).

Sequence Diagrams

Register Flow

mermaid
sequenceDiagram
    autonumber
    participant App as Application
    participant SR as RedisServiceRegistry
    participant EMap as EphemeralInstances Map
    participant Redis as Redis (Lua Script)
    participant Sub as PubSub Subscribers

    App->>SR: register(namespace, serviceInstance)
    SR->>EMap: addEphemeralInstance(namespace, instance)
    SR->>SR: build ARGV (ttl, serviceId, instanceId, schema, host, port, weight, metadata)
    SR->>Redis: EVAL registry_register.lua KEYS=[namespace] ARGV=[...]
    Redis->>Redis: SADD svc_itc_idx:{serviceId} {instanceId}
    Redis->>Redis: SADD svc_idx {serviceId}
    Redis->>Redis: HMSET svc_itc:{instanceId} {fields}
    Redis->>Sub: PUBLISH svc_itc:{instanceId} "register"
    Redis->>Redis: EXPIRE svc_itc:{instanceId} {ttl}
    Redis-->>SR: Boolean (success)
    SR-->>App: Mono<Boolean>

Renew / Heartbeat Flow

mermaid
sequenceDiagram
    autonumber
    participant RS as RenewInstanceService
    participant SR as RedisServiceRegistry
    participant Redis as Redis (Lua Script)
    participant Sub as PubSub Subscribers

    RS->>RS: scheduler tick (every period seconds)
    RS->>RS: iterate registeredEphemeralInstances
    RS->>SR: renew(namespace, instance)
    SR->>Redis: EVAL registry_renew.lua KEYS=[namespace] ARGV=[instanceId, ttl]
    Redis->>Redis: TTL svc_itc:{instanceId} (check exists)
    alt instance exists
        Redis->>Redis: EXPIRE svc_itc:{instanceId} {ttl}
        Redis->>Redis: check publish throttle window
        alt throttle window exceeded
            Redis->>Redis: HSET __last_renew_pub_ttl_at {currentTtlAt}
            Redis->>Sub: PUBLISH svc_itc:{instanceId} "renew"
        end
        Redis-->>SR: 1 (success)
    else instance missing (TTL <= 0)
        Redis-->>SR: -1 or -2 (failure)
        SR->>SR: re-register instance automatically
    end

Deregister Flow

mermaid
sequenceDiagram
    autonumber
    participant App as Application
    participant SR as RedisServiceRegistry
    participant EMap as EphemeralInstances Map
    participant Redis as Redis (Lua Script)
    participant Sub as PubSub Subscribers

    App->>SR: deregister(namespace, instance)
    SR->>EMap: removeEphemeralInstance(namespace, instance)
    SR->>Redis: EVAL registry_deregister.lua KEYS=[namespace] ARGV=[serviceId, instanceId]
    Redis->>Redis: SREM svc_itc_idx:{serviceId} {instanceId}
    alt removed == 1
        Redis->>Sub: PUBLISH svc_itc:{instanceId} "deregister"
        Redis->>Redis: DEL svc_itc:{instanceId}
        Redis-->>SR: true
    else already removed
        Redis-->>SR: false
    end
    SR-->>App: Mono<Boolean>

Redis Key Structure

The registry uses a structured key pattern for organizing service and instance data within each namespace:

Redis KeyTypePurposeExample
{namespace}:svc_idxSETSet of all service IDs in the namespaceproduction:svc_idx
{namespace}:svc_statHASHService ID to instance count statisticsproduction:svc_stat
{namespace}:svc_itc_idx:{serviceId}SETSet of instance IDs for a given serviceproduction:svc_itc_idx:order-service
{namespace}:svc_itc:{instanceId}HASHInstance data (fields: instanceId, serviceId, schema, host, port, weight, ephemeral, ttl_at, metadata)production:svc_itc:order-service@http#10.0.1.5#8080
{namespace}:topology_idxHASHTopology index: consumer name to timestampproduction:topology_idx
{namespace}:topology:{consumer}HASHTopology deps: producer name to timestampproduction:topology:gateway-service

The instance ID format is {serviceId}@{schema}#{host}#{port} (e.g., order-service@http#10.0.1.5#8080), as defined in Instance.asInstanceId.

Class Diagram

mermaid
classDiagram
    class ServiceRegistry {
        <<interface>>
        +register(namespace, serviceInstance) Mono~Boolean~
        +deregister(namespace, serviceInstance) Mono~Boolean~
        +renew(namespace, serviceInstance) Mono~Boolean~
        +setService(namespace, serviceId) Mono~Boolean~
        +removeService(namespace, serviceId) Mono~Boolean~
        +setMetadata(namespace, serviceId, instanceId, key, value) Mono~Boolean~
        +registeredEphemeralInstances Map
    }
    class RedisServiceRegistry {
        -registryProperties: RegistryProperties
        -redisTemplate: ReactiveStringRedisTemplate
        +registeredEphemeralInstances: ConcurrentHashMap
        -registerInternal(namespace, instance) Mono~Boolean~
        -addEphemeralInstance(namespace, instance)
        -removeEphemeralInstance(namespace, instance)
    }
    class RegistryProperties {
        +instanceTtl: Duration
    }
    class RenewProperties {
        +initialDelay: Duration
        +period: Duration
    }
    class RenewInstanceService {
        -renewProperties: RenewProperties
        -serviceRegistry: ServiceRegistry
        -scheduler: Scheduler
        +start()
        +stop()
        -renew()
    }
    class DiscoveryRedisScripts {
        +SCRIPT_REGISTRY_REGISTER: RedisScript~Boolean~
        +SCRIPT_REGISTRY_DEREGISTER: RedisScript~Boolean~
        +SCRIPT_REGISTRY_RENEW: RedisScript~Long~
        +SCRIPT_REGISTRY_SET_METADATA: RedisScript~Boolean~
        +SCRIPT_REGISTRY_SET_SERVICE: RedisScript~Boolean~
        +SCRIPT_REGISTRY_REMOVE_SERVICE: RedisScript~Boolean~
    }
    class ServiceInstanceCodec {
        +encodeMetadataKey(key) String
        +encodeMetadata(preArgs, metadata) List~String~
        +decode(instanceData) ServiceInstance
    }

    ServiceRegistry <|.. RedisServiceRegistry : implements
    RedisServiceRegistry --> RegistryProperties : configured by
    RedisServiceRegistry --> DiscoveryRedisScripts : uses Lua scripts
    RedisServiceRegistry --> ServiceInstanceCodec : uses for encoding
    RenewInstanceService --> ServiceRegistry : renews via
    RenewInstanceService --> RenewProperties : configured by

References

Released under the Apache License 2.0.