
com.avsystem.commons.di.Component.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of commons-core_2.12 Show documentation
Show all versions of commons-core_2.12 Show documentation
AVSystem commons library for Scala
The newest version!
package com.avsystem.commons
package di
import com.avsystem.commons.di.Component.DestroyFunction
import com.avsystem.commons.misc.{GraphUtils, SourceInfo}
import java.util.concurrent.atomic.AtomicReference
import scala.annotation.compileTimeOnly
import scala.annotation.unchecked.uncheckedVariance
case class ComponentInitializationException(component: Component[_], cause: Throwable)
extends Exception(s"failed to initialize component ${component.info}", cause)
case class DependencyCycleException(cyclePath: List[Component[_]])
extends Exception(s"component dependency cycle detected:\n${cyclePath.iterator.map(_.info).map(" " + _).mkString(" ->\n")}")
case class ComponentInfo(
name: String,
filePath: String,
fileName: String,
lineNumber: Int,
) {
override def toString: String = s"$name($fileName:$lineNumber)"
}
object ComponentInfo {
def apply(namePrefix: String, sourceInfo: SourceInfo): ComponentInfo =
new ComponentInfo(namePrefix + sourceInfo.enclosingSymbols.head, sourceInfo.filePath, sourceInfo.fileName, sourceInfo.line)
@compileTimeOnly("implicit ComponentInfo is only available inside code passed to component/singleton macro")
implicit def info: ComponentInfo = sys.error("stub")
}
/**
* Represents a lazily initialized component in a dependency injection setting. The name "component" indicates
* that the value is often an application building block like a database service, data access object, HTTP server etc.
* which is associated with some side-effectful initialization code.
* However, [[Component]] can hold values of any type.
*
* You can think of [[Component]] as a high-level `lazy val` with more features:
* parallel initialization of dependencies, dependency cycle detection, source code awareness.
*/
final class Component[+T](
val info: ComponentInfo,
deps: => IndexedSeq[Component[_]],
creator: IndexedSeq[Any] => ExecutionContext => Future[T],
destroyer: DestroyFunction[T] = Component.emptyDestroy,
cachedStorage: Opt[AtomicReference[Future[T]]] = Opt.Empty,
) {
/**
* Name of the component. Usually this name is inferred from the method name that this component is defined by.
*/
def name: String = info.name
def isCached: Boolean = cachedStorage.isDefined
/**
* Returns dependencies of this component extracted from the component definition.
* You can use this to inspect the dependency graph without initializing any components.
*/
lazy val dependencies: IndexedSeq[Component[_]] = deps
private[this] val storage: AtomicReference[Future[T]] =
cachedStorage.getOrElse(new AtomicReference)
private def sameStorage(otherStorage: AtomicReference[_]): Boolean =
storage eq otherStorage
// equality based on storage identity is important for cycle detection with cached components
override def hashCode(): Int = storage.hashCode()
override def equals(obj: Any): Boolean = obj match {
case c: Component[_] => c.sameStorage(storage)
case _ => false
}
/**
* Phantom method that indicates an asynchronous reference to this component inside definition of some other component.
* This method is rewritten in compile time by [[Components.component]] or [[Components.singleton]] macro.
* The component being referred is extracted as a dependency and initialized before the component that refers to
* it. This way multiple dependencies can be initialized in parallel.
*
* @example
* {{{
* class FooService
* class BarService
* class Application(foo: FooService, bar: BarService)
*
* object MyComponents extends Components {
* def foo: Component[FooService] = singleton(new FooService)
* def bar: Component[BarService] = singleton(new BarService)
*
* // before `app` is initialized, `foo` and `bar` can be initialized in parallel
* def app: Component[Application] = singleton(new Application(foo.ref, bar.ref))
* }
* }}}
*/
@compileTimeOnly(".ref can only be used inside code passed to component/singleton(...) macro")
def ref: T = sys.error("stub")
/**
* Returns the initialized instance of this component, if it was already initialized.
*/
def getIfReady: Option[T] =
storage.get.option.flatMap(_.value.map(_.get))
/**
* Forces a dependency on another component or components.
*/
def dependsOn(moreDeps: Component[_]*): Component[T] =
new Component(info, deps ++ moreDeps, creator, destroyer, cachedStorage)
/**
* Specifies an asynchronous function that will be used to destroy this component, i.e.
* free up any resources that this component allocated (threads, network connections, etc). See [[destroy]].
*/
def asyncDestroyWith(destroyFun: DestroyFunction[T]): Component[T] = {
val newDestroyer: DestroyFunction[T] =
implicit ctx => t => destroyer(ctx)(t).flatMap(_ => destroyFun(ctx)(t))
new Component(info, deps, creator, newDestroyer, cachedStorage)
}
/**
* Specifies a function that will be used to destroy this component, i.e. free up any resources that this
* component allocated (threads, network connections, etc). See [[destroy]].
*/
def destroyWith(destroyFun: T => Unit): Component[T] =
asyncDestroyWith(implicit ctx => t => Future(destroyFun(t)))
private[di] def cached(cachedStorage: AtomicReference[Future[T@uncheckedVariance]], info: ComponentInfo): Component[T] =
new Component(info, deps, creator, destroyer, Opt(cachedStorage))
/**
* Validates this component by checking its dependency graph for cycles.
* A [[DependencyCycleException]] is thrown when a cycle is detected.
*/
def validate(): Unit =
Component.validateAll(List(this))
/**
* Forces initialization of this component and its dependencies (in parallel, using given `ExecutionContext`).
* Returns a `Future` containing the initialized component value.
* NOTE: the component is initialized only once and its value is cached.
*/
def init(implicit ec: ExecutionContext): Future[T] =
doInit(starting = true)
/**
* Destroys this component and all its dependencies (in reverse initialization order, i.e. first the component
* and then its dependencies. Destroying calls the function that was registered with [[destroyWith]] or
* [[asyncDestroyWith]] and clears the cached component instance so that it is created anew if [[init]] is called
* again.
* If possible, independent components are destroyed in parallel, using given `ExecutionContext`.
*/
def destroy(implicit ec: ExecutionContext): Future[Unit] =
Component.destroyAll(List(this))
private def doDestroy(implicit ec: ExecutionContext): Future[Unit] =
getIfReady.fold(Future.unit) { value =>
storage.set(null)
destroyer(ec)(value)
}
private def doInit(starting: Boolean)(implicit ec: ExecutionContext): Future[T] =
storage.getPlain match {
case null =>
val promise = Promise[T]()
if (storage.compareAndSet(null, promise.future)) {
if (starting) {
validate()
}
val resultFuture =
Future.traverse(dependencies)(_.doInit(starting = false))
.flatMap(resolvedDeps => creator(resolvedDeps)(ec))
.recoverNow {
case NonFatal(cause) =>
throw ComponentInitializationException(this, cause)
}
promise.completeWith(resultFuture)
}
storage.get()
case future =>
future
}
}
object Component {
type DestroyFunction[-T] = ExecutionContext => T => Future[Unit]
def emptyDestroy[T]: DestroyFunction[T] =
reusableEmptyDestroy.asInstanceOf[DestroyFunction[T]]
private[this] val reusableEmptyDestroy: DestroyFunction[Any] =
_ => _ => Future.unit
def async[T](definition: => T): ExecutionContext => Future[T] =
implicit ctx => Future(definition)
def validateAll(components: Seq[Component[_]]): Unit =
GraphUtils.dfs(components)(
_.dependencies.toList,
onCycle = (node, stack) => {
val cyclePath = node :: (node :: stack.map(_.node).takeWhile(_ != node)).reverse
throw DependencyCycleException(cyclePath)
},
)
/**
* Destroys all given components and their dependencies by calling their destroy function (registered with
* [[Component.destroyWith()]] or [[Component.asyncDestroyWith()]]) and clearing up cached component instances.
* It is ensured that a component is only destroyed after all components that depend on it are destroyed
* (reverse initialization order).
* Independent components are destroyed in parallel, using given `ExecutionContext`.
*/
def destroyAll(components: Seq[Component[_]])(implicit ec: ExecutionContext): Future[Unit] = {
val reverseGraph = new MHashMap[Component[_], MListBuffer[Component[_]]]
val terminals = new MHashSet[Component[_]]
GraphUtils.dfs(components)(
_.dependencies.toList,
onEnter = { (c, _) =>
reverseGraph.getOrElseUpdate(c, new MListBuffer) // make sure there is entry for all nodes
if (c.dependencies.nonEmpty)
c.dependencies.foreach { dep =>
reverseGraph.getOrElseUpdate(dep, new MListBuffer) += c
}
else
terminals += c
},
)
val destroyFutures = new MHashMap[Component[_], Future[Unit]]
def doDestroy(c: Component[_]): Future[Unit] =
destroyFutures.getOrElseUpdate(c, Future.traverse(reverseGraph(c))(doDestroy).flatMap(_ => c.doDestroy))
Future.traverse(reverseGraph.keys)(doDestroy).toUnit
}
}
/**
* A wrapper over [[Component]] that has an implicit conversion from arbitrary expression
* of type T to [[AutoComponent]]. This is used when you need to accept a parameter that may contain other
* component references.
*
* Using [[AutoComponent]] avoids explicit wrapping of expressions passed as that parameter
* into [[Component]] (using `component` macro).
*/
case class AutoComponent[+T](component: Component[T]) extends AnyVal
© 2015 - 2025 Weber Informatics LLC | Privacy Policy