Skip to content

Comments

Fix thread-safety issues in concurrent access#171

Merged
timlaing merged 5 commits intotimlaing:mainfrom
olenzma:patched
Feb 3, 2026
Merged

Fix thread-safety issues in concurrent access#171
timlaing merged 5 commits intotimlaing:mainfrom
olenzma:patched

Conversation

@olenzma
Copy link

@olenzma olenzma commented Dec 19, 2025

Summary

This PR fixes "dictionary changed size during iteration" errors that occur when multiple threads make concurrent HTTP requests that modify shared state.

Changes

drive.py

  • _get_token_from_cookie(): Snapshot cookies before iteration to avoid race with concurrent HTTP responses adding cookies
  • get_children(): Copy dict before update and copy items list before iteration to prevent mutation during concurrent access

session.py

  • _save_session_data(): Copy _data dict before JSON dump to avoid mutation during concurrent _update_session_data() calls

cookie_jar.py

  • load(): Snapshot cookies before iteration when clearing FMIP cookies
  • save(): Create temporary jar with cookie snapshot for thread-safe save, avoiding iteration over live cookie jar

Background

These issues manifest during parallel iCloud Drive downloads where multiple threads trigger HTTP requests simultaneously. The HTTP responses modify cookies and session data while other threads iterate over these collections.

🤖 Generated with Claude Code

- drive.py: Copy cookies before iteration in _get_token_from_cookie()
- drive.py: Copy dict and items list in get_children() to avoid mutation
- session.py: Copy _data dict before JSON dump in _save_session_data()
- cookie_jar.py: Snapshot cookies before iteration in load() and save()

These fixes prevent "dictionary changed size during iteration" errors
when multiple threads make concurrent HTTP requests that modify
shared state (cookies, session data, drive node items).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved stability of cookie and session handling by fixing potential race condition errors during concurrent operations.
    • Enhanced robustness of token extraction and data persistence to prevent runtime errors during simultaneous requests.

Walkthrough

Added a public PyiCloudCookieJar.copy() and changed cookie, drive and session persistence to operate on snapshots/copies before iterating or saving to avoid mutation/race issues; tests added for cookie-copy behaviour and Drive token extraction edge cases. No breaking API changes besides the new copy() method.

Changes

Cohort / File(s) Summary
Cookie management & persistence
pyicloud/cookie_jar.py
Added PyiCloudCookieJar.copy(); load() and save() use copied snapshots and LWPCookieJar.save for persistence; iterate-over-copy patterns introduced and RuntimeError races are caught and ignored.
Drive service mutation safety & token parsing
pyicloud/services/drive.py
_get_token_from_cookie iterates over a cookie snapshot to extract token; node hydration merges data into a new dict ({**self.data, **node_data}); child construction iterates over a copied items list to avoid mutation during iteration.
Session persistence
pyicloud/session.py
Session data is shallow-copied before being written to the session file to prevent concurrent-mutation issues during serialization.
Tests
tests/services/test_drive.py, tests/test_cookie_jar.py, tests/test_base.py
Added tests for Drive token extraction edge cases and concurrent-modification safety; added a test for PyiCloudCookieJar.copy() semantics; updated tests to mock/expect PyiCloudCookieJar.copy() in cookie save/load flows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I copy jars before they roam,

I patch the crumbs and guard the home,
No racing fox will steal a token,
Cookies safe and never broken,
A quiet hop where code can bloom.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the changeset: fixing thread-safety issues in concurrent access scenarios.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the thread-safety issues being addressed and detailing the specific changes made across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Pylint (4.0.4)
pyicloud/cookie_jar.py
pyicloud/services/drive.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
pyicloud/cookie_jar.py (2)

67-84: Consider removing redundant import.

LWPCookieJar is already imported at line 3, so the alias at line 72 is redundant. You could either use the existing import directly or create a module-level alias for clarity.

🔎 Proposed refactor
         # Copy cookies to avoid "dictionary changed size during iteration"
         # when concurrent HTTP responses modify the cookie jar
         try:
             cookies_snapshot = list(self)
-            # Create temp jar with snapshot for thread-safe save
-            from http.cookiejar import LWPCookieJar as TempJar
-
-            temp_jar = TempJar(filename=resolved)
+            # Create temp jar with snapshot for thread-safe save
+            temp_jar = LWPCookieJar(filename=resolved)
             for cookie in cookies_snapshot:
                 temp_jar.set_cookie(cookie)
             temp_jar.save(
                 filename=resolved,
                 ignore_discard=ignore_discard,
                 ignore_expires=ignore_expires,
             )
         except RuntimeError:
             # If we still hit a race, silently skip this save
             pass

82-84: Silent failure on RuntimeError prevents crash but loses data.

Silently skipping the save on RuntimeError is a reasonable defensive choice to prevent crashes, but users won't be notified if cookies fail to persist. Consider logging a warning if this tradeoff becomes problematic in practice.

🔎 Optional: Add logging for silent failures

Import logging at the top of the file if not already present, then:

         except RuntimeError:
             # If we still hit a race, silently skip this save
-            pass
+            import logging
+            logging.getLogger(__name__).warning(
+                "Failed to save cookies due to concurrent modification; skipping save"
+            )
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfb15f8 and acdee74.

📒 Files selected for processing (3)
  • pyicloud/cookie_jar.py (2 hunks)
  • pyicloud/services/drive.py (2 hunks)
  • pyicloud/session.py (1 hunks)
🔇 Additional comments (4)
pyicloud/session.py (1)

104-105: Good defensive pattern for concurrent access.

The shallow copy prevents "dictionary changed size during iteration" errors when _update_session_data() runs concurrently. Since session data contains simple header strings at the top level, a shallow copy is adequate.

pyicloud/cookie_jar.py (1)

41-50: Defensive snapshot pattern prevents iteration errors.

The fallback to an empty list on RuntimeError is a reasonable tradeoff—it prevents crashes whilst accepting that FMIP cookies might not be cleared in rare race conditions.

pyicloud/services/drive.py (2)

46-58: Robust cookie snapshot prevents concurrent modification errors.

The fallback to an empty list on RuntimeError is well-handled—it leads to a TokenException rather than attempting to extract a token from potentially corrupted state.


415-427: Effective defensive copies for concurrent access.

The dict merging at line 419 and list copy at line 423 both prevent "dictionary changed size during iteration" errors. The shallow copy approach is consistent with the patterns in session.py and adequate for the current data structures.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 19, 2025
Copy link
Owner

@timlaing timlaing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olenzma - many thanks for your contribution, this is greatly appreciated.

If you could make these minor changes and update and tests that would be appreciated.

Thanks

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@pyicloud/cookie_jar.py`:
- Around line 48-62: The current broad RuntimeError handler around building and
clearing cookies (the block that computes cookies_to_clear via self.copy() and
iterates calling self.clear(domain=cookie.domain, path=cookie.path,
name=cookie.name) for cookies matching _FMIP_AUTH_COOKIE_NAME) should be
tightened: catch RuntimeError as e, check that str(e) (or e.args[0]) contains
the specific "dictionary changed size during iteration" text before swallowing;
for that case log a warning including the exception and context (e.g., that FMIP
auth cookie clearing was skipped) using the module logger, and re-raise any
other RuntimeError so unrelated errors are not hidden.
- Around line 74-86: The except RuntimeError block that silently drops save
failures in the cookie persistence logic (around PyiCloudCookieJar.copy() and
LWPCookieJar.save) should surface the failure: replace the silent pass with
logging a warning (include exception details and the filename/resolved variable)
and either re-raise the RuntimeError or return a boolean/raise a custom
exception so callers can react; update the except block to capture the exception
(e.g., as exc), call the project's logger (or the module logger) to log a
warning/error with context including resolved, ignore_discard/ignore_expires,
and then re-raise or propagate the error instead of swallowing it.

In `@pyicloud/services/drive.py`:
- Around line 46-48: The loop claims to copy cookies but iterates directly over
self.session.cookies, so make a snapshot before iterating to avoid
concurrent-modification errors: replace the iteration over self.session.cookies
with iteration over a shallow copy (e.g., cookies_snapshot =
list(self.session.cookies) and then for cookie in cookies_snapshot) so the loop
in the Drive service uses the snapshot instead of the live cookie jar.
🧹 Nitpick comments (2)
tests/test_cookie_jar.py (1)

120-120: Type annotation is inaccurate.

jar.copy() returns PyiCloudCookieJar (as defined in pyicloud/cookie_jar.py:25), not RequestsCookieJar. While this works at runtime due to inheritance, the annotation could be misleading.

✏️ Suggested fix
-        jar2: RequestsCookieJar = jar.copy()
+        jar2: PyiCloudCookieJar = jar.copy()

And update the import:

-from requests.cookies import RequestsCookieJar
+from pyicloud.cookie_jar import _FMIP_AUTH_COOKIE_NAME, PyiCloudCookieJar
tests/services/test_drive.py (1)

729-746: Test name overstates what is being verified.

This test doesn't actually validate concurrent modification safety. It simply passes a list and checks that iteration succeeds, which would pass even without any thread-safety fix. A true concurrent modification test would require multiple threads modifying the cookie jar whilst another iterates.

Consider renaming to test_get_token_from_cookie_iterates_cookies or adding an actual concurrency test using threading.

@timlaing timlaing enabled auto-merge (squash) February 3, 2026 06:15
@timlaing timlaing disabled auto-merge February 3, 2026 06:19
@timlaing timlaing merged commit c097c21 into timlaing:main Feb 3, 2026
11 of 12 checks passed
@timlaing
Copy link
Owner

timlaing commented Feb 3, 2026

@olenzma - many thanks for this update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants