#1166·asynq

[BUG] EnqueueContext 丢失由 Lua 脚本返回的底层 Redis 错误

作者: IceLocke创建于 2026年7月13日更新于 2026年7月13日
标签bug

Describe the bug

Client.EnqueueContext loses the underlying Redis error when the enqueue Lua script fails. The background is, sometimes due to network jitter, the Redis broker may become temporarily unavailable to the Asynq client. In application log we can see this error returned by EnqueueContext:

UNKNOWN: Redis eval error: i/o timeout

The error message is preserved. However, the original typed error is converted to a string, so callers cannot inspect it with errors.Is or errors.As and to retry with exponential backoff if it is a network problem. The cause appears to be the use of fmt.Sprintf in the Lua script helpers:

go
return 0, errors.E(
	op,
	errors.Unknown,
	fmt.Sprintf("Redis eval error: %v", err),
)

Environment (please complete the following information):

  • OS: Linux
  • Go version: Go 1.25.0
  • asynq package version: v0.26.0
  • go-Redis package version: v9.18.0
  • Redis/Valkey version: N/A for the minimal reproduction below; the error is injected through a go-Redis Hook, so no Redis server is required The same behavior is also present on the current Asynq master branch as of commit d135f1439bee74e989b7f9b41ecd542cc87f024a.

To Reproduce

The following example uses only public Asynq and go-Redis APIs. It injects a go-Redis client through asynq.RedisConnOpt, makes the initial SADD succeed, and makes EVALSHA/EVAL return a typed network timeout. No Redis server is required (like the case that Redis is not available).

  1. Create the following go.mod:
go
module example.com/asynq-error-repro
go 1.25.0
require (
	GitHub.com/hibiken/asynq v0.26.0
	GitHub.com/Redis/go-Redis/v9 v9.18.0
)
  1. Create main.go:
go
package main
import (
	"context"
	"errors"
	"fmt"
	"net"
	"GitHub.com/hibiken/asynq"
	"GitHub.com/Redis/go-Redis/v9"
)

// redisConnOpt lets Asynq use the go-Redis client configured by this example. It implements asynq.RedisConnOpt using only public APIs.
type redisConnOpt struct {
	client Redis.UniversalClient
}

// MakeRedisClient returns the go-Redis client configured by this example.
func (o redisConnOpt) MakeRedisClient() any {
	return o.client
}

// timeoutError is the typed network error that should remain discoverable in the error chain returned by EnqueueContext.
type timeoutError struct{}

// Error returns the error message.
func (*timeoutError) Error() string { return "i/o timeout" }

// Timeout returns true if the error is a network error.
func (*timeoutError) Timeout() bool { return true }

// evalFailureHook avoids needing a real Redis server. The first SADD performed by Asynq succeeds, while the enqueue Lua script fails with cause.
type evalFailureHook struct {
	cause error
}

// DialHook returns the next Redis.DialHook.
func (h evalFailureHook) DialHook(next Redis.DialHook) Redis.DialHook {
	return next
}

// ProcessHook returns the next Redis.ProcessHook.
func (h evalFailureHook) ProcessHook(next ZZTERM29ZZ.ProcessHook) ZZTERM30ZZ.ProcessHook {
	return func(ctx context.Context, cmd ZZTERM31ZZ.Cmder) error {
		switch cmd.Name() {
		case "sadd":
			return nil
		case "evalsha", "eval":
			return h.cause
…