Skip to content

Core Module

The cosky-core module is the foundation of the entire CoSky platform. Every other module -- config, discovery, Spring Cloud starters, and the REST API -- depends on it either directly or transitively. Core provides three fundamental abstractions: the namespace model for multi-tenant resource isolation, the Redis key generation framework for consistent key naming and cluster compatibility, and the event listener container interface for reactive Pub/Sub consumption. This module has zero knowledge of service discovery or configuration semantics; it operates purely at the infrastructure level.

At a Glance

ComponentTypeResponsibilityKey File
CoSkyObjectBrand constants: COSKY = "cosky", KEY_SEPARATOR = ":", VERSIONCoSky.kt
NamespacedInterfaceContract for components that operate within a namespaceNamespaced.kt
NamespacedContextObjectVolatile global namespace holder (default: "cosky")NamespacedContext.kt
NamespaceServiceInterfaceCRUD operations for namespaces (reactive Flux/Mono)NamespaceService.kt
RedisNamespaceServiceClassRedis SET-backed implementation of NamespaceServiceRedisNamespaceService.kt
RedisKeysObjectHash-tag wrapping and key utilities for Redis ClusterRedisKeys.kt
EventListenerContainerInterfaceReactive event subscription contract (receive(topic): Flux<E>)EventListenerContainer.kt
RedisEventListenerContainerAbstract ClassRedis Pub/Sub base implementation with error handling and loggingRedisEventListenerContainer.kt

Namespace Model

Every resource in CoSky -- service instances, configurations, statistics -- is scoped to a namespace. This provides logical multi-tenancy within a single Redis instance. The namespace model consists of three cooperating components.

The Namespaced Interface

The Namespaced interface declares a single property:

kotlin
interface Namespaced {
    val namespace: String
        get() = DEFAULT

    companion object {
        const val DEFAULT: String = "${CoSky.COSKY}-{default}"   // "cosky-{default}"
        const val SYSTEM: String = "${CoSky.COSKY}-{system}"     // "cosky-{system}"
    }
}

The DEFAULT namespace is the conventional namespace for general-purpose use. The SYSTEM namespace (cosky-{system}) is reserved for platform-internal data such as the namespace index itself. Implementors can override the default to provide custom namespace resolution.

NamespacedContext -- Global Namespace State

NamespacedContext is a singleton object that holds the current process-wide namespace:

kotlin
object NamespacedContext : Namespaced {
    @Volatile
    override var namespace: String = CoSky.COSKY
}

The @Volatile annotation ensures visibility across threads. Spring Cloud auto-configuration sets this value at startup from spring.cloud.cosky.namespace in CoSkyAutoConfiguration:

kotlin
init {
    NamespacedContext.namespace = coSkyProperties.namespace
}

This design allows service methods throughout the codebase to use NamespacedContext.namespace as a default parameter, so callers rarely need to pass the namespace explicitly. For example, ConfigService.getConfig() defaults to NamespacedContext.namespace.

NamespaceService -- Namespace CRUD

The NamespaceService interface provides reactive operations for managing namespaces:

mermaid
classDiagram
    class NamespaceService {
        <<interface>>
        +namespaces: Flux~String~
        +setNamespace(namespace: String): Mono~Boolean~
        +removeNamespace(namespace: String): Mono~Boolean~
    }

    class RedisNamespaceService {
        -redisTemplate: ReactiveStringRedisTemplate
        +namespaces: Flux~String~
        +setNamespace(namespace: String): Mono~Boolean~
        +removeNamespace(namespace: String): Mono~Boolean~
    }

    class NamespacedContext {
        <<object>>
        +namespace: String = "cosky"
    }

    class Namespaced {
        <<interface>>
        +namespace: String
    }

    NamespaceService <|.. RedisNamespaceService
    Namespaced <|.. NamespacedContext
    NamespaceService ..> NamespacedContext : uses SYSTEM namespace for index key

    style NamespaceService fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RedisNamespaceService fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style NamespacedContext fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Namespaced fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

RedisNamespaceService stores all namespace names in a Redis SET at the key cosky-{system}:ns_idx:

kotlin
companion object {
    const val NAMESPACE_IDX_KEY = "${Namespaced.SYSTEM}:ns_idx"
}
  • setNamespace(namespace) -- SADD the namespace to the SET, returns true if newly added
  • removeNamespace(namespace) -- SREM the namespace from the SET, returns true if existed
  • namespaces -- SMEMBERS to list all registered namespaces

Key Generation

All Redis keys in CoSky follow a consistent pattern: {namespace}:{domain_segment}:{identifier}. The colon (:) separator is defined in CoSky.KEY_SEPARATOR.

The RedisKeys Utility

The RedisKeys object provides cluster-aware key wrapping. In Redis Cluster, keys sharing the same hash tag {...} are guaranteed to land on the same hash slot, enabling multi-key Lua scripts:

kotlin
object RedisKeys {
    fun ofKey(isCluster: Boolean, key: String): String {
        return if (!isCluster) key else hashTag(key)
    }

    fun hasWrap(key: String): Boolean { /* checks for { } */ }
    fun wrap(key: String): String = "{$key}"
    fun unwrap(key: String): String { /* strips { } */ }
    fun hashTag(key: String): String { /* wraps if not already wrapped */ }
}

Key Pattern Reference

The following table shows how keys are structured across all domains. Each module contributes its own key generator (DiscoveryKeyGenerator, ConfigKeyGenerator) that uses the same {namespace}: prefix convention:

mermaid
flowchart LR
    subgraph Namespace Keys
        NK["cosky-{system}:ns_idx<br><i>SET - all namespaces</i>"]
    end

    subgraph Discovery Keys
        SK["{ns}:svc_idx<br><i>SET - service IDs</i>"]
        IK["{ns}:svc_itc_idx:{svcId}<br><i>SET - instance IDs</i>"]
        ID["{ns}:svc_itc:{instanceId}<br><i>STRING+TTL - instance data</i>"]
        ST["{ns}:svc_stat<br><i>HASH - statistics</i>"]
    end

    subgraph Config Keys
        CK["{ns}:cfg_idx<br><i>SET - config IDs</i>"]
        CD["{ns}:cfg:{configId}<br><i>HASH - config data</i>"]
        HI["{ns}:cfg_htr_idx:{configId}<br><i>ZSET - version index</i>"]
        CH["{ns}:cfg_htr:{configId}:{ver}<br><i>HASH - history snapshot</i>"]
    end

    NK -.->|stores namespace| SK
    NK -.->|stores namespace| CK
    SK -->|serviceId links to| IK
    IK -->|instanceId links to| ID
    CK -->|configId links to| CD
    CD -->|version links to| HI
    HI -->|version maps to| CH

    style NK fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SK fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IK fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ID fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ST fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CK fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CD fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style HI fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CH fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
PatternSegmentGenerator MethodExample
{cosky-system}:ns_idxNamespace indexHardcoded in RedisNamespaceServicecosky-{system}:ns_idx
{ns}:svc_idxService indexDiscoveryKeyGenerator.getServiceIdxKey()cosky-{default}:svc_idx
{ns}:svc_itc_idx:{serviceId}Instance indexDiscoveryKeyGenerator.getInstanceIdxKey()cosky-{default}:svc_itc_idx:order-service
{ns}:svc_itc:{instanceId}Instance dataDiscoveryKeyGenerator.getInstanceKey()cosky-{default}:svc_itc:order-service@http#10.0.0.1#8080
{ns}:svc_statService statisticsDiscoveryKeyGenerator.getServiceStatKey()cosky-{default}:svc_stat
{ns}:cfg_idxConfig indexConfigKeyGenerator.getConfigIdxKey()cosky-{default}:cfg_idx
{ns}:cfg:{configId}Config dataConfigKeyGenerator.getConfigKey()cosky-{default}:cfg:application.yaml
{ns}:cfg_htr_idx:{configId}History indexConfigKeyGenerator.getConfigHistoryIdxKey()cosky-{default}:cfg_htr_idx:application.yaml
{ns}:cfg_htr:{configId}:{version}History dataConfigKeyGenerator.getConfigHistoryKey()cosky-{default}:cfg_htr:application.yaml:3

Event System

CoSky uses an event-driven architecture built on Redis Pub/Sub to propagate changes in real time. The event system is defined by two layers: a generic interface in cosky-core and domain-specific implementations in cosky-discovery and cosky-config.

EventListenerContainer Interface

The EventListenerContainer<T, E> interface is a minimal reactive contract:

kotlin
interface EventListenerContainer<T, E : Any> : AutoCloseable {
    fun receive(topic: T): Flux<E>
}
  • T is the topic type (e.g., String for namespace-level events, NamespacedServiceId for instance-level events, NamespacedConfigId for config events)
  • E is the event type (e.g., String for service change notifications, InstanceChangedEvent for instance mutations, ConfigChangedEvent for config mutations)
  • Extends AutoCloseable for lifecycle management

RedisEventListenerContainer Base Class

The RedisEventListenerContainer abstract class wraps Spring's ReactiveRedisMessageListenerContainer and provides error handling (specifically for CancellationException) and lifecycle management:

mermaid
classDiagram
    class EventListenerContainer~T, E~ {
        <<interface>>
        +receive(topic: T): Flux~E~
        +close() void
    }

    class RedisEventListenerContainer~T, E~ {
        <<abstract>>
        #delegate: ReactiveRedisMessageListenerContainer
        +receive(topic: T): Flux~E~
        #receiveEvent(topic: T): Flux~E~*
        +close() void
    }

    class RedisInstanceEventListenerContainer {
        +receiveEvent(topic: NamespacedServiceId): Flux~InstanceChangedEvent~
    }

    class RedisServiceEventListenerContainer {
        +receiveEvent(topic: String): Flux~String~
    }

    class RedisConfigEventListenerContainer {
        +receiveEvent(topic: NamespacedConfigId): Flux~ConfigChangedEvent~
    }

    class InstanceEventListenerContainer {
        <<interface>>
    }

    class ServiceEventListenerContainer {
        <<interface>>
    }

    class ConfigEventListenerContainer {
        <<interface>>
    }

    EventListenerContainer <|.. RedisEventListenerContainer
    EventListenerContainer <|.. InstanceEventListenerContainer
    EventListenerContainer <|.. ServiceEventListenerContainer
    EventListenerContainer <|.. ConfigEventListenerContainer
    RedisEventListenerContainer <|-- RedisInstanceEventListenerContainer
    RedisEventListenerContainer <|-- RedisServiceEventListenerContainer
    RedisEventListenerContainer <|-- RedisConfigEventListenerContainer
    InstanceEventListenerContainer <|.. RedisInstanceEventListenerContainer
    ServiceEventListenerContainer <|.. RedisServiceEventListenerContainer
    ConfigEventListenerContainer <|.. RedisConfigEventListenerContainer

    style EventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RedisEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RedisInstanceEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RedisServiceEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RedisConfigEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style InstanceEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ServiceEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ConfigEventListenerContainer fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Event Flow -- Service Instance Registration

The following sequence diagram traces the complete event flow when a service instance is registered and an external subscriber observes the change:

mermaid
sequenceDiagram
    autonumber
    participant Client as Client App
    participant Reg as RedisServiceRegistry
    participant Redis as Redis
    participant Sub as RedisInstanceEventListenerContainer
    participant App as Subscriber App

    Client->>Reg: register(namespace, serviceInstance)
    Reg->>Redis: EVALSHA registry_register.lua<br>(SADD svc_itc_idx, SET svc_itc data, PUBLISH, INCR stat)
    Redis-->>Redis: Execute Lua atomically
    Redis->>Redis: PUBLISH {ns}:svc_itc:{instanceId} "register"
    Redis->>Sub: Pub/Sub message delivered
    Sub->>Sub: Decode channel to NamespacedServiceId<br>Decode message to InstanceChangedEvent
    Sub->>App: InstanceChangedEvent(namespacedServiceId, REGISTER, instance)
    App->>App: Update local cache or refresh

Event Flow -- Config Change Propagation

Configuration changes follow a similar Pub/Sub pattern. When a config is set or rolled back, the Lua script publishes to the config key channel, and RedisConfigEventListenerContainer decodes the message into a ConfigChangedEvent:

mermaid
sequenceDiagram
    autonumber
    participant Admin as Admin / API
    participant Svc as RedisConfigService
    participant Redis as Redis
    participant Sub as RedisConfigEventListenerContainer
    participant Refresher as CoSkyConfigRefresher

    Admin->>Svc: setConfig(namespace, configId, data)
    Svc->>Redis: EVALSHA config_set.lua<br>(HSET cfg, ZADD cfg_htr_idx, HSET cfg_htr, PUBLISH)
    Redis-->>Redis: Execute Lua atomically
    Redis->>Redis: PUBLISH {ns}:cfg:{configId} "set"
    Redis->>Sub: Pub/Sub message delivered
    Sub->>Sub: Decode to ConfigChangedEvent<br>(NamespacedConfigId, SET)
    Sub->>Refresher: ConfigChangedEvent received
    Refresher->>Refresher: Trigger Spring Environment refresh

Event Types

ModuleEvent ClassTopic TypePossible Events
DiscoveryInstanceChangedEventNamespacedServiceIdREGISTER, DEREGISTER, EXPIRED, RENEW, SET_METADATA
DiscoveryString (namespace name)StringService index changed (service added/removed)
ConfigConfigChangedEventNamespacedConfigIdSET, ROLLBACK, REMOVE

Namespace-Scoped Operations Flow

Every operation in CoSky flows through the namespace model. The following diagram illustrates how a typical service discovery request resolves keys within a namespace scope:

mermaid
flowchart TD
    A[Client calls getInstances] --> B{Namespace provided?}
    B -->|No| C[Use NamespacedContext.namespace]
    B -->|Yes| D[Use provided namespace]
    C --> E[Construct key: ns:svc_itc_idx:serviceId]
    D --> E
    E --> F{Is Redis Cluster?}
    F -->|Yes| G[Apply hash tag via RedisKeys.ofKey]
    F -->|No| H[Use key as-is]
    G --> I[EVALSHA discovery_get_instances.lua]
    H --> I
    I --> J[Read instance data from svc_itc keys]
    J --> K[Decode via ServiceInstanceCodec]
    K --> L[Return Flux of ServiceInstance]

    style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style D fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style E fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style F fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style G fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style H fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style I fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style J fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style K fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Cross-References

References

Released under the Apache License 2.0.