Skip to content

RuntimeError: dictionary changed size during iteration #15100

Description

@YLivay

Hello.

I've been getting the following error randomly:

RuntimeError: dictionary changed size during iteration
at .get_rule ( /app/.venv/lib/python3.12/site-packages/proto/marshal/marshal.py:172 )
at .to_python ( /app/.venv/lib/python3.12/site-packages/proto/marshal/marshal.py:200 )
at .__getattr__ ( /app/.venv/lib/python3.12/site-packages/proto/message.py:882 )
at ._comparator ( /app/.venv/lib/python3.12/site-packages/google/cloud/firestore_v1/base_query.py:1143 )
at .push ( /app/.venv/lib/python3.12/site-packages/google/cloud/firestore_v1/watch.py:565 )
at ._on_snapshot_target_change_no_change ( /app/.venv/lib/python3.12/site-packages/google/cloud/firestore_v1/watch.py:387 )
at .on_snapshot ( /app/.venv/lib/python3.12/site-packages/google/cloud/firestore_v1/watch.py:466 )
at ._thread_main ( /app/.venv/lib/python3.12/site-packages/google/api_core/bidi.py:667 )

This happens when _instances is being mutated while being iterated on.

proto/marshal/marshal.py:BaseMarshal.get_rule:

classBaseMarshal:
....
defget_rule(self, proto_type):
# Rules are needed to convert values between proto-plus and pb.# Retrieve the rule for the specified proto type.# The NoopRule will be used when a rule is not found.rule=self._rules.get(proto_type, self._noop)
# If we don't find a rule, also check under `_instances`# in case there is a rule in another package.# See https://github.com/googleapis/proto-plus-python/issues/349ifrule==self._noopandhasattr(self, "_instances"):
for_, instanceinself._instances.items():
rule=instance._rules.get(proto_type, self._noop)
ifrule!=self._noop:
breakreturnrule

From a quick scan the most likely place _instances is being mutated is when registering a new Marshal instance.

proto/marshal/marshal.py:Marshal.__new__:

classMarshal(BaseMarshal):
....
_instances= {}
def__new__(cls, *, name: str):
"""Create a marshal instance. Args: name (str): The name of the marshal. Instantiating multiple marshals with the same ``name`` argument will provide the same marshal each time. """klass=cls._instances.get(name)
ifklassisNone:
klass=cls._instances[name] =super().__new__(cls)
returnklass

Environment details

  • Programming language: Python
  • OS: Linux
  • Language runtime version: 3.12
  • Package version: 1.26.0

Steps to reproduce

I can't say for sure how to reproduce this, but it seems to happen when a Marshal instance is registered while Marshal.get_rule is busy iterating over self._instances.items() to see which rule can handle a given proto_type.

I'm encountering this randomly when using Firestore (v2.20.1) on_snapshot function to watch for changes on a collection. This on_snapshot is invoked in a separate thread.

To fix

If you're fine with get_rule iterating over potentially slightly stale data, I suggest copying _instances aside in Marshal.new, add the new marshaller to the copy it and set it back to _instances.

If we have to have _instances perfectly in sync, just add a RW lock.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api: firestoreIssues related to the Firestore API.priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions