Skip to content

RuntimeError when using .remove() in separate in very uncommon circumstances #247

@aatle

Description

@aatle

Circumstances:

  1. During Space.step(), a shape is removed (such as from post_solve callback). The shape removal is automatically delayed until the step finishes.
  2. After the step ends, the shape removal occurs and triggers a separate callback.
  3. Inside the separate callback, an object is removed.

This leads to an error:

File "C:\...\site-packages\pymunk\space.py", line 590, in step
    for obj in self._remove_later:
RuntimeError: Set changed size during iteration

What happened was that while the space was iterating over the set of objects to be removed, there was a call to .remove() in separate. Since the space is locked during this, a new object is put into the set of objects to be removed, but this is illegal as the set is being iterated. This bug is minor, and I can't really think of a practical situation where this might possibly be encountered.
In Space.step():

pymunk/pymunk/space.py

Lines 588 to 592 in ce59f9a

self.add(*self._add_later)
self._add_later.clear()
for obj in self._remove_later:
self.remove(obj)
self._remove_later.clear()

I'd recommend that any objects removed from these special separate calls also be removed immediately after since they can be.

Slightly related code improvement

It seems that _add_later and _remove_later are sets instead of lists which doesn't seem like a good choice. Not only does this forget the actual order and is slightly slower, it also allows duplicates (space.remove(body1, body1)) in a step but not outside a step. So, they should be lists instead.

Recommended fix, assuming above code improvement is applied
self.add(*self._add_later)
self._add_later.clear()
# A separate callback may remove objects
while self._remove_later:
    remove_later = self._remove_later
    self._remove_later = []
    self.remove(*remove_later)

With python >=3.8, a walrus operator may be used for the loop condition.

Minimal reproducible example
import pymunk as pm


space = pm.Space()

space.gravity = 0, -100

body1 = pm.Body()
shape1 = pm.Circle(body1, 20)
shape1.density = 5
shape1.collision_type = 111

floor1 = pm.Segment(space.static_body, (-100,-30), (100,-30), 1)

body2 = pm.Body()
body2.position = 500,0
shape2 = pm.Circle(body2, 20)
shape2.density = 5
shape2.collision_type = 222

floor2 = pm.Segment(space.static_body, (400,-30), (600,-30), 1)

space.add(body1, shape1, body2, shape2, floor1, floor2)


def separate(arbiter: pm.Arbiter, space: pm.Space, data):
  print('SEPARATE')
  print('REMOVE 2, remove any object')
  space.remove(floor2)


def post_solve(arbiter: pm.Arbiter, space: pm.Space, data):
  print('POST-SOLVE')
  if i == 30:
    print('REMOVE 1, causes separate during step')
    space.remove(shape1)
    # space.remove(shape1, shape1, shape1, shape1)


space.add_wildcard_collision_handler(111).separate = separate
space.add_wildcard_collision_handler(222).post_solve = post_solve


for i in range(60):
  print('step START', i)
  space.step(1/60)
  print('step END', i)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions