diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Actionable.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Actionable.kt index da9027f8cd..0b646f2dc4 100644 --- a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Actionable.kt +++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Actionable.kt @@ -9,7 +9,9 @@ package it.unibo.alchemist.model +import it.unibo.alchemist.model.observation.Disposable import it.unibo.alchemist.model.observation.Observable +import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner import java.io.Serializable import org.danilopianini.util.ListSet @@ -18,7 +20,9 @@ import org.danilopianini.util.ListSet */ sealed interface Actionable : Comparable>, - Serializable { + Serializable, + Disposable, + LifecycleOwner { /** * @return true if the reaction can be executed (namely, all the conditions * are satisfied). diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt index 2acc820121..9a3ff3f640 100644 --- a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt +++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/Node.kt @@ -9,8 +9,10 @@ package it.unibo.alchemist.model import arrow.core.Option +import it.unibo.alchemist.model.observation.Disposable import it.unibo.alchemist.model.observation.Observable import it.unibo.alchemist.model.observation.ObservableMap +import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner import java.io.Serializable import kotlin.reflect.KClass import kotlin.reflect.full.isSubclassOf @@ -25,7 +27,9 @@ import kotlin.reflect.jvm.jvmErasure interface Node : Serializable, Iterable>, - Comparable> { + Comparable>, + Disposable, + LifecycleOwner { /** * Adds a reaction to this node. * The reaction is added only in the node, diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/Lifecycle.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/Lifecycle.kt new file mode 100644 index 0000000000..ebd24fa226 --- /dev/null +++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/Lifecycle.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2010-2026, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.model.observation.lifecycle + +/** + * Manages the state and listeners. + */ +interface Lifecycle { + + /** + * Returns the current state of the Lifecycle. + */ + val currentState: LifecycleState + + /** + * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes state. + */ + fun addObserver(observer: (LifecycleState) -> Unit) + + /** + * Removes the given observer from the observers list. + */ + fun removeObserver(observer: (LifecycleState) -> Unit) +} diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleOwner.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleOwner.kt new file mode 100644 index 0000000000..07f0c80082 --- /dev/null +++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleOwner.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010-2026, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.model.observation.lifecycle + +import it.unibo.alchemist.model.observation.Disposable +import it.unibo.alchemist.model.observation.Observable +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.DESTROYED +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.STARTED + +/** + * An object which has a [Lifecycle]. + */ +interface LifecycleOwner { + + /** + * The lifecycle of the provider. + */ + val lifecycle: Lifecycle +} + +/** + * Observes this [it.unibo.alchemist.model.observation.Observable] within the context of a [LifecycleOwner]. This + * method is a memory-safe alternative to [it.unibo.alchemist.model.observation.Observable.onChange] in scenarios + * where it is crucial to avoid that the owner subscription leaks. When the + * registrant's state reaches [LifecycleState.DESTROYED], the subscription + * is automatically removed and cleared up. Moreover, the [callback] is only + * invoked if the lifecycle is in an active state ([STARTED]). Finally, + * when the lifecycle moves from an inactive state back to active, the + * [callback] is triggered immediately with the [it.unibo.alchemist.model.observation.Observable.current] value. + * + * @param lifecycleOwner The object controlling the lifecycle of this subscription. + * @param callback The action to perform when the observable emits a value. + * @return a [it.unibo.alchemist.model.observation.Disposable] + * to manually dispose the subscription outside owner's lifecycle. + */ +fun Observable.bindTo(lifecycleOwner: LifecycleOwner, callback: (T) -> Unit): Disposable? = + lifecycleOwner.takeIf { it.lifecycle.currentState != DESTROYED }?.let { + val dataListener: (T) -> Unit = { data -> + // avoid zombie callbacks + if (lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) { + callback(data) + } + } + + var lifecycleObserver: ((LifecycleState) -> Unit)? = null + lifecycleObserver = { state -> + when (state) { + DESTROYED -> { + this.stopWatching(lifecycleOwner) + lifecycleObserver?.let { lifecycleOwner.lifecycle.removeObserver(it) } + } + STARTED -> callback(this.current) + else -> { /* No action needed for other states */ } + } + } + + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + + // set `invokeOnRegistration = false` because we handle the initial data emission manually below + // to strictly respect the `isAtLeast(STARTED)` check. + this.onChange(lifecycleOwner, invokeOnRegistration = false, callback = dataListener) + + if (lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) { + callback(this.current) + } + + return Disposable { + this@bindTo.stopWatching(dataListener) + lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) + } + } diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleRegistry.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleRegistry.kt new file mode 100644 index 0000000000..17d6972dcf --- /dev/null +++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleRegistry.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010-2026, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.model.observation.lifecycle + +/** + * A concrete implementation of [Lifecycle] that handles state transitions and observer notification. + */ +class LifecycleRegistry : Lifecycle { + + private val observers = mutableListOf<(LifecycleState) -> Unit>() + + override var currentState: LifecycleState = LifecycleState.INITIALIZED + private set + + /** + * Transitions the lifecycle to a new [state] and notifies all observers. + */ + fun markState(state: LifecycleState) { + currentState = state + observers.toList().forEach { it(state) } + } + + override fun addObserver(observer: (LifecycleState) -> Unit) { + observers.add(observer) + } + + override fun removeObserver(observer: (LifecycleState) -> Unit) { + observers.remove(observer) + } +} diff --git a/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleState.kt b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleState.kt new file mode 100644 index 0000000000..638c32aef0 --- /dev/null +++ b/alchemist-api/src/main/kotlin/it/unibo/alchemist/model/observation/lifecycle/LifecycleState.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2010-2026, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.model.observation.lifecycle + +/** + * Represents the lifecycle state of a component. + */ +enum class LifecycleState { + + /** + * Destroyed state for a LifecycleOwner. After this state is reached, this Lifecycle will not emit any more events. + */ + DESTROYED, + + /** + * Initialized state for a LifecycleOwner. + */ + INITIALIZED, + + /** + * Started state for a LifecycleOwner. + */ + STARTED, + + ; + + /** + * Checks if the current state is at least the given [state]. + */ + fun isAtLeast(state: LifecycleState): Boolean = this >= state +} diff --git a/alchemist-api/src/test/kotlin/it/unibo/alchemist/model/observation/LifecycleTest.kt b/alchemist-api/src/test/kotlin/it/unibo/alchemist/model/observation/LifecycleTest.kt new file mode 100644 index 0000000000..6110887c5c --- /dev/null +++ b/alchemist-api/src/test/kotlin/it/unibo/alchemist/model/observation/LifecycleTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2010-2026, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.model.observation + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.shouldBe +import it.unibo.alchemist.model.observation.MutableObservable.Companion.observe +import it.unibo.alchemist.model.observation.lifecycle.Lifecycle +import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner +import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.DESTROYED +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.STARTED +import it.unibo.alchemist.model.observation.lifecycle.bindTo + +class LifecycleTest : FunSpec({ + + class TestLifecycleOwner : LifecycleOwner { + private val registry = LifecycleRegistry() + override val lifecycle: Lifecycle get() = registry + + fun start() = registry.markState(STARTED) + fun destroy() = registry.markState(DESTROYED) + } + + context("Lifecycle binding tests") { + + test("bindTo should not invoke callback if Lifecycle is INITIALIZED") { + val owner = TestLifecycleOwner() + val observable = observe(10) + var lastValue = -1 + + observable.bindTo(owner) { lastValue = it } + + lastValue shouldBe -1 + observable.current = 20 + lastValue shouldBe -1 + } + + test("bindTo should invoke callback immediately if Lifecycle is STARTED") { + val owner = TestLifecycleOwner() + owner.start() + val observable = observe(10) + var lastValue = -1 + + observable.bindTo(owner) { lastValue = it } + + lastValue shouldBe 10 + } + + test("bindTo should start receiving updates when Lifecycle moves to STARTED") { + val owner = TestLifecycleOwner() + val observable = observe(10) + var lastValue = -1 + + observable.bindTo(owner) { lastValue = it } + + observable.current = 20 + lastValue shouldBe -1 + + owner.start() + lastValue shouldBe 20 + + observable.current = 30 + lastValue shouldBe 30 + } + + test("bindTo should automatically unsubscribe when Lifecycle is DESTROYED") { + val owner = TestLifecycleOwner() + owner.start() + val observable = observe(10) + var lastValue = -1 + + observable.bindTo(owner) { lastValue = it } + + observable.observers shouldContain owner + + owner.destroy() + + observable.observers shouldNotContain owner + + observable.current = 40 + lastValue shouldBe 10 + } + + test("bindTo should not subscribe at all if Lifecycle is already DESTROYED") { + val owner = TestLifecycleOwner() + owner.destroy() + val observable = observe(10) + + observable.bindTo(owner) { } + + observable.observers shouldNotContain owner + } + + test("Multiple bindings to the same owner should work independently") { + val owner = TestLifecycleOwner() + owner.start() + val obs1 = observe(1) + val obs2 = observe(2) + var val1 = -1 + var val2 = -1 + + obs1.bindTo(owner) { val1 = it } + obs2.bindTo(owner) { val2 = it } + + val1 shouldBe 1 + val2 shouldBe 2 + + owner.destroy() + + obs1.observers shouldNotContain owner + obs2.observers shouldNotContain owner + } + } +}) diff --git a/alchemist-engine/src/main/kotlin/it/unibo/alchemist/core/Engine.kt b/alchemist-engine/src/main/kotlin/it/unibo/alchemist/core/Engine.kt index d26f873d42..fc280bf754 100644 --- a/alchemist-engine/src/main/kotlin/it/unibo/alchemist/core/Engine.kt +++ b/alchemist-engine/src/main/kotlin/it/unibo/alchemist/core/Engine.kt @@ -79,8 +79,10 @@ open class Engine>( } override fun nodeRemoved(node: Node, oldNeighborhood: Neighborhood) { + // copy of reactions due to how [GenericNode.dispose] clears the reactions + val reactions = ArrayList(node.reactions) schedule { - node.reactions.forEach { removeReaction(it) } + reactions.forEach { removeReaction(it) } } } diff --git a/alchemist-implementationbase/src/main/java/it/unibo/alchemist/model/reactions/AbstractReaction.java b/alchemist-implementationbase/src/main/java/it/unibo/alchemist/model/reactions/AbstractReaction.java index e7f4eca92d..150260eac1 100644 --- a/alchemist-implementationbase/src/main/java/it/unibo/alchemist/model/reactions/AbstractReaction.java +++ b/alchemist-implementationbase/src/main/java/it/unibo/alchemist/model/reactions/AbstractReaction.java @@ -24,6 +24,11 @@ import it.unibo.alchemist.model.TimeDistribution; import it.unibo.alchemist.model.observation.Disposable; import it.unibo.alchemist.model.observation.EventObservable; +import it.unibo.alchemist.model.observation.lifecycle.Lifecycle; +import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner; +import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwnerKt; +import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry; +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState; import it.unibo.alchemist.model.observation.MutableObservable; import it.unibo.alchemist.model.observation.Observable; import it.unibo.alchemist.model.observation.ObservableExtensions; @@ -56,7 +61,7 @@ * * @param concentration type */ -public abstract class AbstractReaction implements Reaction, Disposable { +public abstract class AbstractReaction implements Reaction, Disposable, LifecycleOwner { /** * How bigger should be the StringBuffer with respect to the previous @@ -78,13 +83,14 @@ public abstract class AbstractReaction implements Reaction, Disposable { private final TimeDistribution timeDistribution; private final Node node; + private final List subscriptions = new ArrayList<>(0); + private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(); private final EventObservable rescheduleRequest = new EventObservable(); private Observable validity = MutableObservable.Companion.observe(true); - private final List> subscriptions = new ArrayList<>(); private Option canExecute = none(); /** - * Builds a new reaction, starting at time t. + * Builds a new reaction, starting at time t. * * @param node the node this reaction belongs to * @param timeDistribution the time distribution this reaction should follow @@ -143,6 +149,13 @@ public final boolean equals(final Object o) { return this == o; } + @NotNull + @Override + @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "This is intentional") + public final Lifecycle getLifecycle() { + return lifecycleRegistry; + } + /** * The default execution iterates all the actions in order and executes them. Override to change the behavior. */ @@ -244,6 +257,7 @@ public final int hashCode() { @Override public final void initializationComplete(@Nonnull final Time atTime, @Nonnull final Environment environment) { initializeObservableConditions(); + lifecycleRegistry.markState(LifecycleState.STARTED); onInitializationComplete(atTime, environment); } @@ -344,22 +358,18 @@ public void setConditions(@Nonnull final List> conditions * trigger some environment query that may fail in the set-up phase. */ protected final void initializeObservableConditions() { - if (!subscriptions.isEmpty()) { - subscriptions.forEach(s -> s.stopWatching(this)); - subscriptions.clear(); - } - + subscriptions.forEach(Disposable::dispose); + subscriptions.clear(); validity.dispose(); conditions.forEach(condition -> { final var merged = ObservableExtensions.ObservableSetExtensions.mergeObservables( condition.observeInboundDependencies() ); - merged.onChange(this, it -> { + subscriptions.add(LifecycleOwnerKt.bindTo(merged, this, it -> { rescheduleRequest.emit(); - return null; - }); - subscriptions.add(merged); + return Unit.INSTANCE; + })); }); if (!conditions.isEmpty()) { @@ -368,15 +378,11 @@ protected final void initializeObservableConditions() { it -> it.stream().allMatch(b -> b) ).map(it -> getOrElse(it, () -> true)); - // need at least one observer to track validity updates - validity.onChange(this, it -> { + subscriptions.add(LifecycleOwnerKt.bindTo(validity, this, it -> { canExecute = Option.fromNullable(it); - return null; - }); - - subscriptions.add(validity); + return Unit.INSTANCE; + })); } - rescheduleRequest.emit(); } @@ -437,7 +443,8 @@ public final void update( */ @Override public void dispose() { - subscriptions.forEach(it -> it.stopWatching(this)); + lifecycleRegistry.markState(LifecycleState.DESTROYED); + subscriptions.forEach(Disposable::dispose); conditions.forEach(Disposable::dispose); conditions.clear(); subscriptions.clear(); diff --git a/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/environments/AbstractEnvironment.kt b/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/environments/AbstractEnvironment.kt index 315d3e47c2..b4344bc2c1 100644 --- a/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/environments/AbstractEnvironment.kt +++ b/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/environments/AbstractEnvironment.kt @@ -234,22 +234,50 @@ abstract class AbstractEnvironment> protected constructor( centerProvider = centerProvider, radius = range, visibleNodes = initialNodes, - ).apply { regionObservers.add(this) } + ) + + val addRegion = { + runCatching { + val currentCenter = centerProvider() + val currentNodes = getAllNodesInRange(currentCenter, range) + initialNodes.clearAndAddAll(currentNodes.toSet()) + + regionObservers.add(region) + if (node != null) { + var radiusMap = regionNodeCenteredIndex[node.id] + if (radiusMap == null) { + radiusMap = TDoubleObjectHashMap() + regionNodeCenteredIndex.put(node.id, radiusMap) + } + radiusMap.put(range, region) + } else { + regionPositionCenteredIndex + .computeIfAbsent(actualCenter) { TDoubleObjectHashMap() } + .put(range, region) + } + }.onFailure { initialNodes.clearAndAddAll(emptySet()) } + } - if (node != null) { - var radiusMap = regionNodeCenteredIndex[node.id] - if (radiusMap == null) { - radiusMap = TDoubleObjectHashMap() - regionNodeCenteredIndex.put(node.id, radiusMap) + val removeRegion = { + regionObservers.remove(region) + if (node != null) { + regionNodeCenteredIndex[node.id]?.remove(range) + if (regionNodeCenteredIndex[node.id]?.isEmpty == true) { + regionNodeCenteredIndex.remove(node.id) + } + } else { + regionPositionCenteredIndex[actualCenter]?.remove(range) + if (regionPositionCenteredIndex[actualCenter]?.isEmpty == true) { + regionPositionCenteredIndex.remove(actualCenter) + } } - radiusMap.put(range, region) - } else { - regionPositionCenteredIndex - .computeIfAbsent(actualCenter) { TDoubleObjectHashMap() } - .put(range, region) } - return initialNodes + return RefCountObservableSet( + delegate = initialNodes, + onActive = { addRegion() }, + onInactive = { removeRegion() }, + ) } override fun getDistanceBetweenNodes(n1: Node, n2: Node): Double = @@ -444,6 +472,7 @@ abstract class AbstractEnvironment> protected constructor( updateRegionObservers(node, null, null) ifEngineAvailable { it.nodeRemoved(node, neigh) } nodeRemoved(node, neigh) + node.dispose() } private fun runQuery(center: P, range: Double): List> = spatialIndex @@ -574,6 +603,43 @@ abstract class AbstractEnvironment> protected constructor( val visibleNodes: ObservableMutableSet>, ) + /** + * Simple wrapper for [ObservableSet] that manages reference counting to track observers and + * invoke specified callbacks when observers are registered or deregistered. + * It serves as both a way to avoid leaks through the [onInactive] callback (which should clear + * the backing caches), and a lazy evaluation of [onActive] when observers are registered. + * + * + * @param onActive the callback to be invoked when the first observer is added + * @param onInactive the callback to be invoked when the last observer is removed, which + * should clear backing caches and resources. + */ + private class RefCountObservableSet( + private val delegate: ObservableMutableSet, + private val onActive: () -> Unit, + private val onInactive: () -> Unit, + ) : ObservableSet by delegate { + + override fun onChange(registrant: Any, invokeOnRegistration: Boolean, callback: (Set) -> Unit) { + if (delegate.observers.isEmpty()) { + onActive() + } + delegate.onChange(registrant, invokeOnRegistration, callback) + } + + override fun stopWatching(registrant: Any) { + delegate.stopWatching(registrant) + if (delegate.observers.isEmpty()) { + onInactive() + } + } + + override fun dispose() { + delegate.dispose() + onInactive() + } + } + private data class Operation(val origin: Node, val destination: Node, val isAdd: Boolean) { override fun toString(): String = origin.toString() + (if (isAdd) " discovered " else " lost ") + destination } diff --git a/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/nodes/GenericNode.kt b/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/nodes/GenericNode.kt index ad66473c35..2a7bc79aba 100644 --- a/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/nodes/GenericNode.kt +++ b/alchemist-implementationbase/src/main/kotlin/it/unibo/alchemist/model/nodes/GenericNode.kt @@ -18,8 +18,11 @@ import it.unibo.alchemist.model.Node import it.unibo.alchemist.model.NodeProperty import it.unibo.alchemist.model.Reaction import it.unibo.alchemist.model.Time +import it.unibo.alchemist.model.observation.Disposable import it.unibo.alchemist.model.observation.Observable import it.unibo.alchemist.model.observation.ObservableMutableMap +import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState import java.util.Collections import java.util.Spliterator import java.util.concurrent.Semaphore @@ -56,6 +59,12 @@ constructor( environment: Environment, ) : this(environment.incarnation, environment) + override val lifecycle: LifecycleRegistry = LifecycleRegistry() + + init { + lifecycle.markState(LifecycleState.STARTED) + } + override val observableContents: ObservableMutableMap = ObservableMutableMap(molecules) override val observeMoleculeCount: Observable = observableContents.map { it.size } @@ -111,7 +120,9 @@ constructor( } final override fun removeReaction(reactionToRemove: Reaction) { - reactions.remove(reactionToRemove) + if (reactions.remove(reactionToRemove)) { + reactionToRemove.dispose() + } } override fun setConcentration(molecule: Molecule, concentration: T) { @@ -136,6 +147,14 @@ constructor( override fun toString(): String = "Node$id{ properties: $properties, molecules: ${observableContents.current}}" + override fun dispose() { + lifecycle.markState(LifecycleState.DESTROYED) + reactions.forEach(Disposable::dispose) + reactions.clear() + observableContents.dispose() + observeMoleculeCount.dispose() + } + private companion object { private const val serialVersionUID = 2496775909028222278L diff --git a/alchemist-incarnation-protelis/src/main/kotlin/it/unibo/alchemist/model/protelis/ProtelisIncarnation.kt b/alchemist-incarnation-protelis/src/main/kotlin/it/unibo/alchemist/model/protelis/ProtelisIncarnation.kt index f0866dc11a..6f2396a053 100644 --- a/alchemist-incarnation-protelis/src/main/kotlin/it/unibo/alchemist/model/protelis/ProtelisIncarnation.kt +++ b/alchemist-incarnation-protelis/src/main/kotlin/it/unibo/alchemist/model/protelis/ProtelisIncarnation.kt @@ -31,6 +31,7 @@ import it.unibo.alchemist.model.molecules.SimpleMolecule import it.unibo.alchemist.model.nodes.GenericNode import it.unibo.alchemist.model.observation.Observable import it.unibo.alchemist.model.observation.ObservableMap +import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry import it.unibo.alchemist.model.protelis.actions.RunProtelisProgram import it.unibo.alchemist.model.protelis.actions.SendToNeighbor import it.unibo.alchemist.model.protelis.conditions.ComputationalRoundComplete @@ -378,6 +379,8 @@ class ProtelisIncarnation

> : Incarnation { override val reactions: List> = emptyList() + override val lifecycle: LifecycleRegistry = LifecycleRegistry() + override fun iterator(): MutableIterator> = notImplemented() override fun compareTo(@Nonnull other: Node): Int = notImplemented() @@ -400,6 +403,8 @@ class ProtelisIncarnation

> : Incarnation { override fun setConcentration(molecule: Molecule, concentration: Any) = notImplemented() + override fun dispose() = notImplemented() + override fun equals(other: Any?): Boolean = other === this override fun hashCode(): Int = -1 diff --git a/alchemist-incarnation-sapere/src/main/java/it/unibo/alchemist/model/sapere/reactions/SAPEREGradient.java b/alchemist-incarnation-sapere/src/main/java/it/unibo/alchemist/model/sapere/reactions/SAPEREGradient.java index 53c3b6f827..b08e957154 100644 --- a/alchemist-incarnation-sapere/src/main/java/it/unibo/alchemist/model/sapere/reactions/SAPEREGradient.java +++ b/alchemist-incarnation-sapere/src/main/java/it/unibo/alchemist/model/sapere/reactions/SAPEREGradient.java @@ -27,6 +27,7 @@ import it.unibo.alchemist.model.Time; import it.unibo.alchemist.model.TimeDistribution; import it.unibo.alchemist.model.maps.MapEnvironment; +import it.unibo.alchemist.model.observation.Disposable; import it.unibo.alchemist.model.observation.MutableObservable; import it.unibo.alchemist.model.observation.Observable; import it.unibo.alchemist.model.observation.ObservableMutableSet; @@ -366,6 +367,12 @@ public double getRate() { return canRun ? getTimeDistribution().getRate() : 0; } + @Override + public void dispose() { + fakeconds.forEach(Disposable::dispose); + super.dispose(); + } + @Override protected void updateInternalStatus( final Time currentTime, diff --git a/alchemist-physics/src/main/kotlin/it/unibo/alchemist/model/physics/reactions/PhysicsUpdate.kt b/alchemist-physics/src/main/kotlin/it/unibo/alchemist/model/physics/reactions/PhysicsUpdate.kt index e36af69146..ec997b46c7 100644 --- a/alchemist-physics/src/main/kotlin/it/unibo/alchemist/model/physics/reactions/PhysicsUpdate.kt +++ b/alchemist-physics/src/main/kotlin/it/unibo/alchemist/model/physics/reactions/PhysicsUpdate.kt @@ -24,10 +24,12 @@ import it.unibo.alchemist.model.observation.MutableObservable.Companion.observe import it.unibo.alchemist.model.observation.Observable import it.unibo.alchemist.model.observation.ObservableExtensions.ObservableSetExtensions.merge import it.unibo.alchemist.model.observation.ObservableExtensions.combineLatest +import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState +import it.unibo.alchemist.model.observation.lifecycle.bindTo import it.unibo.alchemist.model.physics.PhysicsDependency import it.unibo.alchemist.model.physics.environments.Dynamics2DEnvironment import it.unibo.alchemist.model.timedistributions.DiracComb -import java.util.ArrayList import org.danilopianini.util.ImmutableListSet import org.danilopianini.util.ListSet @@ -59,28 +61,24 @@ class PhysicsUpdate( override val rescheduleRequest: EventObservable = EventObservable() + override val lifecycle: LifecycleRegistry = LifecycleRegistry() + override var actions: List> = listOf() private var validity: Observable = observe(true) private var canExecute: Boolean = true - private val subscriptions: MutableList> = ArrayList() - override var conditions: List> = listOf() set(value) { field = value field.forEach(Disposable::dispose) - subscriptions.forEach { it.stopWatching(this) } - subscriptions.clear() - validity.dispose() value.forEach { condition -> - condition.observeInboundDependencies().merge().apply { - onChange(this@PhysicsUpdate) { rescheduleRequest.emit() } - subscriptions.add(this) + condition.observeInboundDependencies().merge().bindTo(this) { + rescheduleRequest.emit() } } @@ -89,8 +87,7 @@ class PhysicsUpdate( ?.combineLatest { validities -> validities.all { it } } ?.map { it.getOrElse { true } } // none means empty set of conditions i.e. always true. ?.apply { - onChange(this@PhysicsUpdate) { canExecute = it } - subscriptions.add(this) + bindTo(this@PhysicsUpdate) { canExecute = it } } ?: observe(true) rescheduleRequest.emit() @@ -109,5 +106,14 @@ class PhysicsUpdate( override fun update(currentTime: Time, hasBeenExecuted: Boolean, environment: Environment) = Unit - override fun initializationComplete(atTime: Time, environment: Environment) = Unit + override fun initializationComplete(atTime: Time, environment: Environment) { + lifecycle.markState(LifecycleState.STARTED) + } + + override fun dispose() { + lifecycle.markState(LifecycleState.DESTROYED) + validity.dispose() + conditions.forEach(Disposable::dispose) + rescheduleRequest.dispose() + } } diff --git a/alchemist-test/src/main/kotlin/it/unibo/alchemist/test/GlobalTestReaction.kt b/alchemist-test/src/main/kotlin/it/unibo/alchemist/test/GlobalTestReaction.kt index 1008afd22c..f606fe594e 100644 --- a/alchemist-test/src/main/kotlin/it/unibo/alchemist/test/GlobalTestReaction.kt +++ b/alchemist-test/src/main/kotlin/it/unibo/alchemist/test/GlobalTestReaction.kt @@ -21,6 +21,8 @@ import it.unibo.alchemist.model.observation.EventObservable import it.unibo.alchemist.model.observation.Observable import it.unibo.alchemist.model.observation.ObservableExtensions.ObservableSetExtensions.combineLatest import it.unibo.alchemist.model.observation.ObservableMutableSet +import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry +import it.unibo.alchemist.model.observation.lifecycle.LifecycleState import org.danilopianini.util.ListSet import org.danilopianini.util.ListSets @@ -36,6 +38,8 @@ class GlobalTestReaction(override val timeDistribution: TimeDistribution, override var actions: List> = emptyList() + override val lifecycle: LifecycleRegistry = LifecycleRegistry() + override var conditions: List> = emptyList() set(value) { field = value @@ -56,5 +60,15 @@ class GlobalTestReaction(override val timeDistribution: TimeDistribution, override fun update(currentTime: Time, hasBeenExecuted: Boolean, environment: Environment) = Unit - override fun initializationComplete(atTime: Time, environment: Environment) = Unit + override fun initializationComplete(atTime: Time, environment: Environment) { + lifecycle.markState(LifecycleState.STARTED) + } + + override fun dispose() { + lifecycle.markState(LifecycleState.DESTROYED) + observableConditions.dispose() + validity.dispose() + conditions.forEach(Condition::dispose) + rescheduleRequest.dispose() + } }