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
| Component | Type | Responsibility | Key File |
|---|---|---|---|
CoSky | Object | Brand constants: COSKY = "cosky", KEY_SEPARATOR = ":", VERSION | CoSky.kt |
Namespaced | Interface | Contract for components that operate within a namespace | Namespaced.kt |
NamespacedContext | Object | Volatile global namespace holder (default: "cosky") | NamespacedContext.kt |
NamespaceService | Interface | CRUD operations for namespaces (reactive Flux/Mono) | NamespaceService.kt |
RedisNamespaceService | Class | Redis SET-backed implementation of NamespaceService | RedisNamespaceService.kt |
RedisKeys | Object | Hash-tag wrapping and key utilities for Redis Cluster | RedisKeys.kt |
EventListenerContainer | Interface | Reactive event subscription contract (receive(topic): Flux<E>) | EventListenerContainer.kt |
RedisEventListenerContainer | Abstract Class | Redis Pub/Sub base implementation with error handling and logging | RedisEventListenerContainer.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:
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:
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:
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:
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:#e6edf3RedisNamespaceService stores all namespace names in a Redis SET at the key cosky-{system}:ns_idx:
companion object {
const val NAMESPACE_IDX_KEY = "${Namespaced.SYSTEM}:ns_idx"
}setNamespace(namespace)--SADDthe namespace to the SET, returnstrueif newly addedremoveNamespace(namespace)--SREMthe namespace from the SET, returnstrueif existednamespaces--SMEMBERSto 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:
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:
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| Pattern | Segment | Generator Method | Example |
|---|---|---|---|
{cosky-system}:ns_idx | Namespace index | Hardcoded in RedisNamespaceService | cosky-{system}:ns_idx |
{ns}:svc_idx | Service index | DiscoveryKeyGenerator.getServiceIdxKey() | cosky-{default}:svc_idx |
{ns}:svc_itc_idx:{serviceId} | Instance index | DiscoveryKeyGenerator.getInstanceIdxKey() | cosky-{default}:svc_itc_idx:order-service |
{ns}:svc_itc:{instanceId} | Instance data | DiscoveryKeyGenerator.getInstanceKey() | cosky-{default}:svc_itc:order-service@http#10.0.0.1#8080 |
{ns}:svc_stat | Service statistics | DiscoveryKeyGenerator.getServiceStatKey() | cosky-{default}:svc_stat |
{ns}:cfg_idx | Config index | ConfigKeyGenerator.getConfigIdxKey() | cosky-{default}:cfg_idx |
{ns}:cfg:{configId} | Config data | ConfigKeyGenerator.getConfigKey() | cosky-{default}:cfg:application.yaml |
{ns}:cfg_htr_idx:{configId} | History index | ConfigKeyGenerator.getConfigHistoryIdxKey() | cosky-{default}:cfg_htr_idx:application.yaml |
{ns}:cfg_htr:{configId}:{version} | History data | ConfigKeyGenerator.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:
interface EventListenerContainer<T, E : Any> : AutoCloseable {
fun receive(topic: T): Flux<E>
}Tis the topic type (e.g.,Stringfor namespace-level events,NamespacedServiceIdfor instance-level events,NamespacedConfigIdfor config events)Eis the event type (e.g.,Stringfor service change notifications,InstanceChangedEventfor instance mutations,ConfigChangedEventfor config mutations)- Extends
AutoCloseablefor lifecycle management
RedisEventListenerContainer Base Class
The RedisEventListenerContainer abstract class wraps Spring's ReactiveRedisMessageListenerContainer and provides error handling (specifically for CancellationException) and lifecycle management:
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:#e6edf3Event 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:
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 refreshEvent 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:
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 refreshEvent Types
| Module | Event Class | Topic Type | Possible Events |
|---|---|---|---|
| Discovery | InstanceChangedEvent | NamespacedServiceId | REGISTER, DEREGISTER, EXPIRED, RENEW, SET_METADATA |
| Discovery | String (namespace name) | String | Service index changed (service added/removed) |
| Config | ConfigChangedEvent | NamespacedConfigId | SET, 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:
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:#e6edf3Cross-References
- Architecture Overview -- High-level module structure and design principles.
- Configuration Module -- How
cosky-configbuilds on core's namespace and event abstractions. - Service Discovery Module -- How
cosky-discoverybuilds on core's namespace and event abstractions.
References
- CoSky.kt -- Brand constants
- Namespaced.kt -- Namespace contract and constants
- NamespacedContext.kt -- Global namespace state
- NamespaceService.kt -- Namespace CRUD interface
- RedisNamespaceService.kt -- Redis SET-backed namespace storage
- RedisKeys.kt -- Cluster hash-tag and key utilities
- EventListenerContainer.kt -- Reactive event subscription interface
- RedisEventListenerContainer.kt -- Redis Pub/Sub base class
- DiscoveryKeyGenerator.kt -- Service discovery key patterns
- ConfigKeyGenerator.kt -- Configuration key patterns
- InstanceChangedEvent.kt -- Discovery event types
- ConfigChangedEvent.kt -- Config event types