Use local flag instead of $ERROR_INFO to gate ensure-block close#71
Open
Use local flag instead of $ERROR_INFO to gate ensure-block close#71
Conversation
The ensure clause in Base#request previously read $ERROR_INFO to decide
whether the begin block was unwinding from an exception:
close if $ERROR_INFO && @connection_manager.request_in_progress?
$! / $ERROR_INFO is preserved when a method is called from inside a
rescue clause — the called method's ensure sees the *outer* rescued
exception even when its own begin block completed cleanly. On the
:pipelined_get happy path, request_in_progress? is intentionally left
true (the caller still has to drain the responses), so this check
fired close() and tore the socket out from under any pipelined_get
issued from inside a rescue handler — a common cache-fallback pattern.
The new test reproduces the failure on single-server get_multi(keys) {…}
(block form forces :pipelined_get): pre-fix the call returns {} because
make_getkq_requests swallows the resulting DalliError; post-fix it
returns the expected hash.
Replaces the global-state check with a local request_local_completed
flag set true at the end of the begin block, so the ensure decision
is scoped to this method only.
| verify_state(opkey) | ||
|
|
||
| begin | ||
| request_local_completed = false |
There was a problem hiding this comment.
It's okay to be explicit, but this line is unnecessary.
| # pipelined_get emit query but doesn't read the response(s) | ||
| @connection_manager.finish_request! unless opkey == :pipelined_get | ||
|
|
||
| request_local_completed = true |
There was a problem hiding this comment.
I also slightly prefer to call this success but it's fine either way.
samuel-williams-shopify
approved these changes
Apr 30, 2026
drinkbeer
approved these changes
Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #68 and #69. Fixes a real production-symptom-matching bug where
dc.get_multi(...) { … }(and any pipelined_get) called from inside arescueclause silently returns{}instead of the expected results, becauseBase#request's ensure was reading$ERROR_INFO(a.k.a.$!) — which is preserved into nested method calls when the call is made from a rescue handler.The bug
Old ensure:
$!is set during a rescue clause and stays set across method calls made from inside that clause. So when application code does the very common cache-fallback pattern:…inside
Base#request's ensure:$ERROR_INFOis the outer rescued exception (truthy)request_in_progress?istruebecause:pipelined_getintentionally skipsfinish_request!(the caller still has to drain responses)Both conditions met →
closefires, socket torn down mid-pipeline.make_getkq_requestsswallows the resultingDalliError(pipelined_getter.rb:60), the drain finds nothing on the wire, andget_multireturns{}. The application sees an empty result and either treats those keys as cache misses (extra DB load + refill churn) or — worse — interprets the empty hash as a definitive "not present" signal.The fix
Replace the global-state read with a local flag set at the very end of the begin block:
The local flag is scoped to this method invocation; nothing outside this frame can affect it. The same close-on-abort guarantee from #68 is preserved for non-StandardError exceptions like
Async::Stop.Also dropped
require 'English'frombase.rbsince the file no longer references any of those globals.Test
test/integration/test_fiber_concurrency.rbgrows a third case that:rescue StandardError, callsdc.get_multi(%w[a b]) { … }(block form is required to force:pipelined_getrather than the single-server-optimized:read_multi_req)closewas never calledVerified pre/post-fix on
mainhead:lib/dalli/protocol/base.rb):Test plan
{})bundle exec rake testpassesbundle exec rubocopclean