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 */ }
}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. The following diagram illustrates how a typical service discovery request resolves keys within a namespace scope:
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:#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