Skip to content

fix(framework): Make TypedDict mutations thread-safe - #7980

Closed
panh99 wants to merge 3 commits into
mainfrom
codex/typeddict-thread-safe
Closed

fix(framework): Make TypedDict mutations thread-safe#7980
panh99 wants to merge 3 commits into
mainfrom
codex/typeddict-thread-safe

Conversation

@panh99

@panh99 panh99 commented Aug 22, 2026

Copy link
Copy Markdown
Member

Issue

Description

Concurrent writes to TypedDict are not synchronized.

Related issues/PRs

None.

Proposal

Explanation

Add a per-instance lock around item assignment and deletion while leaving read methods unchanged. Recreate the lock when copying or unpickling a TypedDict so existing copy and serialization behavior continues to work.

Checklist

  • Implement proposed change
  • Write tests
  • Update documentation
  • Address LLM-reviewer comments, if applicable (e.g., GitHub Copilot)
  • Make CI checks pass
  • Ping maintainers on Slack (channel #contributions)

Any other comments?

No new tests were added because this is a small synchronization change. Existing validation passed:

  • Ruff lint and formatting
  • mypy for typeddict.py
  • 63 tests in recorddict_test.py

Copilot AI lite review requested due to automatic review settings August 22, 2026 16:46
@panh99
panh99 marked this pull request as ready for review August 22, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a per-instance threading.Lock to TypedDict to synchronize concurrent mutations (assignment/deletion) and ensures copied/unpickled instances get a fresh lock so serialization/copying continues to work.

Changes:

  • Add _lock to TypedDict instances and guard __setitem__/__delitem__ with it.
  • Ensure copy() assigns a new lock on the copied instance.
  • Add __getstate__/__setstate__ to exclude the unpicklable lock from pickling and recreate it on restore.

Critical issues

  • copy() snapshots _data without holding _lock, which can still raise RuntimeError: dictionary changed size during iteration or produce an inconsistent snapshot under concurrent writers.
  • __getstate__ exposes _data to pickling without synchronizing with _lock, so pickling can still fail with RuntimeError under concurrent writers.
  • No tests were added for the new concurrency behavior; a small regression test would help prevent future breakage.

Simplicity/readability suggestions

  • Prefer state.pop("_lock", None) over del state["_lock"] in __getstate__ to make the method more robust to unexpected state shapes.

Consistency concerns

  • None identified beyond the concurrency gaps above.

Whether the PR should be split

  • No; the changes are cohesive and localized.

Brief overall verdict

The core approach is sound, but copy()/pickling still have concurrency failure modes and the change should be backed by at least a minimal concurrency test.

Suppressed comments (1)

framework/py/flwr/app/message/typeddict.py:115

  • __getstate__ returns _data by reference and does not synchronize with _lock. Pickle will iterate _data; if another thread is mutating concurrently, this can raise RuntimeError: dictionary changed size during iteration. Snapshot _data while holding _lock and remove _lock via pop for robustness.
    def __getstate__(self) -> dict[str, object]:
        """Return the state without the unpicklable lock."""
        state = self.__dict__.copy()
        del state["_lock"]
        return state

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 105 to 109
new.__dict__["_check_key_fn"] = self.__dict__["_check_key_fn"]
new.__dict__["_check_value_fn"] = self.__dict__["_check_value_fn"]
new.__dict__["_data"] = cast(dict[K, V], self.__dict__["_data"]).copy()
new.__dict__["_lock"] = Lock()
return new
Comment on lines 43 to +56
@@ -46,11 +47,13 @@ def __setitem__(self, key: K, value: V) -> None:
cast(Callable[[V], None], self.__dict__["_check_value_fn"])(value)

# Set key-value pair
cast(dict[K, V], self.__dict__["_data"])[key] = value
with self.__dict__["_lock"]:
cast(dict[K, V], self.__dict__["_data"])[key] = value

def __delitem__(self, key: K) -> None:
"""Remove the item with the specified key."""
del cast(dict[K, V], self.__dict__["_data"])[key]
with self.__dict__["_lock"]:
del cast(dict[K, V], self.__dict__["_data"])[key]
@github-actions github-actions Bot added the Maintainer Used to determine what PRs (mainly) come from Flower maintainers. label Aug 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de43ab41f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +51
with self.__dict__["_lock"]:
cast(dict[K, V], self.__dict__["_data"])[key] = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Protect compound mutations with the same lock

When two threads call the inherited MutableMapping.setdefault for the same missing key, both perform the unlocked lookup before reaching this lock, then each stores and returns its own default even though only one value remains in the mapping. The inherited pop and popitem have similar lookup-then-delete races. Override these compound mutators so the entire operation is protected by the instance lock rather than locking only the final assignment or deletion.

Useful? React with 👍 / 👎.

@panh99 panh99 closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Maintainer Used to determine what PRs (mainly) come from Flower maintainers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants