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 */ }
}

RedisKeys is an explicit utility, not a global key interceptor. NamespaceService, ConfigKeyGenerator, DiscoveryKeyGenerator, and the REST API preserve the namespace string supplied by the caller. Regardless of Redis topology, CoSkyProperties applies RedisKeys.ofKey(true, namespace) to the Spring Cloud client's configured namespace, so its effective value must exactly match the namespace used by the REST API and RBAC bindings.

For every deployment, choose a namespace containing a non-empty hash tag before writing data—for example, {dev}, {prod}, or cosky-{dev}. This is required for multi-key Lua operations in Redis Cluster and prevents standalone REST clients from diverging from Spring's effective namespace. Plain dev and tagged {dev} generate different physical keys. Converting an existing namespace requires an explicit data migration and rollback plan; changing only configuration makes the old data appear missing.

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. CoSkyProperties wraps the Spring Cloud client's configured namespace before storing it as the default context; explicitly supplied namespaces pass through unchanged. The following diagram illustrates how a typical service discovery request resolves keys within a namespace scope:

mermaid
flowchart TD
    A[Spring client configures namespace] --> B[CoSkyProperties applies RedisKeys.ofKey]
    B --> C[NamespacedContext stores effective namespace]
    D[Client calls getInstances] --> E{Namespace provided?}
    E -->|No| F[Use NamespacedContext.namespace]
    E -->|Yes| G[Use provided namespace unchanged]
    C -.-> F
    F --> H[DiscoveryKeyGenerator interpolates namespace unchanged]
    G --> H
    H --> I[EVALSHA discovery_get_instances.lua]
    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.