Conversation
The @ATMT.timeout and @ATMT.timer decorators register their Timer
objects on the class (one Timer per decorated condition), and
instances never shadowed that dict, so every running instance
shared the same Timer objects. A Timer's runtime state (_time,
_expired, _just_expired) is per-run bookkeeping consumed by
exactly one control thread: _decrement() flips _expired once, and
expired() resets _just_expired for whoever asks first.
With two instances of the same automaton running concurrently -
exactly what Automaton.spawn() and @ATMT.ioevent(as_supersocket=...)
are for - the instance whose control thread decrements a shared
timer first consumes the expiration; the loser's until_next()
keeps returning None and its select() blocks forever. The timeout
silently never fires for it, and since spawn()'s housekeeping only
reclaims instances with isrunning() == False, the wedged instance
also leaks its sockets.
Measured (two instances, one 0.2s timeout each):
unpatched: a.isrunning()=False b.isrunning()=True <- b hung
patched: both instances end within the timeout
Copy the class-level timers per started instance in _do_control,
after parse_args(), so pre-start reconfiguration through
timer_by_name() (which mutates the class-level timers) is still
honored, and class defaults stay pristine across runs (verified
against the existing timer counts and the reconfigure tests).
AI-Assisted: yes (GLM-5.3 via ZCode)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #5199 +/- ##
==========================================
- Coverage 80.92% 80.87% -0.05%
==========================================
Files 393 393
Lines 98107 98114 +7
==========================================
- Hits 79392 79354 -38
- Misses 18715 18760 +45
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
@ATMT.timeoutand@ATMT.timerdecorators create oneTimerobject per decorated condition, registered on the class (scapy/automaton.py:596def deco(f, state=state, timeout=Timer(timeout)), collected into the class-levelcls.timeoutdict). Instances never shadow that dict, so every running instance of the same automaton shares the sameTimerobjects.A
Timer's runtime state (_time,_expired,_just_expired) is per-run bookkeeping that is consumed exactly once:_decrement()flips_expiredthe first time the remaining time hits zero,expired()hands out the timers whose_just_expiredis set and resets that flag for whoever asked first.With two instances of the same automaton running concurrently — exactly what
Automaton.spawn()("start the automaton for each new client") and@ATMT.ioevent(as_supersocket=...)do — whichever control thread decrements a shared timer first consumes the expiration. The other instance'suntil_next()keeps returningNone(blocking) for that timer, so itsselect()blocks forever: the timeout silently never fires for it.spawn()'s housekeeping only reclaims instances withisrunning() == False, so the wedged instance also leaks its sockets.Measured on current master (two instances, one 0.2s timeout each):
Changes
_do_controlcopies the class-level timers into a per-instanceself.timeoutwhen the instance starts (afterparse_args(), so pre-start reconfiguration throughtimer_by_name()— which mutates the class-level timers — is still honored;_funcand the configuration stay shared, only the runtime state becomes per-instance).Existing semantics verified unchanged: the timer-count assertions from
automaton.uts(10/6 counts,timer_by_nametimeouts), the pre-start "reconfigure timers" flow, and class defaults staying pristine across repeated runs.Testing
New case in
test/scapy/automaton.utsraces two instances of a one-timeout automaton and asserts both observe their own timeout (fails on unpatched master by hanging one instance; passes with the patch). Locally:flake8(max-line 88, repo ignores) clean on the changed file aside from pre-existingF401s that only appear with pyflakes ≥3 (type-comment usage; invisible to the pinned flake8<6 CI uses).