Skip to content

⚡ perf: Run Stream Lua Scripts via EVALSHA Instead of Per-Call EVAL - #56

Closed
devin-ai-integration[bot] wants to merge 1 commit into
devfrom
devin/1789718996-redis-evalsha-define-command
Closed

devin-ai-integration[bot] wants to merge 1 commit into
devfrom
devin/1789718996-redis-evalsha-define-command

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

In Redis mode every streamed token runs two Lua scripts through ioredis.eval(), which ships the full script source each time: CHUNK_APPEND_LUA (~3.9 KB) from RedisJobStore.appendChunk and PUBLISH_SEQ_LUA (~1 KB) from RedisEventTransport.evalPublishSequenced. With the default STREAM_DELTA_COALESCE_MS=0 a 1,000-token response sends ~5 MB of Lua text to Redis for a few tens of KB of payload, and Redis SHA1-hashes and re-parses the script body on every call.

The stream store and transport now run every script through evalScript (packages/api/src/cache/redisScript.ts), which sends EVALSHA <sha1> and falls back to EVAL <script> only when the server answers NOSCRIPT (first use, restart, SCRIPT FLUSH). Same Lua, same key/arg layout, same atomicity and return values; both evalsha and eval are already in the telemetry proxy's command set, so instrumentation is unchanged. The one pipeline.eval (owner-membership reconcile, not per token) is left as is.

Chosen over defineCommand because custom commands are attached to the raw client instance, which the instrumentIORedisClient proxy does not observe, and because it keeps the existing eval-based mocks meaningful.

How it works

-const appended = await this.redis.eval(CHUNK_APPEND_LUA, 8, key, jobKey, ...);
+const appended = await evalScript(this.redis, CHUNK_APPEND_LUA, 8, key, jobKey, ...);
evalScript(client, script, numberOfKeys, ...args)
  client.evalsha(sha1(script), numberOfKeys, ...args)   # 40-byte digest, cached per script
  └─ on NOSCRIPT → client.eval(script, numberOfKeys, ...args)   # loads it server-side once

Measured against a local Redis 7 (2,000 calls, script padded to CHUNK_APPEND_LUA's 3,908 bytes, INFO stats total_net_input_bytes delta):

bytes/call latency/call
eval (before) 3,952 66 µs
evalsha (after) 85 46 µs

Per token this removes ~5 KB of Redis ingress across the two hot scripts (≈98%) plus one SHA1 + Lua parse per call on the server, scaling with tokens × concurrent streams.

Change Type

  • Performance improvement (non-breaking)

Testing

  • npx tsc --noEmit and eslint in packages/api
  • jest RedisJobStore.spec RedisEventTransport.spec (mocks gain an evalsha that answers NOSCRIPT, so they exercise the fallback path)
  • USE_REDIS=true stream integration suites (*.stream_integration.spec.ts, 320 tests) against a single Redis 7 node and against the 3-node redis-config cluster. Tests that spied on eval to observe or fail a script now call flushScriptCache first so the next run goes through the observable EVAL fallback, or spy on evalsha when they only inject an error.

Test Configuration:

Redis 7 (docker redis:7-alpine) on 6379; redis-config/start-cluster.sh on 7001–7003.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • Local unit tests pass with my changes

Link to Devin session: https://app.devin.ai/sessions/8d4b42d64bd546ab8a7dbaa95c39f01f
Open in Devin Desktop: https://app.devin.ai/desktop/session/8d4b42d64bd546ab8a7dbaa95c39f01f?variant=devin
Requested by: @berry-13

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@berry-13 berry-13 closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant