Skip to content

Fail fast on unsupported lambda/partial callbacks in schedule_event#3320

Merged
quaquel merged 24 commits intomesa:mainfrom
falloficarus22:schedule-event-lambda-callback-gc
Feb 23, 2026
Merged

Fail fast on unsupported lambda/partial callbacks in schedule_event#3320
quaquel merged 24 commits intomesa:mainfrom
falloficarus22:schedule-event-lambda-callback-gc

Conversation

@falloficarus22
Copy link
Copy Markdown
Contributor

@falloficarus22 falloficarus22 commented Feb 16, 2026

Summary

Aligns Event callback handling with the weakref-based design: callbacks are accepted based on weakref capability (not callable type), and callback lifetime semantics are explicitly documented.

Bug / Issue

Closes #3319
The final agreed behavior is to preserve weakref semantics: callbacks may no-op if the target is garbage-collected before execution.
This PR updates implementation/tests/docs to match that contract and avoids type-based rejection of valid callables like functools.partial.

Implementation

Updated events.py:

  • Added a shared module-level helper to create callback references (_create_callable_reference).
  • Centralized callback reference creation in one place and reused it in __setstate__.
  • Preserved weakref behavior for callbacks (including bound methods via WeakMethod).
  • Removed type-based lambda/partial rejection.
  • Added explicit error path for callable objects that cannot be weak-referenced.
  • Updated Event notes/docstring to reflect callback lifetime semantics.

Updated tests:

  • test_schedule_run.py
    • Replaced rejection tests with coverage showing lambda/partial callbacks execute when a strong reference is retained.
  • test_devs.py
    • Added execution coverage for lambda/partial callbacks with strong refs.

Testing

  • test_schedule_run.py: 22 passed
  • test_dev.py: 23 passed

Additional Notes

  • This PR does not introduce strong-reference retention for callbacks.
  • Runtime behavior remains weakref-driven by design; if the callback target is collected, execution is skipped silently.
  • Main change is contract alignment + capability-based validation.

@falloficarus22 falloficarus22 marked this pull request as draft February 16, 2026 04:40
@falloficarus22 falloficarus22 marked this pull request as ready for review February 16, 2026 04:40
@falloficarus22 falloficarus22 marked this pull request as draft February 16, 2026 04:41
@falloficarus22 falloficarus22 marked this pull request as ready for review February 16, 2026 11:18
@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 16, 2026

It was a deliberate design choice not to allow lambdas, and this behavior is documented. I would need strong reasons to change my mind on this.

@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -0.6% [-1.1%, -0.1%] 🔵 -0.7% [-0.9%, -0.4%]
BoltzmannWealth large 🔵 +1.2% [+0.9%, +1.5%] 🔵 +1.3% [+0.9%, +1.5%]
Schelling small 🔵 +1.9% [+1.5%, +2.3%] 🔵 +0.6% [+0.5%, +0.8%]
Schelling large 🔵 +3.2% [+2.4%, +4.0%] 🔴 +10.2% [+8.4%, +11.7%]
WolfSheep small 🔵 +3.0% [+2.6%, +3.4%] 🔵 -0.2% [-0.3%, +0.0%]
WolfSheep large 🔵 +1.5% [-0.3%, +3.2%] 🔵 -1.1% [-3.4%, +0.9%]
BoidFlockers small 🔵 +0.6% [+0.3%, +0.9%] 🔵 +0.2% [+0.0%, +0.4%]
BoidFlockers large 🔵 +0.9% [+0.5%, +1.4%] 🔵 +0.4% [+0.1%, +0.6%]

@falloficarus22
Copy link
Copy Markdown
Contributor Author

It was a deliberate design choice not to allow lambdas, and this behavior is documented. I would need strong reasons to change my mind on this.

My concern is only that schedule_event(lambda ...) is currently accepted and then fails silently later. I suggest failing fast at scheduling time with a clear error, while keeping the existing weakref behavior for supported callables.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 16, 2026

My concern is only that schedule_event(lambda ...) is currently accepted and then fails silently later. I suggest failing fast at scheduling time with a clear error, while keeping the existing weakref behavior for supported callables.

But that is not what this PR does. It creates an internal hard ref for weak references. Also, the same issue as lambdas also holds potentially for partial functions.

I am potentially open to accepting that lambdas and partials raise a value error when passed to an event.

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

falloficarus22 commented Feb 17, 2026

I am potentially open to accepting that lambdas and partials raise a value error when passed to an event.

I suggest fail-fast validation instead: reject unsupported ephemeral callables at event creation (at least lambdas and functools.partial) with a clear ValueError, while preserving the existing weakref behavior for supported callables.

What are your thoughts?

@falloficarus22 falloficarus22 marked this pull request as draft February 17, 2026 08:29
@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 17, 2026

suggest fail-fast validation instead: reject unsupported ephemeral callables at event creation (at least lambdas and functools.partial) with a clear ValueError, while preserving the existing weakref behavior for supported callables.

This is exactly what I was trying to suggest.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

Should I refactor this PR then?

@falloficarus22 falloficarus22 changed the title Retain inline lambda callbacks for scheduled events Fail fast on unsupported lambda/partial callbacks in schedule_event Feb 17, 2026
@falloficarus22 falloficarus22 marked this pull request as ready for review February 17, 2026 09:34
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +2.0% [+1.3%, +2.8%] 🔵 +0.5% [+0.3%, +0.6%]
BoltzmannWealth large 🔵 +1.0% [+0.0%, +1.9%] 🔵 -0.3% [-4.0%, +3.7%]
Schelling small 🔵 +1.5% [+0.9%, +2.2%] 🔵 +1.1% [+0.8%, +1.5%]
Schelling large 🔵 +1.6% [+1.1%, +2.2%] 🔴 +16.8% [+13.9%, +19.4%]
WolfSheep small 🔴 +7.1% [+6.3%, +7.8%] 🔴 +3.7% [+3.2%, +4.1%]
WolfSheep large 🔵 +1.6% [+0.1%, +2.9%] 🔵 +0.3% [-1.9%, +2.2%]
BoidFlockers small 🔵 +0.6% [+0.3%, +1.0%] 🔵 -1.5% [-1.8%, -1.2%]
BoidFlockers large 🔵 +2.2% [+1.5%, +2.9%] 🔵 -1.8% [-2.3%, -1.3%]

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 17, 2026

Thanks for the update. I have been looking at this issue myself as well for some other reasons.

  1. I would move the check into a separate function rather than a static method. We need the validation in other places as well.
  2. I am still wondering about partials. At the moment, we don't allow args and kwargs in Event, so if we also don't allow partials, we severely limit the functions and methods that can be scheduled. Is there a way to check if a partial function has been assigned to a variable? The other option is to allow args and kwargs on Event.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

There is no way to check whether a partial was “assigned to a variable” in a way that guarantees safe lifetime.
So the reliable way might be to “allow args/kwargs on Event usage path” (or expose them via schedule_event), not trying to detect variable assignment for partials.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 17, 2026

The other option is to issue a warning if the weakref resolves to None. Warnings in general are issued only once, but it would allow users to more quickly diagnose that something might be wrong.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 17, 2026

I would suggest approaching this slightly differently:

        if isinstance(function, MethodType):
            function = WeakMethod(function)
        else:
            function = ref(function)

        if function() is None:
            # error message needs work
            raise TypeError(function is not weakly referencable ...)

        self.fn = function

So rather than checking the type and everything, we just check if we can take a weakref to the function at creation. If we can do that, we accept it. Otherwise, we raise an error. In this way, partials assigned to a variable are still accepted.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

I like the direction, but I want to flag one concern: validating weakref resolution at creation time is only a point-in-time check. A callback can still be GC’d before execution, so we may still get nondeterministic no-ops.
Would you be comfortable making that an explicit contract (“caller must keep a strong reference alive”), or should we prefer stricter fail-fast rules to avoid that class of runtime ambiguity?
If we go with the weakref-capability approach, I’d suggest also keeping a runtime warning when fn() resolves to None so failures are diagnosable.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 17, 2026

A callback can still be GC’d before execution, so we may still get nondeterministic no-ops.

Yes, but that is desirable behavior. Imagine you schedule events for an agent. Somewhere along the way, this agent is removed from the simulation. All events that call this agent now must either be removed or fail silently. With weakrefs, we get silent failures.

Not sure about the warning. I think it might be more confusing than helpful.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 17, 2026

Can you update the starting post to align with the final implementation?

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 18, 2026

The behavior is still not exactly what I have in mind. Let me try to explain it

  • If function is not a callable --> raise a TypeError
  • If function is not weakly referenceable --> raise a TypeError
  • If weakref to function resolves to None when Event is instantiated, raise a ValueError

This last one is the sticky one and also in my own testing I can't seem to get it right. Basically, Event(5, lambda:5) or Event(5, partial(my_func, 1, 2,3)) should raise a ValueError. While

a = partial(my_func, 1,2,3)
Event(5, a)

should run fine.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

Thanks, this helps clarify the intended behavior.
I think we can implement the “tricky” case by doing the liveness check after dropping the local strong reference in Event.__init__:

if not callable -> TypeError
if not weakrefable -> TypeError
create weakref, drop local strong ref, then check wr() is None -> ValueError
That should make Event(5, lambda: 5) and inline Event(5, partial(...)) fail, while a = partial(...); Event(5, a) still works.

@falloficarus22
Copy link
Copy Markdown
Contributor Author

We hit cross-platform instability with the current inline-partial check.
The logic depends on sys.getrefcount, and that value changes across CPython builds/OS/test environments, so a variable-bound partial is sometimes misclassified and rejected.

So the check is not deterministic in CI.
To keep behavior stable, I propose we remove refcount-based partial detection and keep only deterministic guards:

  • callable required
  • weak-referenceable required
  • lambda rejected at creation
  • dead weakref at creation rejected

This keeps fail-fast behavior without flaky platform-dependent failures.

@falloficarus22 falloficarus22 force-pushed the schedule-event-lambda-callback-gc branch from 6fe3571 to 29179f8 Compare February 18, 2026 14:09
@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 19, 2026

code wise, it seems fine now. I want to find some time to test a few things manually on this brach before approving it.

@quaquel
Copy link
Copy Markdown
Member

quaquel commented Feb 22, 2026

the docs are breaking because the event scheduling tutorial is breaking because it uses lambda functions. Can you take a look at fix this?

@falloficarus22
Copy link
Copy Markdown
Contributor Author

Fixed it.

@quaquel quaquel merged commit 3cd5f0f into mesa:main Feb 23, 2026
15 checks passed
@EwoutH
Copy link
Copy Markdown
Member

EwoutH commented Mar 11, 2026

@quaquel backport?

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.

schedule_event silently drops inline lambda callbacks before execution

3 participants