1313# limitations under the License.
1414
1515import abc
16+ import threading
1617
17- from google .protobuf import duration_pb2
18- from google .protobuf import timestamp_pb2
19- from google .protobuf import field_mask_pb2
20- from google .protobuf import struct_pb2
21- from google .protobuf import wrappers_pb2
18+ from google .protobuf import (
19+ duration_pb2 ,
20+ field_mask_pb2 ,
21+ struct_pb2 ,
22+ timestamp_pb2 ,
23+ wrappers_pb2 ,
24+ )
2225
2326from proto .marshal import compat
24- from proto .marshal .collections import MapComposite
25- from proto .marshal .collections import Repeated
26- from proto .marshal .collections import RepeatedComposite
27-
27+ from proto .marshal .collections import MapComposite , Repeated , RepeatedComposite
2828from proto .marshal .rules import bytes as pb_bytes
29- from proto .marshal .rules import stringy_numbers
30- from proto .marshal .rules import dates
31- from proto .marshal .rules import struct
32- from proto .marshal .rules import wrappers
33- from proto .marshal .rules import field_mask
29+ from proto .marshal .rules import dates , field_mask , stringy_numbers , struct , wrappers
3430from proto .primitives import ProtoType
3531
3632
@@ -168,7 +164,10 @@ def get_rule(self, proto_type):
168164 # See https://github.com/googleapis/proto-plus-python/issues/349
169165 if rule == self ._noop and hasattr (self , "_instances" ):
170166 for _ , instance in self ._instances .items ():
171- rule = instance ._rules .get (proto_type , self ._noop )
167+ # Avoid race condition where instance is added to _instances
168+ # but __init__ hasn't run yet.
169+ rules = getattr (instance , "_rules" , {})
170+ rule = rules .get (proto_type , self ._noop )
172171 if rule != self ._noop :
173172 break
174173 return rule
@@ -254,6 +253,7 @@ class Marshal(BaseMarshal):
254253 """
255254
256255 _instances = {}
256+ _lock = threading .Lock ()
257257
258258 def __new__ (cls , * , name : str ):
259259 """Create a marshal instance.
@@ -265,7 +265,18 @@ def __new__(cls, *, name: str):
265265 """
266266 klass = cls ._instances .get (name )
267267 if klass is None :
268- klass = cls ._instances [name ] = super ().__new__ (cls )
268+ with cls ._lock :
269+ # Double check inside lock to confirm another thread hasn't
270+ # created the instance while we were waiting for the lock.
271+ klass = cls ._instances .get (name )
272+ if klass is None :
273+ # Use Copy-on-Write to avoid 'RuntimeError: dictionary changed size during iteration'
274+ # in BaseMarshal.get_rule. This allows other threads to iterate over the old
275+ # dictionary safely while we replace it with a new one atomically.
276+ new_instances = cls ._instances .copy ()
277+ klass = super ().__new__ (cls )
278+ new_instances [name ] = klass
279+ cls ._instances = new_instances
269280
270281 return klass
271282
0 commit comments