How CoSky Manages Service Instance Heartbeats and Renewals: A Deep Dive into the Source Code
CoSky guarantees service instance liveness through a coordinated three-layer architecture involving configurable RenewProperties, a periodic RenewInstanceService scheduler, and atomic Redis Lua scripts that extend TTLs while emitting Pub/Sub events.
In the ahoo-wang/cosky service discovery framework, maintaining accurate registry state requires instances to periodically prove they are alive. The heartbeat mechanism ensures that ephemeral service instances remain visible to consumers without manual intervention, automatically handling network partitions and client crashes through TTL-based expiration and renewal logic.
Configuration: Defining Heartbeat Intervals with RenewProperties
The heartbeat cadence is governed by RenewProperties, located in cosky-discovery/src/main/kotlin/me/ahoo/cosky/discovery/RenewProperties.kt. This data class defines two critical parameters:
- initialDelay: The wait time before the first heartbeat (default 1 second)
- period: The interval between subsequent heartbeats (default 10 seconds)
These values are exposed through CoSkyRegistryProperties.renew, allowing Spring Boot applications to override defaults via application.yaml. The configuration binds directly to the RenewInstanceService constructor, ensuring type-safe initialization without manual bean wiring.
The Renewal Scheduler: How RenewInstanceService Orchestrates Heartbeats
The RenewInstanceService class in cosky-discovery/src/main/kotlin/me/ahoo/cosky/discovery/RenewInstanceService.kt acts as the orchestration layer, translating configuration into scheduled execution.
Starting the Periodic Scheduler
When start() is invoked—typically during CoSkyServiceRegistry initialization—the service creates a single-threaded Scheduler that executes renew() at a fixed rate:
// From RenewInstanceService.kt lines 48-58
fun start() {
scheduler = Executors.newSingleThreadScheduledExecutor()
scheduler.scheduleAtFixedRate(
this::renew,
renewProperties.initialDelay.toMillis(),
renewProperties.period.toMillis(),
TimeUnit.MILLISECONDS
)
}
This design isolates the renewal loop from the reactive service registry implementation, preventing blocking operations from interfering with request handling.
The renew() Method Execution Flow
Each scheduled tick triggers renew() (lines 70-86), which iterates over serviceRegistry.registeredEphemeralInstances and invokes serviceRegistry.renew(namespace, instance) for every registered ephemeral instance:
private fun renew() {
serviceRegistry.registeredEphemeralInstances.forEach { (namespace, instance) ->
try {
serviceRegistry.renew(namespace, instance)
} catch (e: Exception) {
// Logging and error handling
}
}
}
The method filters only ephemeral instances, as persistent services do not require heartbeat-based lifecycle management.
Redis-Backed Heartbeat Logic: ServiceRegistry.renew Implementation
The concrete implementation resides in cosky-discovery/src/main/kotlin/me/ahoo/cosky/discovery/redis/RedisServiceRegistry.kt. Here, the renew() method (lines 60-86) executes the atomic registry_renew.lua script via Redis's EVALSHA command.
The registry_renew.lua Script
Located at cosky-discovery/src/main/resources/registry_renew.lua, this Lua script performs three operations atomically:
- TTL Validation: Checks the remaining TTL of the instance key
- TTL Extension: Resets the expiration to
instanceTtlif the key exists - Event Publishing: If the previous publish time exceeds the tolerance threshold, emits a
"renew"message to the instance's Pub/Sub channel
The script returns the current TTL status. If the key has expired (TTL ≤ 0), the Java caller detects this condition and triggers automatic re-registration.
Automatic Re-registration on Failure
When RedisServiceRegistry.renew() detects a non-positive return from the Lua script, it immediately falls back to register() to recreate the instance entry:
// Simplified logic from RedisServiceRegistry.kt
val result = redisScript.evalSha(args)
if (result <= 0) {
// Instance expired or missing - recreate it
register(namespace, instance)
}
This self-healing mechanism ensures that temporary Redis connection losses or extended GC pauses do not permanently evict healthy instances from the registry.
Configuring CoSky Heartbeat Behavior
Override the default heartbeat intervals through Spring Boot configuration:
cosky:
discovery:
registry:
renew:
initialDelay: 2s # Delay before first heartbeat
period: 5s # Heartbeat interval
For programmatic control during testing or maintenance windows:
@Autowired
lateinit var renewInstanceService: RenewInstanceService
// Pause heartbeats temporarily
renewInstanceService.stop()
// Resume with fresh scheduling
renewInstanceService.start()
Summary
- RenewProperties defines heartbeat timing with 1-second initial delay and 10-second period defaults, configurable via Spring Boot.
- RenewInstanceService schedules
renew()calls using a dedicated single-threaded executor, iterating only over ephemeral instances. - RedisServiceRegistry executes
registry_renew.luaatomically to extend Redis key TTLs and optionally publish renewal events. - The Lua script checks key existence before extending TTL, returning a status code that triggers automatic re-registration if the instance expired.
- This architecture separates scheduling concerns from state mutation, providing fault-tolerant heartbeats that survive network partitions through Redis-backed persistence.
Frequently Asked Questions
What is the default heartbeat interval in CoSky?
CoSky defaults to a 1-second initial delay followed by 10-second periods between heartbeats. These values are defined in RenewProperties and can be overridden via cosky.discovery.registry.renew configuration properties.
Does CoSky require heartbeats for all service instance types?
No. CoSky only requires heartbeats for ephemeral instances. The RenewInstanceService explicitly filters registeredEphemeralInstances during its iteration, meaning persistent service registrations do not participate in the renewal cycle and remain in the registry indefinitely until explicitly deregistered.
How does CoSky handle missed heartbeats or Redis failures?
If the registry_renew.lua script detects that an instance key has expired (returning TTL ≤ 0), or if the renewal fails due to network issues, RedisServiceRegistry automatically falls back to calling register(). This recreates the instance entry with a fresh TTL, ensuring that temporary connectivity issues do not permanently remove healthy instances.
Can I disable automatic heartbeats in CoSky?
While you cannot completely disable the RenewInstanceService bean without custom configuration, you can effectively stop heartbeats by calling renewInstanceService.stop() or by registering instances as persistent rather than ephemeral. Persistent instances bypass the renewal mechanism entirely and rely on explicit deregistration for removal.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →