Conversation
CleanupOldInteractions accumulated every expiring InteractionSessionId into a List<int>, filtering each candidate with List<int>.Contains against the same list it was appending to. That is O(n^2), and it runs before the BulkDeleteInChunks call, so on a channel with tens of millions of expiring interactions the delete was never reached. RunCleanupTask catches the resulting exception into the job result list, so the channel's RetentionDuration was silently never enforced. Observed on a channel that reached 96M rows despite a 60 day retention. Switch the accumulator to a HashSet so the duplicate check is O(1), and use UnionWith to stream the query results into it instead of materializing every id with ToList() before filtering. CleanupUnusedInteractionSessions had two further problems: - The loop bound used integer division ( Count / 1000 ), dropping the final partial chunk on every run and skipping the work entirely when there were fewer than 1000 ids. - Chunks were taken with Skip( x * 1000 ), which re-enumerates the list from the start on each iteration, giving the same O(n^2) behaviour. Round the chunk count up and index into the list with GetRange. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes interaction retention cleanup becoming effectively non-functional on very large interaction channels by removing O(n²) accumulation patterns and correcting chunk-processing logic so deletions reliably run to completion.
Changes:
- Replaces the accumulating
List<int>of expiring interaction session IDs with aHashSet<int>and streams IDs into it viaUnionWithto avoid O(n²)Containschecks and extra materialization. - Fixes session cleanup chunking to process the final partial chunk (and any <1000 remainder) by rounding up the chunk count.
- Improves chunk extraction performance by using indexed
GetRangeinstead of repeatedSkip(...).
Suppressed comments (1)
Rock/Jobs/RockCleanup.cs:1290
- Variable names
interactionSessionsIdsToKeep/interactionSessionsIdsToRemoveread like a pluralization typo ("SessionsIds"), which makes the intent harder to follow in this already performance-sensitive cleanup loop. Rename them tointeractionSessionIdsToKeep/interactionSessionIdsToRemovefor clarity and consistency with the surroundinginteractionSessionIds*naming.
var interactionSessionIdChunk = interactionSessionIds.GetRange( chunkStartIndex, chunkSize );
// Find a list of session IDs in the delete list that are being used for other interactions
var interactionSessionsIdsToKeep = new InteractionService( rockContext )
.Queryable()
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
1271
to
+1275
| var rockContext = new Rock.Data.RockContext(); | ||
| rockContext.Database.CommandTimeout = commandTimeout; | ||
|
|
||
| // process 1K at a time to prevent the exception "Query processor ran out of internal resources". | ||
| for ( int x = 0; x < interactionSessionIds.Count / 1000; x++ ) | ||
| { | ||
| var interactionSessionIdChunk = interactionSessionIds.Skip( x * 1000 ).Take( 1000 ); | ||
| // Round the chunk count up. The previous integer division ( Count / 1000 ) dropped the |
Author
|
Superseded by #14, which applies the same fix against hotfix-1.16.12 where the v16 upgrade work lives. Reopen if we want this on develop / upstream later. |
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.
Problem
RockCleanup's interaction retention silently stops working once a channel gets large, soInteractionChannel.RetentionDurationis never enforced.CleanupOldInteractionsaccumulates every expiringInteractionSessionIdinto aList<int>, filtering each candidate withList<int>.Containsagainst the same list it is appending to:Containsis a linear scan, so the accumulation is O(n²). It also sits before theBulkDeleteInChunkscall, so when it doesn't finish, nothing is deleted at all.RunCleanupTaskcatches the resulting exception into the job result list rather than failing loudly, so the job reports as having run.We hit this on a channel that reached 96M rows despite a 60 day retention — roughly 94M distinct expiring session ids, which is on the order of 4×10¹⁵ comparisons, plus a ~380MB
List<int>materialized before any delete. The channel had been accumulating since an upgrade six months earlier with no visible failure.CleanupUnusedInteractionSessions( List<int> )has two further problems:for ( int x = 0; x < interactionSessionIds.Count / 1000; x++ )— integer division drops the final partial chunk on every run, and skips the work entirely when there are fewer than 1000 ids.interactionSessionIds.Skip( x * 1000 ).Take( 1000 )re-enumerates the list from the start on each iteration, giving the same O(n²) behaviour over the id list.Changes
HashSet<int>, making the duplicate check O(1).UnionWithstreams query results into the set instead ofToList()-ing every id and then filtering, dropping one full materialization of the id list.GetRange(indexed) rather thanSkip.No behavioural change to what gets deleted — the resulting set of session ids is the same, and the parameterless
CleanupUnusedInteractionSessions()overload is untouched.Testing
Worth exercising
CleanupOldInteractionsagainst a channel with a retention duration and a non-multiple-of-1000 number of expiring sessions, which is the case the integer division was silently skipping.Upstream
Both bugs are present in upstream Rock, not SECC customizations — they will return on each upgrade until fixed there. Worth reporting to Spark.