Prevent re-adding deleted sessions to Redis principal index - #3889
Open
seonwooj0810 wants to merge 1 commit into
Open
Prevent re-adding deleted sessions to Redis principal index#3889seonwooj0810 wants to merge 1 commit into
seonwooj0810 wants to merge 1 commit into
Conversation
RedisSession#saveDelta() unconditionally re-adds the session id to the resolved principal's index set whenever the delta contains the principal/security-context attribute key. RedisIndexedSessionRepository #deleteById() first removes the session id via cleanupPrincipalIndex(), then sets maxInactiveInterval to Duration.ZERO and calls save(session), which invokes saveDelta(). Under SaveMode.ALWAYS, the RedisSession constructor unconditionally copies every current attribute (including the principal-index attribute) into the delta, so saveDelta() always takes the principal-index branch on this path. Since the session's own attributes are untouched by deleteById(), resolveIndexesFor() still resolves the same principal, and saveDelta() re-adds the id to the index set that cleanupPrincipalIndex() had just removed it from. The principal's Redis set (e.g. "spring:session:index:...:<principal>") then keeps growing with ids of sessions that were explicitly invalidated, leaking memory. Guard the re-add with RedisSession#isExpired(), which is already true at this point in deleteById() (maxInactiveInterval is set to Duration .ZERO, not a negative "never expires" value). The unconditional removal of the id from the previous principal's set is left untouched, since removal is always correct regardless of expiration state. Closes spring-projectsgh-1843 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com>
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.
Closes gh-1843
Root cause
RedisSession#saveDelta()re-adds the session id to the resolved principal's Redis index set whenever the delta contains the principal/security-context attribute key — this branch does both a removal (of the id from whatever principal it was previously indexed under) and, if a principal still resolves, an unconditional re-add.RedisIndexedSessionRepository#deleteById()callscleanupPrincipalIndex(session)to remove the session id from its principal's index set, then setsmaxInactiveIntervaltoDuration.ZEROand callssave(session), which invokessaveDelta().Under
SaveMode.ALWAYS, theRedisSessionconstructor unconditionally copies every current attribute (including the principal-index attribute) intodelta, sosaveDelta()always takes the principal-index branch when deleting such a session. SincedeleteById()never removes the session's own attributes (it only zeroes the TTL),resolveIndexesFor()still resolves the same principal, andsaveDelta()re-adds the id right back into the set thatcleanupPrincipalIndex()had just removed it from a few lines earlier. The principal's Redis set (...index:...PRINCIPAL_NAME_INDEX_NAME:<principal>) then keeps growing with ids of sessions that were explicitly invalidated (e.g. on logout), leaking memory indefinitely.This is the mechanism behind gh-1843's original report (
SaveMode.ALWAYS+request.invalidate()) and matches the reporter's own code walkthrough in the issue.Scope vs. related issues/PRs
This repo has a cluster of principal-index-staleness reports, but they're two distinct mechanisms:
findByIndexNameAndIndexValue/the expiration-cleanup flow, and is a legitimately different, still-open question — I'm not attempting to resolve it here, and Clean Redis principal index during expiration cleanup #3798 remains orthogonal to this change (it touches the expiration-cleanup flow; this PR touchessaveDelta(), called fromdeleteById()).Change
Guard the re-add in
saveDelta()withRedisSession#isExpired(). At the pointdeleteById()callssave(session),maxInactiveIntervalhas just been set toDuration.ZERO(not a negative "never expires" value), soisExpired()is alreadytruethere — no new field or flag needed. The unconditional removal from the previous principal's set is left untouched, since removing a session id from an index is always correct regardless of expiration state; only the re-add was wrong.Test evidence
Added
deleteWhenSaveModeAlwaysThenPrincipalIndexNotReAddedtoRedisIndexedSessionRepositoryTests, which setsSaveMode.ALWAYS, gives the session a principal-index attribute, callsdeleteById, and asserts the principal set'sremovestill happens whileaddis never invoked. Before this change the test fails withTooManyActualInvocationsonadd(the id is re-added); after the change it passes.Verification done
gh pr list --repo spring-projects/spring-session --search "1843"/--search "3183"returned nothing; the only related open PR is Clean Redis principal index during expiration cleanup #3798, which (per above) addresses a different code path.RedisIndexedSessionRepository.java(1 line) and its test class.3.5.x/mainby readingsaveDelta()anddeleteById()and reproducing it with the new test (fails without the fix, passes with it).waiting-for-triage(labeledtype: bug+in: redis), so the triage-comment gate doesn't apply../gradlew :spring-session-data-redis:test --tests '*RedisIndexedSessionRepositoryTests*'→ 57 tests, 0 failures../gradlew :spring-session-data-redis:checkFormatMain :spring-session-data-redis:checkFormatTest :spring-session-data-redis:checkstyleMain :spring-session-data-redis:checkstyleTest→ all pass. Docker-backed integration tests undersrc/integration-testwere not run (not required for this change, which is a pure unit-testable Redis-key-management fix with no new integration surface).This PR was prepared with AI assistance (Claude); I reviewed the root-cause analysis and diff myself before opening it.