Other Features
Garbage Collection
A garbage collector is included in distage
by default. Given a set of GC root
keys, GC will remove all bindings that are neither direct nor transitive dependencies of the supplied roots ? these bindings will be thrown out and never instantiated.
GC serves two important purposes:
- It enables faster tests by omitting unrequired instantiations and initialization of potentially heavy resources,
- It enables multiple independent applications, aka “Roles” to be hosted within a single
.jar
file.
To use garbage collector, pass GC roots as an argument to Injector.produce*
methods:
import distage._
case class A(b: B)
case class B()
case class C() {
println("C!")
}
val module = new ModuleDef {
make[A]
make[B]
make[C]
}
// module: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.C}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.C)) ((other-features.md:28)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.B}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.B)) ((other-features.md:27)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.A}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.B): repl.Session::repl.Session.App0::repl.Session.App0.A)) ((other-features.md:26)))
// declare `A` as a GC root
val roots = GCMode.GCRoots(Set[DIKey](DIKey.get[A]))
// roots: package.GCMode.GCRoots = GCRoots(Set({type.repl.Session::repl.Session.App0::repl.Session.App0.A}))
// create an object graph from description in `module`
// with `A` as a GC root
val objects = Injector().produceUnsafe(module, roots)
// objects: Locator = izumi.distage.LocatorDefaultImpl@25829cf4
// A and B are in the object graph
objects.find[A]
// res1: Option[A] = Some(A(B()))
objects.find[B]
// res2: Option[B] = Some(B())
// C is missing
objects.find[C]
// res3: Option[C] = None
Class C
was removed because neither B
nor A
depended on it. It’s not present in the Locator
and the "C!"
message was never printed. But, if class B
were to depend on C
as in case class B(c: C)
, it would’ve been retained, because A
- the GC root, would depend on B
which in turns depends on C
.
Circular Dependencies Support
distage
automatically resolves arbitrary circular dependencies, including self-references:
import distage.{GCMode, ModuleDef, Injector}
class A(val b: B)
class B(val a: A)
class C(val c: C)
val locator = Injector().produceUnsafe(new ModuleDef {
make[A]
make[B]
make[C]
}, GCMode.NoGC)
// locator: izumi.distage.model.Locator = izumi.distage.LocatorDefaultImpl@8c63163
locator.get[A] eq locator.get[B].a
// res5: Boolean = true
locator.get[B] eq locator.get[A].b
// res6: Boolean = true
locator.get[C] eq locator.get[C].c
// res7: Boolean = true
Automatic Resolution with generated proxies
The above strategy depends on distage-proxy-cglib
module which is a default dependency of distage-core
.
If you want to disable it, use NoProxies
bootstrap configuration:
Injector.NoProxies()
// res8: Injector = izumi.distage.InjectorDefaultImpl@11551e40
Manual Resolution with by-name parameters
Most cycles can be resolved manually when identified using By Name parameters.
Circular dependencies in the following example are all resolved via Scala’s native By Name, no proxies are generated:
import distage.{GCMode, ModuleDef, Injector}
class A(b0: => B) {
def b: B = b0
}
class B(a0: => A) {
def a: A = a0
}
class C(self: => C) {
def c: C = self
}
val module = new ModuleDef {
make[A]
make[B]
make[C]
}
// module: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App9::repl.Session.App9.C}].from(call(<function1>(repl.Session::repl.Session.App9::repl.Session.App9.C): repl.Session::repl.Session.App9::repl.Session.App9.C)) ((other-features.md:114)), make[{type.repl.Session::repl.Session.App9::repl.Session.App9.B}].from(call(<function1>(repl.Session::repl.Session.App9::repl.Session.App9.A): repl.Session::repl.Session.App9::repl.Session.App9.B)) ((other-features.md:113)), make[{type.repl.Session::repl.Session.App9::repl.Session.App9.A}].from(call(<function1>(repl.Session::repl.Session.App9::repl.Session.App9.B): repl.Session::repl.Session.App9::repl.Session.App9.A)) ((other-features.md:112)))
// disable proxies and execute the module
val locator = Injector.NoProxies()
.produceUnsafe(module, GCMode.NoGC)
// locator: izumi.distage.model.Locator = izumi.distage.LocatorDefaultImpl@598e4ec7
locator.get[A].b eq locator.get[B]
// res10: Boolean = true
locator.get[B].a eq locator.get[A]
// res11: Boolean = true
locator.get[C].c eq locator.get[C]
// res12: Boolean = true
The proxy generation via cglib
is currently enabled by default, because in scenarios with extreme late-binding cycles can emerge unexpectedly, out of control of the origin module.
NB: Currently a limitation applies to by-names - ALL dependencies of a class engaged in a by-name circular dependency must be by-name, otherwise distage will revert to generating proxies.
Auto-Sets
AutoSet Planner Hooks can traverse the plan and collect all future objects that match a predicate.
Using Auto-Sets you can e.g. collect all AutoCloseable
classes and .close()
them after the application has finished work.
NOTE: please use Resource bindings for real lifecycle, this is just an example.
import distage.{BootstrapModuleDef, ModuleDef, Injector, GCMode}
import izumi.distage.model.planning.PlanningHook
import izumi.distage.planning.AutoSetHook
class PrintResource(name: String) {
def start(): Unit = println(s"$name started")
def stop(): Unit = println(s"$name stopped")
}
class A extends PrintResource("A")
class B(val a: A) extends PrintResource("B")
class C(val b: B) extends PrintResource("C")
val bootstrapModule = new BootstrapModuleDef {
many[PlanningHook].add(new AutoSetHook[PrintResource, PrintResource](identity))
}
// bootstrapModule: AnyRef with BootstrapModuleDef = Module(many[{type.scala.collection.immutable.Set[=PlanningHook]}].add[{type.izumi.distage.planning.AutoSetHook[=Session::App13::PrintResource,=Session::App13::PrintResource]}].from(call(izumi.distage.model.definition.dsl.ModuleDefDSL$SetDSLBase$$Lambda$18488/0x0000000844345840@5a0a4315(): izumi.distage.planning.AutoSetHook[=Session::App13::PrintResource,=Session::App13::PrintResource])) ((other-features.md:162)), many[{type.scala.collection.immutable.Set[=PlanningHook]}] ((other-features.md:162)))
val appModule = new ModuleDef {
make[C]
make[B]
make[A]
}
// appModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App13::repl.Session.App13.C}].from(call(<function1>(repl.Session::repl.Session.App13::repl.Session.App13.B): repl.Session::repl.Session.App13::repl.Session.App13.C)) ((other-features.md:167)), make[{type.repl.Session::repl.Session.App13::repl.Session.App13.B}].from(call(<function1>(repl.Session::repl.Session.App13::repl.Session.App13.A): repl.Session::repl.Session.App13::repl.Session.App13.B)) ((other-features.md:168)), make[{type.repl.Session::repl.Session.App13::repl.Session.App13.A}].from(call(<function1>(): repl.Session::repl.Session.App13::repl.Session.App13.A)) ((other-features.md:169)))
val resources = Injector(bootstrapModule)
.produceUnsafe(appModule, GCMode.NoGC)
.get[Set[PrintResource]]
// resources: Set[PrintResource] = ListSet(repl.Session$App13$A@169b8933, repl.Session$App13$B@673450ca, repl.Session$App13$C@35acd75e)
resources.foreach(_.start())
// A started
// B started
// C started
resources.toSeq.reverse.foreach(_.stop())
// C stopped
// B stopped
// A stopped
Calling .foreach
on an auto-set is safe; the actions will be executed in order of dependencies. Auto-Sets preserve ordering, they use ListSet
under the hood, unlike user-defined Sets. e.g. If C
depends on B
depends on A
, autoset order is: A, B, C
, to start call: A, B, C
, to close call: C, B, A
. When you use auto-sets for finalization, you must .reverse
the autoset.
Note: Auto-Sets are NOT subject to Garbage Collection, they are assembled after garbage collection is done, as such they can’t contain garbage by construction. Because of that they also cannot be used as GC Roots.
See also: same concept in MacWire
Weak Sets
Set bindings can contain weak references. References designated as weak will be retained only if there are other dependencies on them except for the set addition.
Example:
import distage._
sealed trait Elem
final class Strong extends Elem {
println("Strong constructed")
}
final class Weak extends Elem {
println("Weak constructed")
}
val module = new ModuleDef {
make[Strong]
make[Weak]
many[Elem]
.ref[Strong]
.weak[Weak]
}
// module: AnyRef with ModuleDef = Module(many[{type.scala.collection.immutable.Set[=Session::App16::Elem]}].add[{type.repl.Session::repl.Session.App16::repl.Session.App16.Weak}].from(weak[{type.repl.Session::repl.Session.App16::repl.Session.App16.Weak}]) ((other-features.md:212)), many[{type.scala.collection.immutable.Set[=Session::App16::Elem]}].add[{type.repl.Session::repl.Session.App16::repl.Session.App16.Strong}].from(using[{type.repl.Session::repl.Session.App16::repl.Session.App16.Strong}]) ((other-features.md:211)), many[{type.scala.collection.immutable.Set[=Session::App16::Elem]}] ((other-features.md:210)), make[{type.repl.Session::repl.Session.App16::repl.Session.App16.Strong}].from(call(<function1>(): repl.Session::repl.Session.App16::repl.Session.App16.Strong)) ((other-features.md:207)), make[{type.repl.Session::repl.Session.App16::repl.Session.App16.Weak}].from(call(<function1>(): repl.Session::repl.Session.App16::repl.Session.App16.Weak)) ((other-features.md:208)))
// Designate Set[Elem] as the garbage collection root,
// everything that Set[Elem] does not strongly depend on will be garbage collected
// and will not be constructed.
val roots = Set[DIKey](DIKey.get[Set[Elem]])
// roots: Set[DIKey] = Set({type.scala.collection.immutable.Set[=Session::App16::Elem]})
val locator = Injector().produceUnsafe(PlannerInput(module, roots))
// Strong constructed
// locator: Locator = izumi.distage.LocatorDefaultImpl@ef5a65b
locator.get[Set[Elem]].size == 1
// res17: Boolean = true
The Weak
class was not required by any dependency of Set[Elem]
, so it was pruned. The Strong
class remained, because the reference to it was strong, so it was counted as a dependency of Set[Elem]
.
If we change Strong
to depend on the Weak
, then Weak
will be retained:
final class Strong(weak: Weak) extends Elem {
println("Strong constructed")
}
val locator = Injector().produceUnsafe(PlannerInput(module, roots))
// Weak constructed
// Strong constructed
// locator: Locator = izumi.distage.LocatorDefaultImpl@2326d5a6
locator.get[Set[Elem]].size == 2
// res19: Boolean = true
Inner Classes and Path-Dependent Types
Path-dependent types with a value prefix will instantiate normally:
import distage.{GCMode, ModuleDef, Injector}
class Path {
class A
}
val path = new Path
// path: Path = repl.Session$App20$Path@2aa6109
val module = new ModuleDef {
make[path.A]
}
// module: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App20::repl.Session.App20.Path::repl.Session.App20.Path.A}].from(call(<function1>(): repl.Session::repl.Session.App20::repl.Session.App20.Path::repl.Session.App20.Path.A)) ((other-features.md:287)))
Injector()
.produceUnsafe(module, GCMode.NoGC)
.get[path.A]
// res21: path.A = repl.Session$App20$Path$A@6d5d5afe
Since version 0.10
, path-dependent types with a type (non-value) prefix are no longer supported, see issue: https://github.com/7mind/izumi/issues/764
Depending on Locator
Objects can depend on the Locator (container of the final object graph):
import distage._
class A(all: LocatorRef) {
def c = all.get.get[C]
}
class B
class C
val module = new ModuleDef {
make[A]
make[B]
make[C]
}
// module: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App22::repl.Session.App22.C}].from(call(<function1>(): repl.Session::repl.Session.App22::repl.Session.App22.C)) ((other-features.md:319)), make[{type.repl.Session::repl.Session.App22::repl.Session.App22.B}].from(call(<function1>(): repl.Session::repl.Session.App22::repl.Session.App22.B)) ((other-features.md:318)), make[{type.repl.Session::repl.Session.App22::repl.Session.App22.A}].from(call(<function1>(izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef): repl.Session::repl.Session.App22::repl.Session.App22.A)) ((other-features.md:317)))
val locator = Injector().produceUnsafe(module, GCMode.NoGC)
// locator: Locator = izumi.distage.LocatorDefaultImpl@3ade8c6f
assert(locator.get[A].c eq locator.get[C])
Locator contains metadata about the plan and the bindings from which it was ultimately created:
// Plan that created this locator
val plan: OrderedPlan = locator.plan
// plan: OrderedPlan = {type.izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef} (other-features.md:317) := import {type.izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef} // required for {type.repl.Session::repl.Session.App22::repl.Session.App22.A}
// {type.repl.Session::repl.Session.App22::repl.Session.App22.B} (other-features.md:318) := call(<function1>(): repl.Session::repl.Session.App22::repl.Session.App22.B) {}
// {type.repl.Session::repl.Session.App22::repl.Session.App22.C} (other-features.md:319) := call(<function1>(): repl.Session::repl.Session.App22::repl.Session.App22.C) {}
// {type.repl.Session::repl.Session.App22::repl.Session.App22.A} (other-features.md:317) := call(<function1>(izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef): repl.Session::repl.Session.App22::repl.Session.App22.A) {
// arg all: izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef = lookup({type.izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef})
// }
// Bindings from which the Plan was built
val moduleDef: ModuleBase = plan.definition
// moduleDef: ModuleBase = Module(make[{type.repl.Session::repl.Session.App22::repl.Session.App22.B}].from(call(<function1>(): repl.Session::repl.Session.App22::repl.Session.App22.B)) ((other-features.md:318)), make[{type.repl.Session::repl.Session.App22::repl.Session.App22.C}].from(call(<function1>(): repl.Session::repl.Session.App22::repl.Session.App22.C)) ((other-features.md:319)), make[{type.repl.Session::repl.Session.App22::repl.Session.App22.A}].from(call(<function1>(izumi.distage.model.Locator::izumi.distage.model.Locator.LocatorRef): repl.Session::repl.Session.App22::repl.Session.App22.A)) ((other-features.md:317)))