Skip to content

Manual flushing of commands using multi/exec (single theaded) works with async() but not sync()... #3632

@vachagan-balayan-bullish

Description

Attaching AI generated slop that confirms this. I'm not sure if its actual deadlock, but i know it never returns if i'm in using sync() interface and calling multi() and have autoflushing off...

package temp

import io.lettuce.core.RedisClient
import kotlin.concurrent.thread

fun main() {
    val redisClient = RedisClient.create("redis://localhost:6379")

    println("===========================================")
    println("SYNC vs ASYNC with Manual Flushing Demo")
    println("===========================================\n")

    // EXAMPLE 1: SYNC with manual flush - DEADLOCKS
    println("1️⃣ SYNC with manual flush (will deadlock):")
    println("-------------------------------------------")

    val connection1 = redisClient.connect()
    val deadlockThread = thread(start = true) {
        try {
            connection1.setAutoFlushCommands(false)  // Disable auto-flush
            val sync = connection1.sync()

            println("   Calling sync.multi()...")
            sync.multi()  // ❌ DEADLOCK HERE! Waits forever for response

            println("   ⚠️ This line is never reached!")
            sync.set("key1", "value1")
            sync.exec()
            connection1.flushCommands()
        } catch (e: InterruptedException) {
            println("   ❌ Thread interrupted due to deadlock!")
        }
    }

    // Wait 2 seconds, then check if thread is still blocked
    Thread.sleep(2000)
    if (deadlockThread.isAlive) {
        println("   ❌ DEADLOCK CONFIRMED: sync.multi() is still waiting...")
        println("   → Command was queued but never sent to Redis")
        println("   → sync.multi() blocks waiting for response")
        println("   → flushCommands() is never reached\n")
        deadlockThread.interrupt()
    }
    connection1.setAutoFlushCommands(true)
    connection1.close()

    // EXAMPLE 2: ASYNC with manual flush - WORKS PERFECTLY
    println("2️⃣ ASYNC with manual flush (works perfectly):")
    println("----------------------------------------------")

    val connection2 = redisClient.connect()
    try {
        connection2.setAutoFlushCommands(false)  // Disable auto-flush
        val async = connection2.async()

        println("   Calling async.multi()...")
        val multiFuture = async.multi()  // ✅ Returns Future immediately

        println("   Adding SET commands...")
        val set1 = async.set("key1", "value1")
        val set2 = async.set("key2", "value2")
        val set3 = async.set("key3", "value3")

        println("   Calling async.exec()...")
        val execFuture = async.exec()  // ✅ Returns Future immediately

        println("   Flushing all commands to Redis...")
        connection2.flushCommands()  // ✅ Sends MULTI + 3 SETs + EXEC in one go

        println("   Waiting for results...")
        multiFuture.get()  // Now wait for MULTI response
        val results = execFuture.get()  // Wait for EXEC response

        println("   ✅ SUCCESS: Transaction completed!")
        println("   → All commands sent in 1 network round-trip")
        println("   → Transaction executed ${results.size()} operations\n")

    } finally {
        connection2.setAutoFlushCommands(true)
        connection2.close()
    }

    // SUMMARY
    println("===========================================")
    println("SUMMARY:")
    println("===========================================")
    println("❌ SYNC + Manual Flush = DEADLOCK")
    println("   sync.multi() blocks immediately, waiting for")
    println("   a response that can't come until flushCommands()")
    println("   is called, but we never reach that line!")
    println()
    println("✅ ASYNC + Manual Flush = WORKS")
    println("   async.multi() returns a Future immediately,")
    println("   allowing us to queue all commands and then")
    println("   flush them together in a single network call.")
    println()
    println("RULE: Always use async() with manual flushing!")

    redisClient.shutdown()
}

note that using sync() without manual flushing but using multi/exec works fine as it should, without blocking or anything (but brilliantly sending network requests for every single redis operation)...

  1. if sync() is not supposed to be used with manual flushing, throw an exception or document it at least.
  2. who thinks that multi/exec having this default behaviour is a good idea? if you know i'm starting a batch/atomic operation don't you expect that exec is going to be called? and since its going to be called why wouldn't you send the whole thing in one network request (or chunk it down if its too big)?

this library continues to amaze me, in the worst possible ways

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions