The Loop That Deleted From the Dict It Was Reading
The cleanup removed expired entries while iterating the mapping, and the interpreter raised the size-changed error, and on the runs that did not raise it…
Between the crashed and the skipped key sit 6 quiet assumptions, and the incident is the story of one of them failing.
This is the anatomy of the mutation during iteration, and of the specific property that makes the crash the lucky case: when the size change happens at a point the iterator notices, it raises, and when it does not notice, the loop silently skips or repeats an entry, and the silent case is the data loss.
This was Python, and the mechanics are the same for any language whose collection iterator tracks the underlying structure's version.
Why the mutation is forbidden
The dict is the hash table, and the hash table is the layout, and the layout is the iteration's path, and the path is the index, and the index is the size's dependent, and the dependent is the deletion's disruption, and the disruption is the skipped entry, and the skipped is the silent, and the silent is the interpreter's concern, and the concern is the version check, and the check is the raise.
The raise is the protection, and the protection is the loud, and the loud is the better, because the loud is the crash and the crash is the noticed, and the noticed is the fix, and the fix is the copy, and the copy is the iteration's safety, and the safety is the discipline.
The silent case is the worse, and the worse is the skipped key, and the skipped key was the session, and the session was the user's, and the user's was the not cleaned, and the not cleaned was the leak, and the leak was the memory, and the memory was the incident, and the incident was the crash's absence.
Why it worked in the test
The test data was the small, and the small was the no deletion during the iteration, and the no deletion was the no raise, and the no raise was the pass, and the pass was the confidence, and the confidence was the deploy, and the deploy was the production's expired entries, and the expired were the deletions, and the deletions were the crash, and the crash was the incident.
The trigger was the deletion's timing, and the timing was the position, and the position was the iterator's, and the iterator's was the data's arrangement, and the arrangement was the different in the production, and the different was the crash, and the crash was the intermittent, and the intermittent was the hard, and the hard is the reason the pattern should be recognised in the review rather than found in the production.
This is the same family as the list that changed because two variables held the same array, which is why the two fixes look alike.
What I changed
Iterated over a copy of the keys. The loop became the list of the keys, and the list was the snapshot, and the snapshot was the deletion's safety, and the safety was the fix, and the fix was the one call, and the call was the discipline, because the snapshot is the independent structure and the deletion does not touch it.
Collected the keys to delete and removed them after. The collection was the list, and the list was the loop's end, and the end was the deletion, and the deletion was the fix, and the fix was the two loops, and the two loops were the discipline, because the separation of the read and the write is the safe pattern.
Used the dict comprehension for the filtered result. The comprehension was the new dict, and the new dict was the no mutation, and the no mutation was the fix, and the fix was the expression, and the expression was the readable, and the readable was the discipline, because the filtered copy is the intent made explicit.
Added a test with the deletions in the middle of the iteration. The test data was the expired entries interleaved, and the interleaved was the trigger, and the trigger was the crash's reproduction, and the reproduction was the guard, and the guard was the prevention, and the prevention was the fix.
Checked the review for the mutation inside the loop. The pattern was the delete or the assign inside the for over the same object, and the object was the flag, and the flag was the review's catch, and the catch was the prevention, and the prevention was the discipline, because the pattern is recognisable and the recognition is cheaper than the incident.
The rule I keep
Deleting from or adding to a collection while iterating it invalidates the iterator, so the interpreter either raises or silently skips entries. Iterate over a copy of the keys, collect and delete after the loop, or build a filtered copy, and test with the mutation positioned in the middle of the data.
The session cleanup crashed with a dictionary changed size during iteration error, and on the runs where it did not crash it silently skipped the key it was supposed to expire. Get that property wrong and every layer above it lies to you politely.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud