Skip to content

fix: use weakref in EventGenerator to prevent memory leak#3360

Merged
quaquel merged 12 commits intomesa:mainfrom
Krishsharma179:feature-event-generator
Feb 25, 2026
Merged

fix: use weakref in EventGenerator to prevent memory leak#3360
quaquel merged 12 commits intomesa:mainfrom
Krishsharma179:feature-event-generator

Conversation

@Krishsharma179
Copy link
Copy Markdown
Contributor

Summary

This PR fixes the memory leak by:

  • Using weak references instead of hard references (like Event class)
  • Adding error handling for invalid callables
  • Implementing silent no-op when references become invalid
  • Adding comprehensive tests

Bug / Issue

Pr for #3332
EventGenerator.function is a hard reference rather than a weak ref as used in Event. This can lead to memory leaks if a generator is used in combination with, e.g., an Agent. If the agent is removed from the model but remains part of an EventGenerator, it cannot be garbage-collected.

Implementation

As per the coversation in #3320
The behavior of both Event and EventGenerator should be the same
so,

  • if the function is not callable -> TypeError
  • if the function is not weakrefable -> TypeError
  • create weakref, drop local strong ref, then check wr() is None -> ValueError
  • so we are failing fast during the initialization but if the weakref resolve to None we are just stoping the generator(no-op)
  • made a __getstate__ and __setstate__ for the pickling issue

Testing

  • Error cases (non-callable, non-weakly-referenceable)
    It is work as expected Event(5, lambda: 5) fail, while a = lambada:10; Event(5, a) still works.
  • Weak reference behavior and garbage collection
  • Pickling/unpickling support
  • No-op behavior when weakref dies during execution

Additional Notes

  • Matches Event class weak reference pattern for consistency
  • Maintains backward compatibility
  • Comprehensive tests for weakrefs, garbage collection, and pickling
  • Proper error handling for invalid callables

@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch from 1afe307 to b939c4f Compare February 21, 2026 18:53
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +1.4% [+0.6%, +2.1%] 🔵 +0.2% [-0.0%, +0.4%]
BoltzmannWealth large 🔵 -0.3% [-0.9%, +0.3%] 🔵 -2.5% [-4.5%, -0.4%]
Schelling small 🔵 +0.4% [-0.2%, +0.9%] 🔵 -1.1% [-1.4%, -0.9%]
Schelling large 🔵 -0.8% [-1.5%, -0.2%] 🔵 -2.0% [-3.6%, -0.4%]
WolfSheep small 🔵 -0.2% [-0.5%, +0.2%] 🔵 +0.9% [+0.6%, +1.1%]
WolfSheep large 🔵 -0.8% [-1.6%, -0.0%] 🔵 -0.8% [-2.2%, +0.4%]
BoidFlockers small 🔵 -3.3% [-3.6%, -3.0%] 🔵 -1.6% [-1.8%, -1.3%]
BoidFlockers large 🔵 -1.7% [-2.3%, -1.1%] 🔵 -1.2% [-1.5%, -0.8%]

@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch 2 times, most recently from 7e79eb8 to 7ce0b03 Compare February 21, 2026 19:11
@Krishsharma179
Copy link
Copy Markdown
Contributor Author

@quaquel This is the updated PR for the issue #3332 It passes all the checks just required your review please let me know if there is any changes required this PR follows the conversation #3320

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 23, 2026

Thanks for this cleaning and PR. I just merged #3320, which adds a new function for creating the weakref and do some tests on it. Can you update this PR by merging with upstream main and evaluate whether the new function is useful for this PR?

f"function {function} is not weakly referenceable. "
f"Use a user-defined function or method instead."
) from e
# Here it will raise a value error if the function returns none
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

all of this is replaceable by the new function.

Comment on lines +244 to +247
if weak_fun() is None:
raise ValueError(
"Cannot create EventGenerator with a function that is already garbage collected"
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we left this out of #3320, because it is very OS specific whether this adds value. I am still considering adding a check like this, but I need to check its behavior across different operating systems.

Comment on lines +257 to +262
@property
def function(self) -> Callable:
"""Get the function. If garbage collected, returns no-op."""
func = self._function()

return func if func is not None else lambda: None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why did you add this property?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This property checks if the weak reference is dead (garbage collected) and then it will return none

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know what the code does, I am asking why you deemed it necessary to put this into a property.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Upon closer look, I realize the @property might be redundant.

We already check the weakref safely in _execute_and_reschedule:

func = self._function()
if func is None:
    self.stop()
    return

Since the safety check happens at execution time anyway, I'm okay to remove this @Property completely and rely on the check in _execute_and_reschedule.

return # Silent no-op (no error raised)

# Execute the function
func()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest not calling this variable func. This is a protected term in Python.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok i will change it

assert lifetimes.Step.max() == 2


class TestEventGeneratorMemoryLeak(unittest.TestCase):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest moving these tests into test_events.py, rather than here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok I will move it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I meant all tests you added to test_lifespan.py

@quaquel quaquel added the enhancement Release notes label label Feb 23, 2026
@Krishsharma179
Copy link
Copy Markdown
Contributor Author

Thanks for this cleaning and PR. I just merged #3320, which adds a new function for creating the weakref and do some tests on it. Can you update this PR by merging with upstream main and evaluate whether the new function is useful for this PR?

Hello @quaquel , I was going through the merged #3320 code and in the function

if isinstance(function, types.FunctionType) and function.__name__ == "<lambda>":
        raise ValueError("function must be alive at Event creation.")

this line will raise the value error on both the cases

my_func = lambda x: x + 10
event = Event(my_func)  

# This is unsafe (inline), also blocked:
event = Event(lambda x: x + 10)  

Question: Is blocking Case 1 intentional? Since my_func holds a reference, it shouldn't be garbage collected. Should we allow stored lambdas, or is the strict blocking deliberate to avoid edge cases?

On the new function: Yes, function is definitely useful for this PR - it centralizes the weak reference logic and makes the code cleaner. I'll update my implementation to use it.
Thanks!

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 24, 2026

Assigning a a lambda function to a variable defeats the purpose of a lambda as an anonymous function. Most linters will also flag it as a problem and it's considered an anti pattern. So, I am fine with just blocking lambda's in general.

@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch from 06c637d to ed51889 Compare February 24, 2026 12:12
@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch from a605785 to 0ed3c79 Compare February 24, 2026 13:48
@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch from 51c46bb to 8b1a4b2 Compare February 25, 2026 06:00
@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch from 4fc42d7 to 685b1cc Compare February 25, 2026 06:15
@Krishsharma179 Krishsharma179 force-pushed the feature-event-generator branch from 4f67f5f to 8d5fb89 Compare February 25, 2026 06:29
@Krishsharma179
Copy link
Copy Markdown
Contributor Author

@quaquel i have made the changes please check

Comment on lines +234 to +278
@@ -270,7 +273,16 @@ def _execute_and_reschedule(self) -> None:
if not self._active:
return

self.function()
# Check weakref HERE (execution time), not in property getter
# This matches Event class behavior - weakref check during execution
fn = self._function()
Copy link
Copy Markdown
Member

@quaquel quaquel Feb 25, 2026

Choose a reason for hiding this comment

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

Suggested change
# Check weakref HERE (execution time), not in property getter
# This matches Event class behavior - weakref check during execution
fn = self.function()

@quaquel quaquel merged commit 5219e82 into mesa:main Feb 25, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Release notes label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants