Repository navigation
perf(cachekit): an encrypted set() on Node.js awaits no promise around the synchronous encrypt (LAB-12935) - #319
Conversation
…d the synchronous encrypt
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
Kody Code Review — 1 suggested fix. 🛠️ Open Agent Prompt |
|
No actionable comments were generated in the recent review. Summary by CodeRabbit
WalkthroughThe encryption manager and cache write path now support synchronous or asynchronous encryption. Tests cover results, error handling and ciphertext storage. The instruction-count benchmark and documentation include encrypted ChangesEncrypted cache writes
Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Refactor Sequence Diagram(s)sequenceDiagram
participant CacheImpl
participant EncryptionManagerCore
participant EncryptionBinding
participant MemoryBackend
CacheImpl->>EncryptionManagerCore: encryptNow(data, cacheKey)
EncryptionManagerCore->>EncryptionBinding: encrypt data
EncryptionBinding-->>EncryptionManagerCore: ciphertext or Promise
EncryptionManagerCore-->>CacheImpl: ciphertext or Promise
CacheImpl->>MemoryBackend: store ciphertext after encryption resolves
Possibly related PRs
Merge Risk:
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
|
@coderabbitai review |
Action not completed
|
Instruction counts: PASSHead
All 41 workloads: 1 improved, 40 ok
|
|
@coderabbitai review |
Action performed
|
On Node.js an encrypted
set()no longer awaits the encryption manager around the synchronous NAPI encrypt. Inbench:ir, the newsecure-set-100bdrops from 71,719 to 70,007 instructions per op (−2.39%) andsecure-set-getfrom 490,893 to 488,909 (−0.40%).napi-encryptedreads +0.05%, from a comment-only change. Every A/A spread is 0.00%.Closes LAB-12935
Change
EncryptionManagerCore.encryptNow()(package-internal). This mirrorsdecryptNow()from #318. Once the keys are derived, a binding that returns a plain value encrypts in the call: it returns the ciphertext, or throws the sameNonceExhaustedErrororEncryptionErrorthatencrypt()rejects with. Before the keys are derived, afterdispose(), and on Workers (WebCrypto encrypts asynchronously) it returns a Promise, so those paths behave as before. The publicencrypt()is byte-identical.setEntryawaits the encrypt only when it is a thenable. This is the one call site; it uses the sharedisThenable, notinstanceof Promise, for the reason #318 gives.encryptNow()adds no await before that call.encrypt()keeps its own copy of the error classifier instead of sharing one withencryptNow(), asdecrypt()does: in #318, routingdecrypt()through a shared helper changed how V8 inlinednapi-encryptedand cost it 5.6% more instructions. A parity test holds the copies equal.Unchanged: the AAD, the keys, every stored byte, the nonce-exhaustion classification, and the set's failure handling (counted, then thrown with degradation off and absorbed with one rate-limited warning with it on).
Sibling paths
set()encrypt, Node.jswrap()miss and SWR refresh writesetEntryset()encrypt, Workersset()plaintext-set-64breads −0.02% andplaintext-set-1kb+0.00%Tests
cache.encryption-l1.test.ts: on Node.js, once the keys are derived, an encryptedset()settles in as many microtask turns as a plaintextset(), and stores ciphertext in L1 and L2. With bindings that return Promises, as Workers' do, the set takes more turns and stores the same. With the globalPromisereplaced by a subclass, a fresh cache's firstset(), which encrypts through the asyncencrypt(), stores ciphertext that a second cache decodes.manager-core.test.ts:encryptNow()returns a Promise before key derivation, a plain value after it, throwsNonceExhaustedErrorin the call on nonce exhaustion, rejects afterdispose(), and returns a binding's Promise with its rejection mapped asencrypt()maps it. A parity test runs one failing binding, throwing and rejecting, throughdecrypt()anddecryptNow()and throughencrypt()andencryptNow(), and asserts the same error class, message andcause: an Error, nonce exhaustion under both spellings, and a thrown value that is not an Error.utils/thenable.test.tstestsisThenableover a table: a native Promise, a Promise subclass, a plain thenable, an object whosethenis not a function, aUint8Array, a{ value }wrapper,nullandundefined.encryption.protocol.test.ts: theaad_hexvectors and the v0x03 layout rows now also check the AADencryptNow()hands the binding.encryptNow, which the cache calls. The write-failure suite's encrypt step now throws in the call, as the NAPI encrypt does, and one test rejects a Promise, as the Workers bindings do.Mutants: putting the await back unconditionally in
setEntryfails the settle test; checkinginstanceof Promiseinstead of a thenable fails the replaced-Promisetest; makingencryptNow()always go throughencrypt()fails eleven tests; dropping the mapping on its Promise branch fails five; dropping one spelling fromencryptionFailed()'s nonce check, or changingdecryptionFailed()'s fallback message, fails the parity test.Local runs: on Node.js 24.22.1,
vitest runpassed 2,885 tests (1 skipped),test:protocol1,004 andtest:workers307. The full suite also passed on Node.js 22.23.4 and 26.12.1. eslint,type-check,type-check:tests, prettier andcheck:workers-bundleare clean.Benchmarks
bench:irgainssecure-set-100b: an encryptedcache.set()with L1 on, 100 B of msgpack, keys derived in setup. It takes the default 2,000 warm-up ops: under--trace-opt, no compile lands in its window after 2,000 (two did after 1,000). Both builds ran 8 workloads with 3 repeats, from trees built as the CI gate builds them, with the same workload file, on the same machine (Node.js 24.22.1, valgrind 3.22.0). The branch side is this PR's head.mainsecure-set-100b(new)secure-set-get(L1 off)napi-encryptedsecure-get-l1-hitsecure-wrap-l1-hit-100bplaintext-set-64bplaintext-set-1kbget-l1-hitThe A/A spread on
mainis 0.00% for all eight. An encrypted set saves about 1,700 instructions (1,712);secure-set-getsaves about 2,000, which includes the set.napi-encryptedawaitsencrypt()anddecrypt(), whose code this PR does not change. It read 41,507 (−0.00%) at an earlier commit of this branch; the +19 instructions came with comment-only edits tomanager-core.ts, and swapping that commit'smanager-core.tsback into the head build restores 41,507. That is code layout, the effect CONTRIBUTING's "Measure it" practice describes, and well inside the workload's 4% gate.secure-set-100bhas noir-seconds.jsonrow yet, so the shard split counts it as the median row.