Skip to content

Commit 3456a4c

Browse files
committed
refactor lifecycle components to improve initialization and disposal handling
1 parent 54eb8e1 commit 3456a4c

File tree

4 files changed

+65
-37
lines changed

4 files changed

+65
-37
lines changed

core/jvm/src/test/scala/com/avsystem/commons/di/LifeCycleComponentTest.scala

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.avsystem.commons
22
package di
33

4-
import monix.execution.atomic.{Atomic, AtomicBoolean, AtomicInt}
4+
import monix.execution.atomic.{Atomic, AtomicInt}
55
import org.scalatest.funsuite.AsyncFunSuite
66

7-
class LifeCycleComponentTest extends AsyncFunSuite with Components {
7+
final class LifeCycleComponentTest extends AsyncFunSuite with Components {
88

99
object init {
1010
val inits: AtomicInt = Atomic(0)
@@ -27,22 +27,22 @@ class LifeCycleComponentTest extends AsyncFunSuite with Components {
2727
}
2828

2929
object disposable {
30-
val destroyed: AtomicBoolean = Atomic(false)
30+
val destroys: AtomicInt = Atomic(0)
3131
val component: Component[DisposableComponent] = singleton {
3232
new DisposableComponent {
33-
def destroy(): Unit = destroyed.set(true)
33+
def destroy(): Unit = destroys += 1
3434
}
3535
}
3636
}
3737

3838

3939
object asyncDisposable {
40-
val destroyed: AtomicBoolean = Atomic(false)
40+
val destroys: AtomicInt = Atomic(0)
4141
val component: Component[AsyncDisposableComponent] = singleton {
4242
new AsyncDisposableComponent {
4343
def destroy()(implicit ec: ExecutionContext): Future[Unit] = Future {
44-
destroyed.set(true)
45-
}(ec)
44+
destroys += 1
45+
}(using ec)
4646
}
4747
}
4848
}
@@ -65,17 +65,27 @@ class LifeCycleComponentTest extends AsyncFunSuite with Components {
6565
_ = assert(asyncInit.inits.get() == 1)
6666
} yield assert(c1 eq c2))
6767

68-
test("DisposableComponent destroy triggers side effect") {
69-
assert(!disposable.destroyed.get())
70-
disposable.component.destroy.map { _ =>
71-
assert(disposable.destroyed.get())
72-
}
73-
}
68+
test("DisposableComponent destroy triggers side effect")(for {
69+
_ <- Future.unit
70+
_ <- disposable.component.destroy
71+
_ = assert(disposable.destroys.get() == 0)
72+
_ <- disposable.component.init
73+
_ = assert(disposable.destroys.get() == 0)
74+
_ <- disposable.component.destroy
75+
_ = assert(disposable.destroys.get() == 1)
76+
_ <- disposable.component.destroy
77+
_ = assert(disposable.destroys.get() == 1)
78+
} yield succeed)
7479

75-
test("AsyncDisposableComponent destroy triggers side effect") {
76-
assert(!asyncDisposable.destroyed.get())
77-
asyncDisposable.component.destroy.map { _ =>
78-
assert(asyncDisposable.destroyed.get())
79-
}
80-
}
80+
test("AsyncDisposableComponent destroy triggers side effect")(for {
81+
_ <- Future.unit
82+
_ <- asyncDisposable.component.destroy
83+
_ = assert(asyncDisposable.destroys.get() == 0)
84+
_ <- asyncDisposable.component.init
85+
_ = assert(asyncDisposable.destroys.get() == 0)
86+
_ <- asyncDisposable.component.destroy
87+
_ = assert(asyncDisposable.destroys.get() == 1)
88+
_ <- asyncDisposable.component.destroy
89+
_ = assert(asyncDisposable.destroys.get() == 1)
90+
} yield succeed)
8191
}

core/src/main/scala/com/avsystem/commons/di/Component.scala

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import java.util.concurrent.atomic.AtomicReference
88
import scala.annotation.compileTimeOnly
99
import scala.annotation.unchecked.uncheckedVariance
1010

11-
case class ComponentInitializationException(component: Component[_], cause: Throwable)
11+
case class ComponentInitializationException(component: Component[?], cause: Throwable)
1212
extends Exception(s"failed to initialize component ${component.info}", cause)
1313

14-
case class DependencyCycleException(cyclePath: List[Component[_]])
14+
case class DependencyCycleException(cyclePath: List[Component[?]])
1515
extends Exception(s"component dependency cycle detected:\n${cyclePath.iterator.map(_.info).map(" " + _).mkString(" ->\n")}")
1616

1717
case class ComponentInfo(
@@ -41,7 +41,7 @@ object ComponentInfo {
4141
*/
4242
final class Component[+T](
4343
val info: ComponentInfo,
44-
deps: => IndexedSeq[Component[_]],
44+
deps: => IndexedSeq[Component[?]],
4545
creator: IndexedSeq[Any] => ExecutionContext => Future[T],
4646
destroyer: DestroyFunction[T] = Component.emptyDestroy,
4747
cachedStorage: Opt[AtomicReference[Future[T]]] = Opt.Empty,
@@ -58,12 +58,12 @@ final class Component[+T](
5858
* Returns dependencies of this component extracted from the component definition.
5959
* You can use this to inspect the dependency graph without initializing any components.
6060
*/
61-
lazy val dependencies: IndexedSeq[Component[_]] = deps
61+
lazy val dependencies: IndexedSeq[Component[?]] = deps
6262

6363
private[this] val storage: AtomicReference[Future[T]] =
6464
cachedStorage.getOrElse(new AtomicReference)
6565

66-
private def sameStorage(otherStorage: AtomicReference[_]): Boolean =
66+
private def sameStorage(otherStorage: AtomicReference[?]): Boolean =
6767
storage eq otherStorage
6868

6969
// equality based on storage identity is important for cycle detection with cached components
@@ -106,7 +106,7 @@ final class Component[+T](
106106
/**
107107
* Forces a dependency on another component or components.
108108
*/
109-
def dependsOn(moreDeps: Component[_]*): Component[T] =
109+
def dependsOn(moreDeps: Component[?]*): Component[T] =
110110
new Component(info, deps ++ moreDeps, creator, destroyer, cachedStorage)
111111

112112
/**
@@ -126,7 +126,7 @@ final class Component[+T](
126126
def destroyWith(destroyFun: T => Unit): Component[T] =
127127
asyncDestroyWith(implicit ctx => t => Future(destroyFun(t)))
128128

129-
private[di] def cached(cachedStorage: AtomicReference[Future[T@uncheckedVariance]], info: ComponentInfo): Component[T] =
129+
private[di] def cached(cachedStorage: AtomicReference[Future[T @uncheckedVariance]], info: ComponentInfo): Component[T] =
130130
new Component(info, deps, creator, destroyer, Opt(cachedStorage))
131131

132132
/**
@@ -171,10 +171,15 @@ final class Component[+T](
171171
val resultFuture =
172172
Future.traverse(dependencies)(_.doInit(starting = false))
173173
.flatMap(resolvedDeps => creator(resolvedDeps)(ec))
174+
.flatMap {
175+
case component: AsyncInitializingComponent => component.init().map(_ => component)
176+
case component: InitializingComponent => Future(component.init()).map(_ => component)
177+
case component => Future.successful(component)
178+
}
174179
.recoverNow {
175180
case NonFatal(cause) =>
176181
throw ComponentInitializationException(this, cause)
177-
}
182+
}.asInstanceOf[Future[T]]
178183
promise.completeWith(resultFuture)
179184
}
180185
storage.get()
@@ -194,7 +199,7 @@ object Component {
194199
def async[T](definition: => T): ExecutionContext => Future[T] =
195200
implicit ctx => Future(definition)
196201

197-
def validateAll(components: Seq[Component[_]]): Unit =
202+
def validateAll(components: Seq[Component[?]]): Unit =
198203
GraphUtils.dfs(components)(
199204
_.dependencies.toList,
200205
onCycle = (node, stack) => {
@@ -210,9 +215,9 @@ object Component {
210215
* (reverse initialization order).
211216
* Independent components are destroyed in parallel, using given `ExecutionContext`.
212217
*/
213-
def destroyAll(components: Seq[Component[_]])(implicit ec: ExecutionContext): Future[Unit] = {
214-
val reverseGraph = new MHashMap[Component[_], MListBuffer[Component[_]]]
215-
val terminals = new MHashSet[Component[_]]
218+
def destroyAll(components: Seq[Component[?]])(implicit ec: ExecutionContext): Future[Unit] = {
219+
val reverseGraph = new MHashMap[Component[?], MListBuffer[Component[?]]]
220+
val terminals = new MHashSet[Component[?]]
216221
GraphUtils.dfs(components)(
217222
_.dependencies.toList,
218223
onEnter = { (c, _) =>
@@ -225,9 +230,9 @@ object Component {
225230
terminals += c
226231
},
227232
)
228-
val destroyFutures = new MHashMap[Component[_], Future[Unit]]
233+
val destroyFutures = new MHashMap[Component[?], Future[Unit]]
229234

230-
def doDestroy(c: Component[_]): Future[Unit] =
235+
def doDestroy(c: Component[?]): Future[Unit] =
231236
destroyFutures.getOrElseUpdate(c, Future.traverse(reverseGraph(c))(doDestroy).flatMap(_ => c.doDestroy))
232237

233238
Future.traverse(reverseGraph.keys)(doDestroy).toUnit

core/src/main/scala/com/avsystem/commons/di/LifeCycleComponents.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package di
33

44
trait InitializingComponent {
55
def init(): Unit
6-
final def initialized(): this.type = init().thenReturn(this)
76
}
87

98
trait DisposableComponent {
@@ -12,7 +11,6 @@ trait DisposableComponent {
1211

1312
trait AsyncInitializingComponent {
1413
def init()(implicit ec: ExecutionContext): Future[Unit]
15-
final def initialized()(implicit ec: ExecutionContext): Future[this.type] = init()(using ec).map[this.type](_ => this)(using ec)
1614
}
1715

1816
trait AsyncDisposableComponent {

macros/src/main/scala/com/avsystem/commons/macros/di/ComponentMacros.scala

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package macros.di
44
import com.avsystem.commons.macros.AbstractMacroCommons
55

66
import scala.collection.mutable.ListBuffer
7+
import scala.concurrent.{ExecutionContext, Future}
78
import scala.reflect.macros.blackbox
89

910
class ComponentMacros(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) {
@@ -19,6 +20,11 @@ class ComponentMacros(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) {
1920
lazy val InjectSym: Symbol = getType(tq"$DiPkg.Components").member(TermName("inject"))
2021
lazy val ComponentInfoSym: Symbol = getType(tq"$DiPkg.ComponentInfo.type").member(TermName("info"))
2122

23+
lazy val DisposableComponentTpe: Type = getType(tq"$DiPkg.DisposableComponent")
24+
lazy val AsyncDisposableComponentTpe: Type = getType(tq"$DiPkg.AsyncDisposableComponent")
25+
lazy val ExecutionContextTpe: Type = typeOf[ExecutionContext]
26+
lazy val FutureApplySym: Symbol = typeOf[Future.type].member(TermName("apply"))
27+
2228
object ComponentRef {
2329
def unapply(tree: Tree): Option[Tree] = tree match {
2430
case Select(component, TermName("ref")) if tree.symbol == ComponentRefSym =>
@@ -90,16 +96,25 @@ class ComponentMacros(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) {
9096
if (needsRetyping) c.untypecheck(transformedDefinition) else definition
9197

9298
val asyncDefinition =
93-
if(async) finalDefinition
99+
if (async) finalDefinition
94100
else q"$DiPkg.Component.async($finalDefinition)"
95101

102+
val destroyer =
103+
if (tpe <:< DisposableComponentTpe)
104+
q"(ec: $ExecutionContextTpe) => (t: $tpe) => $FutureApplySym(t.destroy())(using ec)"
105+
else if (tpe <:< AsyncDisposableComponentTpe)
106+
q"(ec: $ExecutionContextTpe) => (t: $tpe) => t.destroy()(using ec)"
107+
else
108+
q"$DiPkg.Component.emptyDestroy"
109+
96110
val result =
97111
q"""
98112
val $infoName = ${c.prefix}.componentInfo($sourceInfo)
99113
new $DiPkg.Component[$tpe](
100114
$infoName,
101115
$ScalaPkg.IndexedSeq(..${depsBuf.result()}),
102-
($depArrayName: $ScalaPkg.IndexedSeq[$ScalaPkg.Any]) => $asyncDefinition
116+
($depArrayName: $ScalaPkg.IndexedSeq[$ScalaPkg.Any]) => $asyncDefinition,
117+
$destroyer,
103118
)
104119
"""
105120

0 commit comments

Comments
 (0)