# How CoSky Manages Service Instance Heartbeats and Renewals: A Deep Dive into the Source Code

> Explore CoSky source code to understand its 3-layer architecture for managing service instance heartbeats and renewals using Redis Lua scripts for TTL extension and Pub/Sub events.

- Repository: [Ahoo Wang/cosky](https://github.com/ahoo-wang/cosky)
- Tags: deep-dive
- Published: 2026-02-23

---

**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`](https://github.com/ahoo-wang/cosky/blob/main/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`](https://github.com/ahoo-wang/cosky/blob/main/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`](https://github.com/ahoo-wang/cosky/blob/main/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:

```kotlin
// 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:

```kotlin
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`](https://github.com/ahoo-wang/cosky/blob/main/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`](https://github.com/ahoo-wang/cosky/blob/main/registry_renew.lua) script via Redis's `EVALSHA` command.

### The registry_renew.lua Script

Located at [`cosky-discovery/src/main/resources/registry_renew.lua`](https://github.com/ahoo-wang/cosky/blob/main/cosky-discovery/src/main/resources/registry_renew.lua), this Lua script performs three operations atomically:

1. **TTL Validation**: Checks the remaining TTL of the instance key
2. **TTL Extension**: Resets the expiration to `instanceTtl` if the key exists
3. **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:

```kotlin
// 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:

```yaml
cosky:
  discovery:
    registry:
      renew:
        initialDelay: 2s    # Delay before first heartbeat

        period: 5s          # Heartbeat interval

```

For programmatic control during testing or maintenance windows:

```kotlin
@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.lua`](https://github.com/ahoo-wang/cosky/blob/main/registry_renew.lua) atomically 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`](https://github.com/ahoo-wang/cosky/blob/main/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.