A summary of data about the Ruby ecosystem.

Recent Releases of https://github.com/petergoldstein/dalli

https://github.com/petergoldstein/dalli - v3.2.10

  • Fix Rack::Session::Dalli with connection_pool 3.x, which only accepts keyword arguments (backport of #1051)
  • Add logger as 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/decr default with the meta protocol (GHSA-6wmv-xq9m-fmp7)
    • With protocol: :meta, the default argument of incr/decr was 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
    • default must now be an Integer, or a String of decimal digits; anything else raises ArgumentError before a request is sent
    • As defense in depth, the meta RequestFormatter now converts every numeric flag it writes (D, J, N, T) to an Integer
    • Thanks to oss-security-shop for the report

- 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, the default argument of incr/decr, and fetch_with_lock's lock_ttl and recache_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 ArgumentError before a request is sent
    • As defense in depth, the meta RequestFormatter now converts every numeric flag it writes (D, J, N, R, T) to an Integer
    • Thanks to oss-security-shop for the report

- 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 default argument of incr/decr, and fetch_with_lock's lock_ttl and recache_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 ArgumentError before a request is sent
    • As defense in depth, RequestFormatter now converts every numeric flag it writes (D, J, N, R, T) to an Integer
    • Thanks to oss-security-shop for the report

- 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 default argument of incr/decr, and fetch_with_lock's lock_ttl and recache_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 ArgumentError before a request is sent
    • As defense in depth, RequestFormatter now 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

Performance:

  • Reduce Ruby overhead on the single-key get path by about 28% (#1160)
    • A plain get builds its mg request with one string interpolation instead of going through meta_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 get hit drop from 23 to 16
    • Thanks to Julian Richard Contreras for this contribution
  • Speed up multi-server get_multi by 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 plain get_multi no longer requests the CAS value it discards (get_multi_cas still 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-statement disable/enable pairs become disable-next directives, which older RuboCop versions do not recognize
    • Removes a misplaced # encoding: ascii comment in client.rb that Ruby had always ignored

- Ruby
Published by petergoldstein 12 days ago

https://github.com/petergoldstein/dalli - v5.1.0

Features:

  • Add opaque routing tokens: :p_token and :l_token request 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_token options, appended to the wire protocol as P<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 ArgumentError before the request reaches the socket, both in Dalli::Client and in RequestFormatter, 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_cas were 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, and delete_multi (#1145, #1153)

    • :invalidate marks the item stale instead of removing it, so #get_with_metadata / #get_multi_with_metadata report stale: true and a reader can tell "another process is repopulating this" apart from "this was never here" -- a tombstoned key is not a miss
    • :tombstone_ttl controls how long the stale marker lives; requires :invalidate, since memcached only honors the TTL on a delete when it accompanies the invalidate flag
    • :drop_value removes the item's value but leaves the item; on its own it is not a tombstone -- reads are an ordinary hit with an empty value
    • delete_multi applies 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 :invalidate it reports how many keys were tombstoned rather than removed
    • Extracted from #1130; thanks to Jianbin Chen for this contribution
  • Add :miss and :return_ttl_remaining to get_with_metadata (#1143)

    • :miss is now always present in the returned Hash, distinguishing a true miss from a stored nil under cache_nils or a tombstoned, stale hit -- neither of which a nil :value alone can tell apart
    • :return_ttl_remaining exposes the meta protocol's t flag as :ttl_remaining (seconds remaining, or -1 for 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_metadata for stale-aware bulk reads (#1144)

    • Returns { key => { value:, cas:, stale:, miss: } } for the keys that were found; genuine misses are omitted, matching get_multi / get_multi_cas -- a tombstoned item is a hit at the protocol level, so it is still returned, with stale: 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

Other changes:

  • Raise the documented minimum supported memcached version to 1.6.27 (#1140)
    • Groundwork for the features above: drop_value tombstone deletes require 1.6.27, the highest floor of anything landing from #1130
    • Not enforced at runtime -- MIN_SUPPORTED_MEMCACHED_VERSION only gates the test harness and the README's support statement, so this changes no running client's behavior

Bug Fixes:

  • Retry a transient network error during a liveness check instead of treating it as terminal (#1150)

    • Dalli::Protocol::Base#alive? caught any NetworkError -- including RetryableNetworkError, a subclass -- and unconditionally converted it to false, 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, letting error_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 until socket_max_failures is reached, then raises a terminal NetworkError, which is still converted to false as before
    • Behavior change: a server that was previously marked "down" (engaging the down_retry_delay cooldown) 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, and Ring'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_multi failover integration test noted in #1149: that fix addressed the send/receive phase, but the liveness-check retry it introduced can itself force a fresh connect() mid-retry, giving this separate, pre-existing gap in alive? 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 the down_retry_delay cooldown 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 (b flag) 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 b flag) 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.rb during the routing-token work in #1130 / #1147; unrelated to that change and predates it
  • Retry transient network errors in get_multi, set_multi and delete_multi instead of silently swallowing them (#1149)

    • All three methods group keys by server and issue one request per server. Each per-server rescue clause caught DalliError and NetworkError together and swallowed both, just debug-logging -- since RetryableNetworkError < 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, PipelinedDeleter and the single_server_* fast paths now retry a transient RetryableNetworkError, 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::NetworkError instead 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 behavior delete_multi's own docs already described; it was just not reliably true for this failure path before. Code that calls these methods without rescuing Dalli::NetworkError should account for this if it can reach a fully unreachable server
    • Likely the cause of an intermittently failing get_multi failover 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

- 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_set requests (#1131)

    • In quiet mode memcached suppresses the ms response entirely, so the CAS requested by the c flag 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#multi blocks and the pipelined setter
    • Extracted from #1130; thanks to Jianbin Chen for this contribution
  • Reduce allocations in KeyRegularizer and multi-key request paths (#1120)

    • Decomposed KeyRegularizer#encode into separate needs_encoding? and encode calls so the common happy path avoids allocating an intermediate array for the two-element return value
    • Refactored multi_get/multi_set/multi_delete command generation into RequestFormatter to share its key-encoding helpers
    • Thanks to Jean Boussier for this contribution
  • Reduce allocations in ResponseBuffer pipelined getk parsing (#1117)

    • process_single_getk_response was 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_cas benchmark (562 MB → 550 MB total allocated)
    • Thanks to Jean Boussier for this contribution
  • Reduce allocations in ResponseProcessor#value_from_tokens (#1113)

    • token[1..].to_i was allocating a new string for every token parsed; replaced with in-place slice! followed by a token reset to avoid poisoning subsequent token comparisons
    • Saves 4 allocations per entry in get_multi_cas workloads (a hotspot for IdentityCache)
    • Thanks to Jean Boussier for this contribution
  • Reduce allocations in common operation paths (#1111)

    • Use Symbol#name over Symbol#to_s to return a frozen string without allocation
    • Skip trace attribute hash construction when OpenTelemetry instrumentation is disabled
    • Use argument forwarding (...) in Client#perform and Threadsafe#request to avoid splat array allocation
    • Use match? in KeyRegularizer#encode to avoid MatchData object allocation
    • Reduces objects allocated by ~26% and memory by ~6% for a simple get workload
    • Thanks to Jean Boussier for this contribution
  • Fix pathological memory behavior in ResponseBuffer (#1114)

    • compact_if_needed was intended to reclaim memory by slicing off consumed bytes, but buffer.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_cas benchmark over 10,000 iterations
    • Accompanied by new unit tests for ResponseBuffer (#1115)
    • Thanks to Jean Boussier for this contribution

Features:

  • delete_multi now 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 (or nil) 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-StandardError aborts a request (#1136)

    • Async::Stop and Thread#kill descend from Exception rather than StandardError, so the rescue clauses in Protocol::Base#request never saw them; a scheduler cancelling a fiber parked on a response read skipped close entirely, 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 under connection_pool
    • Protocol::Base#request now closes in an ensure unless the request ran to completion, and ConnectionManager#close performs its state cleanup in an ensure so a second cancellation landing inside @sock.close cannot leave the socket non-nil with the request still marked in progress
    • Dalli::DalliError and Dalli::MarshalError now 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 and ConnectionManager#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 ResponseBuffer compaction logic (#1119)

    • COMPACT_THRESHOLD was 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 by memmove) for true in-place compaction
    • Adds targeted tests covering the compaction threshold and shrink behavior
    • Thanks to Jean Boussier for this contribution

Maintenance:

  • Scope StrictWarnings to Dalli's own source (#1134)

    • The test suite runs under -w and prepends a hook to Warning.singleton_class that 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
    • json 2.21.2's pure-Ruby generator (used on JRuby, where the C extension is unavailable) warns method redefined; discarding old to_hash at require time, which took the jruby-10 CI job red with no change to Dalli
    • Warnings are now attributed to a source file and only raise for lib/ and test/; 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#warn callers 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#warn shim (active on JRuby but not CRuby), and resolving relative backtrace paths
  • Make raw and namespace fast path tests actually use those options (#1129)

    • Followup to #1127: the raw and namespace variants 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
  • Benchmark set_multi and add a delete_multi target (#1132)

    • Enables the two set_multi reports that were commented out pending the arrival of set_multi, resolving the accompanying TODO
    • Adds a delete_multi target comparing the pipelined path against N single deletes
    • Extracted from #1130; thanks to Jianbin Chen for this contribution
  • 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 PIDCache module (#1125)

    • Process.pid is 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_THRESHOLD constant from ResponseBuffer (#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#byteindex instead of String#index when searching for the response terminator in getk_response_from_buffer (#1112)

    • The result feeds directly into byteslice; byteindex makes the intent explicit, though both return the same value since the buffer encoding is always BINARY
    • Thanks to Jean Boussier for this contribution
  • 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_client test helper that builds a client with a single address, and uses it in the affected get_multi, set_multi, and delete_multi tests
    • Thanks to Iliana Hadzhiatanasova for this contribution

- Ruby
Published by petergoldstein 2 months ago