Replies: 1 comment
-
|
The main problem is the fact that b1 = NewBase() # Creates 'NewBase_0'
b2 = NewBase() # Creates 'NewBase_1'
b1 = b2 # Original b1 object becomes unreachable
gc.collect() # Does NOT free the object
session.all_names() # Still shows ['NewBase_0', 'NewBase_1']This causes memory leaks and dangling refs. dispose()that is, b1 = NewBase() # Creates 'NewBase_0'
b2 = NewBase() # Creates 'NewBase_1'
b1.dispose()
b1 = b2 # Original b1 object becomes unreachable
session.all_names() # shows now ['NewBase_1'] as it shouldThis is clearly unacceptable. WE HAVE TO USE WEAKREFs |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Continuation of #218 with
Sessionobject used for hard ref tracking.Session-Based Object Registry Replacing WeakRef GlobalObject
Elimination of race conditions caused by iterating a
WeakValueDictionaryinside a global singleton (GlobalObject+Map).Context and Problem Statement
Every EasyScience object (
NewBaseand its descendants) registered itself at construction time in a process-wide singleton:Map._storewas aWeakValueDictionary. Objects were pruned automatically by the garbage collector viaweakref.finalizecallbacks.This design produced three types of bugs:
Concurrent-modification crashes –
RuntimeError: dictionary changed size during iterationoccurred when GC ran while other code iterated_store. The workaround (while True: try: list(...) except: continue) bounded neither latency nor correctness.Global mutable state – a single shared registry made isolated testing impossible without calling
global_object._clear()between every test, and prevented two independent object graphs from coexisting in the same process.Entangled concerns – naming, graph tracking, undo/redo history, and script recording were all funnelled through one singleton, making each concern hard to evolve independently.
The question addressed here is: what should replace the GlobalObject + WeakValueDictionary combination, and why is
Sessionthe right anwer?Considered Options
Option A – Class-level counter on
NewBase(no separate class)As described in #218
All state lives on the class itself.
Option B – Module-level plain dict + functions
Also proposed in #218
State lives in module globals, accessed through free functions.
Option C –
Sessionas a dedicated mutable-state holder - PROPOSED HERE.State is an instance. A module-level default session provides the single-global behaviour of the old code. Callers that need isolation create their own
Sessionand pass it via the constructor.Proposed solutuion: Option C
The key insight is that the problematic property of the old design was not global state per se. It was uncontrollable global state.
Why
Sessionmust be a separate class, not state onNewBaseThis is the main point of the design.
1. The state is shared across instances, not per-instance
A registry maps names -> objects. Name counters track how many objects of each class prefix exist. Parent/child edges connect pairs of objects. None of this state belongs to any single object - it is relational state that spans many objects simultaneously. Attaching it to
NewBaseinstances would require every setter and method that mutates the registry to reach across to class-level or module-level storage anyway, producing the same coupling as the old GlobalObject.3. The lock must co-locate with the data it protects
threading.RLockis only meaningful when the lock and the data it guards move together. If the registry lived in a class attribute, every test that resets the class attribute would also have to reset the lock — creating a TOCTOU window. BecauseSession.__init__creates both the lock and the empty dicts in one atomic step, the invariant "lock and data are always in sync" is structurally enforced.4. Lifecycle is explicit and testable
A
Sessioncan be created, used, and discarded. Disposal of all objects inside it is well-defined. With class-level or module-level dicts, "clearing the registry" means mutating shared state in place, which is still a global side effect observable by any concurrent code. Creating a freshSession()creates a completely new namespace.5. Future extensibility without breaking
NewBaseSerialising an entire session, sharing sessions across processes, attaching per-session event hooks - all these potential future features have a natural home on
Sessionwithout touchingNewBase's public API. With class-level state there is nowhere clean to attach such behaviour.Positive Consequences
WeakValueDictionaryandweakref.finalizeare gone.set_default_session(Session())in anautousefixture. No inter-test pollution possible.Sessionholdself._lock; the guarantee is documented and auditable.NewBaseconstructor signature gainssession=None, which is backward-compatible: all existing call sites continue to work and receive the default session transparently.Negative Consequences / Known Limitations
dispose()is now the caller's responsibility. The old design used GC finalizers for cleanup; the new design requires explicitobj.dispose(). Forgetting to call it leaks entries in the session registry (strong references, so no GC recovery). This is a deliberate trade-off: predictable leaks are easier to debug than unpredictable GC races.@property_stackdecorator has been removed fromdisplay_name.setterin this change. Those features will be addressed in a follow-up ADR once aSession-scoped undo stack design is agreed.Beta Was this translation helpful? Give feedback.
All reactions