Repository: 7mind/izumi
Pull request #847 (2020-01-22 14:53:19)

feature/migrate-ws-on-io

codecov-io: # [Codecov](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=h1) Report
> Merging [#847](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=desc) into [develop](https://codecov.io/gh/7mind/izumi/commit/da8359df28fad7c3b563f1428b8ff00ed45be7d8?src=pr&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `91.37%`.

[![Impacted file tree graph](https://codecov.io/gh/7mind/izumi/pull/847/graphs/tree.svg?width=650&token=77pa3UU1Je&height=150&src=pr)](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           develop     #847      +/-   ##
===========================================
+ Coverage    70.25%   70.26%   +0.01%     
===========================================
  Files          549      549              
  Lines        14102    14108       +6     
  Branches       722      722              
===========================================
+ Hits          9907     9913       +6     
  Misses        4195     4195
```


| [Impacted Files](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...umi/idealingua/runtime/rpc/http4s/HttpServer.scala](https://codecov.io/gh/7mind/izumi/pull/847/diff?src=pr&el=tree#diff-aWRlYWxpbmd1YS12MS9pZGVhbGluZ3VhLXYxLXJ1bnRpbWUtcnBjLWh0dHA0cy9zcmMvbWFpbi9zY2FsYS9penVtaS9pZGVhbGluZ3VhL3J1bnRpbWUvcnBjL2h0dHA0cy9IdHRwU2VydmVyLnNjYWxh) | `69.87% <90.47%> (-0.36%)` | :arrow_down: |
| [...untime/rpc/http4s/WithWebsocketClientContext.scala](https://codecov.io/gh/7mind/izumi/pull/847/diff?src=pr&el=tree#diff-aWRlYWxpbmd1YS12MS9pZGVhbGluZ3VhLXYxLXJ1bnRpbWUtcnBjLWh0dHA0cy9zcmMvbWFpbi9zY2FsYS9penVtaS9pZGVhbGluZ3VhL3J1bnRpbWUvcnBjL2h0dHA0cy9XaXRoV2Vic29ja2V0Q2xpZW50Q29udGV4dC5zY2FsYQ==) | `83.33% <91.89%> (+1.64%)` | :arrow_up: |
| [...izumi/fundamentals/platform/language/IzScala.scala](https://codecov.io/gh/7mind/izumi/pull/847/diff?src=pr&el=tree#diff-ZnVuZGFtZW50YWxzL2Z1bmRhbWVudGFscy1wbGF0Zm9ybS9zcmMvbWFpbi9zY2FsYS9penVtaS9mdW5kYW1lbnRhbHMvcGxhdGZvcm0vbGFuZ3VhZ2UvSXpTY2FsYS5zY2FsYQ==) | `55.55% <0%> (-11.12%)` | :arrow_down: |
| [...mentals/reflection/macrortti/LightTypeTagRef.scala](https://codecov.io/gh/7mind/izumi/pull/847/diff?src=pr&el=tree#diff-ZnVuZGFtZW50YWxzL2Z1bmRhbWVudGFscy1yZWZsZWN0aW9uL3NyYy9tYWluL3NjYWxhL2l6dW1pL2Z1bmRhbWVudGFscy9yZWZsZWN0aW9uL21hY3JvcnR0aS9MaWdodFR5cGVUYWdSZWYuc2NhbGE=) | `75.71% <0%> (-1.43%)` | :arrow_down: |
| [.../main/scala/izumi/functional/bio/impl/BIOZio.scala](https://codecov.io/gh/7mind/izumi/pull/847/diff?src=pr&el=tree#diff-ZnVuZGFtZW50YWxzL2Z1bmRhbWVudGFscy1iaW8vc3JjL21haW4vc2NhbGEvaXp1bWkvZnVuY3Rpb25hbC9iaW8vaW1wbC9CSU9aaW8uc2NhbGE=) | `34.61% <0%> (+1.28%)` | :arrow_up: |
| [...rc/main/scala/izumi/functional/bio/BIOSyntax.scala](https://codecov.io/gh/7mind/izumi/pull/847/diff?src=pr&el=tree#diff-ZnVuZGFtZW50YWxzL2Z1bmRhbWVudGFscy1iaW8vc3JjL21haW4vc2NhbGEvaXp1bWkvZnVuY3Rpb25hbC9iaW8vQklPU3ludGF4LnNjYWxh) | `24% <0%> (+1.33%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=footer). Last update [da8359d...70eb6df](https://codecov.io/gh/7mind/izumi/pull/847?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala
index 8135cc76d..97dfa8d08 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala
@@ -91,7 +91,7 @@ object AnyConstructorMacro {
     val macroUniverse = StaticDIUniverse(c)
     val reflectionProvider = ReflectionProviderDefaultImpl(macroUniverse)
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("AnyConstructor", targetType)
 
     if (reflectionProvider.isConcrete(targetType)) {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
index a5ed08aea..6254e8d7e 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
@@ -16,7 +16,7 @@ object ClassConstructorMacro {
   def mkClassConstructor[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[ClassConstructor[T]] = {
     import c.universe._
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("ClassConstructor", targetType)
 
     targetType match {
@@ -28,7 +28,7 @@ object ClassConstructorMacro {
           case _ => q"${t.termSymbol}"
         }
         c.Expr[ClassConstructor[T]] {
-          q"{ new ${weakTypeOf[ClassConstructor[T]]}($providerMagnet.pure[$targetType]($term)) }"
+          q"{ new ${weakTypeOf[ClassConstructor[T]]}($providerMagnet.singleton[$targetType]($term)) }"
         }
 
       case _ =>
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala
index e4373bf0a..f21eca7a8 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala
@@ -25,7 +25,7 @@ object FactoryConstructorMacro {
     import macroUniverse.Wiring._
     import macroUniverse._
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("FactoryConstructor", targetType)
 
     val factory@Factory(factoryMethods, factoryTraitDependencies) = reflectionProvider.symbolToWiring(targetType)
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala
index a25ceb812..d2ac0253d 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala
@@ -16,7 +16,7 @@ object TraitConstructorMacro {
   def mkTraitConstructor[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[TraitConstructor[T]] = {
     import c.universe._
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("FactoryConstructor", targetType)
 
     val macroUniverse = StaticDIUniverse(c)
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala
index 6b6e7dd92..3e5c3d658 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala
@@ -24,11 +24,11 @@ trait DIEffect[F[_]] {
     * this will _also_ intercept Throwable defects in `ZIO`, not only typed errors */
   def definitelyRecover[A](action: => F[A])(recover: Throwable => F[A]): F[A]
 
-  /** `definitelyRecover`, but the Throwable is not guaranteed to be
-    * the original Throwable, it can be a Throwable enhanced with a
-    * Monad's own debugging information (e.g. in case of `ZIO` it's always `FiberFailure`)
+  /** `definitelyRecover`, but second Throwable is not the original Throwable,
+    * it can be a Throwable enhanced with an effect's own debugging information
+    * (e.g. in case of `ZIO` it's always `FiberFailure`)
     */
-  def definitelyRecoverCause[A](action: => F[A])(recoverCause: Throwable => F[A]): F[A]
+  def definitelyRecoverCause[A](action: => F[A])(recoverCause: (Throwable, Throwable) => F[A]): F[A]
 
   def fail[A](t: => Throwable): F[A]
 
@@ -74,8 +74,8 @@ object DIEffect extends LowPriorityDIEffectInstances {
         case t: Throwable => recover(t)
       }
     }
-    override def definitelyRecoverCause[A](action: => Identity[A])(recoverCause: Throwable => Identity[A]): Identity[A] = {
-      definitelyRecover(action)(recoverCause)
+    override def definitelyRecoverCause[A](action: => Identity[A])(recoverCause: (Throwable, Throwable) => Identity[A]): Identity[A] = {
+      definitelyRecover(action)(e => recoverCause(e, e))
     }
     override def bracket[A, B](acquire: => Identity[A])(release: A => Identity[Unit])(use: A => Identity[B]): Identity[B] = {
       val a = acquire
@@ -106,8 +106,8 @@ object DIEffect extends LowPriorityDIEffectInstances {
       override def definitelyRecover[A](action: => F[E, A])(recover: Throwable => F[E, A]): F[E, A] = {
         suspendF(action).sandbox.catchAll(recover apply _.toThrowable)
       }
-      override def definitelyRecoverCause[A](action: => F[Throwable, A])(recover: Throwable => F[Throwable, A]): F[Throwable, A] = {
-        suspendF(action).sandbox.catchAll(recover apply _.trace.toThrowable)
+      override def definitelyRecoverCause[A](action: => F[Throwable, A])(recover: (Throwable, Throwable) => F[Throwable, A]): F[Throwable, A] = {
+        suspendF(action).sandbox.catchAll(e => recover(e.toThrowable, e.trace.toThrowable))
       }
 
       override def fail[A](t: => Throwable): F[Throwable, A] = F.fail(t)
@@ -146,8 +146,8 @@ private[effect] sealed trait LowPriorityDIEffectInstances {
       override def definitelyRecover[A](action: => F[A])(recover: Throwable => F[A]): F[A] = {
         F.handleErrorWith(F.suspend(action))(recover)
       }
-      override def definitelyRecoverCause[A](action: => F[A])(recoverCause: Throwable => F[A]): F[A] = {
-        definitelyRecover(action)(recoverCause)
+      override def definitelyRecoverCause[A](action: => F[A])(recoverCause: (Throwable, Throwable) => F[A]): F[A] = {
+        definitelyRecover(action)(e => recoverCause(e, e))
       }
       override def fail[A](t: => Throwable): F[A] = F.suspend(F.raiseError(t))
       override def bracket[A, B](acquire: => F[A])(release: A => F[Unit])(use: A => F[B]): F[B] = {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala
index 5cb27b728..5ad7e690f 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala
@@ -20,6 +20,8 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
 
   implicit val diEffectParIdentity: DIEffectAsync[Identity] = {
     new DIEffectAsync[Identity] {
+      final val DIEffectAsyncIdentityPool = ExecutionContext.fromExecutorService(null)
+
       override def parTraverse_[A](l: Iterable[A])(f: A => Unit): Unit = {
         parTraverse(l)(f)
         ()
@@ -29,9 +31,7 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
       }
 
       override def parTraverse[A, B](l: Iterable[A])(f: A => Identity[B]): Identity[List[B]] = {
-        implicit val ec = ExecutionContext.global
-        val future = Future.sequence(l.map(a => Future(f(a))))
-        Await.result(future, Duration.Inf).toList
+        parTraverseIdentity(DIEffectAsyncIdentityPool)(l)(f)
       }
     }
   }
@@ -50,6 +50,12 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
     }
   }
 
+  private[izumi] def parTraverseIdentity[A, B](ec0: ExecutionContext)(l: Iterable[A])(f: A => Identity[B]): Identity[List[B]] = {
+    implicit val ec = ec0
+    val future = Future.sequence(l.map(a => Future(scala.concurrent.blocking(f(a)))))
+    Await.result(future, Duration.Inf).toList
+  }
+
 }
 
 private[effect] sealed trait LowPriorityDIEffectAsyncInstances {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
index df8756851..afe280a31 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
@@ -187,6 +187,17 @@ object ProviderMagnet {
     )
   }
 
+  def singleton[A <: Singleton: Tag](a: A): ProviderMagnet[A] = {
+    new ProviderMagnet[A](
+      Provider.ProviderImpl[A](
+        parameters = Seq.empty,
+        ret = SafeType.get[A],
+        fun = (_: Seq[Any]) => a,
+        isGenerated = true,
+      )
+    )
+  }
+
   def identityKey(key: DIKey): ProviderMagnet[_] = {
     val tpe = key.tpe
     val symbolInfo = firstParamSymbolInfo(tpe)
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala
index 233a19898..ac8da99e2 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala
@@ -123,7 +123,7 @@ object DockerContainer {
         for {
           containers <- DIEffect[F]
             .maybeSuspend {
-              // FIXME: temporary hack to allow missing containers to skip tests
+              // FIXME: temporary hack to allow missing containers to skip tests (happens when both DockerWrapper & integration check that depends on Docker.Container are memoized)
               try {
                 client.listContainersCmd()
                   .withAncestorFilter(List(config.image).asJava)
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala b/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala
index 4c67f01f5..c2f8ae90f 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala
@@ -26,7 +26,7 @@ object RoleAppPlanner {
                           bsModule: BootstrapModule,
                           logger: IzLogger,
                         ) extends RoleAppPlanner[F] { self =>
-    private[this] val injector = Injector.Standard(bsModule)
+    private[this] val injector = Injector(bsModule)
 
     override def reboot(bsOverride: BootstrapModule): RoleAppPlanner[F] = {
       new RoleAppPlanner.Impl[F](options, bsModule overridenBy bsOverride, logger)
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
index 19d5c9384..470353737 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
@@ -21,17 +21,16 @@ import org.http4s.server.websocket.WebSocketBuilder
 import org.http4s.websocket.WebSocketFrame
 import org.http4s.websocket.WebSocketFrame.{Binary, Close, Pong, Text}
 
-class HttpServer[C <: Http4sContext]
-(
-  val c: C#IMPL[C]
-, val muxer: IRTServerMultiplexor[C#BiIO, C#RequestContext, C#MethodContext]
-, val codec: IRTClientMultiplexor[C#BiIO]
-, val contextProvider: AuthMiddleware[C#MonoIO, C#RequestContext]
-, val wsContextProvider: WsContextProvider[C#BiIO, C#RequestContext, C#ClientId]
-, val wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext]
-, val listeners: Seq[WsSessionListener[C#ClientId]]
-, logger: IzLogger
-, printer: Printer
+class HttpServer[C <: Http4sContext](
+  val c: C#IMPL[C],
+  val muxer: IRTServerMultiplexor[C#BiIO, C#RequestContext, C#MethodContext],
+  val codec: IRTClientMultiplexor[C#BiIO],
+  val contextProvider: AuthMiddleware[C#MonoIO, C#RequestContext],
+  val wsContextProvider: WsContextProvider[C#BiIO, C#RequestContext, C#ClientId],
+  val wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext],
+  val listeners: Seq[WsSessionListener[C#BiIO, C#ClientId]],
+  logger: IzLogger,
+  printer: Printer
 ) {
 
   import c._
@@ -67,15 +66,15 @@ class HttpServer[C <: Http4sContext]
   }
 
   protected def handler(): PartialFunction[AuthedRequest[MonoIO, RequestContext], MonoIO[Response[MonoIO]]] = {
-    case request@GET -> Root / "ws" as ctx =>
+    case request @ GET -> Root / "ws" as ctx =>
       val result = setupWs(request, ctx)
       result
 
-    case request@GET -> Root / service / method as ctx =>
+    case request @ GET -> Root / service / method as ctx =>
       val methodId = IRTMethodId(IRTServiceId(service), IRTMethodName(method))
       run(new HttpRequestContext(request, ctx), body = "{}", methodId)
 
-    case request@POST -> Root / service / method as ctx =>
+    case request @ POST -> Root / service / method as ctx =>
       val methodId = IRTMethodId(IRTServiceId(service), IRTMethodName(method))
       request.req.decode[String] {
         body =>
@@ -83,62 +82,55 @@ class HttpServer[C <: Http4sContext]
       }
   }
 
-  protected def handleWsClose(context: WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]): Unit = {
-    logger.debug(s"${context -> null}: Websocket client disconnected")
+  protected def handleWsClose(context: WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]): MonoIO[Unit] = {
+    F.sync(logger.debug(s"${context -> null}: Websocket client disconnected")) *>
     context.finish()
   }
 
-  protected def onWsOpened(): Unit = {
-  }
+  protected def onWsOpened(): MonoIO[Unit] = F.unit
 
-  protected def onWsUpdate(maybeNewId: Option[C#ClientId], old: WsClientId[ClientId]): Unit = {
+  protected def onWsUpdate(maybeNewId: Option[C#ClientId], old: WsClientId[ClientId]): MonoIO[Unit] = F.sync {
     (maybeNewId, old).forget
   }
 
-  protected def onWsClosed(): Unit = {
-  }
+  protected def onWsClosed(): MonoIO[Unit] = F.unit
 
   protected def setupWs(request: AuthedRequest[MonoIO, RequestContext], initialContext: RequestContext): MonoIO[Response[MonoIO]] = {
     val context = new WebsocketClientContextImpl[C](c, request, initialContext, listeners, wsSessionStorage, logger) {
-
-      override def onWsSessionOpened(): Unit = {
-          onWsOpened()
-          super.onWsSessionOpened()
+      override def onWsSessionOpened(): C#MonoIO[Unit] = {
+        onWsOpened() *> super.onWsSessionOpened()
       }
-
-      override def onWsClientIdUpdate(maybeNewId: Option[C#ClientId], oldId: WsClientId[C#ClientId]): Unit = {
-        onWsUpdate(maybeNewId, oldId)
-        super.onWsClientIdUpdate(maybeNewId, oldId)
+      override def onWsClientIdUpdate(maybeNewId: Option[C#ClientId], oldId: WsClientId[C#ClientId]): C#MonoIO[Unit] = {
+        onWsUpdate(maybeNewId, oldId) *> super.onWsClientIdUpdate(maybeNewId, oldId)
       }
-
-      override def onWsSessionClosed(): Unit = {
-        onWsClosed()
-        super.onWsSessionClosed()
+      override def onWsSessionClosed(): C#MonoIO[Unit] = {
+        onWsClosed() *> super.onWsSessionClosed()
       }
     }
-    context.start()
-    logger.debug(s"${context -> null}: Websocket client connected")
-
-    context.queue.flatMap[Throwable, Response[MonoIO]] { q =>
-      val dequeueStream = q.dequeue.through {
-        stream =>
-          stream
-            .evalMap(handleWsMessage(context))
-            .collect { case Some(v) => WebSocketFrame.Text(v) }
+    for {
+      _ <- context.start()
+      _ <- F.sync(logger.debug(s"${context -> null}: Websocket client connected"))
+      response <- context.queue.flatMap[Throwable, Response[MonoIO]] {
+        q =>
+          val dequeueStream = q.dequeue.through {
+            stream =>
+              stream
+                .evalMap(handleWsMessage(context))
+                .collect { case Some(v) => WebSocketFrame.Text(v) }
+          }
+          val enqueueSink = q.enqueue
+          WebSocketBuilder[MonoIO].build(
+            send = dequeueStream.merge(context.outStream).merge(context.pingStream),
+            receive = enqueueSink,
+            onClose = handleWsClose(context)
+          )
       }
-      val enqueueSink = q.enqueue
-      WebSocketBuilder[MonoIO].build(
-        send = dequeueStream.merge(context.outStream).merge(context.pingStream)
-      , receive = enqueueSink
-      , onClose = F.syncThrowable(handleWsClose(context))
-      )
-    }
+    } yield response
   }
 
   protected def handleWsMessage(context: WebsocketClientContextImpl[C], requestTime: ZonedDateTime = IzTime.utcNow): WebSocketFrame => MonoIO[Option[String]] = {
     case Text(msg, _) =>
-      makeResponse(context, msg)
-        .sandboxBIOExit
+      makeResponse(context, msg).sandboxBIOExit
         .map(handleResult(context, _))
 
     case Close(_) =>
@@ -178,7 +170,7 @@ class HttpServer[C <: Http4sContext]
       parsed <- F.fromEither(parse(message))
       unmarshalled <- F.fromEither(parsed.as[RpcPacket])
       id <- wsContextProvider.toId(context.initialContext, context.id, unmarshalled)
-      _ <- F.syncThrowable(context.updateId(id))
+      _ <- context.updateId(id)
       response <- respond(context, unmarshalled).sandbox.catchAll {
         case BIOExit.Termination(exception, allExceptions, trace) =>
           logger.error(s"${context -> null}: WS processing terminated, $message, $exception, $allExceptions, $trace")
@@ -195,8 +187,7 @@ class HttpServer[C <: Http4sContext]
       case RpcPacket(RPCPacketKind.RpcRequest, None, _, _, _, _, _) =>
         wsContextProvider.handleEmptyBodyPacket(context.id, context.initialContext, input).flatMap {
           case (id, eff) =>
-          context.updateId(id)
-          eff
+            context.updateId(id) *> eff
         }
 
       case RpcPacket(RPCPacketKind.RpcRequest, Some(data), Some(id), _, Some(service), Some(method), _) =>
@@ -219,7 +210,7 @@ class HttpServer[C <: Http4sContext]
         context.requestState.handleResponse(id, data).as(None)
 
       case RpcPacket(RPCPacketKind.BuzzFailure, Some(data), _, Some(id), _, _, _) =>
-        context.requestState.respond(id, RawResponse.BadRawResponse())
+        F.sync(context.requestState.respond(id, RawResponse.BadRawResponse())) *>
         F.fail(new IRTGenericFailure(s"Buzzer has returned failure: $data"))
 
       case k =>
@@ -239,7 +230,6 @@ class HttpServer[C <: Http4sContext]
     }
   }
 
-
   protected def run(context: HttpRequestContext[MonoIO, RequestContext], body: String, method: IRTMethodId): MonoIO[Response[MonoIO]] = {
     val ioR = for {
       parsed <- F.fromEither(parse(body))
@@ -252,7 +242,11 @@ class HttpServer[C <: Http4sContext]
       .flatMap(handleResult(context, method, _))
   }
 
-  private def handleResult(context: HttpRequestContext[MonoIO, RequestContext], method: IRTMethodId, result: BIOExit[Throwable, Option[Json]]): BiIO[Throwable, Response[MonoIO]] = {
+  private def handleResult(
+    context: HttpRequestContext[MonoIO, RequestContext],
+    method: IRTMethodId,
+    result: BIOExit[Throwable, Option[Json]]
+  ): C#MonoIO[Response[MonoIO]] = {
     result match {
       case Success(v) =>
         v match {
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala
index e76085998..ac9c58690 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala
@@ -4,74 +4,77 @@ import java.time.ZonedDateTime
 import java.util.concurrent.atomic.AtomicReference
 import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedDeque, TimeUnit, TimeoutException}
 
+import fs2.concurrent.Queue
+import io.circe.Json
+import io.circe.syntax._
+import izumi.functional.bio.BIOApplicative
 import izumi.fundamentals.platform.language.Quirks
 import izumi.fundamentals.platform.time.IzTime
 import izumi.fundamentals.platform.uuid.UUIDGen
 import izumi.idealingua.runtime.rpc._
-import fs2.concurrent.Queue
-import io.circe.Json
-import io.circe.syntax._
 import logstage.IzLogger
 import org.http4s.AuthedRequest
 import org.http4s.websocket.WebSocketFrame
 import org.http4s.websocket.WebSocketFrame.{Ping, Text}
 
-import scala.jdk.CollectionConverters._
 import scala.concurrent.duration._
+import scala.jdk.CollectionConverters._
 
-trait WebsocketClientContext[B[+ _, + _], ClientId, Ctx] {
-  def requestState: RequestState[B]
+trait WebsocketClientContext[F[+_, +_], ClientId, Ctx] {
+  def requestState: RequestState[F]
 
   def id: WsClientId[ClientId]
 
-  def enqueue(method: IRTMethodId, data: Json): RpcPacketId
+  def enqueue(method: IRTMethodId, data: Json): F[Throwable, RpcPacketId]
 
-  def finish(): Unit
+  def finish(): F[Throwable, Unit]
 }
 
-trait WsSessionsStorage[B[+ _, + _], ClientId, Ctx] {
-  def addClient(id: WsSessionId, ctx: WebsocketClientContext[B, ClientId, Ctx]): WebsocketClientContext[B, ClientId, Ctx]
+trait WsSessionsStorage[F[+_, +_], ClientId, Ctx] {
+  def addClient(id: WsSessionId, ctx: WebsocketClientContext[F, ClientId, Ctx]): F[Throwable, WebsocketClientContext[F, ClientId, Ctx]]
 
-  def deleteClient(id: WsSessionId): WebsocketClientContext[B, ClientId, Ctx]
+  def deleteClient(id: WsSessionId): F[Throwable, WebsocketClientContext[F, ClientId, Ctx]]
 
-  def allClients(): Seq[WebsocketClientContext[B, ClientId, Ctx]]
+  def allClients(): F[Throwable, Seq[WebsocketClientContext[F, ClientId, Ctx]]]
 
-  def buzzersFor(clientId: ClientId): Option[IRTDispatcher[B]]
+  def buzzersFor(clientId: ClientId): F[Throwable, Option[IRTDispatcher[F]]]
 }
 
-trait WsSessionListener[ClientId] {
-  def onSessionOpened(context: WsClientId[ClientId]): Unit
+trait WsSessionListener[F[+_, +_], ClientId] {
+  def onSessionOpened(context: WsClientId[ClientId]): F[Throwable, Unit]
 
-  def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): Unit
+  def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): F[Throwable, Unit]
 
-  def onSessionClosed(context: WsClientId[ClientId]): Unit
+  def onSessionClosed(context: WsClientId[ClientId]): F[Throwable, Unit]
 }
 
 object WsSessionListener {
-  def empty[ClientId]: WsSessionListener[ClientId] = new WsSessionListener[ClientId] {
-    override def onSessionOpened(context: WsClientId[ClientId]): Unit = {}
-
-    override def onSessionClosed(context: WsClientId[ClientId]): Unit = {}
-
-    override def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): Unit = {}
+  def empty[F[+_, +_]: BIOApplicative, ClientId]: WsSessionListener[F, ClientId] = new WsSessionListener[F, ClientId] {
+    import izumi.functional.bio.F
+    override def onSessionOpened(context: WsClientId[ClientId]): F[Throwable, Unit] = F.unit
+    override def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): F[Throwable, Unit] = F.unit
+    override def onSessionClosed(context: WsClientId[ClientId]): F[Throwable, Unit] = F.unit
   }
 }
 
+trait WsContextProvider[F[+_, +_], Ctx, ClientId] {
+  def toContext(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): F[Throwable, Ctx]
 
-trait WsContextProvider[B[+ _, + _], Ctx, ClientId] {
-  def toContext(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): B[Throwable, Ctx]
-
-  def toId(initial: Ctx, currentId: WsClientId[ClientId], packet: RpcPacket): B[Throwable, Option[ClientId]]
+  def toId(initial: Ctx, currentId: WsClientId[ClientId], packet: RpcPacket): F[Throwable, Option[ClientId]]
 
   // TODO: we use this to mangle with authorization but it's dirty
-  def handleEmptyBodyPacket(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): B[Throwable, (Option[ClientId], B[Throwable, Option[RpcPacket]])]
+  def handleEmptyBodyPacket(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): F[Throwable, (Option[ClientId], F[Throwable, Option[RpcPacket]])]
 }
 
 class IdContextProvider[C <: Http4sContext](val c: C#IMPL[C]) extends WsContextProvider[C#BiIO, C#RequestContext, C#ClientId] {
 
   import c._
 
-  override def handleEmptyBodyPacket(id: WsClientId[ClientId], initial: C#RequestContext, packet: RpcPacket): C#BiIO[Throwable, (Option[ClientId], C#BiIO[Throwable, Option[RpcPacket]])] = {
+  override def handleEmptyBodyPacket(
+    id: WsClientId[ClientId],
+    initial: C#RequestContext,
+    packet: RpcPacket
+  ): C#BiIO[Throwable, (Option[ClientId], C#BiIO[Throwable, Option[RpcPacket]])] = {
     Quirks.discard(id, initial, packet)
     F.pure((None, F.pure(None)))
   }
@@ -81,17 +84,16 @@ class IdContextProvider[C <: Http4sContext](val c: C#IMPL[C]) extends WsContextP
     F.pure(initial)
   }
 
-  override def toId(initial:  C#RequestContext, currentId:  WsClientId[C#ClientId], packet:  RpcPacket): C#BiIO[Throwable, Option[ClientId]] = {
+  override def toId(initial: C#RequestContext, currentId: WsClientId[C#ClientId], packet: RpcPacket): C#BiIO[Throwable, Option[ClientId]] = {
     Quirks.discard(initial, packet)
     F.pure(None)
   }
 }
 
-class WsSessionsStorageImpl[C <: Http4sContext]
-(
-  val c: C#IMPL[C]
-  , logger: IzLogger
-  , codec: IRTClientMultiplexor[C#BiIO]
+class WsSessionsStorageImpl[C <: Http4sContext](
+  val c: C#IMPL[C],
+  logger: IzLogger,
+  codec: IRTClientMultiplexor[C#BiIO]
 ) extends WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext] {
 
   import c._
@@ -103,70 +105,70 @@ class WsSessionsStorageImpl[C <: Http4sContext]
   protected val timeout: FiniteDuration = 20.seconds
   protected val pollingInterval: FiniteDuration = 50.millis
 
-  def addClient(id: WsSessionId, ctx: WSC): WSC = {
+  override def addClient(
+    id: WsSessionId,
+    ctx: WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]
+  ): C#BiIO[Throwable, WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]] = F.sync {
     logger.debug(s"Adding a client with session - $id")
     clients.put(id, ctx)
   }
 
-  def deleteClient(id: WsSessionId): WSC = {
+  override def deleteClient(id: WsSessionId): C#BiIO[Throwable, WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]] = F.sync {
     logger.debug(s"Deleting a client with session - $id")
     clients.remove(id)
   }
 
-  def allClients(): Seq[WSC] = {
+  override def allClients(): C#BiIO[Throwable, Seq[WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]]] = F.sync {
     clients.values().asScala.toSeq
   }
 
-  def buzzersFor(clientId: ClientId): Option[IRTDispatcher[BiIO]] = {
-    val buzzerClient = allClients()
-      .find(_.id.id.contains(clientId))
-
-    logger.debug(s"Asked for buzzer for $clientId. Found: $buzzerClient")
-
-    buzzerClient
-      .map {
-        session =>
-          new IRTDispatcher[BiIO] {
-            override def dispatch(request: IRTMuxRequest): BiIO[Throwable, IRTMuxResponse] = {
-              for {
-                json <- codec.encode(request)
-                id <- F.sync(session.enqueue(request.method, json))
-                resp <- F.bracket(F.pure(id)) {
-                  id =>
-                    logger.debug(s"${request.method -> "method"}, ${id -> "id"}: cleaning request state")
-                    F.sync(session.requestState.forget(id))
-                } {
-                  id =>
-                    session.requestState.poll(id, pollingInterval, timeout).flatMap {
-                      case Some(value: RawResponse.GoodRawResponse) =>
-                        logger.debug(s"${request.method -> "method"}, $id: Have response: $value")
-                        codec.decode(value.data, value.method)
-
-                      case Some(value: RawResponse.BadRawResponse) =>
-                        logger.debug(s"${request.method -> "method"}, $id: Generic failure response: $value")
-                        F.fail(new IRTGenericFailure(s"${request.method -> "method"}, $id: generic failure: $value"))
-
-                      case None =>
-                        F.fail(new TimeoutException(s"${request.method -> "method"}, $id: No response in $timeout"))
-                    }
+  override def buzzersFor(clientId: ClientId): C#BiIO[Throwable, Option[IRTDispatcher[BiIO]]] = {
+    allClients().map(_.find(_.id.id.contains(clientId))).map {
+      buzzerClient =>
+        logger.debug(s"Asked for buzzer for $clientId. Found: $buzzerClient")
+        buzzerClient.map {
+          session =>
+            new IRTDispatcher[BiIO] {
+              override def dispatch(request: IRTMuxRequest): BiIO[Throwable, IRTMuxResponse] = {
+                for {
+                  json <- codec.encode(request)
+                  id <- session.enqueue(request.method, json)
+                  resp <- F.bracket(F.pure(id)) {
+                    id =>
+                      logger.debug(s"${request.method -> "method"}, ${id -> "id"}: cleaning request state")
+                      F.sync(session.requestState.forget(id))
+                  } {
+                    id =>
+                      session.requestState.poll(id, pollingInterval, timeout).flatMap {
+                        case Some(value: RawResponse.GoodRawResponse) =>
+                          logger.debug(s"${request.method -> "method"}, $id: Have response: $value")
+                          codec.decode(value.data, value.method)
+
+                        case Some(value: RawResponse.BadRawResponse) =>
+                          logger.debug(s"${request.method -> "method"}, $id: Generic failure response: $value")
+                          F.fail(new IRTGenericFailure(s"${request.method -> "method"}, $id: generic failure: $value"))
+
+                        case None =>
+                          F.fail(new TimeoutException(s"${request.method -> "method"}, $id: No response in $timeout"))
+                      }
+                  }
+                } yield {
+                  resp
                 }
-              } yield {
-                resp
               }
             }
-          }
-      }
+        }
+    }
   }
 }
 
-class WebsocketClientContextImpl[C <: Http4sContext]
-(
-  val c: C#IMPL[C]
-  , val initialRequest: AuthedRequest[C#MonoIO, C#RequestContext]
-  , val initialContext: C#RequestContext
-  , listeners: Seq[WsSessionListener[C#ClientId]]
-  , wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext]
-  , logger: IzLogger
+class WebsocketClientContextImpl[C <: Http4sContext](
+  val c: C#IMPL[C],
+  val initialRequest: AuthedRequest[C#MonoIO, C#RequestContext],
+  val initialContext: C#RequestContext,
+  listeners: Seq[WsSessionListener[C#BiIO, C#ClientId]],
+  wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext],
+  logger: IzLogger
 ) extends WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext] {
 
   import c._
@@ -192,7 +194,7 @@ class WebsocketClientContextImpl[C <: Http4sContext]
     FiniteDuration(d.toNanos, TimeUnit.NANOSECONDS)
   }
 
-  def enqueue(method: IRTMethodId, data: Json): RpcPacketId = {
+  def enqueue(method: IRTMethodId, data: Json): C#MonoIO[RpcPacketId] = F.sync {
     val request = RpcPacket.buzzerRequestRndId(method, data)
     val id = request.id.get
     logger.debug(s"Enqueue $request with $id to request state & send queue")
@@ -201,32 +203,32 @@ class WebsocketClientContextImpl[C <: Http4sContext]
     id
   }
 
-  def onWsSessionOpened(): Unit = {
-  }
+  def onWsSessionOpened(): C#MonoIO[Unit] = F.unit
 
-  def onWsClientIdUpdate(maybeNewId: Option[ClientId], oldId: WsClientId[ClientId]): Unit = {
+  def onWsClientIdUpdate(maybeNewId: Option[ClientId], oldId: WsClientId[ClientId]): C#MonoIO[Unit] = F.sync {
     logger.debug(s"Id updated to $maybeNewId, was: ${oldId.id}")
   }
 
-  def onWsSessionClosed(): Unit = {
+  def onWsSessionClosed(): C#MonoIO[Unit] = F.sync {
     logger.debug("Finish called. Clear request state")
   }
 
-  protected[http4s] def updateId(maybeNewId: Option[ClientId]): Unit = {
+  protected[http4s] def updateId(maybeNewId: Option[ClientId]): C#MonoIO[Unit] = {
     val oldId = id
     maybeId.set(maybeNewId)
     val newId = id
 
-    def notifyListeners(): Unit = {
-      onWsClientIdUpdate(maybeNewId, oldId)
-      listeners.foreach { listener =>
-        listener.onClientIdUpdate(newId, oldId)
+    def notifyListeners(): C#MonoIO[Unit] = {
+      onWsClientIdUpdate(maybeNewId, oldId) *>
+      F.traverse_(listeners) {
+        listener =>
+          listener.onClientIdUpdate(newId, oldId)
       }
     }
 
     (newId, oldId) match {
       case (WsClientId(_, Some(prev)), WsClientId(_, Some(next))) if prev != next => notifyListeners()
-      case (WsClientId(_, None), WsClientId(_, None)) => ()
+      case (WsClientId(_, None), WsClientId(_, None)) => F.unit
       case _ => notifyListeners()
     }
   }
@@ -246,20 +248,22 @@ class WebsocketClientContextImpl[C <: Http4sContext]
   protected[http4s] val pingStream: fs2.Stream[MonoIO, WebSocketFrame] =
     fs2.Stream.awakeEvery[MonoIO](pingTimeout) >> fs2.Stream(Ping())
 
-  override def finish(): Unit = {
-    onWsSessionClosed()
-    Quirks.discard(wsSessionStorage.deleteClient(sessionId))
-    listeners.foreach { listener =>
-      listener.onSessionClosed(id)
-    }
-    requestState.clear()
+  override def finish(): C#MonoIO[Unit] = {
+    onWsSessionClosed() *>
+    wsSessionStorage.deleteClient(sessionId) *>
+    F.traverse_(listeners) {
+      listener =>
+        listener.onSessionClosed(id)
+    } *>
+    F.sync(requestState.clear())
   }
 
-  protected[http4s] def start(): Unit = {
-    Quirks.discard(wsSessionStorage.addClient(sessionId, this))
-    onWsSessionOpened()
-    listeners.foreach { listener =>
-      listener.onSessionOpened(id)
+  protected[http4s] def start(): C#MonoIO[Unit] = {
+    wsSessionStorage.addClient(sessionId, this) *>
+    onWsSessionOpened() *>
+    F.traverse_(listeners) {
+      listener =>
+        listener.onSessionOpened(id)
     }
   }
 
diff --git a/project/Versions.scala b/project/Versions.scala
index 582e4702c..eba3764ac 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -20,7 +20,7 @@ object V {
   val circe_config = "0.7.0"
   val jawn = "0.14.3"
 
-  val http4s = "0.21.0-M6"
+  val http4s = "0.21.0-RC1"
 
   val scalameta = "4.3.0"
   val fastparse = "2.2.3"


Test patch:
diff --git a/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala b/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala
index f86aa09da..75d7f1cff 100644
--- a/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala
+++ b/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala
@@ -24,9 +24,9 @@ class AnimalModel extends AnyWordSpec with MkInjector {
       val debug = false
 
       val injector = if (debug) {
-        Injector.Standard(GraphDumpBootstrapModule())
+        Injector(GraphDumpBootstrapModule())
       } else {
-        Injector.Standard()
+        Injector()
       }
 
       val plan = injector.plan(definition)
diff --git a/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala b/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala
index c9b810025..b512bb66f 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala
@@ -167,8 +167,8 @@ object ResourceCases {
           case Right(value) => Right(value)
         })
       }
-      override def definitelyRecoverCause[A](action: => Suspend2[E, A])(recoverCause: Throwable => Suspend2[E, A]): Suspend2[E, A] = {
-        definitelyRecover(action)(recoverCause)
+      override def definitelyRecoverCause[A](action: => Suspend2[E, A])(recoverCause: (Throwable, Throwable) => Suspend2[E, A]): Suspend2[E, A] = {
+        definitelyRecover(action)(e => recoverCause(e, e))
       }
 
       override def bracket[A, B](acquire: => Suspend2[E, A])(release: A => Suspend2[E, Unit])(use: A => Suspend2[E, B]): Suspend2[E, B] =
diff --git a/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala b/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala
index 0a02a1ff2..2fd9af478 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala
@@ -5,6 +5,8 @@ import izumi.distage.fixtures.BasicCases.BasicCase4.ClassTypeAnnT
 import izumi.distage.fixtures.ProviderCases.ProviderCase1
 import izumi.distage.model.providers.ProviderMagnet
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse.TypedRef
+import izumi.fundamentals.platform.build.ProjectAttributeMacro
+import izumi.fundamentals.platform.language.IzScala.ScalaRelease
 import izumi.fundamentals.platform.language.Quirks._
 import org.scalatest.wordspec.AnyWordSpec
 
@@ -305,6 +307,29 @@ class ProviderMagnetTest extends AnyWordSpec {
       assert(fn.get.ret == SafeType.get[Int])
     }
 
+    "ProviderMagnet.singleton is correct" in {
+      val xa = new {}
+      val fn = ProviderMagnet.singleton[xa.type](xa)
+
+      assert(fn.get.unsafeApply(Seq()).asInstanceOf[AnyRef] eq xa)
+      assert(fn.get.parameters.isEmpty)
+      assert(fn.get.ret == SafeType.get[xa.type])
+    }
+
+    "ProviderMagnet.singleton is correct with constant types" in {
+      import Ordering.Implicits._
+      assume(ScalaRelease.parse(ProjectAttributeMacro.extractScalaVersion().get) >= ScalaRelease.`2_13`(0))
+      assertCompiles(
+        """
+      val fn = ProviderMagnet.singleton["xa"]("xa")
+
+      assert(fn.get.unsafeApply(Seq()) == "xa")
+      assert(fn.get.parameters.isEmpty)
+      assert(fn.get.ret == SafeType.get["xa"])
+        """
+      )
+    }
+
     "generic parameters without TypeTag should fail" in {
       assertTypeError(
         """def fn[T]  = ProviderMagnet.apply((x: T @Id("gentypeann")) => x).get"""
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
index d45881637..9ddc26c07 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
@@ -19,7 +19,7 @@ class AutoSetTest extends AnyWordSpec with MkInjector {
       make[ServiceD]
     }
 
-    val injector = Injector.Standard(new BootstrapModuleDef {
+    val injector = Injector(new BootstrapModuleDef {
       many[PlanningHook]
         .add(new AutoSetHook[Ordered, Ordered](identity))
     })
@@ -39,7 +39,7 @@ class AutoSetTest extends AnyWordSpec with MkInjector {
         .addValue(5)
     }
 
-    val injector = Injector.Standard(new BootstrapModuleDef {
+    val injector = Injector(new BootstrapModuleDef {
       many[PlanningHook]
         .add(new AutoSetHook[Int, Int](identity))
     })
diff --git a/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala b/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala
index b95296467..af026fb34 100644
--- a/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala
+++ b/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala
@@ -33,7 +33,7 @@ class LoggerInjectionTest extends AnyWordSpec {
 
       val loggerModule = new LogstageModule(router, false)
 
-      val injector = Injector.Standard(loggerModule)
+      val injector = Injector(loggerModule)
       val plan = injector.plan(definition)
       val context = injector.produceUnsafe(plan)
       assert(context.get[ExampleApp].test == 265)
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
index 7176b4ca1..861d324ba 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
@@ -1,7 +1,6 @@
 package izumi.distage.testkit.docker
 
 import distage.DIKey
-import izumi.distage.docker.examples.{DynamoDocker, PostgresDocker}
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.docker.fixtures.PgSvcExample
 import izumi.distage.testkit.scalatest.DistageBIOSpecScalatest
@@ -32,8 +31,9 @@ final class DistageTestDockerBIO extends DistageBIOSpecScalatest[IO] {
   override protected def config: TestConfig = {
     super.config.copy(
       memoizationRoots = Set(
-        DIKey.get[DynamoDocker.Container],
-        DIKey.get[PostgresDocker.Container],
+        DIKey.get[PgSvcExample],
+//        DIKey.get[DynamoDocker.Container],
+//        DIKey.get[PostgresDocker.Container],
       ))
   }
 }
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
index 6032d85f8..60712a206 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
@@ -8,9 +8,18 @@ import izumi.distage.plugins.PluginDef
 import izumi.distage.docker.Docker.AvailablePort
 import izumi.distage.docker.examples.{DynamoDocker, PostgresDocker}
 import izumi.distage.docker.modules.DockerContainerModule
+import izumi.distage.framework.model.IntegrationCheck
+import izumi.fundamentals.platform.integration.{PortCheck, ResourceCheck}
 import zio.Task
 
-class PgSvcExample(val pg: AvailablePort @Id("pg"), val ddb: AvailablePort @Id("ddb"))
+class PgSvcExample(
+                    val pg: AvailablePort @Id("pg"),
+                    val ddb: AvailablePort @Id("ddb"),
+                  ) extends IntegrationCheck {
+  override def resourcesAvailable(): ResourceCheck = {
+    new PortCheck(10).checkPort(pg.hostV4, pg.port)
+  }
+}
 
 object MonadPlugin extends PluginDef
   with CatsDIEffectModule
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
index 11257edd9..31cfce980 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
@@ -32,7 +32,7 @@ trait DistageTestEnv {
     }
   }
 
-  private def makeEnv(logger: IzLogger, testConfig: TestConfig, pluginLoader: PluginLoader, roles: RolesInfo, mergeStrategy: PluginMergeStrategy): TestEnvironment = {
+  protected[distage] def makeEnv(logger: IzLogger, testConfig: TestConfig, pluginLoader: PluginLoader, roles: RolesInfo, mergeStrategy: PluginMergeStrategy): TestEnvironment = {
     val appPlugins = pluginLoader.load(testConfig.pluginConfig)
     val bsPlugins = pluginLoader.load(testConfig.bootstrapPluginConfig)
     val appModule = mergeStrategy.merge(appPlugins) overridenBy testConfig.moduleOverrides
@@ -72,8 +72,7 @@ trait DistageTestEnv {
 }
 
 object DistageTestEnv {
-  private lazy val cache = new SyncCache[EnvCacheKey, TestEnvironment]
-
-  case class EnvCacheKey(config: TestConfig, rolesInfo: RolesInfo, mergeStrategy: PluginMergeStrategy)
+  private[distage] lazy val cache = new SyncCache[EnvCacheKey, TestEnvironment]
 
+  final case class EnvCacheKey(config: TestConfig, rolesInfo: RolesInfo, mergeStrategy: PluginMergeStrategy)
 }
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
index df5238798..5c2e2ec56 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
@@ -2,7 +2,7 @@ package izumi.distage.testkit.services.dstest
 
 import java.util.concurrent.TimeUnit
 
-import distage.{DIKey, Injector, PlannerInput}
+import distage.{Activation, DIKey, Injector, PlannerInput}
 import izumi.distage.framework.model.IntegrationCheck
 import izumi.distage.framework.model.exceptions.IntegrationCheckException
 import izumi.distage.framework.services.{IntegrationChecker, PlanCircularDependencyCheck}
@@ -15,15 +15,16 @@ import izumi.distage.model.exceptions.ProvisioningException
 import izumi.distage.model.plan.{OrderedPlan, TriSplittedPlan}
 import izumi.distage.model.providers.ProviderMagnet
 import izumi.distage.testkit.DebugProperties
-import izumi.distage.testkit.services.dstest.DistageTestRunner.{DistageTest, SuiteData, TestReporter, TestStatus}
+import izumi.distage.testkit.services.dstest.DistageTestRunner._
 import izumi.fundamentals.platform.console.TrivialLogger
 import izumi.fundamentals.platform.functional.Identity
 import izumi.fundamentals.platform.integration.ResourceCheck
 import izumi.fundamentals.platform.language.CodePosition
+import izumi.fundamentals.platform.strings.IzString._
 import izumi.fundamentals.reflection.Tags.TagK
 
-import scala.concurrent.duration.FiniteDuration
-import izumi.fundamentals.platform.strings.IzString._
+import scala.concurrent.ExecutionContext
+import scala.concurrent.duration.{Duration, FiniteDuration}
 
 class DistageTestRunner[F[_]: TagK]
 (
@@ -32,7 +33,8 @@ class DistageTestRunner[F[_]: TagK]
   runnerEnvironment: SpecEnvironment,
   tests: Seq[DistageTest[F]],
   isTestSkipException: Throwable => Boolean,
-  parallelTests: Boolean
+  parallelTests: Boolean,
+  parallelEnvs: Boolean,
 ) {
   def run(): Unit = {
     val groups = tests.groupBy(_.environment)
@@ -45,20 +47,23 @@ class DistageTestRunner[F[_]: TagK]
     val checker = new PlanCircularDependencyCheck(options, logger)
 
     logger.info(s"Starting tests across ${groups.size -> "num envs"}, ${groups.values.map(_.size).toList -> "num tests"} in ${TagK[F].tag -> "monad"}")
-    debugLogger.log(
-      s"""Env contents:
-         |test environments=${groups.keys.niceList()}
-         |tests=${groups.values.map(_.map(_.meta.id).niceList()).zipWithIndex.map(_.swap).niceList()}""".stripMargin)
-
-    groups.foreach {
-      case (env, tests) =>
+    debugLogger.log {
+      val printedEnvs = groups.toList.map {
+        case (t, tests) =>
+          final case class TestEnvironment(Activation: Activation, memoizationRoots: DIKey => Boolean, tests: String)
+          TestEnvironment(t.activation, t.memoizationRoots, tests.map(_.meta.id).niceList().indent(2))
+      }.niceList()
+      s"Env contents: $printedEnvs"
+    }
 
+    try configuredForeach(groups) {
+      case (env, tests) => try {
         // here we scan our classpath to enumerate of our components (we have "bootstrap" components - injector plugins, and app components)
         val provider = runnerEnvironment.makeModuleProvider(options, config, logger, env.roles, env.activationInfo, env.activation)
         val bsModule = provider.bootstrapModules().merge overridenBy env.bsModule overridenBy runnerEnvironment.bootstrapOverrides
         val appModule = provider.appModules().merge overridenBy env.appModule overridenBy runnerEnvironment.moduleOverrides
 
-        val injector = Injector.Standard(bsModule)
+        val injector = Injector(bsModule)
 
         // first we need to plan runtime for our monad. Identity is also supported
         val runtimeGcRoots: Set[DIKey] = Set(
@@ -115,13 +120,14 @@ class DistageTestRunner[F[_]: TagK]
             implicit val F: DIEffect[F] = runtimeLocator.get[DIEffect[F]]
             implicit val P: DIEffectAsync[F] = runtimeLocator.get[DIEffectAsync[F]]
 
-            try {
-              runner.run {
-                // now we produce integration components for our shared plan
-                checker.verify(shared.side)
+            runner.run {
+              // now we produce integration components for our shared plan
+              checker.verify(shared.shared)
 
+              F.definitelyRecoverCause {
                 Injector.inherit(runtimeLocator).produceF[F](shared.shared).use {
                   sharedLocator =>
+                    checker.verify(shared.side)
 
                     Injector.inherit(sharedLocator).produceF[F](shared.side).use {
                       sharedIntegrationLocator =>
@@ -130,19 +136,26 @@ class DistageTestRunner[F[_]: TagK]
                         }
                     }
                 }
+              } {
+                case (ProvisioningIntegrationException(integrations), _) =>
+                  // FIXME: temporary hack to allow missing containers to skip tests (happens when both DockerWrapper & integration check that depends on Docker.Container are memoized)
+                  F.maybeSuspend(ignoreIntegrationCheckFailedTests(tests, integrations))
+                case (_, cause) =>
+                  // fail all tests (if an exception reached here, it must have happened before the individual test runs)
+                  F.maybeSuspend(failAllTests(tests, cause))
               }
-            } catch {
-              case p: ProvisioningException =>
-                val integrations = p.getSuppressed.collect { case i: IntegrationCheckException => i.failures }.toSeq
-                if (integrations.nonEmpty) {
-                  ignoreIntegrationCheckFailedTests(tests, integrations.flatten)
-                } else throw p
             }
         }
-    }
+      } catch {
+        case t: Throwable =>
+          // fail all tests (if an exception reaches here, it must have happened before the runtime was successfully produced)
+          failAllTests(tests, t)
+          reporter.onFailure(t)
+      }
+    } finally reporter.endAll()
   }
 
-  private def ifIntegChecksOk(integLocator: Locator)(testplans: Seq[DistageTest[F]], plans: TriSplittedPlan)(onSuccess: => F[Unit])(implicit F: DIEffect[F]): F[Unit] = {
+  protected def ifIntegChecksOk(integLocator: Locator)(testplans: Seq[DistageTest[F]], plans: TriSplittedPlan)(onSuccess: => F[Unit])(implicit F: DIEffect[F]): F[Unit] = {
     integrationChecker.collectFailures(plans.side.declaredRoots, integLocator).flatMap {
       case Left(failures) =>
         F.maybeSuspend(ignoreIntegrationCheckFailedTests(testplans, failures))
@@ -152,36 +165,46 @@ class DistageTestRunner[F[_]: TagK]
     }
   }
 
-  private def ignoreIntegrationCheckFailedTests(tests: Seq[DistageTest[F]], failures: Seq[ResourceCheck.Failure]): Unit = {
+  protected def ignoreIntegrationCheckFailedTests(tests: Seq[DistageTest[F]], failures: Seq[ResourceCheck.Failure]): Unit = {
     tests.foreach {
       test =>
         reporter.testStatus(test.meta, TestStatus.Ignored(failures))
     }
   }
 
-  private def proceed(appmodule: ModuleBase,
-                      checker: PlanCircularDependencyCheck,
-                      testplans: Seq[(DistageTest[F], OrderedPlan)],
-                      shared: TriSplittedPlan,
-                      parent: Locator,
-                     )(implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Unit] = {
+  protected def failAllTests(tests: Seq[DistageTest[F]], cause: Throwable): Unit = {
+    tests.foreach {
+      test =>
+        reporter.testStatus(test.meta, TestStatus.Failed(cause, Duration.Zero))
+    }
+  }
+
+  protected def proceed(appmodule: ModuleBase,
+                        checker: PlanCircularDependencyCheck,
+                        testplans: Seq[(DistageTest[F], OrderedPlan)],
+                        shared: TriSplittedPlan,
+                        parent: Locator,
+                       )(implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Unit] = {
     // here we produce our shared plan
     checker.verify(shared.primary)
+
     Injector.inherit(parent).produceF[F](shared.primary).use {
       mainSharedLocator =>
         val testInjector = Injector.inherit(mainSharedLocator)
 
+        val testsBySuite = testplans.groupBy {
+          t =>
+            val testId = t._1.meta.id
+            SuiteData(testId.suiteName, testId.suiteId, testId.suiteClassName)
+        }
         // now we are ready to run each individual test
         // note: scheduling here is custom also and tests may automatically run in parallel for any non-trivial monad
-        val tests = configuredTraverse_(testplans.groupBy {
-          t =>
-            val id = t._1.meta.id
-            SuiteData(id.suiteName, id.suiteId, id.suiteClassName)
-        }) {
-          case (id, plans) =>
-            for {
-              _ <- F.maybeSuspend(reporter.beginSuite(id))
-              _ <- configuredTraverse_(plans) {
+        configuredTraverse_(testsBySuite) {
+          case (suiteData, plans) =>
+            F.bracket(
+              acquire = F.maybeSuspend(reporter.beginSuite(suiteData))
+            )(release = _ => F.maybeSuspend(reporter.endSuite(suiteData))) { _ =>
+              configuredTraverse_(plans) {
                 case (test, testplan) =>
                   val allSharedKeys = mainSharedLocator.allInstances.map(_.key).toSet
 
@@ -205,58 +228,47 @@ class DistageTestRunner[F[_]: TagK]
                       }
                   }
               }
-              _ <- F.maybeSuspend(reporter.endSuite(id))
-            } yield ()
-        }
-
-        F.definitelyRecover(tests.flatMap(_ => F.maybeSuspend(reporter.endAll()))) {
-          f =>
-            F.maybeSuspend(reporter.onFailure(f)).flatMap(_ => F.fail(f))
+            }
         }
-
     }
   }
 
-  private def proceedIndividual(test: DistageTest[F], newtestplan: TriSplittedPlan, parent: Locator)(implicit F: DIEffect[F]): F[Unit] = {
-    Injector.inherit(parent)
-      .produceF[F](newtestplan.primary).use {
-      testLocator =>
-        def doRun(before: Long): F[Unit] = {
-          for {
-            _ <- F.definitelyRecover(testLocator.run(test.test).flatMap {
-              _ =>
-                F.maybeSuspend {
-                  val after = System.nanoTime()
-                  val testDuration = FiniteDuration(after - before, TimeUnit.NANOSECONDS)
-                  reporter.testStatus(test.meta, TestStatus.Succeed(testDuration))
-                }
-            }) {
-              case s if isTestSkipException(s) =>
-                F.maybeSuspend {
-                  val after = System.nanoTime()
-                  val testDuration = FiniteDuration(after - before, TimeUnit.NANOSECONDS)
-                  reporter.testStatus(test.meta, TestStatus.Cancelled(s.getMessage, testDuration))
-                }
-              case o =>
-                F.fail(o)
-            }
-          } yield ()
-        }
+  protected def proceedIndividual(test: DistageTest[F], newtestplan: TriSplittedPlan, parent: Locator)(implicit F: DIEffect[F]): F[Unit] = {
+    def testDuration(before: Option[Long]): FiniteDuration = {
+      before.fold(Duration.Zero) {
+        before =>
+          val after = System.nanoTime()
+          FiniteDuration(after - before, TimeUnit.NANOSECONDS)
+      }
+    }
+
+    def doRecover(before: Option[Long])(action: => F[Unit]): F[Unit] = {
+      F.definitelyRecoverCause(action) {
+        case (s, _) if isTestSkipException(s) =>
+          F.maybeSuspend {
+            reporter.testStatus(test.meta, TestStatus.Cancelled(s.getMessage, testDuration(before)))
+          }
+        case (_, cause) =>
+          F.maybeSuspend {
+            reporter.testStatus(test.meta, TestStatus.Failed(cause, testDuration(before)))
+          }
+      }
+    }
 
-        def doRecover(before: Long): Throwable => F[Unit] = {
-          // TODO: here we may also ignore individual tests
-          throwable =>
-            F.maybeSuspend {
-              val after = System.nanoTime()
-              reporter.testStatus(test.meta, TestStatus.Failed(throwable, FiniteDuration(after - before, TimeUnit.NANOSECONDS)))
+    doRecover(None) {
+      Injector.inherit(parent).produceF[F](newtestplan.primary).use {
+        testLocator =>
+          F.suspendF {
+            val before = System.nanoTime()
+            reporter.testStatus(test.meta, TestStatus.Running)
+
+            doRecover(Some(before)) {
+              testLocator
+                .run(test.test)
+                .flatMap(_ => F.maybeSuspend(reporter.testStatus(test.meta, TestStatus.Succeed(testDuration(Some(before))))))
             }
+          }
         }
-
-        for {
-          before <- F.maybeSuspend(System.nanoTime())
-          _ <- F.maybeSuspend(reporter.testStatus(test.meta, TestStatus.Running))
-          _ <- F.definitelyRecoverCause(doRun(before))(doRecover(before))
-        } yield ()
     }
   }
 
@@ -268,6 +280,17 @@ class DistageTestRunner[F[_]: TagK]
     }
   }
 
+  protected def configuredForeach[A](environments: Iterable[A])(f: A => Unit): Unit = {
+    if (parallelEnvs && environments.size > 1) {
+      val ec = ExecutionContext.fromExecutorService(null)
+      try {
+        DIEffectAsync.diEffectParIdentity.parTraverse_(environments)(f)
+      } finally ec.shutdown()
+    } else {
+      environments.foreach(f)
+    }
+  }
+
   lazy val debugLogger: TrivialLogger = TrivialLogger.make[DistageTestRunner[F]](DebugProperties.`izumi.distage.testkit.debug`)
 }
 
@@ -283,12 +306,10 @@ object DistageTestRunner {
 
   sealed trait TestStatus
   object TestStatus {
-    //    case object Scheduled extends TestStatus
     case object Running extends TestStatus
 
     sealed trait Done extends TestStatus
     final case class Ignored(checks: Seq[ResourceCheck.Failure]) extends Done
-
     sealed trait Finished extends Done
     final case class Cancelled(clue: String, duration: FiniteDuration) extends Finished
     final case class Succeed(duration: FiniteDuration) extends Finished
@@ -303,4 +324,12 @@ object DistageTestRunner {
     def testStatus(test: TestMeta, testStatus: TestStatus): Unit
   }
 
+  object ProvisioningIntegrationException {
+    def unapply(arg: ProvisioningException): Option[Seq[ResourceCheck.Failure]] = {
+      Some(arg.getSuppressed.collect { case i: IntegrationCheckException => i.failures }.toSeq)
+        .filter(_.nonEmpty)
+        .map(_.flatten)
+    }
+  }
+
 }
diff --git a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
index b135ea8b9..7121e0ba8 100644
--- a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
+++ b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
@@ -4,10 +4,8 @@ import distage.SafeType
 import izumi.distage.model.definition.Activation
 import izumi.distage.model.definition.StandardAxis._
 import izumi.distage.plugins.PluginConfig
-import izumi.distage.plugins.load.{PluginLoader, PluginLoaderDefaultImpl}
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.services.dstest.{DistageTestEnv, TestEnvironment}
-import izumi.fundamentals.platform.language.unused
 import izumi.fundamentals.reflection.Tags.TagK
 import izumi.logstage.api.IzLogger
 
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
index 9e899022a..e3f915d33 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
@@ -3,7 +3,6 @@ package izumi.distage.testkit.services.scalatest.dstest
 import distage.{Tag, TagK, TagKK}
 import izumi.distage.model.effect.DIEffect
 import izumi.distage.model.providers.ProviderMagnet
-import izumi.distage.plugins.load.PluginLoader
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{DistageTest, TestId, TestMeta}
 import izumi.distage.testkit.services.dstest._
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
index 08cd04ab5..190fd7fbb 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
@@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicBoolean
 
 import distage.{SafeType, TagK}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.DistageTest
+import izumi.functional.Value
 import izumi.fundamentals.platform.language.Quirks._
 import org.scalatest.{StatefulStatus, Tracker}
 
@@ -14,7 +15,7 @@ object DistageTestsRegistrySingleton {
   protected[this] type Fake[T]
   private[this] object Fake
   private[this] val registry = new mutable.HashMap[SafeType, mutable.ArrayBuffer[DistageTest[Fake]]]()
-  private[this] val statuses = new mutable.HashMap[SafeType, mutable.HashMap[String, StatefulStatus]]()
+  private[this] val statuses = new mutable.HashMap[SafeType, Option[mutable.HashMap[String, StatefulStatus]]]()
   private[this] val trackers = new mutable.HashMap[String, Either[mutable.ArrayBuffer[Tracker => Unit], Tracker]]()
   private[this] val runTracker = new ConcurrentHashMap[SafeType, Fake.type]()
   private[this] val knownSuites = new ConcurrentHashMap[(SafeType, String), Fake.type]()
@@ -31,7 +32,9 @@ object DistageTestsRegistrySingleton {
 
   def register[F[_]: TagK](t: DistageTest[F]): Unit = synchronized {
     if (registrationOpen.get()) {
-      registry.getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty).append(t.asInstanceOf[DistageTest[Fake]]).discard()
+      registry
+        .getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty)
+        .append(castTest(t))
     }
   }
 
@@ -45,24 +48,33 @@ object DistageTestsRegistrySingleton {
   }
 
   def registeredTests[F[_]: TagK]: Seq[DistageTest[F]] = synchronized {
-    registry.getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty).map(_.asInstanceOf[DistageTest[F]]).toSeq
+    val arr = registry.getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty)
+    castArray(arr).toSeq
   }
 
   def registerStatus[F[_]: TagK](suiteId: String): StatefulStatus = synchronized {
     statuses
-      .getOrElseUpdate(SafeType.getK[F], mutable.HashMap.empty)
-      .getOrElseUpdate(suiteId, new StatefulStatus)
+      .getOrElseUpdate(SafeType.getK[F], Some(mutable.HashMap.empty))
+      .fold {
+        // return completed test if the runner has already ran before this test got registered
+        Value(new StatefulStatus).eff(_.setCompleted()).get
+      } {
+        _.getOrElseUpdate(suiteId, new StatefulStatus)
+      }
   }
 
   def completeStatuses[F[_]: TagK](): Unit = synchronized {
-    statuses
-      .getOrElseUpdate(SafeType.getK[F], mutable.HashMap.empty)
-      .valuesIterator.foreach {
-        status =>
-          if (!status.isCompleted) {
-            status.setCompleted()
-          }
+    statuses.get(SafeType.getK[F])
+      .flatten
+      .foreach {
+        _.valuesIterator.foreach {
+          status =>
+            if (!status.isCompleted) {
+              status.setCompleted()
+            }
+        }
       }
+    statuses.put(SafeType.getK[F], None).discard()
   }
 
   def runReport(suiteId: String)(f: Tracker => Unit): Unit = synchronized {
@@ -82,4 +94,7 @@ object DistageTestsRegistrySingleton {
       case Right(_) =>
     }
   }
+
+  @inline private[this] def castTest[F[_]](t: DistageTest[F]): DistageTest[Fake] = t.asInstanceOf[DistageTest[Fake]]
+  @inline private[this] def castArray[C[_], F[_]](a: C[DistageTest[Fake]]): C[DistageTest[F]] = a.asInstanceOf[C[DistageTest[F]]]
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
index 91478f6aa..56ad0a58a 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
@@ -43,16 +43,19 @@ object ScalatestInitWorkaround {
     def doScan[F[_]](instance: DistageScalatestTestSuiteRunner[F]): Unit = {
       if (classpathScanned.compareAndSet(false, true)) {
         val classLoader = instance.getClass.getClassLoader
-        val nearest2Packages = instance.getClass.getPackage.getName.split('.').toSeq.inits.take(2).map(_.mkString(".")).toSeq
         val scan = new ClassGraph()
           .whitelistJars("distage-testkit-scalatest*")
-          .whitelistPackages(nearest2Packages: _*)
           .enableClassInfo()
           .addClassLoader(classLoader)
           .scan()
         try {
-          val specs = scan.getSubclasses(classOf[DistageScalatestTestSuiteRunner[Identity]].getCanonicalName).asScala.filterNot(_.isAbstract)
-          specs.foreach(spec => Try(spec.loadClass().getDeclaredConstructor().newInstance()))
+          val suiteClassName = classOf[DistageScalatestTestSuiteRunner[Identity]].getName
+          val testClasses = scan.getSubclasses(suiteClassName).asScala.filterNot(_.isAbstract)
+          lazy val debugLogger = TrivialLogger.make[ScalatestInitWorkaroundImpl.type](DebugProperties.`izumi.distage.testkit.debug`)
+          testClasses.foreach(classInfo => Try {
+            debugLogger.log(s"Added scanned class `${classInfo.getName}` to current test run")
+            classInfo.loadClass().getDeclaredConstructor().newInstance()
+          })
           DistageTestsRegistrySingleton.disableRegistration()
           latch.countDown()
         } finally {
@@ -60,7 +63,6 @@ object ScalatestInitWorkaround {
         }
       }
     }
-
   }
 
 }
@@ -195,7 +197,7 @@ abstract class DistageScalatestTestSuiteRunner[F[_]](implicit override val tagMo
         val runner = {
           val logger = IzLogger(Log.Level.Debug)("phase" -> "test")
           val checker = new IntegrationChecker.Impl(logger)
-          new DistageTestRunner[F](testReporter, checker, specEnv, toRun, _.isInstanceOf[TestCanceledException], parallelTestsAlways)
+          new DistageTestRunner[F](testReporter, checker, specEnv, toRun, _.isInstanceOf[TestCanceledException], parallelExecution, parallelEnvExecution)
         }
         runner.run()
       }
@@ -204,7 +206,8 @@ abstract class DistageScalatestTestSuiteRunner[F[_]](implicit override val tagMo
     }
   }
 
-  protected def parallelTestsAlways: Boolean = true
+  protected def parallelExecution: Boolean = true
+  protected def parallelEnvExecution: Boolean = true
 
   private def mkTestReporter(args: Args): TestReporter = {
     val scalatestReporter = new ScalatestReporter(args.reporter)
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
index 707e3407f..b973ef0e9 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
@@ -1,15 +1,14 @@
 package org.scalatest
 
-import izumi.distage.testkit.services.dstest.DistageTestRunner.TestStatus.{Done, Finished, Ignored, Running}
+import izumi.distage.testkit.services.dstest.DistageTestRunner.TestStatus.{Done, Running}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{SuiteData, TestMeta, TestReporter, TestStatus}
-import izumi.fundamentals.platform.language.Quirks._
 import izumi.fundamentals.platform.language.unused
 
 import scala.collection.mutable
 
 class SafeTestReporter(underlying: TestReporter) extends TestReporter {
   private val delayedReports = new mutable.LinkedHashMap[TestMeta, TestStatus]()
-  private var runningTest: Option[TestMeta] = None
+  private val runningTests = new mutable.HashMap[String, TestMeta]()
 
   override def onFailure(f: Throwable): Unit = synchronized {
     endAll()
@@ -17,7 +16,7 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
   }
 
   override def endAll(): Unit = synchronized {
-    finish()
+    finish(_ => true)
     underlying.endAll()
   }
 
@@ -27,35 +26,32 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
 
   override def testStatus(test: TestMeta, testStatus: TestStatus): Unit = synchronized {
     testStatus match {
-//      case TestStatus.Scheduled => reportStatus(test, testStatus)
-
       case TestStatus.Running =>
-        runningTest match {
+        runningTests.get(test.id.suiteId) match {
           case Some(_) =>
+            // a test is running in this suite, delay new test
             delayedReports.put(test, testStatus)
           case None =>
+            // no test is running in this suite, report this one and set it as running
             reportStatus(test, testStatus)
-            runningTest = Some(test)
+            runningTests(test.id.suiteId) = test
         }
-
-      case _: Ignored =>
-        runningTest match {
-          case None =>
-            reportStatus(test, TestStatus.Running)
+      case _: Done =>
+        runningTests.get(test.id.suiteId) match {
+          case Some(value) if value == test =>
+            // finished current running test in suite,
+            // report delayed reports in suite &
+            // switch to another currently running test in suite if exists
+            runningTests.remove(test.id.suiteId)
             reportStatus(test, testStatus)
+            finish(_.id.suiteId == test.id.suiteId)
           case Some(_) =>
-            // Ignores happen before running, so an ignored test cannot be the same as `signalled`
-            delayedReports.put(test, testStatus)
-        }
-
-      case _: Finished =>
-        runningTest match {
-          case Some(value) if value == test =>
-            finishTest(test, testStatus)
-            runningTest = None
-            finish()
-          case _ =>
+            // another test is running in this suite, delay finish report
             delayedReports.put(test, testStatus)
+          case None =>
+            // no other test is running in this suite, report directly
+            reportStatus(test, TestStatus.Running)
+            reportStatus(test, testStatus)
         }
     }
     ()
@@ -65,26 +61,24 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
     underlying.testStatus(test, testStatus)
   }
 
-  private def finish(): Unit = {
-    val finishedTests = delayedReports.collect { case (t, s: Done) => (t, s) }
+  private def finish(predicate: TestMeta => Boolean): Unit = synchronized {
+    val finishedTests = delayedReports.collect { case (t, s: Done) if predicate(t) => (t, s) }
 
     // report all finished tests
     finishedTests
       .foreach {
         case (t, s) =>
           reportStatus(t, TestStatus.Running)
-          finishTest(t, s)
+          reportStatus(t, s)
+          delayedReports.remove(t)
       }
     // switch to another test if it's already running
-    delayedReports.collectFirst {
-      case (t, Running) =>
+    delayedReports.toList.foreach {
+      case (t, Running) if predicate(t) && !runningTests.contains(t.id.suiteId) =>
+        runningTests(t.id.suiteId) = t
         reportStatus(t, TestStatus.Running)
-        runningTest = Some(t)
-    }.discard()
-  }
-
-  private def finishTest(test: TestMeta, testStatus: TestStatus): Option[TestStatus] = {
-    reportStatus(test, testStatus)
-    delayedReports.remove(test)
+        delayedReports.remove(t)
+      case _ =>
+    }
   }
 }
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala
index a9a2159c8..7dcba0d6e 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala
@@ -65,10 +65,14 @@ class Http4sTransportTest extends AnyWordSpec {
         assert(BIOR.unsafeRun(greeterClient.greet("John", "Smith")) == "Hi, John Smith!")
         assert(BIOR.unsafeRun(greeterClient.alternative()) == "value")
 
-        ioService.wsSessionStorage.buzzersFor("user").foreach {
-          buzzer =>
-            val client = new GreeterServiceClientWrapped(buzzer)
-            assert(BIOR.unsafeRun(client.greet("John", "Buzzer")) == "Hi, John Buzzer!")
+        BIOR.unsafeRunSyncAsEither(ioService.wsSessionStorage.buzzersFor("user")) match {
+          case Success(buzzers) =>
+            buzzers.foreach {
+              buzzer =>
+                val client = new GreeterServiceClientWrapped(buzzer)
+                assert(BIOR.unsafeRun(client.greet("John", "Buzzer")) == "Hi, John Buzzer!")
+            }
+          case v => fail(s"Expected success result but got $v")
         }
 
         disp.setupCredentials("user", "badpass")
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala
index 9c4c83b89..dd84ba2fe 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala
@@ -114,9 +114,7 @@ object Http4sTestContext {
     AuthMiddleware(authUser),
     wsContextProvider,
     storage,
-    Seq(
-      WsSessionListener.empty[String]
-    ),
+    Seq(WsSessionListener.empty[rt.BiIO, String]),
     RT.logger,
     RT.printer,
   )