Recent Releases of https://github.com/petergoldstein/dalli
https://github.com/petergoldstein/dalli - v3.2.10
- Fix
Rack::Session::Dalliwith connection_pool 3.x, which only accepts keyword arguments (backport of #1051) - Add
loggeras a runtime dependency, since it is no longer a default gem as of Ruby 4.0 (backport of #1010, olleolleolle) - CI: patch memcached 1.5.22 so it builds on ubuntu-22.04, and run tests there (backport of #1033)
- Ruby
Published by petergoldstein 12 days ago
https://github.com/petergoldstein/dalli - v3.2.9
Security release. Fixes GHSA-6wmv-xq9m-fmp7, a memcached command injection through numeric arguments. Upgrading is recommended.
Security:
- Fix memcached command injection through the
incr/decrdefault with the meta protocol (GHSA-6wmv-xq9m-fmp7)- With
protocol: :meta, thedefaultargument ofincr/decrwas written into the command without conversion, so a String containing CRLF injected additional memcached commands (e.g.set,flush_all) on the connection. The default binary protocol is not affected defaultmust now be an Integer, or a String of decimal digits; anything else raisesArgumentErrorbefore a request is sent- As defense in depth, the meta
RequestFormatternow converts every numeric flag it writes (D,J,N,T) to an Integer - Thanks to oss-security-shop for the report
- With
- Ruby
Published by petergoldstein 12 days ago
https://github.com/petergoldstein/dalli - v4.3.4
Security release. Fixes GHSA-6wmv-xq9m-fmp7, a memcached command injection through numeric arguments. Upgrading is recommended.
Security:
- Fix memcached command injection through numeric arguments with the meta protocol (GHSA-6wmv-xq9m-fmp7)
- With
protocol: :meta, thedefaultargument ofincr/decr, andfetch_with_lock'slock_ttlandrecache_threshold, were written into the command without conversion, so a String containing CRLF injected additional memcached commands (e.g.set,flush_all) on the connection. The default binary protocol is not affected - These arguments must now be Integers, or Strings of decimal digits; anything else raises
ArgumentErrorbefore a request is sent - As defense in depth, the meta
RequestFormatternow converts every numeric flag it writes (D,J,N,R,T) to an Integer - Thanks to oss-security-shop for the report
- With
- Ruby
Published by petergoldstein 12 days ago
https://github.com/petergoldstein/dalli - v5.0.7
Security release. Fixes GHSA-6wmv-xq9m-fmp7, a memcached command injection through numeric arguments. Upgrading is recommended.
Security:
- Fix memcached command injection through numeric arguments (GHSA-6wmv-xq9m-fmp7)
- The
defaultargument ofincr/decr, andfetch_with_lock'slock_ttlandrecache_threshold, were written into the meta protocol command without conversion, so a String containing CRLF injected additional memcached commands (e.g.set,flush_all) on the connection - These arguments must now be Integers, or Strings of decimal digits; anything else raises
ArgumentErrorbefore a request is sent - As defense in depth,
RequestFormatternow converts every numeric flag it writes (D,J,N,R,T) to an Integer - Thanks to oss-security-shop for the report
- The
- Ruby
Published by petergoldstein 12 days ago
https://github.com/petergoldstein/dalli - v5.1.1
Security release. Fixes GHSA-6wmv-xq9m-fmp7, a memcached command injection through numeric arguments. Upgrading is recommended.
Security:
- Fix memcached command injection through numeric arguments (GHSA-6wmv-xq9m-fmp7)
- The
defaultargument ofincr/decr, andfetch_with_lock'slock_ttlandrecache_threshold, were written into the meta protocol command without conversion, so a String containing CRLF injected additional memcached commands (e.g.set,flush_all) on the connection - These arguments must now be Integers, or Strings of decimal digits; anything else raises
ArgumentErrorbefore a request is sent - As defense in depth,
RequestFormatternow converts every numeric flag it writes (D,J,N,R,T) to an Integer - Affects 3.2.0 and later (3.2.x only with
protocol: :meta); fixed in 5.1.1 and 4.3.4 - Thanks to oss-security-shop for the report
- The
Performance:
- Reduce Ruby overhead on the single-key
getpath by about 28% (#1160)- A plain
getbuilds itsmgrequest with one string interpolation instead of going throughmeta_get's keyword arguments, and skips option handling when called without options - A
VA <size> f<flags>hit line is parsed in place instead of being split into tokens - The key check for control characters and whitespace uses a byte class that matches the same ASCII bytes as
[\p{Cntrl}\s], about 5x faster; this applies to every operation that sends a key - Allocations per
gethit drop from 23 to 16 - Thanks to Julian Richard Contreras for this contribution
- A plain
- Speed up multi-server
get_multiby about 30% (4 servers, 100 keys), and bring small batches in line with 2.7.11 (#1161)- Each server's queries and terminating noop are sent before the next server's are built, so memcached answers earlier servers while later ones are prepared
- A server's queries are built in one pass with
RequestFormatter.multi_meta_get, and plainget_multino longer requests the CAS value it discards (get_multi_casstill does) - Pipelined replies are parsed in one pass over the returned flags
- Routing many keys checks each server's
alive?once per call instead of twice per key, and the ring's binary search runs over plain integers - Thanks to Julian Richard Contreras for this contribution
Development:
- Fix offenses reported by RuboCop 1.91 and require
rubocop >= 1.91(#1162)- RuboCop 1.91 adds
Style/DirectiveScope; single-statementdisable/enablepairs becomedisable-nextdirectives, which older RuboCop versions do not recognize - Removes a misplaced
# encoding: asciicomment inclient.rbthat Ruby had always ignored
- RuboCop 1.91 adds
- Ruby
Published by petergoldstein 12 days ago
https://github.com/petergoldstein/dalli - v5.1.0
Features:
-
Add opaque routing tokens:
:p_tokenand:l_tokenrequest options (#1147, #1154)get,gat,get_cas,get_with_metadata,fetch_with_lock,set/add/replace/set_cas/replace_cas,append/prepend,incr/decr,cas/cas!,delete/delete_cas, and the bulk operations (get_multi,get_multi_cas,get_multi_with_metadata,set_multi,delete_multi) all accept per-request:p_token/:l_tokenoptions, appended to the wire protocol asP<token>/L<token>-- applied to every key on the bulk methods- memcached itself ignores these tokens; per the meta protocol spec they exist as hints for a proxy or router sitting between the client and memcached
- CRLF and NUL bytes raise
ArgumentErrorbefore the request reaches the socket, both inDalli::Clientand inRequestFormatter, so a bad token can't be used for wire-protocol injection and can't close the connection out from under the caller the way a formatter-only check would - The bulk methods and
delete/delete_caswere deferred out of #1147 to avoid racing other in-flight PRs touching the same method signatures; #1154 completes them, including both the single-server fast path and the multi-server pipelined path for each - Extracted from #1130; thanks to Nick Herson for the original idea and Jianbin Chen for porting it forward
-
Support tombstone (mark-stale) deletes on
delete,delete_cas, anddelete_multi(#1145, #1153):invalidatemarks the item stale instead of removing it, so#get_with_metadata/#get_multi_with_metadatareportstale: trueand a reader can tell "another process is repopulating this" apart from "this was never here" -- a tombstoned key is not a miss:tombstone_ttlcontrols how long the stale marker lives; requires:invalidate, since memcached only honors the TTL on a delete when it accompanies the invalidate flag:drop_valueremoves the item's value but leaves the item; on its own it is not a tombstone -- reads are an ordinary hit with an empty valuedelete_multiapplies the same options to every key in the batch, on both the single-server and pipelined paths; its return value keeps counting keys the server found and acted on, so under:invalidateit reports how many keys were tombstoned rather than removed- Extracted from #1130; thanks to Jianbin Chen for this contribution
-
Add
:missand:return_ttl_remainingtoget_with_metadata(#1143):missis now always present in the returned Hash, distinguishing a true miss from a storednilundercache_nilsor a tombstoned, stale hit -- neither of which anil:valuealone can tell apart:return_ttl_remainingexposes the meta protocol'stflag as:ttl_remaining(seconds remaining, or-1for an item with no expiry), following the same opt-in shape as:return_hit_status/:return_last_access- Extracted from #1130; thanks to Jianbin Chen for this contribution
-
Add
get_multi_with_metadatafor stale-aware bulk reads (#1144)- Returns
{ key => { value:, cas:, stale:, miss: } }for the keys that were found; genuine misses are omitted, matchingget_multi/get_multi_cas-- a tombstoned item is a hit at the protocol level, so it is still returned, withstale: true - Routes to the same single-server fast path / pipelined-getter split as
get_multi - Extracted from #1130; thanks to Jianbin Chen for this contribution
- Returns
Other changes:
- Raise the documented minimum supported memcached version to 1.6.27 (#1140)
- Groundwork for the features above:
drop_valuetombstone deletes require 1.6.27, the highest floor of anything landing from #1130 - Not enforced at runtime --
MIN_SUPPORTED_MEMCACHED_VERSIONonly gates the test harness and the README's support statement, so this changes no running client's behavior
- Groundwork for the features above:
Bug Fixes:
-
Retry a transient network error during a liveness check instead of treating it as terminal (#1150)
Dalli::Protocol::Base#alive?caught anyNetworkError-- includingRetryableNetworkError, a subclass -- and unconditionally converted it tofalse, with no retry. This didn't match the retry-then-raise contract every other network-facing path in Dalli follows: a single transient connection hiccup during the liveness check itself (as opposed to an actual request) permanently reported a healthy server as down for that check- Now retries once on
RetryableNetworkError, lettingerror_on_request!'s own fail-count threshold decide when to actually give up (the same mechanism the rest of the codebase relies on): it keeps retrying untilsocket_max_failuresis reached, then raises a terminalNetworkError, which is still converted tofalseas before - Behavior change: a server that was previously marked "down" (engaging the
down_retry_delaycooldown) only via an actual request could now also reach that state via a liveness check (alive?, and anything that calls it --Dalli::Client#stats,#reset_stats, andRing's own server selection) exhausting its retries. Previously, a solitary transient failure during a liveness check was silently forgotten rather than tracked, so the cooldown was inconsistently applied depending on which code path first observed the failure - Likely a contributing cause of the same intermittently failing
get_multifailover integration test noted in #1149: that fix addressed the send/receive phase, but the liveness-check retry it introduced can itself force a freshconnect()mid-retry, giving this separate, pre-existing gap inalive?more chances to fire - Also fixed a test (
test_ring.rb, "detect when a dead server is up again") that had been unknowingly relying on the old behavior: it never engaged thedown_retry_delaycooldown from a single transient failure, so its 0.5s delay never actually gated anything. Updated to use a 0s delay, since the test's intent is to verify reconnection is detected, not to test cooldown timing
-
Base64-encode keys containing control characters, not just NUL (#1148)
KeyRegularizer.required?decided whether a key needed base64 encoding using/\s/, which matches most whitespace but none of the C0 control range (0x00-0x1F) or DEL (0x7F) -- a key that was otherwise ASCII-only and contained no whitespace (e.g."foo\x00bar"or a key with an embedded ESC byte) went out on the wire unencoded- Not a command-injection risk: the text protocol splits commands on CRLF, not other control bytes. The risk is key confusion -- anything downstream that treats one of these bytes specially (a C string terminating at NUL, a terminal or log line interpreting an escape byte) could silently act on a different key than Dalli believes it sent
- A raw control byte in a key was never protocol-compliant in the first place: memcached's own spec (
protocol.txt) states a key "must not include control characters or whitespace." The only sanctioned way to carry such content in a key is the meta protocol's base64 (bflag) path -- the one whitespace and non-ASCII keys already used, and the one these keys now use too. The check is now/[\p{Cntrl}\s]/, matching that rule directly rather than special-casing NUL - Behavior change: a key containing a control character now produces different bytes on the wire (base64-encoded, per the meta protocol's
bflag) than before. Existing cache entries stored under the old, unencoded form of such a key will read as a miss once every reader has upgraded. During a rolling deploy, old and new Dalli versions disagree about which physical key such a logical key maps to -- not just a one-time cutover, but ongoing inconsistency between the old-version and new-version server pools for the duration of the rollout. Harmless for an ordinary cached value (worst case, extra cache misses); worth accounting for if such a key ever backs something stateful, like a lock or counter. Expected to be rare in practice: embedding a raw control byte in a cache key is unusual, and doing so was already outside what the protocol permits - Found while auditing
request_formatter.rbduring the routing-token work in #1130 / #1147; unrelated to that change and predates it
-
Retry transient network errors in
get_multi,set_multianddelete_multiinstead of silently swallowing them (#1149)- All three methods group keys by server and issue one request per server. Each per-server rescue clause caught
DalliErrorandNetworkErrortogether and swallowed both, just debug-logging -- sinceRetryableNetworkError < NetworkError, this also silently swallowed transient, retryable failures, dropping that server's keys from the result instead of the whole operation retrying (get_multi/set_multi's single-server fast path did not even attempt a retry, on any failure) - Six rescue sites across
PipelinedGetter,PipelinedSetter,PipelinedDeleterand thesingle_server_*fast paths now retry a transientRetryableNetworkError, matching sibling rescue sites in the same files that already did this correctly - Behavior change: if a server remains unreachable after retrying (not just a transient blip), these three methods now raise
Dalli::NetworkErrorinstead of silently returning an incomplete or empty result. This matches how every other Dalli::Client method already behaves on a hard network failure, and is the retry-then-raise behaviordelete_multi's own docs already described; it was just not reliably true for this failure path before. Code that calls these methods without rescuingDalli::NetworkErrorshould account for this if it can reach a fully unreachable server - Likely the cause of an intermittently failing
get_multifailover integration test that recurred across multiple PRs, though this could not be directly confirmed: the failure (an empty result with no exception in the logs) never reproduced locally despite repeated attempts, consistent with needing a genuine transient network hiccup that is far more likely on a loaded CI runner than an idle dev machine
- All three methods group keys by server and issue one request per server. Each per-server rescue clause caught
- Ruby
Published by github-actions[bot] about 2 months ago
https://github.com/petergoldstein/dalli - v5.0.6
Performance:
-
Skip the cas-return flag on quiet
meta_setrequests (#1131)- In quiet mode memcached suppresses the
msresponse entirely, so the CAS requested by thecflag can never be read; sending it only added two bytes to every request - Applies to the bulk-write paths, where quiet sets are emitted:
Dalli::Client#multiblocks and the pipelined setter - Extracted from #1130; thanks to Jianbin Chen for this contribution
- In quiet mode memcached suppresses the
-
Reduce allocations in
KeyRegularizerand multi-key request paths (#1120)- Decomposed
KeyRegularizer#encodeinto separateneeds_encoding?andencodecalls so the common happy path avoids allocating an intermediate array for the two-element return value - Refactored
multi_get/multi_set/multi_deletecommand generation intoRequestFormatterto share its key-encoding helpers - Thanks to Jean Boussier for this contribution
- Decomposed
-
Reduce allocations in
ResponseBufferpipelined getk parsing (#1117)process_single_getk_responsewas building a fresh array to return results alongside the updated offset; refactored to store the offset as the last element of the existing tokens array and pop it, saving one allocation per response- Also skips trailing nils in the token array
- Thanks to Jean Boussier for this contribution
-
Enable frozen string literals in
RequestFormatter(#1118)- Frozen string literals had been inadvertently disabled; re-enabling reduces allocations by ~300,000 objects in a 10,000-iteration
get_multi_casbenchmark (562 MB → 550 MB total allocated) - Thanks to Jean Boussier for this contribution
- Frozen string literals had been inadvertently disabled; re-enabling reduces allocations by ~300,000 objects in a 10,000-iteration
-
Reduce allocations in
ResponseProcessor#value_from_tokens(#1113)token[1..].to_iwas allocating a new string for every token parsed; replaced with in-placeslice!followed by a token reset to avoid poisoning subsequent token comparisons- Saves 4 allocations per entry in
get_multi_casworkloads (a hotspot for IdentityCache) - Thanks to Jean Boussier for this contribution
-
Reduce allocations in common operation paths (#1111)
- Use
Symbol#nameoverSymbol#to_sto return a frozen string without allocation - Skip trace attribute hash construction when OpenTelemetry instrumentation is disabled
- Use argument forwarding (
...) inClient#performandThreadsafe#requestto avoid splat array allocation - Use
match?inKeyRegularizer#encodeto avoidMatchDataobject allocation - Reduces objects allocated by ~26% and memory by ~6% for a simple
getworkload - Thanks to Jean Boussier for this contribution
- Use
-
Fix pathological memory behavior in
ResponseBuffer(#1114)compact_if_neededwas intended to reclaim memory by slicing off consumed bytes, butbuffer.byteslice(@offset..)on an unfrozen string causes Ruby to allocate a hidden third string as the copy-on-write owner rather than freeing the original- Redesigns buffer management to pass reusable buffer objects directly to
read/read_nonblock, avoiding reallocation on each response read - Reduces allocations from ~2.38 GB to ~649 MB in a
get_multi_casbenchmark over 10,000 iterations - Accompanied by new unit tests for
ResponseBuffer(#1115) - Thanks to Jean Boussier for this contribution
Features:
delete_multinow returns the number of keys found and deleted (#1126)- Previously the return value was unspecified; callers (e.g. Rails, see rails/rails#58071) had no way to tell how many keys were actually removed
- The count is derived from the meta protocol's quiet-mode delete responses with no extra round-trips: successful deletes are suppressed while misses report
NF, so any response received before the terminator is a key that was not deleted - The single-server fast path now shares the pipelined path's bounded retry on transient (
RetryableNetworkError) network errors, so both paths behave consistently; the returned count is best-effort and may under-report if a network error triggers a retry, since keys deleted before the error are not recounted - Thanks to Iliana Hadzhiatanasova for this contribution
Bug Fixes:
-
Raise instead of returning a truncated value when the peer closes mid-response (#1135)
IO#read(count)on a blocking socket accumulates across TCP chunks and hands back a shorter buffer (ornil) in only one case: the stream hit EOF. That short buffer was passed through as the response body, so a memcached restart, proxy drop, or load balancer timeout partway through a response could surface a truncated but still decodable value to the caller, indistinguishable from a real one- A short read is now treated as the premature EOF it is, raising and closing the dirty socket so the request is retried on a fresh connection
- CRuby only; the JRuby path already used
Socket#readfull, which enforces the same contract - Extracted from #1130; thanks to Ian Ker-Seymer for the original fix and Jianbin Chen for the port
-
Tear down the connection when a non-
StandardErroraborts a request (#1136)Async::StopandThread#killdescend fromExceptionrather thanStandardError, so the rescue clauses inProtocol::Base#requestnever saw them; a scheduler cancelling a fiber parked on a response read skippedcloseentirely, leaving the connection marked as having a request in progress with partial response bytes still unread on the wire, and returning that half-used client to the pool underconnection_poolProtocol::Base#requestnow closes in anensureunless the request ran to completion, andConnectionManager#closeperforms its state cleanup in anensureso a second cancellation landing inside@sock.closecannot leave the socket non-nil with the request still marked in progressDalli::DalliErrorandDalli::MarshalErrornow close the connection at the point of failure rather than at the start of the next request; those paths already left the request in progress andConnectionManager#confirm_ready!closed on the next call, so this changes when the close happens rather than adding one- Extracted from #1130; thanks to Dan Mayer for the original fix and Jianbin Chen for the port
-
Fix
ResponseBuffercompaction logic (#1119)COMPACT_THRESHOLDwas removed in #1116 as apparently unused, but the constant was referenced by the compaction guard; its absence silently disabled buffer compaction- Restored the constant, corrected the compaction condition, and improved the buffer-shrinking implementation to use
String#bytesplice(backed bymemmove) for true in-place compaction - Adds targeted tests covering the compaction threshold and shrink behavior
- Thanks to Jean Boussier for this contribution
Maintenance:
-
Scope
StrictWarningsto Dalli's own source (#1134)- The test suite runs under
-wand prepends a hook toWarning.singleton_classthat turns warnings into failures, but that hook is global: a warning emitted while loading any third-party gem aborted the whole suite before a single test ran json2.21.2's pure-Ruby generator (used on JRuby, where the C extension is unavailable) warnsmethod redefined; discarding old to_hashat require time, which took thejruby-10CI job red with no change to Dalli- Warnings are now attributed to a source file and only raise for
lib/andtest/; attribution prefers the location Ruby embeds in the message, since the stack at that point describes the require chain rather than the offending code - Portable attribution of
Kernel#warncallers also required walking the stack rather than indexing it (Ruby 3.3/3.4 push an<internal:warning>frame that 4.0 does not), skipping RubyGems'Kernel#warnshim (active on JRuby but not CRuby), and resolving relative backtrace paths
- The test suite runs under
-
Make raw and namespace fast path tests actually use those options (#1129)
- Followup to #1127: the
rawandnamespacevariants passed those options to the helper that starts memcached, which configures the client the tests then discarded, so neither option was ever exercised - Passes the options to the client under test and adds assertions that fail if they are absent
- Thanks to Iliana Hadzhiatanasova for this contribution
- Followup to #1127: the
-
Benchmark
set_multiand add adelete_multitarget (#1132)- Enables the two
set_multireports that were commented out pending the arrival ofset_multi, resolving the accompanying TODO - Adds a
delete_multitarget comparing the pipelined path against N single deletes - Extracted from #1130; thanks to Jianbin Chen for this contribution
- Enables the two
-
Bump CI memcached to 1.6.41 and run benchmarks on pull requests (#1133)
- The tests workflow moves from 1.6.40 to 1.6.41; the benchmarks and profile workflows had drifted back on 1.6.23
- Extracted from #1130; thanks to Jianbin Chen for this contribution
-
Disable RuboCop metrics cops (#1128)
- Thanks to Jean Boussier for this contribution
-
Remove
PIDCachemodule (#1125)Process.pidis cached natively by Ruby 3.3+ (via https://bugs.ruby-lang.org/issues/19443), making the manual cache unnecessary now that Dalli requires Ruby 3.3+- Thanks to Jean Boussier for this contribution
-
Remove unused
COMPACT_THRESHOLDconstant fromResponseBuffer(#1116)- Followup cleanup after the buffer management redesign in #1114
- Note: subsequently found to be in use; restored and corrected in #1119
- Thanks to Jean Boussier for this contribution
-
Use
String#byteindexinstead ofString#indexwhen searching for the response terminator ingetk_response_from_buffer(#1112)- The result feeds directly into
byteslice;byteindexmakes the intent explicit, though both return the same value since the buffer encoding is alwaysBINARY - Thanks to Jean Boussier for this contribution
- The result feeds directly into
-
Make single-server fast path tests actually exercise the fast path (#1127)
- The batch-operation tests built clients through a helper that registers two address aliases for the same memcached process, so every client had a 2-server ring and the tests always ran through the pipelined path instead of the single-server fast path
- Adds a
single_server_clienttest helper that builds a client with a single address, and uses it in the affectedget_multi,set_multi, anddelete_multitests - Thanks to Iliana Hadzhiatanasova for this contribution
- Ruby
Published by petergoldstein 2 months ago