Summary
PUBLISH delivers its message immediately at command execution time, not at transaction commit. A Lua script runs as one transaction and is transparently re-executed from the top on OCC conflicts (retry_on_occ_error, default true) — so a script that publishes and then conflicts re-publishes on every retry.
Details
src/redis_command.cpp:3668-3671: PublishCommand::Execute calls redis_impl->Publish(chan_, message_) directly — delivery is a side effect outside the transactional write set.
src/redis_service.cpp:143 + :2606/:2625: whole-script (and simple-command) auto-retry on OCC-conflict error codes.
So EVAL "redis.call('publish', ...); redis.call('incr', KEYS[1])" under contention delivers the message once per retry attempt (at-least-once with duplicates), while the data effects commit exactly once.
Impact
Subscribers observe duplicated messages whose multiplicity depends on contention — surprising for users treating scripts as atomic. Same applies to a PUBLISH queued in MULTI if the EXEC body is retried.
Suggested fix
Buffer publishes issued inside a transaction/script and flush them after successful commit (Redis semantics: effects of a script become visible atomically), or at minimum disable auto-retry for scripts that contain PUBLISH and surface the conflict to the client.
Found during the module-docs review (#492); see docs/04-scripting-pubsub-blocking.md Gotchas.
🤖 Found with Claude Code
Summary
PUBLISHdelivers its message immediately at command execution time, not at transaction commit. A Lua script runs as one transaction and is transparently re-executed from the top on OCC conflicts (retry_on_occ_error, default true) — so a script that publishes and then conflicts re-publishes on every retry.Details
src/redis_command.cpp:3668-3671:PublishCommand::Executecallsredis_impl->Publish(chan_, message_)directly — delivery is a side effect outside the transactional write set.src/redis_service.cpp:143+:2606/:2625: whole-script (and simple-command) auto-retry on OCC-conflict error codes.So
EVAL "redis.call('publish', ...); redis.call('incr', KEYS[1])"under contention delivers the message once per retry attempt (at-least-once with duplicates), while the data effects commit exactly once.Impact
Subscribers observe duplicated messages whose multiplicity depends on contention — surprising for users treating scripts as atomic. Same applies to a PUBLISH queued in MULTI if the EXEC body is retried.
Suggested fix
Buffer publishes issued inside a transaction/script and flush them after successful commit (Redis semantics: effects of a script become visible atomically), or at minimum disable auto-retry for scripts that contain PUBLISH and surface the conflict to the client.
Found during the module-docs review (#492); see
docs/04-scripting-pubsub-blocking.mdGotchas.🤖 Found with Claude Code