KubeMQ
Client SDKsKotlinHow-to guidesRPC

Command Timeout

Configure KubeMQ command execution timeouts and handle expiry using the Kotlin SDK RPC client.

Overview

A command timeout is the deadline you attach to a single RPC call so a caller never blocks forever waiting on a handler that isn't there or isn't responding. Commands are synchronous by design — the sender is suspended until a reply arrives — so without a bound, a missing subscriber or a crashed handler turns one request into an indefinite hang that ties up a coroutine and cascades into upstream timeouts.

The timeout is set per call with timeoutMs on the command builder, and it's enforced by the broker itself, not by client-side polling: the broker tracks the deadline server-side and fails the request the moment it expires, regardless of what the calling coroutine is doing. When the window elapses with no response, sendCommand returns a response with a populated error instead of a clean success — your signal to retry or fall back.

Gotchas: a command timeout is a broker-enforced deadline, not a coroutine withTimeout or job cancellation, so don't assume cancelling the coroutine also stops the broker from waiting; a slow-but-alive handler and a completely absent one produce the same timeout error, so you can't tell them apart from the response alone; and setting timeoutMs too short under normal load turns transient latency into false failures.

Prerequisites

  • KubeMQ server running on localhost:50000
  • Kotlin SDK installed (implementation("io.kubemq.sdk:kubemq-sdk-kotlin:1.0.1"))

Code

CommandTimeoutExample.kt
package io.kubemq.sdk.examples.commands

import io.kubemq.sdk.client.KubeMQClient
import io.kubemq.sdk.cq.commandMessage
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

private const val ADDRESS = "localhost:50000"
private const val CLIENT_ID = "kotlin-commands-command-timeout"
private const val CHANNEL = "kotlin-commands.command-timeout"

fun main() = runBlocking {
    val client = KubeMQClient.cq {
        address = ADDRESS
        clientId = CLIENT_ID
    }

    client.use {
        try {
            client.createCommandsChannel(CHANNEL)

            // Subscribe to handle commands (handler intentionally delays 3s)
            val subJob = launch {
                client.subscribeToCommands {
                    channel = CHANNEL
                }.collect { cmd ->
                    println("Handler received: ${String(cmd.body)}, delaying 3s...")
                    delay(3000)
                    val response = cmd.respond {
                        executed = true
                    }
                    client.sendCommandResponse(response)
                    println("Handler sent response.")
                }
            }

            delay(500)

            // Send command with short timeout (expect timeout)
            println("Sending command with 1s timeout (handler takes 3s)...")
            try {
                val resp = client.sendCommand(commandMessage {
                    channel = CHANNEL
                    body = "Slow command".toByteArray()
                    timeoutMs = 1000
                })
                if (!resp.executed && resp.error.isNotEmpty()) {
                    println("Timeout (expected): ${resp.error}")
                    println("  executed=${resp.executed}")
                } else {
                    println("Unexpected success: executed=${resp.executed}")
                }
            } catch (e: Exception) {
                println("Exception (timeout): ${e.message}")
            }

            // Wait for handler to finish before sending second command
            delay(4000)

            // Send command with sufficient timeout (expect success)
            println("\nSending command with 5s timeout (handler takes 3s)...")
            try {
                val resp = client.sendCommand(commandMessage {
                    channel = CHANNEL
                    body = "Normal command".toByteArray()
                    timeoutMs = 5000
                })
                println("Success: executed=${resp.executed}")
            } catch (e: Exception) {
                println("Error: ${e.message}")
            }

            subJob.cancel()
        } finally {
            try { client.deleteCommandsChannel(CHANNEL) } catch (_: Exception) {}
        }
        println("Done.")
    }
}

How It Works

  • The handler intentionally delays 3 seconds to simulate slow processing.
  • A 1-second timeout expires before the handler responds, resulting in a timeout error.
  • A 5-second timeout allows the handler enough time to respond successfully.
  • Set timeoutMs based on expected processing time plus network latency.

Was this page helpful?

On this page