Skip to content

commandTimeout can fire while a command is still queued offline #2130

Description

@MicroMilo

Summary

commandTimeout is described as a timeout for a command that does not return a reply, but Redis.sendCommand() starts the per-command timer before deciding whether the command is writable or must be queued in offlineQueue. With a pending connection, a command can reject with Command timed out while the client is still connecting and the command has not been written to Redis.

Code path

Producer / lifecycle / feedback anchors:

  • lib/redis/RedisOptions.ts:16-20: the option text describes a command not returning a reply.
  • lib/Redis.ts:465-467: command.setTimeout(this.options.commandTimeout) is called before the writable/offline-queue decision.
  • lib/Redis.ts:489-520: if the stream is not writable and offline queue is enabled, the command is pushed into offlineQueue.
  • lib/Command.ts:375-380: the timer rejects with Command timed out.
  • lib/Command.ts:488-495 and lib/Command.ts:527-533: reject clears timers and rejects the promise, while only resolve marks isResolved = true.

Steps to reproduce

Validation level: dynamic reproduction plus source-control-flow validation.

Use a public Connector option whose connect() never resolves, then issue the first command with lazyConnect: true and a short commandTimeout.

const Redis = require('ioredis')

class HangingConnector {
  connect () {
    return new Promise(() => {})
  }
  disconnect () {}
  check () { return true }
}

async function main () {
  const redis = new Redis({
    Connector: HangingConnector,
    lazyConnect: true,
    commandTimeout: 20,
    retryStrategy: null,
  })

  const started = Date.now()
  try {
    await redis.get('offline-key')
  } catch (err) {
    console.log(`elapsed_ms=${Date.now() - started}`)
    console.log(`status=${redis.status}`)
    console.log(`error.message=${err.message}`)
  } finally {
    redis.disconnect()
  }
}

main()

Observed output from the local repro:

elapsed_ms=22
status=connecting
error.message=Command timed out

Expected behavior

If commandTimeout means reply timeout, it should start when the command is actually written and awaiting a Redis reply. If it is intended to include offline queue wait time, the queued item should be removed/settled when it times out and the error should clearly describe queue wait timeout semantics.

Actual behavior

The timer starts before the command is writable. The user receives Command timed out while the client is still connecting and before the command has been sent.

Existing coverage

I searched current issues and PRs for commandTimeout offlineQueue, Command timed out connecting, and broader commandTimeout matches. I found adjacent issues such as #1604, #1431, and #1535, but they do not appear to cover this standalone sendCommand() timer placement before the writable/offline-queue decision.

Suggested fix

Either arm commandTimeout only after a command is written to the stream, or explicitly treat offline-queue waiting as part of the timeout contract and remove/mark the queued command when that timeout fires.

Suggested tests

  • Add a standalone test with a custom Connector whose connect() stays pending, lazyConnect: true, and a short commandTimeout.
  • Assert that reply-only semantics do not produce Command timed out before write.
  • If queue-timeout semantics are intended, assert the timed-out command is removed from offlineQueue and cannot later be flushed.

Submitted with Codex.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions