From 837aaa14e5d4815bd8ef76b4fe0267f39cbee210 Mon Sep 17 00:00:00 2001 From: Zachary Albia Date: Tue, 4 Oct 2022 12:13:39 +0800 Subject: [PATCH 01/18] ZIO 2 migration (#154) Co-authored-by: Peter Kotula --- build.sbt | 27 +- .../zio/webhooks/example/BasicExample.scala | 32 +- .../example/BasicExampleWithBatching.scala | 32 +- .../example/BasicExampleWithRetrying.scala | 53 +- .../example/ComprehensiveExample.scala | 104 +- .../example/CustomConfigExample.scala | 51 +- .../example/EventRecoveryExample.scala | 140 +- .../example/ManualServerExample.scala | 31 +- .../example/ShutdownOnFatalError.scala | 33 +- project/plugins.sbt | 1 + .../WebhookServerIntegrationSpec.scala | 265 +-- .../zio/webhooks/WebhookServerSpec.scala | 1684 +++++++++-------- .../testkit/TestWebhookEventRepo.scala | 45 +- .../testkit/TestWebhookHttpClient.scala | 22 +- .../webhooks/testkit/TestWebhookRepo.scala | 38 +- .../testkit/TestWebhookStateRepo.scala | 3 +- .../scala/zio/webhooks/WebhookEventRepo.scala | 2 +- .../scala/zio/webhooks/WebhookServer.scala | 124 +- .../zio/webhooks/WebhookServerConfig.scala | 7 +- .../scala/zio/webhooks/WebhookStateRepo.scala | 8 +- .../scala/zio/webhooks/WebhooksProxy.scala | 19 +- .../backends/InMemoryWebhookStateRepo.scala | 4 +- .../backends/JsonPayloadSerialization.scala | 4 +- .../backends/sttp/WebhookSttpClient.scala | 19 +- .../webhooks/internal/BatchDispatcher.scala | 12 +- .../zio/webhooks/internal/DequeueUtils.scala | 75 + .../webhooks/internal/RetryController.scala | 98 +- .../webhooks/internal/RetryDispatcher.scala | 24 +- .../zio/webhooks/internal/RetryState.scala | 4 +- .../src/main/scala/zio/webhooks/package.scala | 4 +- 30 files changed, 1526 insertions(+), 1439 deletions(-) create mode 100644 webhooks/src/main/scala/zio/webhooks/internal/DequeueUtils.scala diff --git a/build.sbt b/build.sbt index 14ad236a..faffdab6 100755 --- a/build.sbt +++ b/build.sbt @@ -31,11 +31,10 @@ inThisBuild( addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "1.0.16" -val zioHttpVersion = "1.0.0.0-RC17" -val zioJson = "0.1.5" -val zioMagicVersion = "0.3.12" -val zioPreludeVersion = "1.0.0-RC8-1" +val zioVersion = "2.0.2" +val zioHttpVersion = "2.0.0-RC11" +val zioJson = "0.3.0" +val zioPreludeVersion = "1.0.0-RC15" val sttpVersion = "3.8.0" lazy val `zio-webhooks` = @@ -55,7 +54,7 @@ lazy val zioWebhooksCore = module("zio-webhooks-core", "webhooks") "dev.zio" %% "zio-streams" % zioVersion, "dev.zio" %% "zio-test" % zioVersion, "com.softwaremill.sttp.client3" %% "core" % sttpVersion, - "com.softwaremill.sttp.client3" %% "zio1" % sttpVersion + "com.softwaremill.sttp.client3" %% "zio" % sttpVersion ) ) .settings( @@ -68,11 +67,10 @@ lazy val zioWebhooksTest = module("zio-webhooks-test", "webhooks-test") Defaults.itSettings, publish / skip := true, libraryDependencies ++= Seq( - "dev.zio" %% "zio-test" % zioVersion % "it,test", - "dev.zio" %% "zio-test-sbt" % zioVersion % "it,test", - "dev.zio" %% "zio-json" % zioJson % "it", - "io.github.kitlangton" %% "zio-magic" % zioMagicVersion % "it,test", - "io.d11" %% "zhttp" % zioHttpVersion % "it" + "dev.zio" %% "zio-test" % zioVersion % "it,test", + "dev.zio" %% "zio-test-sbt" % zioVersion % "it,test", + "dev.zio" %% "zio-json" % zioJson % "it", + "io.d11" %% "zhttp" % zioHttpVersion % "it" ), testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) ) @@ -93,10 +91,9 @@ lazy val examples = module("zio-webhooks-examples", "examples") publish / skip := true, fork := true, libraryDependencies ++= Seq( - "dev.zio" %% "zio-test" % zioVersion % "test", - "dev.zio" %% "zio-test-sbt" % zioVersion % "test", - "io.d11" %% "zhttp" % zioHttpVersion, - "io.github.kitlangton" %% "zio-magic" % zioMagicVersion + "dev.zio" %% "zio-test" % zioVersion % "test", + "dev.zio" %% "zio-test-sbt" % zioVersion % "test", + "io.d11" %% "zhttp" % zioHttpVersion ), testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) ) diff --git a/examples/src/main/scala/zio/webhooks/example/BasicExample.scala b/examples/src/main/scala/zio/webhooks/example/BasicExample.scala index 6a4bd2fc..f22ca686 100644 --- a/examples/src/main/scala/zio/webhooks/example/BasicExample.scala +++ b/examples/src/main/scala/zio/webhooks/example/BasicExample.scala @@ -3,14 +3,13 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ import zio.webhooks.{ WebhooksProxy, _ } +import zio.{ Random, ZIOAppDefault } +import zio.Console.{ printLine, printLineError } /** * Runs a webhook server and a zio-http server to which webhook events are delivered. The webhook @@ -20,10 +19,10 @@ import zio.webhooks.{ WebhooksProxy, _ } * and the events are delivered to an endpoint one-by-one. The zio-http endpoint prints out the * contents of each payload as it receives them. */ -object BasicExample extends App { +object BasicExample extends ZIOAppDefault { // JSON webhook event stream - private lazy val events = UStream + private lazy val events = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -34,15 +33,16 @@ object BasicExample extends App { None ) } + .schedule(Schedule.spaced(100.milli)) // reliable endpoint - private val httpApp = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => + private val httpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => for { - randomDelay <- random.nextIntBounded(300).map(_.millis) - response <- ZIO - .foreach(request.getBodyAsString)(str => putStrLn(s"""SERVER RECEIVED PAYLOAD: "$str"""")) - .as(Response.status(Status.OK)) + randomDelay <- Random.nextIntBounded(300).map(_.millis) + response <- request.body.asString + .flatMap(str => printLine(s"""SERVER RECEIVED PAYLOAD: "$str"""")) + .as(Response.status(Status.Ok)) .delay(randomDelay) // random delay to simulate latency } yield response } @@ -54,8 +54,8 @@ object BasicExample extends App { private def program = for { - _ <- httpEndpointServer.start(port, httpApp).fork - _ <- WebhookServer.getErrors.use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))).fork + _ <- httpEndpointServer.start(port, httpApp).forkScoped + _ <- WebhookServer.getErrors.flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))).forkScoped _ <- TestWebhookRepo.setWebhook(webhook) _ <- events.schedule(Schedule.spaced(50.micros).jittered).foreach(TestWebhookEventRepo.createEvent) } yield () @@ -63,9 +63,9 @@ object BasicExample extends App { /** * The webhook server is started as part of the layer construction. See `WebhookServer.live`. */ - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provideSome[Scope]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookEventRepo.test, diff --git a/examples/src/main/scala/zio/webhooks/example/BasicExampleWithBatching.scala b/examples/src/main/scala/zio/webhooks/example/BasicExampleWithBatching.scala index f3311e17..75bc293f 100644 --- a/examples/src/main/scala/zio/webhooks/example/BasicExampleWithBatching.scala +++ b/examples/src/main/scala/zio/webhooks/example/BasicExampleWithBatching.scala @@ -3,23 +3,22 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.{ WebhooksProxy, _ } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ +import zio.{ Random, ZIOAppDefault } +import zio.Console.{ printLine, printLineError } /** * Differs from the [[BasicExample]] in that events are batched with the default batching setting * of 128 elements per batch. The server dispatches all events queued up for each webhook since the * last delivery and sends them in a batch. */ -object BasicExampleWithBatching extends App { +object BasicExampleWithBatching extends ZIOAppDefault { - private lazy val events = UStream + private lazy val events = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -31,15 +30,14 @@ object BasicExampleWithBatching extends App { ) } - private val httpApp = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => + private val httpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => for { - randomDelay <- random.nextIntBetween(10, 20).map(_.millis) - response <- ZIO - .foreach(request.getBodyAsString) { str => - putStrLn(s"""SERVER RECEIVED PAYLOAD: "$str"""") - } - .as(Response.status(Status.OK)) + randomDelay <- Random.nextIntBetween(10, 20).map(_.millis) + response <- request.body.asString.flatMap { str => + printLine(s"""SERVER RECEIVED PAYLOAD: "$str"""") + } + .as(Response.status(Status.Ok)) .delay(randomDelay) } yield response } @@ -52,14 +50,14 @@ object BasicExampleWithBatching extends App { private def program = for { _ <- httpEndpointServer.start(port, httpApp).fork - _ <- WebhookServer.getErrors.use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))).fork + _ <- WebhookServer.getErrors.flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))).fork _ <- TestWebhookRepo.setWebhook(webhook) _ <- events.schedule(Schedule.spaced(50.micros).jittered).foreach(TestWebhookEventRepo.createEvent) } yield () - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provideSome[Scope]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookRepo.test, diff --git a/examples/src/main/scala/zio/webhooks/example/BasicExampleWithRetrying.scala b/examples/src/main/scala/zio/webhooks/example/BasicExampleWithRetrying.scala index a634308f..652fb324 100644 --- a/examples/src/main/scala/zio/webhooks/example/BasicExampleWithRetrying.scala +++ b/examples/src/main/scala/zio/webhooks/example/BasicExampleWithRetrying.scala @@ -3,14 +3,13 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.{ WebhooksProxy, _ } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ +import zio.{ Clock, Random, ZIOAppDefault } +import zio.Console.{ printLine, printLineError } /** * Differs from the [[BasicExample]] in that the zio-http server responds with a non-200 status some @@ -18,9 +17,9 @@ import zio.webhooks.testkit._ * retrying events for a webhook with at-least-once delivery semantics one-by-one until the server * successfully marks all `n` events delivered. */ -object BasicExampleWithRetrying extends App { +object BasicExampleWithRetrying extends ZIOAppDefault { - private lazy val events = UStream + private lazy val events = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -34,23 +33,20 @@ object BasicExampleWithRetrying extends App { .take(n) // a flaky server answers with 200 60% of the time, 404 the other - private lazy val httpApp = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => - val payload = request.getBodyAsString + private lazy val httpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => for { - n <- random.nextIntBounded(100) - tsString <- clock.instant.map(_.toString).map(ts => s"[$ts]: ") - response <- ZIO - .foreach(payload) { payload => - if (n < 60) - putStrLn(tsString + payload + " Response: OK") *> - UIO(Response.status(Status.OK)) - else - putStrLn(tsString + payload + " Response: NOT_FOUND") *> - UIO(Response.status(Status.NOT_FOUND)) - } - .orDie - } yield response.getOrElse(Response.fromHttpError(HttpError.BadRequest("empty body"))) + n <- Random.nextIntBounded(100) + tsString <- Clock.instant.map(_.toString).map(ts => s"[$ts]: ") + response <- request.body.asString.flatMap { payload => + if (n < 60) + printLine(tsString + payload + " Response: Ok") *> + ZIO.succeed(Response.status(Status.Ok)) + else + printLine(tsString + payload + " Response: NotFound") *> + ZIO.succeed(Response.status(Status.NotFound)) + }.orDie + } yield response } // just an alias for a zio-http server to disambiguate it with the webhook server @@ -62,16 +58,19 @@ object BasicExampleWithRetrying extends App { private def program = for { - _ <- httpEndpointServer.start(port, httpApp).fork - _ <- WebhookServer.getErrors.use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))).fork + _ <- printLine("starting http") + _ <- httpEndpointServer.start(port, httpApp).forkScoped + _ <- WebhookServer.getErrors.flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))).forkScoped + _ <- printLine("set webhook") _ <- TestWebhookRepo.setWebhook(webhook) + _ <- printLine("scheduling events") _ <- events.schedule(Schedule.spaced(50.micros).jittered).foreach(TestWebhookEventRepo.createEvent) - _ <- clock.sleep(Duration.Infinity) + _ <- Clock.sleep(Duration.Infinity) } yield () - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provideSome[Scope]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookRepo.test, diff --git a/examples/src/main/scala/zio/webhooks/example/ComprehensiveExample.scala b/examples/src/main/scala/zio/webhooks/example/ComprehensiveExample.scala index 96bc6eab..76360f90 100644 --- a/examples/src/main/scala/zio/webhooks/example/ComprehensiveExample.scala +++ b/examples/src/main/scala/zio/webhooks/example/ComprehensiveExample.scala @@ -3,10 +3,6 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.random.Random import zio.stream.{ UStream, ZStream } import zio.webhooks._ import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } @@ -15,17 +11,19 @@ import zio.webhooks.example.RestartingWebhookServer.testWebhooks import zio.webhooks.testkit._ import java.io.IOException +import zio.{ Clock, Random, ZIOAppDefault } +import zio.Console.{ printLine, printLineError } /** * Runs an example that simulates a comprehensive suite of scenarios that may occur during the * operation of a webhook server. */ -object ComprehensiveExample extends App { +object ComprehensiveExample extends ZIOAppDefault { - def events: ZStream[Random, Nothing, WebhookEvent] = - UStream + def events: UStream[WebhookEvent] = + ZStream .iterate(0L)(_ + 1) - .zip(UStream.repeatEffect(random.nextIntBetween(0, 1000))) + .zip(ZStream.repeatZIO(Random.nextIntBetween(0, 1000))) .map { case (i, webhookId) => WebhookEvent( @@ -39,16 +37,16 @@ object ComprehensiveExample extends App { private def program = for { - _ <- ZIO.foreach_(testWebhooks)(TestWebhookRepo.setWebhook) + _ <- ZIO.foreachDiscard(testWebhooks)(TestWebhookRepo.setWebhook) _ <- RestartingWebhookServer.start.fork _ <- RandomEndpointBehavior.run.fork _ <- events.schedule(Schedule.spaced(25.micros).jittered).foreach(TestWebhookEventRepo.createEvent) - _ <- clock.sleep(Duration.Infinity) + _ <- Clock.sleep(Duration.Infinity) } yield () - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provide( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookEventRepo.test, @@ -64,7 +62,7 @@ object ComprehensiveExample extends App { sealed trait RandomEndpointBehavior extends Product with Serializable { self => import RandomEndpointBehavior._ - def start: ZIO[ZEnv, Throwable, Any] = + def start: ZIO[Any, Throwable, Any] = self match { case RandomEndpointBehavior.Down => ZIO.unit @@ -80,41 +78,37 @@ object RandomEndpointBehavior { case object Flaky extends RandomEndpointBehavior case object Normal extends RandomEndpointBehavior - val flakyBehavior = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" / id => - val payload = request.getBodyAsString + val flakyBehavior: UHttpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" / id => val response = for { - n <- random.nextIntBounded(100) - timeString <- clock.instant.map(_.toString).map(ts => s"[$ts]: ") - randomDelay <- random.nextIntBounded(200).map(_.millis) - response <- ZIO - .foreach(payload) { payload => - val line = s"$timeString webhook $id $payload" - if (n < 60) - putStrLn(line + " Response: OK") *> UIO(Response.status(Status.OK)) - else - putStrLn(line + " Response: NOT_FOUND") *> UIO(Response.status(Status.NOT_FOUND)) - } - .orDie + n <- Random.nextIntBounded(100) + timeString <- Clock.instant.map(_.toString).map(ts => s"[$ts]: ") + randomDelay <- Random.nextIntBounded(200).map(_.millis) + response <- request.body.asString.flatMap { payload => + val line = s"$timeString webhook $id $payload" + if (n < 60) + printLine(line + " Response: Ok") *> ZIO.succeed(Response.status(Status.Ok)) + else + printLine(line + " Response: NotFound") *> ZIO.succeed(Response.status(Status.NotFound)) + }.orDie .delay(randomDelay) - } yield response.getOrElse(Response.fromHttpError(HttpError.BadRequest("empty body"))) + } yield response response.uninterruptible } // just an alias for a zio-http server to tell it apart from the webhook server lazy val httpEndpointServer: Server.type = Server - val normalBehavior = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" / id => + val normalBehavior = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" / id => val response = for { - randomDelay <- random.nextIntBounded(200).map(_.millis) - response <- ZIO - .foreach(request.getBodyAsString) { str => - putStrLn(s"""SERVER RECEIVED PAYLOAD: webhook: $id $str OK""") - } - .as(Response.status(Status.OK)) + randomDelay <- Random.nextIntBounded(200).map(_.millis) + response <- request.body.asString.flatMap { str => + printLine(s"""SERVER RECEIVED PAYLOAD: webhook: $id $str Ok""") + } + .as(Response.status(Status.Ok)) .orDie .delay(randomDelay) } yield response @@ -123,17 +117,17 @@ object RandomEndpointBehavior { private lazy val port = 8080 - val randomBehavior: URIO[Random, RandomEndpointBehavior] = - random.nextIntBounded(3).map { + val randomBehavior: UIO[RandomEndpointBehavior] = + Random.nextIntBounded(3).map { case 0 => Normal case 1 => Flaky case _ => Down } - def run: ZIO[ZEnv, IOException, Unit] = - UStream.repeatEffect(randomBehavior).foreach { behavior => + def run: ZIO[Any, IOException, Unit] = + ZStream.repeatZIO(randomBehavior).foreach { behavior => for { - _ <- putStrLn(s"Endpoint server behavior: $behavior") + _ <- printLine(s"Endpoint server behavior: $behavior") f <- behavior.start.fork.delay(2.seconds) _ <- f.interrupt.delay(1.minute) } yield () @@ -149,19 +143,21 @@ object RestartingWebhookServer { private def runServerThenShutdown = for { - _ <- putStrLn("Server starting") - _ <- WebhookServer.start.use { server => - for { - _ <- putStrLn("Server started") - f <- server.subscribeToErrors - .use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))) - .fork - _ <- TestWebhookEventRepo.enqueueNew - duration <- random.nextIntBetween(3000, 5000).map(_.millis) - _ <- f.interrupt.delay(duration) - } yield () + _ <- printLine("Server starting") + _ <- ZIO.scoped { + WebhookServer.start.flatMap { server => + for { + _ <- printLine("Server started") + f <- server.subscribeToErrors + .flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))) + .fork + _ <- TestWebhookEventRepo.enqueueNew + duration <- Random.nextIntBetween(3000, 5000).map(_.millis) + _ <- f.interrupt.delay(duration) + } yield () + } } - _ <- putStrLn("Server shut down") + _ <- printLine("Server shut down") } yield () lazy val testWebhooks = (0 until 250).map { i => diff --git a/examples/src/main/scala/zio/webhooks/example/CustomConfigExample.scala b/examples/src/main/scala/zio/webhooks/example/CustomConfigExample.scala index cd578192..b334c22f 100644 --- a/examples/src/main/scala/zio/webhooks/example/CustomConfigExample.scala +++ b/examples/src/main/scala/zio/webhooks/example/CustomConfigExample.scala @@ -3,14 +3,13 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.{ WebhooksProxy, _ } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ +import zio.{ Clock, Random, ZIOAppDefault } +import zio.Console.{ printLine, printLineError } /** * Differs from the [[BasicExampleWithRetrying]] in that a custom configuration is provided. @@ -18,9 +17,9 @@ import zio.webhooks.testkit._ * batches when delivery fails. A max retry backoff of 2 seconds should be seen when running this * example. */ -object CustomConfigExample extends App { +object CustomConfigExample extends ZIOAppDefault { - private lazy val customConfig: ULayer[Has[WebhookServerConfig]] = + private lazy val customConfig: ULayer[WebhookServerConfig] = ZLayer.succeed( WebhookServerConfig( errorSlidingCapacity = 64, @@ -37,7 +36,7 @@ object CustomConfigExample extends App { ) ) - private lazy val events = UStream + private lazy val events = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -51,23 +50,20 @@ object CustomConfigExample extends App { .take(n) // server answers with 200 40% of the time, 404 the other - private lazy val httpApp = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => - val payload = request.getBodyAsString + private lazy val httpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => for { - n <- random.nextIntBounded(100) - tsString <- clock.instant.map(_.toString).map(ts => s"[$ts]: ") - response <- ZIO - .foreach(payload) { payload => - if (n < 40) - putStrLn(tsString + payload + " Response: OK") *> - UIO(Response.status(Status.OK)) - else - putStrLn(tsString + payload + " Response: NOT_FOUND") *> - UIO(Response.status(Status.NOT_FOUND)) - } - .orDie - } yield response.getOrElse(Response.fromHttpError(HttpError.BadRequest("empty body"))) + n <- Random.nextIntBounded(100) + tsString <- Clock.instant.map(_.toString).map(ts => s"[$ts]: ") + response <- request.body.asString.flatMap { payload => + if (n < 40) + printLine(tsString + payload + " Response: Ok") *> + ZIO.succeed(Response.status(Status.Ok)) + else + printLine(tsString + payload + " Response: NotFound") *> + ZIO.succeed(Response.status(Status.NotFound)) + }.orDie + } yield response } // just an alias for a zio-http server to disambiguate it with the webhook server @@ -80,15 +76,15 @@ object CustomConfigExample extends App { private def program = for { _ <- httpEndpointServer.start(port, httpApp).fork - _ <- WebhookServer.getErrors.use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))).fork + _ <- WebhookServer.getErrors.flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))).fork _ <- TestWebhookRepo.setWebhook(webhook) _ <- events.schedule(Schedule.spaced(50.micros).jittered).foreach(TestWebhookEventRepo.createEvent) - _ <- zio.clock.sleep(Duration.Infinity) + _ <- zio.Clock.sleep(Duration.Infinity) } yield () - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provideSome[Scope]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookRepo.test, @@ -109,4 +105,5 @@ object CustomConfigExample extends App { WebhookDeliveryMode.BatchedAtLeastOnce, None ) + } diff --git a/examples/src/main/scala/zio/webhooks/example/EventRecoveryExample.scala b/examples/src/main/scala/zio/webhooks/example/EventRecoveryExample.scala index 84fc4ca9..9be30a2f 100644 --- a/examples/src/main/scala/zio/webhooks/example/EventRecoveryExample.scala +++ b/examples/src/main/scala/zio/webhooks/example/EventRecoveryExample.scala @@ -3,14 +3,13 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ import zio.webhooks.{ WebhooksProxy, _ } +import zio.{ Clock, Random, ZIOAppDefault } +import zio.Console.{ printLine, printLineError } /** * An example of a webhook server performing event recovery on restart for a webhook with @@ -19,9 +18,9 @@ import zio.webhooks.{ WebhooksProxy, _ } * Events that haven't been marked delivered prior to shutdown are retried on restart. All `n` * events are eventually delivered. */ -object EventRecoveryExample extends App { +object EventRecoveryExample extends ZIOAppDefault { - private lazy val events = UStream + private lazy val events = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -36,29 +35,26 @@ object EventRecoveryExample extends App { // server answers with 200 70% of the time, 404 the other private def httpApp(payloads: Ref[Set[String]]) = - HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => - val payload = request.getBodyAsString + Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => for { - n <- random.nextIntBounded(100) - tsString <- clock.instant.map(_.toString).map(ts => s"[$ts]") - response <- ZIO - .foreach(payload) { payload => - if (n < 70) - for { - newSize <- payloads.modify { set => - val newSet = set + payload - (newSet.size, newSet) - } - line = s"$tsString: $payload Response: OK, events delivered: $newSize" - _ <- putStrLn(line) - } yield Response.status(Status.OK) - else - putStrLn(s"$tsString: $payload Response: NOT_FOUND") *> - UIO(Response.status(Status.NOT_FOUND)) - } - .orDie - } yield response.getOrElse(Response.fromHttpError(HttpError.BadRequest("empty body"))) + n <- Random.nextIntBounded(100) + tsString <- Clock.instant.map(_.toString).map(ts => s"[$ts]") + response <- request.body.asString.flatMap { payload => + if (n < 70) + for { + newSize <- payloads.modify { set => + val newSet = set + payload + (newSet.size, newSet) + } + line = s"$tsString: $payload Response: Ok, events delivered: $newSize" + _ <- printLine(line) + } yield Response.status(Status.Ok) + else + printLine(s"$tsString: $payload Response: NotFound") *> + ZIO.succeed(Response.status(Status.NotFound)) + }.orDie + } yield response } // just an alias for a zio-http server to disambiguate it with the webhook server @@ -70,53 +66,59 @@ object EventRecoveryExample extends App { private def program = for { - _ <- WebhookServer.start.use { server => - for { - _ <- server.subscribeToErrors - .use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))) - .fork - payloads <- Ref.make(Set.empty[String]) - _ <- httpEndpointServer.start(port, httpApp(payloads)).fork - _ <- TestWebhookRepo.setWebhook(webhook) - _ <- events - .take(n / 3) - .schedule(Schedule.spaced(50.micros)) - .foreach(TestWebhookEventRepo.createEvent) - } yield () + _ <- ZIO.scoped { + WebhookServer.start.flatMap { server => + for { + _ <- server.subscribeToErrors + .flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))) + .fork + payloads <- Ref.make(Set.empty[String]) + _ <- httpEndpointServer.start(port, httpApp(payloads)).fork + _ <- TestWebhookRepo.setWebhook(webhook) + _ <- events + .take(n / 3) + .schedule(Schedule.spaced(50.micros)) + .foreach(TestWebhookEventRepo.createEvent) + } yield () + } } - _ <- putStrLn("Shutdown successful") - _ <- WebhookServer.start.use { server => - for { - _ <- server.subscribeToErrors - .use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))) - .fork - _ <- putStrLn("Restart successful") - _ <- events - .drop(n / 3) - .take(n / 3) - .schedule(Schedule.spaced(50.micros)) - .foreach(TestWebhookEventRepo.createEvent) - } yield () + _ <- printLine("Shutdown successful") + _ <- ZIO.scoped { + WebhookServer.start.flatMap { server => + for { + _ <- server.subscribeToErrors + .flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))) + .fork + _ <- printLine("Restart successful") + _ <- events + .drop(n.toInt / 3) + .take(n / 3) + .schedule(Schedule.spaced(50.micros)) + .foreach(TestWebhookEventRepo.createEvent(_)) + } yield () + } } - _ <- putStrLn("Shutdown successful") - _ <- WebhookServer.start.use { server => - for { - _ <- server.subscribeToErrors - .use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))) - .fork - _ <- putStrLn("Restart successful") - _ <- events - .drop(2 * n / 3) - .schedule(Schedule.spaced(50.micros)) - .foreach(TestWebhookEventRepo.createEvent) - _ <- clock.sleep(Duration.Infinity) - } yield () + _ <- printLine("Shutdown successful") + _ <- ZIO.scoped { + WebhookServer.start.flatMap { server => + for { + _ <- server.subscribeToErrors + .flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))) + .fork + _ <- printLine("Restart successful") + _ <- events + .drop(2 * n.toInt / 3) + .schedule(Schedule.spaced(50.micros)) + .foreach(TestWebhookEventRepo.createEvent(_)) + _ <- Clock.sleep(Duration.Infinity) + } yield () + } } } yield () - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provide( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookRepo.test, diff --git a/examples/src/main/scala/zio/webhooks/example/ManualServerExample.scala b/examples/src/main/scala/zio/webhooks/example/ManualServerExample.scala index 50b0c932..77629854 100644 --- a/examples/src/main/scala/zio/webhooks/example/ManualServerExample.scala +++ b/examples/src/main/scala/zio/webhooks/example/ManualServerExample.scala @@ -3,24 +3,23 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ import zio.webhooks.{ WebhooksProxy, _ } +import zio.ZIOAppDefault +import zio.Console.{ printLine, printLineError } /** * An example of manually starting a server. The server is shut down as its release action, * releasing its dependencies as well. Other than that, this is the same scenario as in the * [[BasicExample]]. */ -object ManualServerExample extends App { +object ManualServerExample extends ZIOAppDefault { // JSON webhook event stream - private lazy val events = UStream + private lazy val events = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -32,11 +31,11 @@ object ManualServerExample extends App { ) } - private val httpApp = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => - ZIO - .foreach(request.getBodyAsString)(str => putStrLn(s"""SERVER RECEIVED PAYLOAD: "$str"""")) - .as(Response.status(Status.OK)) + private val httpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => + request.body.asString + .flatMap(str => printLine(s"""SERVER RECEIVED PAYLOAD: "$str"""")) + .as(Response.status(Status.Ok)) } // just an alias for a zio-http server to disambiguate it with the webhook server @@ -47,18 +46,18 @@ object ManualServerExample extends App { // Server is created and shut down manually. On shutdown, all existing work is finished before // the example finishes. private def program = - WebhookServer.start.use { server => + WebhookServer.start.flatMap { server => for { - _ <- server.subscribeToErrors.use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))).fork + _ <- server.subscribeToErrors.flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printLineError(_))).fork _ <- httpEndpointServer.start(port, httpApp).fork _ <- TestWebhookRepo.setWebhook(webhook) _ <- events.schedule(Schedule.spaced(50.micros).jittered).foreach(TestWebhookEventRepo.createEvent) } yield () - } *> putStrLn("Shutdown successful").orDie + } *> printLine("Shutdown successful").orDie - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provideSome[Scope]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookRepo.test, diff --git a/examples/src/main/scala/zio/webhooks/example/ShutdownOnFatalError.scala b/examples/src/main/scala/zio/webhooks/example/ShutdownOnFatalError.scala index 16dbadf8..bc9ce04a 100644 --- a/examples/src/main/scala/zio/webhooks/example/ShutdownOnFatalError.scala +++ b/examples/src/main/scala/zio/webhooks/example/ShutdownOnFatalError.scala @@ -3,22 +3,23 @@ package zio.webhooks.example import zhttp.http._ import zhttp.service.Server import zio._ -import zio.console._ -import zio.duration._ -import zio.magic._ import zio.stream._ import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.backends.sttp.WebhookSttpClient import zio.webhooks.testkit._ import zio.webhooks.{ WebhooksProxy, _ } +import zio.ZIOAppDefault +import zio.Console.{ printLine, printLineError } + +import java.io.IOException /** * An example of how the server shuts down when encountering a fatal error: in this case a missing * webhook. */ -object ShutdownOnFatalError extends App { +object ShutdownOnFatalError extends ZIOAppDefault { - private lazy val events = goodEvents.take(2) ++ UStream(eventWithoutWebhook) ++ goodEvents.drop(2) + private lazy val events = goodEvents.take(2) ++ ZStream(eventWithoutWebhook) ++ goodEvents.drop(2) private lazy val eventWithoutWebhook = WebhookEvent( WebhookEventKey(WebhookEventId(-1), WebhookId(-1)), @@ -28,7 +29,7 @@ object ShutdownOnFatalError extends App { None ) - private val goodEvents = UStream + private val goodEvents = ZStream .iterate(0L)(_ + 1) .map { i => WebhookEvent( @@ -40,11 +41,11 @@ object ShutdownOnFatalError extends App { ) } - private val httpApp = HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" => - ZIO - .foreach(request.getBodyAsString)(str => putStrLn(s"""SERVER RECEIVED PAYLOAD: "$str"""")) - .as(Response.status(Status.OK)) + private val httpApp = Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" => + request.body.asString + .flatMap(str => printLine(s"""SERVER RECEIVED PAYLOAD: "$str"""")) + .as(Response.status(Status.Ok)) } // just an alias for a zio-http server to disambiguate it with the webhook server @@ -52,20 +53,20 @@ object ShutdownOnFatalError extends App { private lazy val port = 8080 - private def program = + private def program: ZIO[Scope with WebhookServer with TestWebhookRepo with TestWebhookEventRepo, IOException, Unit] = for { - errorFiber <- WebhookServer.getErrors.use(UStream.fromQueue(_).runHead).fork + errorFiber <- WebhookServer.getErrors.flatMap(ZStream.fromQueue(_).runHead).fork _ <- TestWebhookRepo.setWebhook(webhook) eventFiber <- events.schedule(Schedule.spaced(50.micros).jittered).foreach(TestWebhookEventRepo.createEvent).fork httpFiber <- httpEndpointServer.start(port, httpApp).fork _ <- errorFiber.join - .flatMap(error => putStrLnErr(error.toString)) + .flatMap(error => printLineError(error.toString)) .onExit(_ => (eventFiber zip httpFiber).interrupt) } yield () - def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = + override def run = program - .injectCustom( + .provideSome[Scope]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookEventRepo.test, diff --git a/project/plugins.sbt b/project/plugins.sbt index aefcf914..4fc26596 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,3 +6,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.3") addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1032048a") addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34") diff --git a/webhooks-test/src/it/scala/zio/webhooks/WebhookServerIntegrationSpec.scala b/webhooks-test/src/it/scala/zio/webhooks/WebhookServerIntegrationSpec.scala index bca7ea45..7537efc0 100644 --- a/webhooks-test/src/it/scala/zio/webhooks/WebhookServerIntegrationSpec.scala +++ b/webhooks-test/src/it/scala/zio/webhooks/WebhookServerIntegrationSpec.scala @@ -2,52 +2,55 @@ package zio.webhooks import zhttp.http._ import zhttp.service.Server -import zio._ -import zio.clock.Clock -import zio.console.{ putStrLn, putStrLnErr } -import zio.duration._ +import zio.Console.{ printError, printLine } +import zio.{ Random, _ } import zio.json._ -import zio.magic._ -import zio.random.Random import zio.stream._ +import zio.test.TestAspect._ import zio.test._ -import zio.test.TestAspect.{ sequential, timeout } import zio.webhooks.WebhookServerIntegrationSpecUtil._ -import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.backends.sttp.WebhookSttpClient +import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } import zio.webhooks.testkit._ -object WebhookServerIntegrationSpec extends DefaultRunnableSpec { +object WebhookServerIntegrationSpec extends ZIOSpecDefault { val spec = suite("WebhookServerIntegrationSpec")( - testM("all events are delivered eventually") { + test("all events are delivered eventually") { val n = 10000L // number of events - def test(delivered: SubscriptionRef[Set[Int]]) = + def testDelivered(delivered: SubscriptionRef[Set[Int]]) = for { - _ <- ZIO.foreach_(testWebhooks)(TestWebhookRepo.setWebhook) + _ <- ZIO.foreachDiscard(testWebhooks)(TestWebhookRepo.setWebhook) _ <- delivered.changes.collect { case set if set.size % 100 == 0 || set.size / n.toDouble >= 0.99 => set.size.toString } - .foreach(size => putStrLn(s"delivered so far: $size").orDie) + .foreach(size => printLine(s"delivered so far: $size").orDie) .fork - _ <- WebhookServer.start.use_ { - for { - reliableEndpoint <- httpEndpointServer.start(port, reliableEndpoint(delivered)).fork - // create events for webhooks with single delivery, at-most-once semantics - _ <- singleAtMostOnceEvents(n) - // pace events so we don't overwhelm the endpoint - .schedule(Schedule.spaced(50.micros).jittered) - .foreach(TestWebhookEventRepo.createEvent) - // create events for webhooks with batched delivery, at-most-once semantics - // no need to pace events as batching minimizes requests sent - _ <- batchedAtMostOnceEvents(n).foreach(TestWebhookEventRepo.createEvent) - // wait to get half - _ <- delivered.changes.filter(_.size == n / 2).runHead - _ <- reliableEndpoint.interrupt - } yield () + _ <- ZIO.scoped { + WebhookServer.start *> { + for { + reliableEndpoint <- httpEndpointServer.start(port, reliableEndpoint(delivered)).fork + // create events for webhooks with single delivery, at-most-once semantics + _ <- singleAtMostOnceEvents(n) + // pace events so we don't overwhelm the endpoint + .schedule(Schedule.spaced(50.micros).jittered) + .foreach(TestWebhookEventRepo.createEvent) + .delay(100.millis) // give time for endpoint to be ready + // create events for webhooks with batched delivery, at-most-once semantics + // no need to pace events as batching minimizes requests sent + _ <- batchedAtMostOnceEvents(n).foreach(TestWebhookEventRepo.createEvent) + // wait to get half + _ <- printLine("delivered").orDie + _ <- delivered.changes.filter(_.size == n / 2).runHead + _ <- printLine("reliableEndpoint").orDie + _ <- reliableEndpoint.interrupt.fork + _ <- printLine("reliableEndpoint inter").orDie + } yield () + } } + _ <- printLine("RestartingWebhookServer").orDie // start restarting server and endpoint with random behavior _ <- RestartingWebhookServer.start.fork _ <- RandomEndpointBehavior.run(delivered).fork @@ -62,24 +65,23 @@ object WebhookServerIntegrationSpec extends DefaultRunnableSpec { (for { delivered <- SubscriptionRef.make(Set.empty[Int]) - _ <- test(delivered).ensuring( + _ <- testDelivered(delivered).ensuring( // dump repo and events not delivered on timeout for { - deliveredSize <- delivered.ref.get.map(_.size) + deliveredSize <- delivered.get.map(_.size) _ <- TestWebhookEventRepo.dumpEventIds .map(_.toList.sortBy(_._1)) .debug("all IDs in repo") .when(deliveredSize < n) - _ <- delivered.ref.get + _ <- delivered.get .map(delivered => ((0 until n.toInt).toSet diff delivered).toList.sorted) .debug("not delivered") .when(deliveredSize < n) } yield () ) } yield assertCompletes) - .provideSomeLayer[IntegrationEnv](Clock.live ++ console.Console.live ++ random.Random.live) - } @@ timeout(3.minutes), - testM("slow subscribers do not slow down fast ones") { + } @@ timeout(3.minutes) @@ withLiveClock @@ withLiveConsole, + test("slow subscribers do not slow down fast ones") { val webhookCount = 100 val eventsPerWebhook = 1000 val testWebhooks = (0 until webhookCount).map { i => @@ -95,9 +97,9 @@ object WebhookServerIntegrationSpec extends DefaultRunnableSpec { // 100 streams with 1000 events each val eventStreams = - UStream.mergeAllUnbounded()( + ZStream.mergeAllUnbounded()( (0L until webhookCount.toLong).map(webhookId => - UStream + ZStream .iterate(0L)(_ + 1) .map { eventId => WebhookEvent( @@ -114,29 +116,30 @@ object WebhookServerIntegrationSpec extends DefaultRunnableSpec { (for { delivered <- SubscriptionRef.make(Set.empty[Int]) - _ <- delivered.changes.map(_.size).foreach(size => putStrLn(s"delivered so far: $size").orDie).fork + _ <- delivered.changes.map(_.size).foreach(size => printLine(s"delivered so far: $size").orDie).fork _ <- httpEndpointServer.start(port, slowEndpointsExceptFirst(delivered)).fork - _ <- ZIO.foreach_(testWebhooks)(TestWebhookRepo.setWebhook) - testResult <- WebhookServer.start.use { server => - for { - _ <- server.subscribeToErrors - .use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))) - .fork - _ <- eventStreams.foreach(TestWebhookEventRepo.createEvent).fork - _ <- delivered.changes.filter(_.size == eventsPerWebhook).runHead - } yield assertCompletes + _ <- ZIO.foreachDiscard(testWebhooks)(TestWebhookRepo.setWebhook) + testResult <- ZIO.scoped { + WebhookServer.start.flatMap { server => + for { + _ <- server.subscribeToErrors + .flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printError(_))) + .forkScoped + _ <- eventStreams.foreach(TestWebhookEventRepo.createEvent).forkScoped + _ <- delivered.changes.filter(_.size == eventsPerWebhook).runHead + } yield assertCompletes + } } - } yield testResult).provideSomeLayer[IntegrationEnv](Clock.live ++ console.Console.live ++ random.Random.live) - } @@ timeout(1.minute) - ).injectCustom(integrationEnv) @@ sequential + } yield testResult) + } @@ timeout(3.minutes) @@ TestAspect.withLiveClock @@ TestAspect.withLiveConsole + ).provide(integrationEnv) @@ sequential } object WebhookServerIntegrationSpecUtil { // limit max backoff to 1 second so tests don't take too long def customConfig = - WebhookServerConfig.default.map { hasConfig => - val config = hasConfig.get + WebhookServerConfig.default.update { config => config.copy( retry = config.retry.copy( maxBackoff = 1.second @@ -154,10 +157,10 @@ object WebhookServerIntegrationSpecUtil { } } - def events(webhookIdRange: (Int, Int)): ZStream[Random, Nothing, WebhookEvent] = - UStream + def events(webhookIdRange: (Int, Int)): ZStream[Any, Nothing, WebhookEvent] = + ZStream .iterate(0L)(_ + 1) - .zip(UStream.repeatEffect(random.nextIntBetween(webhookIdRange._1, webhookIdRange._2))) + .zip(ZStream.repeatZIO(Random.nextIntBetween(webhookIdRange._1, webhookIdRange._2))) .map { case (i, webhookId) => WebhookEvent( @@ -173,30 +176,30 @@ object WebhookServerIntegrationSpecUtil { events(webhookIdRange = (0, 250)).take(n / 4) def batchedAtMostOnceEvents(n: Long) = - events(webhookIdRange = (250, 500)).drop(n / 4).take(n / 4) + events(webhookIdRange = (250, 500)).drop(n.toInt / 4).take(n / 4) def singleAtLeastOnceEvents(n: Long) = - events(webhookIdRange = (500, 750)).drop(n / 2).take(n / 4) + events(webhookIdRange = (500, 750)).drop(n.toInt / 2).take(n / 4) def batchedAtLeastOnceEvents(n: Long) = - events(webhookIdRange = (750, 1000)).drop(3 * n / 4).take(n / 4) - - type IntegrationEnv = Has[WebhookEventRepo] - with Has[TestWebhookEventRepo] - with Has[WebhookRepo] - with Has[TestWebhookRepo] - with Has[WebhookStateRepo] - with Has[WebhookHttpClient] - with Has[WebhooksProxy] - with Has[WebhookServerConfig] - with Has[SerializePayload] + events(webhookIdRange = (750, 1000)).drop(3 * n.toInt / 4).take(n / 4) + + type IntegrationEnv = WebhookEventRepo + with TestWebhookEventRepo + with WebhookRepo + with TestWebhookRepo + with WebhookStateRepo + with WebhookHttpClient + with WebhooksProxy + with WebhookServerConfig + with SerializePayload // alias for zio-http endpoint server lazy val httpEndpointServer = Server - lazy val integrationEnv: URLayer[Clock, IntegrationEnv] = + lazy val integrationEnv: ULayer[IntegrationEnv] = ZLayer - .wireSome[Clock, IntegrationEnv]( + .make[IntegrationEnv]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookEventRepo.test, @@ -211,47 +214,46 @@ object WebhookServerIntegrationSpecUtil { lazy val port = 8081 def reliableEndpoint(delivered: SubscriptionRef[Set[Int]]) = - HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" / (id @ _) => + Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" / (id @ _) => for { - randomDelay <- random.nextIntBounded(200).map(_.millis) - response <- ZIO - .foreach_(request.getBodyAsString) { body => - val singlePayload = body.fromJson[Int].map(Left(_)) - val batchPayload = body.fromJson[List[Int]].map(Right(_)) - val payload = singlePayload.orElseThat(batchPayload).toOption - ZIO.foreach_(payload) { - case Left(i) => - delivered.ref.update(set => UIO(set + i)) - case Right(is) => - delivered.ref.update(set => UIO(set ++ is)) - } + randomDelay <- Random.nextIntBounded(200).map(_.millis) + response <- request.body.asString.flatMap { body => + val singlePayload = body.fromJson[Int].map(Left(_)) + val batchPayload = body.fromJson[List[Int]].map(Right(_)) + val payload = singlePayload.orElseThat(batchPayload).toOption + ZIO.foreachDiscard(payload) { + case Left(i) => + delivered.updateZIO(set => ZIO.succeed(set + i)) + case Right(is) => + delivered.updateZIO(set => ZIO.succeed(set ++ is)) } - .as(Response.status(Status.OK)) + } + .as(Response.status(Status.Ok)) .delay(randomDelay) // simulate network/server latency } yield response } def slowEndpointsExceptFirst(delivered: SubscriptionRef[Set[Int]]) = - HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" / id if id == "0" => + Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" / id if id == "0" => for { - _ <- ZIO.foreach_(request.getBodyAsString) { body => + _ <- request.body.asString.flatMap { body => val singlePayload = body.fromJson[Int].map(Left(_)) val batchPayload = body.fromJson[List[Int]].map(Right(_)) val payload = singlePayload.orElseThat(batchPayload).toOption ZIO - .foreach_(payload) { + .foreachDiscard(payload) { case Left(i) => - delivered.ref.update(set => UIO(set + i)) + delivered.updateZIO(set => ZIO.succeed(set + i)) case Right(is) => - delivered.ref.update(set => UIO(set ++ is)) + delivered.updateZIO(set => ZIO.succeed(set ++ is)) } } - response <- UIO(Response.status(Status.OK)) + response <- ZIO.succeed(Response.status(Status.Ok)) } yield response - case _ => - UIO(Response.status(Status.OK)).delay(1.minute) + case _ => + ZIO.succeed(Response.status(Status.Ok)).delay(1.minute) } lazy val testWebhooks: IndexedSeq[Webhook] = (0 until 250).map { i => @@ -312,42 +314,41 @@ object RandomEndpointBehavior { case object Flaky extends RandomEndpointBehavior def flakyBehavior(delivered: SubscriptionRef[Set[Int]]) = - HttpApp.collectM { - case request @ Method.POST -> Root / "endpoint" / (id @ _) => + Http.collectZIO[Request] { + case request @ Method.POST -> !! / "endpoint" / (id @ _) => for { - n <- random.nextIntBounded(100) - randomDelay <- random.nextIntBounded(200).map(_.millis) - response <- ZIO - .foreach(request.getBodyAsString) { body => - val singlePayload = body.fromJson[Int].map(Left(_)) - val batchPayload = body.fromJson[List[Int]].map(Right(_)) - val payload = singlePayload.orElseThat(batchPayload).toOption - if (n < 60) - ZIO - .foreach_(payload) { - case Left(i) => - delivered.ref.update(set => UIO(set + i)) - case Right(is) => - delivered.ref.update(set => UIO(set ++ is)) - } - .as(Response.status(Status.OK)) - else - UIO(Response.status(Status.NOT_FOUND)) - } + n <- Random.nextIntBounded(100) + randomDelay <- Random.nextIntBounded(200).map(_.millis) + response <- request.body.asString.flatMap { body => + val singlePayload = body.fromJson[Int].map(Left(_)) + val batchPayload = body.fromJson[List[Int]].map(Right(_)) + val payload = singlePayload.orElseThat(batchPayload).toOption + if (n < 60) + ZIO + .foreachDiscard(payload) { + case Left(i) => + delivered.updateZIO(set => ZIO.succeed(set + i)) + case Right(is) => + delivered.updateZIO(set => ZIO.succeed(set ++ is)) + } + .as(Response.status(Status.Ok)) + else + ZIO.succeed(Response.status(Status.NotFound)) + } .delay(randomDelay) - } yield response.getOrElse(Response.fromHttpError(HttpError.BadRequest("empty body"))) + } yield response } // just an alias for a zio-http server to tell it apart from the webhook server lazy val httpEndpointServer: Server.type = Server - val randomBehavior: URIO[Random, RandomEndpointBehavior] = - random.nextIntBounded(100).map(n => if (n < 80) Flaky else Down) + val randomBehavior: UIO[RandomEndpointBehavior] = + Random.nextIntBounded(100).map(n => if (n < 80) Flaky else Down) def run(delivered: SubscriptionRef[Set[Int]]) = - UStream.repeatEffect(randomBehavior).foreach { behavior => + ZStream.repeatZIO(randomBehavior).foreach { behavior => for { - _ <- putStrLn(s"Endpoint server behavior: $behavior") + _ <- printLine(s"Endpoint server behavior: $behavior") f <- behavior.start(delivered).fork _ <- behavior match { @@ -367,18 +368,20 @@ object RestartingWebhookServer { private def runServerThenShutdown = for { - _ <- putStrLn("Server starting") - _ <- WebhookServer.start.use { server => - for { - _ <- putStrLn("Server started") - f <- server.subscribeToErrors - .use(UStream.fromQueue(_).map(_.toString).foreach(putStrLnErr(_))) - .fork - _ <- TestWebhookEventRepo.enqueueNew - duration <- random.nextIntBetween(3000, 5000).map(_.millis) - _ <- f.interrupt.delay(duration) - } yield () + _ <- printLine("Server starting") + _ <- ZIO.scoped { + WebhookServer.start.flatMap { server => + for { + _ <- printLine("Server started") + f <- server.subscribeToErrors + .flatMap(ZStream.fromQueue(_).map(_.toString).foreach(printError(_))) + .forkScoped + _ <- TestWebhookEventRepo.enqueueNew + duration <- Random.nextIntBetween(3000, 5000).map(_.millis) + _ <- f.interrupt.delay(duration) + } yield () + } } - _ <- putStrLn("Server shut down") + _ <- printLine("Server shut down") } yield () } diff --git a/webhooks-test/src/test/scala/zio/webhooks/WebhookServerSpec.scala b/webhooks-test/src/test/scala/zio/webhooks/WebhookServerSpec.scala index f840b024..642d28a0 100644 --- a/webhooks-test/src/test/scala/zio/webhooks/WebhookServerSpec.scala +++ b/webhooks-test/src/test/scala/zio/webhooks/WebhookServerSpec.scala @@ -1,884 +1,900 @@ package zio.webhooks import zio._ -import zio.clock.Clock -import zio.console.Console -import zio.duration._ import zio.json._ -import zio.magic._ import zio.stream._ import zio.test.Assertion._ -import zio.test.TestAspect.{ failing, timeout } +import zio.test.TestAspect._ import zio.test._ -import zio.test.environment._ import zio.webhooks.WebhookError._ import zio.webhooks.WebhookServerSpecUtil._ import zio.webhooks.WebhookUpdate.WebhookChanged import zio.webhooks.backends.{ InMemoryWebhookStateRepo, JsonPayloadSerialization } +import zio.webhooks.internal.DequeueUtils._ import zio.webhooks.internal.PersistentRetries import zio.webhooks.testkit.TestWebhookHttpClient._ import zio.webhooks.testkit._ import java.time.Instant -object WebhookServerSpec extends DefaultRunnableSpec { +object WebhookServerSpec extends ZIOSpecDefault { val spec = - suite("WebhookServerSpec")( - suite("batching disabled")( - suite("webhooks with at-most-once delivery")( - testM("dispatches correct request given event") { - val webhook = singleWebhook(0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload", - jsonContentHeaders, - None - ) - - val expectedRequest = WebhookHttpRequest(webhook.url, event.content, event.headers) + ( + suite("WebhookServerSpec")( + suite("batching disabled")( + suite("webhooks with at-most-once delivery")( + test("dispatches correct request given event") { + val webhook = singleWebhook(0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) + + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload", + jsonContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = List(event), - ScenarioInterest.Requests - )((requests, _) => assertM(requests.take)(equalTo(expectedRequest))) - }, - testM("webhook stays enabled on dispatch success") { - val webhook = singleWebhook(0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload", - jsonContentHeaders, - None - ) + val expectedRequest = WebhookHttpRequest(webhook.url, event.content, event.headers) - webhooksTestScenario( - initialStubResponses = UStream(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = List(event), - ScenarioInterest.Webhooks - )((webhooks, _) => assertM(webhooks.take)(equalTo(WebhookChanged(webhook)))) - }, - testM("event is marked Delivering, then Delivered on successful dispatch") { - val webhook = singleWebhook(0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload", - jsonContentHeaders, - None - ) + webhooksTestScenario( + initialStubResponses = ZStream(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = List(event), + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.take)(equalTo(expectedRequest))) + }, + test("webhook stays enabled on dispatch success") { + val webhook = singleWebhook(0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - val expectedStatuses = List(WebhookEventStatus.Delivering, WebhookEventStatus.Delivered) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload", + jsonContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = List(event), - ScenarioInterest.Events - ) { (events, _) => - val eventStatuses = events.filterOutput(!_.isNew).map(_.status).takeBetween(2, 3) - assertM(eventStatuses)(hasSameElements(expectedStatuses)) - } - }, - testM("can dispatch single event to n webhooks") { - val n = 100 - val webhooks = createWebhooks(n)(WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - val eventsToNWebhooks = webhooks.map(_.id).flatMap(webhook => createPlaintextEvents(1)(webhook)) + webhooksTestScenario( + initialStubResponses = ZStream(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = List(event), + ScenarioInterest.Webhooks + )((webhooks, _) => assertZIO(webhooks.take)(equalTo(WebhookChanged(webhook)))) + }, + test("event is marked Delivering, then Delivered on successful dispatch") { + val webhook = singleWebhook(0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) + + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload", + jsonContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = webhooks, - events = eventsToNWebhooks, - ScenarioInterest.Requests - )((requests, _) => assertM(requests.takeBetween(n, n + 1))(hasSize(equalTo(n)))) - }, - testM("dispatches no events for disabled webhooks") { - val n = 100 - val webhook = singleWebhook(0, WebhookStatus.Disabled, WebhookDeliveryMode.SingleAtMostOnce) + val expectedStatuses = List(WebhookEventStatus.Delivering, WebhookEventStatus.Delivered) - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = createPlaintextEvents(n)(webhook.id), - ScenarioInterest.Requests - )((requests, _) => requests.take *> assertCompletesM) - } @@ timeout(50.millis) @@ failing, - testM("dispatches no events for unavailable webhooks") { - val n = 100 - val webhook = - singleWebhook(0, WebhookStatus.Unavailable(Instant.EPOCH), WebhookDeliveryMode.SingleAtMostOnce) + webhooksTestScenario( + initialStubResponses = ZStream(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = List(event), + ScenarioInterest.Events + ) { (events, _) => + val eventStatuses = events.filterOutput(!_.isNew).map(_.status).takeBetween(2, 3) + assertZIO(eventStatuses)(hasSameElements(expectedStatuses)) + } + }, + test("can dispatch single event to n webhooks") { + val n = 100 + val webhooks = createWebhooks(n)(WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) + val eventsToNWebhooks = webhooks.map(_.id).flatMap(webhook => createPlaintextEvents(1)(webhook)) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = webhooks, + events = eventsToNWebhooks, + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.takeBetween(n, n + 1))(hasSize(equalTo(n)))) + }, + test("dispatches no events for disabled webhooks") { + val n = 100 + val webhook = singleWebhook(0, WebhookStatus.Disabled, WebhookDeliveryMode.SingleAtMostOnce) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = createPlaintextEvents(n)(webhook.id), + ScenarioInterest.Requests + )((requests, _) => requests.take *> assertCompletesZIO) + } @@ timeout(50.millis) @@ failing, + test("dispatches no events for unavailable webhooks") { + val n = 100 + val webhook = + singleWebhook(0, WebhookStatus.Unavailable(Instant.EPOCH), WebhookDeliveryMode.SingleAtMostOnce) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = createPlaintextEvents(n)(webhook.id), + ScenarioInterest.Requests + )((requests, _) => requests.take *> assertCompletesZIO) + } @@ timeout(50.millis) @@ failing, + test("doesn't batch when no batching configuration is given") { + val n = 100 + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = createPlaintextEvents(n)(webhook.id), + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.takeBetween(n, n + 1))(hasSize(equalTo(n)))) + }, + test("a webhook receiver returning non-200 fails events") { + val n = 100 + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(404))), + webhooks = List(webhook), + events = createPlaintextEvents(n)(webhook.id), + ScenarioInterest.Events + ) { (events, _) => + assertZIO( + events.map(_.status).filterOutput(_ == WebhookEventStatus.Failed).takeBetween(n, n + 1) + )(hasSize(equalTo(n))) + } + }, + test("server dies with a fatal error when webhooks are missing") { + val idRange = 401L to 404L + val missingWebhookIds = idRange.map(WebhookId(_)) + val eventsMissingWebhooks = missingWebhookIds.flatMap(id => createPlaintextEvents(1)(id)) + + webhooksTestScenario( + initialStubResponses = ZStream(Right(WebhookHttpResponse(200))), + webhooks = List.empty, + events = eventsMissingWebhooks, + ScenarioInterest.Errors + )((errors, _) => assertZIO(errors.take)(isSubtype[FatalError](anything))) + }, + test("bad webhook URL errors are published") { + val webhookWithBadUrl = Webhook( + id = WebhookId(0), + url = "ne'er-do-well URL", + label = "webhook with a bad url", + WebhookStatus.Enabled, + WebhookDeliveryMode.SingleAtMostOnce, + None + ) - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = createPlaintextEvents(n)(webhook.id), - ScenarioInterest.Requests - )((requests, _) => requests.take *> assertCompletesM) - } @@ timeout(50.millis) @@ failing, - testM("doesn't batch when no batching configuration is given") { - val n = 100 - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), WebhookId(0)), + WebhookEventStatus.New, + "test event payload", + plaintextContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = createPlaintextEvents(n)(webhook.id), - ScenarioInterest.Requests - )((requests, _) => assertM(requests.takeBetween(n, n + 1))(hasSize(equalTo(n)))) - }, - testM("a webhook receiver returning non-200 fails events") { - val n = 100 - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) + val expectedError = BadWebhookUrlError(webhookWithBadUrl.url, "'twas a ne'er do-well") - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(404))), - webhooks = List(webhook), - events = createPlaintextEvents(n)(webhook.id), - ScenarioInterest.Events - ) { (events, _) => - assertM( - events.map(_.status).filterOutput(_ == WebhookEventStatus.Failed).takeBetween(n, n + 1) - )(hasSize(equalTo(n))) - } - }, - testM("server dies with a fatal error when webhooks are missing") { - val idRange = 401L to 404L - val missingWebhookIds = idRange.map(WebhookId(_)) - val eventsMissingWebhooks = missingWebhookIds.flatMap(id => createPlaintextEvents(1)(id)) + webhooksTestScenario( + initialStubResponses = ZStream(Left(Some(expectedError))), + webhooks = List(webhookWithBadUrl), + events = List(event), + ScenarioInterest.Errors + ) { (errors, _) => + assertZIO(errors.take)(equalTo(expectedError)) + } + }, + test("do not retry events") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - webhooksTestScenario( - initialStubResponses = UStream(Right(WebhookHttpResponse(200))), - webhooks = List.empty, - events = eventsMissingWebhooks, - ScenarioInterest.Errors - )((errors, _) => assertM(errors.take)(isSubtype[FatalError](anything))) - }, - testM("bad webhook URL errors are published") { - val webhookWithBadUrl = Webhook( - id = WebhookId(0), - url = "ne'er-do-well URL", - label = "webhook with a bad url", - WebhookStatus.Enabled, - WebhookDeliveryMode.SingleAtMostOnce, - None - ) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), WebhookId(0)), + WebhookEventStatus.New, + "test event payload", + plaintextContentHeaders, + None + ) - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), WebhookId(0)), - WebhookEventStatus.New, - "test event payload", - plaintextContentHeaders, - None - ) + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Left(None)), + webhooks = List(webhook), + events = List(event), + ScenarioInterest.Requests + ) { (requests, _) => + for { + _ <- requests.take + secondRequest <- requests.take.timeout(100.millis) + } yield assert(secondRequest)(isNone) + } + } @@ withLiveClock, + test("writes warning to console when delivering to a slow webhook, i.e. queue gets full") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - val expectedError = BadWebhookUrlError(webhookWithBadUrl.url, "'twas a ne'er do-well") + for { + capacity <- ZIO.service[WebhookServerConfig].map(_.webhookQueueCapacity) + testEvents = createPlaintextEvents(capacity + 2)(webhook.id) // + 2 because the first one gets taken + testResult <- webhooksTestScenario( + initialStubResponses = ZStream + .fromZIO(ZIO.right(WebhookHttpResponse(200)).delay(1.minute)), + webhooks = List(webhook), + events = List.empty, + ScenarioInterest.Events + ) { (_, _) => + for { + _ <- ZIO.foreachDiscard(testEvents)(TestWebhookEventRepo.createEvent) + _ <- TestConsole.output.repeatUntil(_.nonEmpty) + } yield assertCompletes + } + } yield testResult + } @@ timeout(50.millis) @@ flaky + ), + suite("webhooks with at-least-once delivery")( + test("immediately retries once on non-200 response") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + + val events = createPlaintextEvents(1)(webhook.id) + + webhooksTestScenario( + initialStubResponses = ZStream(Right(WebhookHttpResponse(500)), Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = events, + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.takeBetween(2, 3))(hasSize(equalTo(2)))) + }, + test("immediately retries once on IOException") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + + val events = createPlaintextEvents(1)(webhook.id) + + webhooksTestScenario( + initialStubResponses = ZStream(Left(None), Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = events, + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.takeBetween(2, 3))(hasSize(equalTo(2)))) + }, + test("retries until success before retry timeout") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + + val events = createPlaintextEvents(1)(webhook.id) + + webhooksTestScenario( + initialStubResponses = ZStream(Left(None), Left(None), Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = events, + ScenarioInterest.Requests + ) { (requests, _) => + for { + request1 <- requests.take.as(true) + request2 <- requests.take.as(true) + _ <- TestClock.adjust(10.millis) + request3 <- requests.take.as(true) + } yield assertTrue(request1 && request2 && request3) + } + }, + test("webhook is set unavailable after retry timeout") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val events = createPlaintextEvents(1)(webhook.id) + + webhooksTestScenario( + initialStubResponses = ZStream(Left(None), Left(None)), + webhooks = List(webhook), + events = events, + ScenarioInterest.Webhooks + ) { (webhooks, _) => + for { + status <- webhooks.take.map(_.status) + status2 <- (webhooks.take.map(_.status) race TestClock.adjust(7.days).forever) + } yield assert(status)(isSome(equalTo(WebhookStatus.Enabled))) && + assert(status2)(isSome(isSubtype[WebhookStatus.Unavailable](Assertion.anything))) + } + }, + test("marks all a webhook's events failed when marked unavailable") { + val n = 2 + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val events = createPlaintextEvents(n)(webhook.id) + + webhooksTestScenario( + initialStubResponses = ZStream(Left(None), Left(None)), + webhooks = List(webhook), + events = events, + ScenarioInterest.Events + ) { (events, _) => + ZStream + .fromQueue(events) + .filter(_.status == WebhookEventStatus.Failed) + .take(n.toLong) + .mergeHaltLeft(ZStream.repeatZIO(TestClock.adjust(7.days))) + .runDrain *> assertCompletesZIO + } + }, + test("retries past first one back off exponentially") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val events = createPlaintextEvents(1)(webhook.id) + + webhooksTestScenario( + initialStubResponses = + ZStream.fromIterable(List.fill(5)(Left(None))) ++ ZStream(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = events, + ScenarioInterest.Requests + ) { + (requests, _) => + for { + _ <- requests.take // 1st failure + _ <- requests.take // 1st retry immediately after + _ <- TestClock.adjust(10.millis) + _ <- requests.take // 2nd retry after 10ms + _ <- TestClock.adjust(20.millis) + _ <- requests.take // 3rd retry after 20ms + _ <- TestClock.adjust(10.millis) + poll <- requests.poll + _ <- TestClock.adjust(30.millis) + _ <- requests.take // 4th retry after 40ms + _ <- TestClock.adjust(80.millis) + _ <- requests.take // 5th retry after 80ms + } yield assert(poll)(isNone) + } + }, + test("doesn't retry requests after requests succeed again") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + + val events = createPlaintextEvents(3)(webhook.id) + + webhooksTestScenario( + initialStubResponses = ZStream(Left(None)) ++ ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = events, + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.takeBetween(4, 5))(hasSize(equalTo(4)))) + }, + test("retries for multiple webhooks") { + val n = 100 + val webhooks = createWebhooks(n)(WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val eventsToNWebhooks = webhooks.map(_.id).map { webhookId => + WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhookId), + WebhookEventStatus.New, + webhookId.value.toString, + Chunk(("Accept", "*/*"), ("Content-Type", "text/plain")), + None + ) + } - webhooksTestScenario( - initialStubResponses = UStream(Left(Some(expectedError))), - webhooks = List(webhookWithBadUrl), - events = List(event), - ScenarioInterest.Errors - ) { (errors, _) => - assertM(errors.take)(equalTo(expectedError)) - } - }, - testM("do not retry events") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), WebhookId(0)), - WebhookEventStatus.New, - "test event payload", - plaintextContentHeaders, - None - ) + val expectedCount = n * 2 - webhooksTestScenario( - initialStubResponses = UStream.repeat(Left(None)), - webhooks = List(webhook), - events = List(event), - ScenarioInterest.Requests - ) { (requests, _) => for { - _ <- requests.take - secondRequest <- requests.take.timeout(100.millis).provideLayer(Clock.live) - } yield assert(secondRequest)(isNone) + queues <- ZIO.collectAll(Chunk.fill(100)(Queue.bounded[StubResponse](2))) + _ <- ZIO.collectAll(queues.map(_.offerAll(List(Left(None), Right(WebhookHttpResponse(200)))))) + testResult <- webhooksTestScenario( + stubResponses = request => queues.lift(request.content.toInt), + webhooks = webhooks, + events = eventsToNWebhooks, + ScenarioInterest.Requests + ) { requests => + assertZIO(requests.takeBetween(expectedCount, expectedCount + 1))( + hasSize(equalTo(expectedCount)) + ) + } + } yield testResult } - }, - testM("writes warning to console when delivering to a slow webhook, i.e. queue gets full") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtMostOnce) - - for { - capacity <- ZIO.service[WebhookServerConfig].map(_.webhookQueueCapacity) - clock <- ZIO.environment[Clock] - testEvents = createPlaintextEvents(capacity + 2)(webhook.id) // + 2 because the first one gets taken - testResult <- webhooksTestScenario( - initialStubResponses = - UStream.fromEffect(UIO.right(WebhookHttpResponse(200)).delay(1.minute)).provide(clock), - webhooks = List(webhook), - events = List.empty, - ScenarioInterest.Events - ) { (_, _) => - for { - _ <- ZIO.foreach_(testEvents)(TestWebhookEventRepo.createEvent) - _ <- TestConsole.output.repeatUntil(_.nonEmpty) - } yield assertCompletes - } - } yield testResult - } - ), - suite("webhooks with at-least-once delivery")( - testM("immediately retries once on non-200 response") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + ), + suite("on webhook changes")( + test("changing a webhook's URL eventually changes the request URL") { + val firstUrl = "first url" + val secondUrl = "second url" + + val webhook = + Webhook( + WebhookId(0), + firstUrl, + "test webhook", + WebhookStatus.Enabled, + WebhookDeliveryMode.SingleAtMostOnce, + None + ) - val events = createPlaintextEvents(1)(webhook.id) + val firstEvent = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload 0", + plaintextContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Right(WebhookHttpResponse(500)), Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = events, - ScenarioInterest.Requests - )((requests, _) => assertM(requests.takeBetween(2, 3))(hasSize(equalTo(2)))) - }, - testM("immediately retries once on IOException") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val nextEvents = ZStream + .iterate(0L)(_ + 1) + .map { eventId => + WebhookEvent( + WebhookEventKey(WebhookEventId(eventId), webhook.id), + WebhookEventStatus.New, + s"event payload $eventId", + plaintextContentHeaders, + None + ) + } + .drop(1) + .schedule(Schedule.spaced(1.milli)) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = List.empty, + ScenarioInterest.Requests + ) { (requests, _) => + for { + _ <- TestWebhookEventRepo.createEvent(firstEvent) + actualFirstUrl <- requests.take.map(_.url) + _ <- TestWebhookRepo.setWebhook(webhook.copy(url = secondUrl)) + _ <- nextEvents.foreach(TestWebhookEventRepo.createEvent).fork + _ <- requests.filterOutput(_.url == secondUrl).take + } yield assertTrue(actualFirstUrl == firstUrl) + } + } @@ withLiveClock, + test("toggling a webhook's status toggles event delivery") { + val webhook = + Webhook( + WebhookId(0), + "test url", + "test webhook", + WebhookStatus.Enabled, + WebhookDeliveryMode.SingleAtMostOnce, + None + ) - val events = createPlaintextEvents(1)(webhook.id) + val firstEvent = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload 0", + plaintextContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Left(None), Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = events, - ScenarioInterest.Requests - )((requests, _) => assertM(requests.takeBetween(2, 3))(hasSize(equalTo(2)))) - }, - testM("retries until success before retry timeout") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val nextEvents = ZStream + .iterate(0L)(_ + 1) + .map { eventId => + WebhookEvent( + WebhookEventKey(WebhookEventId(eventId), webhook.id), + WebhookEventStatus.New, + s"event payload $eventId", + plaintextContentHeaders, + None + ) + } + .drop(1) + .schedule(Schedule.spaced(1.milli)) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = List.empty, + ScenarioInterest.Events + ) { + (events, _) => + for { + _ <- TestWebhookEventRepo.createEvent(firstEvent) + deliveringEvents = events.filterOutput(_.isDelivering) + _ <- deliveringEvents.take + _ <- TestWebhookRepo.setWebhook(webhook.disable) + _ <- nextEvents.foreach(TestWebhookEventRepo.createEvent).fork + _ <- deliveringEvents.take + .timeout(2.millis) + .repeatUntil(_.isEmpty) + + _ <- TestWebhookRepo.setWebhook(webhook.enable) + _ <- deliveringEvents.take + .timeout(2.millis) + .repeatUntil(_.isDefined) + + } yield assertCompletes + } + } @@ withLiveClock, + test("setting a webhook's delivery semantics to at-least-once enables retries") { + val webhook = + Webhook( + WebhookId(0), + "test url", + "test webhook", + WebhookStatus.Enabled, + WebhookDeliveryMode.SingleAtMostOnce, + None + ) - val events = createPlaintextEvents(1)(webhook.id) + val firstEvent = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload 0", + plaintextContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Left(None), Left(None), Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = events, - ScenarioInterest.Requests - ) { (requests, _) => - for { - request1 <- requests.take.as(true) - request2 <- requests.take.as(true) - _ <- TestClock.adjust(10.millis) - request3 <- requests.take.as(true) - } yield assertTrue(request1 && request2 && request3) - } - }, - testM("webhook is set unavailable after retry timeout") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val events = createPlaintextEvents(1)(webhook.id) + val nextEvents = ZStream + .iterate(0L)(_ + 1) + .map { eventId => + WebhookEvent( + WebhookEventKey(WebhookEventId(eventId), webhook.id), + WebhookEventStatus.New, + s"event payload $eventId", + plaintextContentHeaders, + None + ) + } + .drop(1) + .schedule(Schedule.spaced(1.milli)) + + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Left(None)), + webhooks = List(webhook), + events = List.empty, + ScenarioInterest.Requests + ) { + (requests, _) => + for { + _ <- TestWebhookEventRepo.createEvent(firstEvent) + ref <- Ref.make(Set.empty[WebhookHttpRequest]) + _ <- requests.take.flatMap(req => ref.modify(attempted => (attempted(req), attempted + req))) + _ <- TestWebhookRepo.setWebhook( + webhook.copy(deliveryMode = WebhookDeliveryMode.SingleAtLeastOnce) + ) + _ <- nextEvents.foreach(TestWebhookEventRepo.createEvent).fork + _ <- requests.take + .flatMap(req => ref.modify(attempted => (attempted(req), attempted + req))) + .repeatUntil(Predef.identity) + _ <- ref.set(Set.empty) + } yield assertCompletes + } + } @@ withLiveClock, + test("disabling a webhook with at-least-once delivery semantics halts retries") { + val webhook = + Webhook( + WebhookId(0), + "test url", + "test webhook", + WebhookStatus.Enabled, + WebhookDeliveryMode.SingleAtLeastOnce, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Left(None), Left(None)), - webhooks = List(webhook), - events = events, - ScenarioInterest.Webhooks - ) { (webhooks, _) => - for { - status <- webhooks.take.map(_.status) - status2 <- (webhooks.take.map(_.status) race TestClock.adjust(7.days).forever) - } yield assert(status)(isSome(equalTo(WebhookStatus.Enabled))) && - assert(status2)(isSome(isSubtype[WebhookStatus.Unavailable](Assertion.anything))) - } - }, - testM("marks all a webhook's events failed when marked unavailable") { - val n = 2 - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val events = createPlaintextEvents(n)(webhook.id) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), webhook.id), + WebhookEventStatus.New, + "event payload 0", + plaintextContentHeaders, + None + ) - webhooksTestScenario( - initialStubResponses = UStream(Left(None), Left(None)), - webhooks = List(webhook), - events = events, - ScenarioInterest.Events - ) { (events, _) => - UStream - .fromQueue(events) - .filter(_.status == WebhookEventStatus.Failed) - .take(n.toLong) - .mergeTerminateLeft(UStream.repeatEffect(TestClock.adjust(7.days))) - .runDrain *> assertCompletesM + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Left(None)), + webhooks = List(webhook), + events = List.empty, + ScenarioInterest.Requests + ) { (requests, _) => + for { + _ <- TestWebhookEventRepo.createEvent(event) + _ <- requests.take + _ <- TestWebhookRepo.setWebhook(webhook.copy(status = WebhookStatus.Disabled)) + waitForHalt = requests.take.timeout(50.millis).repeatUntil(_.isEmpty) + _ <- waitForHalt race TestClock.adjust(10.millis).forever + } yield assertCompletes + } } - }, - testM("retries past first one back off exponentially") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val events = createPlaintextEvents(1)(webhook.id) + ) + ).provide(specEnv, WebhookServerConfig.default), + suite("batching enabled")( + test("batches events queued up since last request") { + val n = 100 + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) + val batches = createPlaintextEvents(n)(webhook.id).grouped(10).toList + + val expectedRequestsMade = 10 webhooksTestScenario( - initialStubResponses = - UStream.fromIterable(List.fill(5)(Left(None))) ++ UStream(Right(WebhookHttpResponse(200))), + initialStubResponses = ZStream.empty, webhooks = List(webhook), - events = events, + events = Iterable.empty, ScenarioInterest.Requests - ) { - (requests, _) => + ) { (requests, responseQueue) => + val actualRequests = ZIO.foreach(batches) { batch => for { - _ <- requests.take // 1st failure - _ <- requests.take // 1st retry immediately after - _ <- TestClock.adjust(10.millis) - _ <- requests.take // 2nd retry after 10ms - _ <- TestClock.adjust(20.millis) - _ <- requests.take // 3rd retry after 20ms - _ <- TestClock.adjust(10.millis) - poll <- requests.poll - _ <- TestClock.adjust(30.millis) - _ <- requests.take // 4th retry after 40ms - _ <- TestClock.adjust(80.millis) - _ <- requests.take // 5th retry after 80ms - } yield assert(poll)(isNone) + _ <- ZIO.foreachDiscard(batch)(TestWebhookEventRepo.createEvent) + _ <- responseQueue.offer(Right(WebhookHttpResponse(200))) + request <- requests.take + } yield request + } + assertZIO(actualRequests)(hasSize(equalTo(expectedRequestsMade))) } }, - testM("doesn't retry requests after requests succeed again") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + test("batches for multiple webhooks") { + val eventCount = 100 + val webhookCount = 10 + val webhooks = createWebhooks(webhookCount)( + WebhookStatus.Enabled, + WebhookDeliveryMode.BatchedAtMostOnce + ) + val events = webhooks.map(_.id).flatMap { webhookId => + createPlaintextEvents(eventCount / webhookCount)(webhookId) + } - val events = createPlaintextEvents(3)(webhook.id) + val minRequestsMade = eventCount / webhookCount // 10 + val maxRequestsMade = minRequestsMade * 2 webhooksTestScenario( - initialStubResponses = UStream(Left(None)) ++ UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), + initialStubResponses = ZStream.empty, + webhooks = webhooks, events = events, ScenarioInterest.Requests - )((requests, _) => assertM(requests.takeBetween(4, 5))(hasSize(equalTo(4)))) - }, - testM("retries for multiple webhooks") { - val n = 100 - val webhooks = createWebhooks(n)(WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val eventsToNWebhooks = webhooks.map(_.id).map { webhookId => - WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhookId), - WebhookEventStatus.New, - webhookId.value.toString, - Chunk(("Accept", "*/*"), ("Content-Type", "text/plain")), - None - ) + ) { (requests, responseQueue) => + for { + _ <- responseQueue.offerAll(List.fill(10)(Right(WebhookHttpResponse(200)))) + requests <- requests.takeBetween(minRequestsMade, minRequestsMade + 1) + } yield assertTrue((minRequestsMade <= requests.size) && (requests.size <= maxRequestsMade)) } - - val expectedCount = n * 2 - - for { - queues <- ZIO.collectAll(Chunk.fill(100)(Queue.bounded[StubResponse](2))) - _ <- ZIO.collectAll(queues.map(_.offerAll(List(Left(None), Right(WebhookHttpResponse(200)))))) - testResult <- webhooksTestScenario( - stubResponses = request => queues.lift(request.content.toInt), - webhooks = webhooks, - events = eventsToNWebhooks, - ScenarioInterest.Requests - ) { requests => - assertM(requests.takeBetween(expectedCount, expectedCount + 1))( - hasSize(equalTo(expectedCount)) - ) - } - } yield testResult - } - ), - suite("on webhook changes")( - testM("changing a webhook's URL eventually changes the request URL") { - val firstUrl = "first url" - val secondUrl = "second url" - - val webhook = - Webhook( - WebhookId(0), - firstUrl, - "test webhook", - WebhookStatus.Enabled, - WebhookDeliveryMode.SingleAtMostOnce, - None - ) - - val firstEvent = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload 0", - plaintextContentHeaders, - None - ) - - val nextEvents = UStream - .iterate(0L)(_ + 1) - .map { eventId => - WebhookEvent( - WebhookEventKey(WebhookEventId(eventId), webhook.id), - WebhookEventStatus.New, - s"event payload $eventId", - plaintextContentHeaders, - None - ) - } - .drop(1) - .schedule(Schedule.spaced(1.milli)) - .provideLayer(Clock.live) + }, + test("events dispatched by batch are marked delivered") { + val n = 100 + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) + val batchCount = 10 + val testEvents = createPlaintextEvents(n)(webhook.id).grouped(batchCount).toList webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), + initialStubResponses = ZStream.empty, webhooks = List(webhook), - events = List.empty, - ScenarioInterest.Requests - ) { (requests, _) => + events = Iterable.empty, + ScenarioInterest.Events + ) { (events, responseQueue) => for { - _ <- TestWebhookEventRepo.createEvent(firstEvent) - actualFirstUrl <- requests.take.map(_.url) - _ <- TestWebhookRepo.setWebhook(webhook.copy(url = secondUrl)) - _ <- nextEvents.foreach(TestWebhookEventRepo.createEvent).fork - _ <- requests.filterOutput(_.url == secondUrl).take - } yield assertTrue(actualFirstUrl == firstUrl) + _ <- ZIO.foreachDiscard(testEvents) { + ZIO.foreachDiscard(_)(TestWebhookEventRepo.createEvent) *> + responseQueue.offer(Right(WebhookHttpResponse(200))) + } + deliveredEvents <- events + .filterOutput(_.status == WebhookEventStatus.Delivered) + .takeBetween(batchCount, n) + } yield assertTrue(batchCount <= deliveredEvents.size && deliveredEvents.size <= n) } }, - testM("toggling a webhook's status toggles event delivery") { - val webhook = - Webhook( - WebhookId(0), - "test url", - "test webhook", - WebhookStatus.Enabled, - WebhookDeliveryMode.SingleAtMostOnce, - None - ) - - val firstEvent = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload 0", - plaintextContentHeaders, - None - ) + test("batches events on webhook and content-type") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) - val nextEvents = UStream - .iterate(0L)(_ + 1) - .map { eventId => + val jsonEvents = + (0 until 4).map { i => WebhookEvent( - WebhookEventKey(WebhookEventId(eventId), webhook.id), + WebhookEventKey(WebhookEventId(i.toLong), webhook.id), WebhookEventStatus.New, - s"event payload $eventId", - plaintextContentHeaders, + s"""{"event":"payload$i"}""", + jsonContentHeaders, None ) } - .drop(1) - .schedule(Schedule.spaced(1.milli)) - .provideLayer(Clock.live) - - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = List.empty, - ScenarioInterest.Events - ) { - (events, _) => - for { - _ <- TestWebhookEventRepo.createEvent(firstEvent) - deliveringEvents = events.filterOutput(_.isDelivering) - _ <- deliveringEvents.take - _ <- TestWebhookRepo.setWebhook(webhook.disable) - _ <- nextEvents.foreach(TestWebhookEventRepo.createEvent).fork - _ <- deliveringEvents.take - .timeout(2.millis) - .repeatUntil(_.isEmpty) - .provideLayer(Clock.live) - _ <- TestWebhookRepo.setWebhook(webhook.enable) - _ <- deliveringEvents.take - .timeout(2.millis) - .repeatUntil(_.isDefined) - .provideLayer(Clock.live) - } yield assertCompletes - } - }, - testM("setting a webhook's delivery semantics to at-least-once enables retries") { - val webhook = - Webhook( - WebhookId(0), - "test url", - "test webhook", - WebhookStatus.Enabled, - WebhookDeliveryMode.SingleAtMostOnce, - None - ) - - val firstEvent = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload 0", - plaintextContentHeaders, - None - ) - val nextEvents = UStream - .iterate(0L)(_ + 1) - .map { eventId => + val plaintextEvents = + (4 until 8).map { i => WebhookEvent( - WebhookEventKey(WebhookEventId(eventId), webhook.id), + WebhookEventKey(WebhookEventId(i.toLong), webhook.id), WebhookEventStatus.New, - s"event payload $eventId", + "event payload " + i, plaintextContentHeaders, None ) } - .drop(1) - .schedule(Schedule.spaced(1.milli)) - .provideLayer(Clock.live) webhooksTestScenario( - initialStubResponses = UStream.repeat(Left(None)), + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), webhooks = List(webhook), - events = List.empty, + events = jsonEvents ++ plaintextEvents, ScenarioInterest.Requests - ) { - (requests, _) => - for { - _ <- TestWebhookEventRepo.createEvent(firstEvent) - ref <- Ref.make(Set.empty[WebhookHttpRequest]) - _ <- requests.take.flatMap(req => ref.modify(attempted => (attempted(req), attempted + req))) - _ <- TestWebhookRepo.setWebhook( - webhook.copy(deliveryMode = WebhookDeliveryMode.SingleAtLeastOnce) - ) - _ <- nextEvents.foreach(TestWebhookEventRepo.createEvent).fork - _ <- requests.take - .flatMap(req => ref.modify(attempted => (attempted(req), attempted + req))) - .repeatUntil(identity) - _ <- ref.set(Set.empty) - } yield assertCompletes - } + )((requests, _) => assertZIO(requests.takeBetween(2, 3))(hasSize(equalTo(2)))) }, - testM("disabling a webhook with at-least-once delivery semantics halts retries") { - val webhook = - Webhook( - WebhookId(0), - "test url", - "test webhook", - WebhookStatus.Enabled, - WebhookDeliveryMode.SingleAtLeastOnce, - None - ) - - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), webhook.id), - WebhookEventStatus.New, - "event payload 0", - plaintextContentHeaders, - None - ) + test("batched JSON event contents are always serialized into a JSON array") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) + val jsonEvents = createJsonEvents(100)(webhook.id) webhooksTestScenario( - initialStubResponses = UStream.repeat(Left(None)), + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), webhooks = List(webhook), - events = List.empty, + events = jsonEvents, ScenarioInterest.Requests - ) { (requests, _) => - for { - _ <- TestWebhookEventRepo.createEvent(event) - _ <- requests.take - _ <- TestWebhookRepo.setWebhook(webhook.copy(status = WebhookStatus.Disabled)) - waitForHalt = requests.take.timeout(50.millis).repeatUntil(_.isEmpty).provideLayer(Clock.live) - _ <- waitForHalt race TestClock.adjust(10.millis).forever - } yield assertCompletes - } - } - ) - ).injectSome[TestEnvironment](specEnv, WebhookServerConfig.default), - suite("batching enabled")( - testM("batches events queued up since last request") { - val n = 100 - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) - val batches = createPlaintextEvents(n)(webhook.id).grouped(10).toList - - val expectedRequestsMade = 10 - - webhooksTestScenario( - initialStubResponses = UStream.empty, - webhooks = List(webhook), - events = Iterable.empty, - ScenarioInterest.Requests - ) { (requests, responseQueue) => - val actualRequests = ZIO.foreach(batches) { batch => - for { - _ <- ZIO.foreach_(batch)(TestWebhookEventRepo.createEvent) - _ <- responseQueue.offer(Right(WebhookHttpResponse(200))) - request <- requests.take - } yield request - } - assertM(actualRequests)(hasSize(equalTo(expectedRequestsMade))) - } - }, - testM("batches for multiple webhooks") { - val eventCount = 100 - val webhookCount = 10 - val webhooks = createWebhooks(webhookCount)( - WebhookStatus.Enabled, - WebhookDeliveryMode.BatchedAtMostOnce - ) - val events = webhooks.map(_.id).flatMap { webhookId => - createPlaintextEvents(eventCount / webhookCount)(webhookId) - } + )((requests, _) => assertZIO(requests.take.map(_.content))(matchesRegex(jsonPayloadPattern))) + }, + test("batched plain text event contents are concatenated") { + val n = 2 + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) + val plaintextEvents = createPlaintextEvents(n)(webhook.id) - val minRequestsMade = eventCount / webhookCount // 10 - val maxRequestsMade = minRequestsMade * 2 - - webhooksTestScenario( - initialStubResponses = UStream.empty, - webhooks = webhooks, - events = events, - ScenarioInterest.Requests - ) { (requests, responseQueue) => - for { - _ <- responseQueue.offerAll(List.fill(10)(Right(WebhookHttpResponse(200)))) - requests <- requests.takeBetween(minRequestsMade, minRequestsMade + 1) - } yield assertTrue((minRequestsMade <= requests.size) && (requests.size <= maxRequestsMade)) - } - }, - testM("events dispatched by batch are marked delivered") { - val n = 100 - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) - val batchCount = 10 - val testEvents = createPlaintextEvents(n)(webhook.id).grouped(batchCount).toList - - webhooksTestScenario( - initialStubResponses = UStream.empty, - webhooks = List(webhook), - events = Iterable.empty, - ScenarioInterest.Events - ) { (events, responseQueue) => - for { - _ <- ZIO.foreach_(testEvents) { - ZIO.foreach_(_)(TestWebhookEventRepo.createEvent) *> - responseQueue.offer(Right(WebhookHttpResponse(200))) - } - deliveredEvents <- events - .filterOutput(_.status == WebhookEventStatus.Delivered) - .takeBetween(batchCount, n) - } yield assertTrue(batchCount <= deliveredEvents.size && deliveredEvents.size <= n) + webhooksTestScenario( + initialStubResponses = ZStream.repeat(Right(WebhookHttpResponse(200))), + webhooks = List(webhook), + events = plaintextEvents, + ScenarioInterest.Requests + )((requests, _) => assertZIO(requests.take.map(_.content))(matchesRegex("(?:event payload \\d+)+"))) } - }, - testM("batches events on webhook and content-type") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) - - val jsonEvents = - (0 until 4).map { i => - WebhookEvent( - WebhookEventKey(WebhookEventId(i.toLong), webhook.id), + ).provide(specEnv, WebhookServerConfig.defaultWithBatching), + suite("manual server start and shutdown")( + suite("on shutdown")( + test("takes no new events on shut down right after startup") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val testEvent = WebhookEvent( + WebhookEventKey(WebhookEventId(0), WebhookId(0)), WebhookEventStatus.New, - s"""{"event":"payload$i"}""", - jsonContentHeaders, + "event payload", + plaintextContentHeaders, None ) - } - val plaintextEvents = - (4 until 8).map { i => - WebhookEvent( - WebhookEventKey(WebhookEventId(i.toLong), webhook.id), + ZIO.scoped { + TestWebhookEventRepo.subscribeToEvents + .map(_.filterOutput(_.status == WebhookEventStatus.Delivering)) + .flatMap { + events => + for { + responses <- Queue.unbounded[StubResponse] + _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) + _ <- responses.offerAll( + List(Right(WebhookHttpResponse(200)), Right(WebhookHttpResponse(200))) + ) + _ <- ZIO.scoped { + WebhookServer.start + } + _ <- TestWebhookRepo.setWebhook(webhook) + _ <- TestWebhookEventRepo.createEvent(testEvent) + take <- events.take.timeout(1.second) + } yield assertTrue(take.isEmpty) + } + } + } @@ withLiveClock, + test("stops subscribing to new events") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val testEvents = createPlaintextEvents(2)(WebhookId(0)) + ZIO.scoped { + TestWebhookEventRepo.subscribeToEvents + .map(_.filterOutput(_.status == WebhookEventStatus.Delivering)) + .flatMap { + events => + for { + responses <- Queue.unbounded[StubResponse] + _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) + _ <- responses.offerAll( + List(Right(WebhookHttpResponse(200)), Right(WebhookHttpResponse(200))) + ) + event1 <- ZIO.scoped { + WebhookServer.start.flatMap { _ => + for { + _ <- TestWebhookRepo.setWebhook(webhook) + _ <- TestWebhookEventRepo.createEvent(testEvents(0)) + event1 <- events.take.as(true) + } yield event1 + } + } + _ <- TestWebhookEventRepo.createEvent(testEvents(1)) + take <- events.take.timeout(1.second) + } yield assertTrue(event1 && take.isEmpty) + } + } + } @@ withLiveClock, + test("retry state is saved") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), WebhookId(0)), WebhookEventStatus.New, - "event payload " + i, + "event payload", plaintextContentHeaders, None ) + ZIO.scoped { + TestWebhookHttpClient.getRequests.flatMap { + requests => + for { + responses <- Queue.unbounded[StubResponse] + _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) + _ <- responses.offerAll(List(Left(None), Left(None))) + _ <- ZIO.scoped { + WebhookServer.start *> { + for { + _ <- TestWebhookRepo.setWebhook(webhook) + _ <- TestWebhookEventRepo.createEvent(event) + _ <- requests.takeN(2) // wait for 2 requests to come through + } yield () + } + } + saveState <- WebhookStateRepo.loadState + .repeatUntil(_.isDefined) + .map { + _.map(_.fromJson[PersistentRetries]) + .toRight("No save-state") + .flatMap(Predef.identity) + } + } yield assertTrue(saveState.isRight) + } + } } - - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = jsonEvents ++ plaintextEvents, - ScenarioInterest.Requests - )((requests, _) => assertM(requests.takeBetween(2, 3))(hasSize(equalTo(2)))) - }, - testM("batched JSON event contents are always serialized into a JSON array") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) - val jsonEvents = createJsonEvents(100)(webhook.id) - - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = jsonEvents, - ScenarioInterest.Requests - )((requests, _) => assertM(requests.take.map(_.content))(matchesRegex(jsonPayloadPattern))) - }, - testM("batched plain text event contents are concatenated") { - val n = 2 - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.BatchedAtMostOnce) - val plaintextEvents = createPlaintextEvents(n)(webhook.id) - - webhooksTestScenario( - initialStubResponses = UStream.repeat(Right(WebhookHttpResponse(200))), - webhooks = List(webhook), - events = plaintextEvents, - ScenarioInterest.Requests - )((requests, _) => assertM(requests.take.map(_.content))(matchesRegex("(?:event payload \\d+)+"))) - } - ).injectSome[TestEnvironment](specEnv, WebhookServerConfig.defaultWithBatching), - suite("manual server start and shutdown")( - suite("on shutdown")( - testM("takes no new events on shut down right after startup") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val testEvent = WebhookEvent( - WebhookEventKey(WebhookEventId(0), WebhookId(0)), - WebhookEventStatus.New, - "event payload", - plaintextContentHeaders, - None - ) - - TestWebhookEventRepo.subscribeToEvents.map(_.filterOutput(_.status == WebhookEventStatus.Delivering)).use { - events => - for { - responses <- Queue.unbounded[StubResponse] - _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) - _ <- responses.offerAll( - List(Right(WebhookHttpResponse(200)), Right(WebhookHttpResponse(200))) - ) - _ <- WebhookServer.start.useNow - _ <- TestWebhookRepo.setWebhook(webhook) - _ <- TestWebhookEventRepo.createEvent(testEvent) - take <- events.take.timeout(1.second).provideLayer(Clock.live) - } yield assertTrue(take.isEmpty) - } - }, - testM("stops subscribing to new events") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val testEvents = createPlaintextEvents(2)(WebhookId(0)) - - TestWebhookEventRepo.subscribeToEvents.map(_.filterOutput(_.status == WebhookEventStatus.Delivering)).use { - events => - for { - responses <- Queue.unbounded[StubResponse] - _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) - _ <- responses.offerAll( - List(Right(WebhookHttpResponse(200)), Right(WebhookHttpResponse(200))) - ) - event1 <- WebhookServer.start.use_ { - for { - _ <- TestWebhookRepo.setWebhook(webhook) - _ <- TestWebhookEventRepo.createEvent(testEvents(0)) - event1 <- events.take.as(true) - } yield event1 - } - _ <- TestWebhookEventRepo.createEvent(testEvents(1)) - take <- events.take.timeout(1.second).provideLayer(Clock.live) - } yield assertTrue(event1 && take.isEmpty) - } - }, - testM("retry state is saved") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), WebhookId(0)), - WebhookEventStatus.New, - "event payload", - plaintextContentHeaders, - None - ) - - TestWebhookHttpClient.getRequests.use { - requests => - for { - responses <- Queue.unbounded[StubResponse] - _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) - _ <- responses.offerAll(List(Left(None), Left(None))) - _ <- WebhookServer.start.use_ { - for { - _ <- TestWebhookRepo.setWebhook(webhook) - _ <- TestWebhookEventRepo.createEvent(event) - _ <- requests.takeN(2) // wait for 2 requests to come through - } yield () - } - saveState <- WebhookStateRepo.loadState - .repeatUntil(_.isDefined) - .map { - _.map(_.fromJson[PersistentRetries]) - .toRight("No save-state") - .flatMap(Predef.identity) - } - } yield assertTrue(saveState.isRight) - } - } - ), - suite("on restart")( - testM("continues persisted retries") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), WebhookId(0)), - WebhookEventStatus.New, - "recovered event", - plaintextContentHeaders, - None - ) - - TestWebhookHttpClient.getRequests.use { - requests => - for { - responses <- Queue.unbounded[StubResponse] - _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) - _ <- responses.offerAll(List(Left(None), Left(None), Right(WebhookHttpResponse(200)))) - _ <- WebhookServer.start.use_ { - for { - _ <- TestWebhookRepo.setWebhook(webhook) - _ <- TestWebhookEventRepo.createEvent(event) - _ <- requests.takeN(2) - } yield () - } - _ <- WebhookServer.start.use_(requests.take.fork) - } yield assertCompletes - } - }, - testM("resumes timeout duration for retries") { - val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) - val event = WebhookEvent( - WebhookEventKey(WebhookEventId(0), WebhookId(0)), - WebhookEventStatus.New, - "event content", - plaintextContentHeaders, - None - ) - - (TestWebhookHttpClient.getRequests zip TestWebhookRepo.subscribeToWebhooks).use { - case (requests, webhooks) => - for { - responses <- Queue.unbounded[StubResponse] - _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) - _ <- responses.offerAll(List(Left(None), Left(None), Right(WebhookHttpResponse(200)))) - _ <- WebhookServer.start.use_ { - for { - _ <- TestWebhookRepo.setWebhook(webhook) - _ <- TestWebhookEventRepo.createEvent(event) - _ <- requests.takeN(2) - _ <- TestClock.adjust(3.days) - } yield () - } - lastStatus <- WebhookServer.start.use_ { - for { - _ <- TestClock.adjust(4.days) - lastStatus <- webhooks.takeN(2).map(_.last.status) - _ <- requests.take - } yield lastStatus - } - - } yield assert(lastStatus)(isSome(isSubtype[WebhookStatus.Unavailable](anything))) + ), + suite("on restart")( + test("continues persisted retries") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), WebhookId(0)), + WebhookEventStatus.New, + "recovered event", + plaintextContentHeaders, + None + ) + ZIO.scoped { + TestWebhookHttpClient.getRequests.flatMap { + requests => + for { + responses <- Queue.unbounded[StubResponse] + _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) + _ <- responses.offerAll(List(Left(None), Left(None), Right(WebhookHttpResponse(200)))) + _ <- ZIO.scoped { + WebhookServer.start *> { + for { + _ <- TestWebhookRepo.setWebhook(webhook) + _ <- TestWebhookEventRepo.createEvent(event) + _ <- requests.takeN(2) + } yield () + } + } + _ <- ZIO.scoped(WebhookServer.start *> requests.take.forkScoped) + } yield assertCompletes + } + } + }, + test("resumes timeout duration for retries") { + val webhook = singleWebhook(id = 0, WebhookStatus.Enabled, WebhookDeliveryMode.SingleAtLeastOnce) + val event = WebhookEvent( + WebhookEventKey(WebhookEventId(0), WebhookId(0)), + WebhookEventStatus.New, + "event content", + plaintextContentHeaders, + None + ) + ZIO.scoped { + (TestWebhookHttpClient.getRequests zip TestWebhookRepo.subscribeToWebhooks).flatMap { + case (requests, webhooks) => + for { + responses <- Queue.unbounded[StubResponse] + _ <- TestWebhookHttpClient.setResponse(_ => Some(responses)) + _ <- responses.offerAll(List(Left(None), Left(None), Right(WebhookHttpResponse(200)))) + _ <- ZIO.scoped { + WebhookServer.start *> { + for { + _ <- TestWebhookRepo.setWebhook(webhook) + _ <- TestWebhookEventRepo.createEvent(event) + _ <- requests.takeN(2) + _ <- TestClock.adjust(3.days) + } yield () + } + } + lastStatus <- ZIO.scoped { + WebhookServer.start *> { + for { + _ <- TestClock.adjust(4.days) + lastStatus <- webhooks.takeN(2).map(_.last.status) + _ <- requests.take + } yield lastStatus + } + } + + } yield assert(lastStatus)(isSome(isSubtype[WebhookStatus.Unavailable](anything))) + } + } + }, + test("clears persisted state after loading") { + for { + _ <- ZIO.scoped(WebhookServer.start) + persistedState <- ZIO.serviceWithZIO[WebhookStateRepo](_.loadState) + _ <- ZIO.scoped { + WebhookServer.start *> ZIO.serviceWithZIO[WebhookStateRepo](_.loadState).repeatUntil(_.isEmpty) + } + } yield assert(persistedState)(isSome(anything)) } - }, - testM("clears persisted state after loading") { - for { - _ <- WebhookServer.start.useNow - persistedState <- ZIO.serviceWith[WebhookStateRepo](_.loadState) - _ <- WebhookServer.start.use_( - ZIO.serviceWith[WebhookStateRepo](_.loadState).repeatUntil(_.isEmpty) - ) - } yield assert(persistedState)(isSome(anything)) - } - // TODO: write unit tests for persistent retry backoff when needed - ) - ).injectSome[TestEnvironment](mockEnv, WebhookServerConfig.default) - ) @@ timeout(20.seconds) + // TODO: write unit tests for persistent retry backoff when needed + ) + ).provide(mockEnv, WebhookServerConfig.default) + ) + ) @@ timeout(10.seconds) } object WebhookServerSpecUtil { @@ -913,19 +929,19 @@ object WebhookServerSpecUtil { val jsonPayloadPattern: String = """(?:\[\{\"event\":\"payload\d+\"}(?:,\{\"event\":\"payload\d+\"})*\])""" - type MockEnv = Has[WebhookEventRepo] - with Has[TestWebhookEventRepo] - with Has[WebhookRepo] - with Has[TestWebhookRepo] - with Has[WebhookStateRepo] - with Has[TestWebhookHttpClient] - with Has[WebhookHttpClient] - with Has[WebhooksProxy] - with Has[SerializePayload] - - lazy val mockEnv: ZLayer[Clock with Has[WebhookServerConfig], Nothing, MockEnv] = + type MockEnv = WebhookEventRepo + with TestWebhookEventRepo + with WebhookRepo + with TestWebhookRepo + with WebhookStateRepo + with TestWebhookHttpClient + with WebhookHttpClient + with WebhooksProxy + with SerializePayload + + lazy val mockEnv: URLayer[Any, MockEnv] = ZLayer - .fromSomeMagic[Clock with Has[WebhookServerConfig], MockEnv]( + .make[MockEnv]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookEventRepo.test, @@ -944,10 +960,10 @@ object WebhookServerSpecUtil { case object Requests extends ScenarioInterest[WebhookHttpRequest] case object Webhooks extends ScenarioInterest[WebhookUpdate] - final def dequeueFor[A](scenarioInterest: ScenarioInterest[A]): URManaged[SpecEnv, Dequeue[A]] = + final def dequeueFor[A](scenarioInterest: ScenarioInterest[A]): URIO[Scope with SpecEnv, Dequeue[A]] = scenarioInterest match { case ScenarioInterest.Errors => - ZManaged.service[WebhookServer].flatMap(_.subscribeToErrors) + ZIO.serviceWithZIO[WebhookServer](_.subscribeToErrors) case ScenarioInterest.Events => TestWebhookEventRepo.subscribeToEvents case ScenarioInterest.Requests => @@ -967,19 +983,19 @@ object WebhookServerSpecUtil { None ) - type SpecEnv = Has[WebhookEventRepo] - with Has[TestWebhookEventRepo] - with Has[WebhookRepo] - with Has[TestWebhookRepo] - with Has[WebhookStateRepo] - with Has[TestWebhookHttpClient] - with Has[WebhookHttpClient] - with Has[WebhookServer] - with Has[WebhooksProxy] - - lazy val specEnv: URLayer[Clock with Console with Has[WebhookServerConfig], SpecEnv] = + type SpecEnv = WebhookEventRepo + with TestWebhookEventRepo + with WebhookRepo + with TestWebhookRepo + with WebhookStateRepo + with TestWebhookHttpClient + with WebhookHttpClient + with WebhookServer + with WebhooksProxy + + lazy val specEnv: URLayer[WebhookServerConfig, SpecEnv] = ZLayer - .fromSomeMagic[Clock with Console with Has[WebhookServerConfig], SpecEnv]( + .makeSome[WebhookServerConfig, SpecEnv]( InMemoryWebhookStateRepo.live, JsonPayloadSerialization.live, TestWebhookEventRepo.test, @@ -997,15 +1013,17 @@ object WebhookServerSpecUtil { events: Iterable[WebhookEvent], scenarioInterest: ScenarioInterest[A] )( - assertion: Dequeue[A] => URIO[TestClock, TestResult] - ): URIO[SpecEnv with TestClock with Has[WebhookServer] with Clock, TestResult] = - ScenarioInterest.dequeueFor(scenarioInterest).map(assertion).flatMap(_.forkManaged).use { testFiber => - for { - _ <- TestWebhookHttpClient.setResponse(stubResponses) - _ <- ZIO.foreach_(webhooks)(TestWebhookRepo.setWebhook) - _ <- ZIO.foreach_(events)(TestWebhookEventRepo.createEvent) - testResult <- testFiber.join - } yield testResult + assertion: Dequeue[A] => UIO[TestResult] + ): URIO[SpecEnv with WebhookServer, TestResult] = + ZIO.scoped { + ScenarioInterest.dequeueFor(scenarioInterest).map(assertion).flatMap(_.forkScoped).flatMap { testFiber => + for { + _ <- TestWebhookHttpClient.setResponse(stubResponses) + _ <- ZIO.foreachDiscard(webhooks)(TestWebhookRepo.setWebhook) + _ <- ZIO.foreachDiscard(events)(TestWebhookEventRepo.createEvent) + testResult <- testFiber.join + } yield testResult + } } def webhooksTestScenario[A]( @@ -1014,17 +1032,19 @@ object WebhookServerSpecUtil { events: Iterable[WebhookEvent], scenarioInterest: ScenarioInterest[A] )( - assertion: (Dequeue[A], Queue[StubResponse]) => URIO[SpecEnv with TestClock with TestConsole, TestResult] - ): URIO[SpecEnv with TestClock with TestConsole with Has[WebhookServer] with Clock, TestResult] = - ScenarioInterest.dequeueFor(scenarioInterest).use { dequeue => - for { - responseQueue <- Queue.bounded[StubResponse](1) - testFiber <- assertion(dequeue, responseQueue).fork - _ <- TestWebhookHttpClient.setResponse(_ => Some(responseQueue)) - _ <- ZIO.foreach_(webhooks)(TestWebhookRepo.setWebhook) - _ <- ZIO.foreach_(events)(TestWebhookEventRepo.createEvent) - _ <- initialStubResponses.run(ZSink.fromQueue(responseQueue)).fork - testResult <- testFiber.join - } yield testResult + assertion: (Dequeue[A], Queue[StubResponse]) => URIO[SpecEnv, TestResult] + ): URIO[SpecEnv with WebhookServer, TestResult] = + ZIO.scoped { + ScenarioInterest.dequeueFor(scenarioInterest).flatMap { dequeue => + for { + responseQueue <- Queue.bounded[StubResponse](1) + testFiber <- assertion(dequeue, responseQueue).forkScoped + _ <- TestWebhookHttpClient.setResponse(_ => Some(responseQueue)) + _ <- ZIO.foreachDiscard(webhooks)(TestWebhookRepo.setWebhook) + _ <- ZIO.foreachDiscard(events)(TestWebhookEventRepo.createEvent) + _ <- initialStubResponses.run(ZSink.fromQueue(responseQueue)).forkScoped + testResult <- testFiber.join + } yield testResult + } } } diff --git a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookEventRepo.scala b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookEventRepo.scala index da287be2..38e66a1a 100644 --- a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookEventRepo.scala +++ b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookEventRepo.scala @@ -2,8 +2,9 @@ package zio.webhooks.testkit import zio._ import zio.prelude.NonEmptySet -import zio.stream.UStream +import zio.stream.{ UStream, ZStream } import zio.webhooks._ +import zio.webhooks.internal.DequeueUtils import scala.util.control.NoStackTrace @@ -14,34 +15,34 @@ trait TestWebhookEventRepo { def enqueueNew: UIO[Unit] - def subscribeToEvents: UManaged[Dequeue[WebhookEvent]] + def subscribeToEvents: URIO[Scope, Dequeue[WebhookEvent]] } object TestWebhookEventRepo { // Accessor Methods - def createEvent(event: WebhookEvent): URIO[Has[TestWebhookEventRepo], Unit] = - ZIO.serviceWith(_.createEvent(event)) + def createEvent(event: WebhookEvent): URIO[TestWebhookEventRepo, Unit] = + ZIO.serviceWithZIO(_.createEvent(event)) - def dumpEventIds: URIO[Has[TestWebhookEventRepo], Set[(Long, WebhookEventStatus)]] = - ZIO.serviceWith(_.dumpEventIds) + def dumpEventIds: URIO[TestWebhookEventRepo, Set[(Long, WebhookEventStatus)]] = + ZIO.serviceWithZIO(_.dumpEventIds) - def enqueueNew: URIO[Has[TestWebhookEventRepo], Unit] = - ZIO.serviceWith(_.enqueueNew) + def enqueueNew: URIO[TestWebhookEventRepo, Unit] = + ZIO.serviceWithZIO(_.enqueueNew) - def subscribeToEvents: URManaged[Has[TestWebhookEventRepo], Dequeue[WebhookEvent]] = - ZManaged.service[TestWebhookEventRepo].flatMap(_.subscribeToEvents) + def subscribeToEvents: URIO[Scope with TestWebhookEventRepo, Dequeue[WebhookEvent]] = + ZIO.serviceWithZIO[TestWebhookEventRepo](_.subscribeToEvents) // Layer Definitions - val test: ULayer[Has[WebhookEventRepo] with Has[TestWebhookEventRepo]] = { - for { - ref <- Ref.make(Map.empty[WebhookEventKey, WebhookEvent]) - hub <- Hub.unbounded[WebhookEvent] - impl = TestWebhookEventRepoImpl(ref, hub) - } yield Has.allOf[WebhookEventRepo, TestWebhookEventRepo](impl, impl) - }.toLayerMany + val test: ULayer[WebhookEventRepo with TestWebhookEventRepo] = + ZLayer.fromZIO { + for { + ref <- Ref.make(Map.empty[WebhookEventKey, WebhookEvent]) + hub <- Hub.unbounded[WebhookEvent] + } yield TestWebhookEventRepoImpl(ref, hub) + } } final private case class TestWebhookEventRepoImpl( @@ -57,10 +58,10 @@ final private case class TestWebhookEventRepoImpl( ref.get.map(_.map { case (key, ev) => (key.eventId.value, ev.status) }.toSet) def enqueueNew: UIO[Unit] = - ref.get.flatMap(map => ZIO.foreach_(map.values.filter(_.isNew))(hub.publish)) + ref.get.flatMap(map => ZIO.foreachDiscard(map.values.filter(_.isNew))(hub.publish)) def recoverEvents: UStream[WebhookEvent] = - UStream.fromIterableM(ref.get.map(_.values.filter(_.isDelivering))) + ZStream.fromIterableZIO(ref.get.map(_.values.filter(_.isDelivering))) def setAllAsFailedByWebhookId(webhookId: WebhookId): UIO[Unit] = for { @@ -104,9 +105,9 @@ final private case class TestWebhookEventRepoImpl( } } yield () - def subscribeToEvents: UManaged[Dequeue[WebhookEvent]] = + def subscribeToEvents: URIO[Scope, Dequeue[WebhookEvent]] = hub.subscribe - def subscribeToNewEvents: UManaged[Dequeue[WebhookEvent]] = - subscribeToEvents.map(_.filterOutput(_.isNew)) + def subscribeToNewEvents: URIO[Scope, Dequeue[WebhookEvent]] = + subscribeToEvents.map(d => DequeueUtils.filterOutput[WebhookEvent](d, _.isNew)) } diff --git a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookHttpClient.scala b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookHttpClient.scala index cda787b9..bd7d0bf5 100644 --- a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookHttpClient.scala +++ b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookHttpClient.scala @@ -10,7 +10,7 @@ import java.io.IOException // TODO: scaladoc trait TestWebhookHttpClient { - def requests: UManaged[Dequeue[WebhookHttpRequest]] + def requests: URIO[Scope, Dequeue[WebhookHttpRequest]] def setResponse(f: WebhookHttpRequest => StubResponses): UIO[Unit] } @@ -18,21 +18,21 @@ trait TestWebhookHttpClient { object TestWebhookHttpClient { // Accessors - def getRequests: URManaged[Has[TestWebhookHttpClient], Dequeue[WebhookHttpRequest]] = - ZManaged.service[TestWebhookHttpClient].flatMap(_.requests) + def getRequests: URIO[Scope with TestWebhookHttpClient, Dequeue[WebhookHttpRequest]] = + ZIO.serviceWithZIO[TestWebhookHttpClient](_.requests) def setResponse( f: WebhookHttpRequest => StubResponses - ): URIO[Has[TestWebhookHttpClient], Unit] = - ZIO.serviceWith(_.setResponse(f)) + ): URIO[TestWebhookHttpClient, Unit] = + ZIO.serviceWithZIO(_.setResponse(f)) - val test: ULayer[Has[TestWebhookHttpClient] with Has[WebhookHttpClient]] = { + val test: ULayer[TestWebhookHttpClient with WebhookHttpClient] = ZLayer.scoped { for { - ref <- Ref.makeManaged[WebhookHttpRequest => StubResponses](_ => None) - queue <- Hub.unbounded[WebhookHttpRequest].toManaged_ + ref <- Ref.make[WebhookHttpRequest => StubResponses](_ => None) + queue <- Hub.unbounded[WebhookHttpRequest] impl = TestWebhookHttpClientImpl(ref, queue) - } yield Has.allOf[TestWebhookHttpClient, WebhookHttpClient](impl, impl) - }.toLayerMany + } yield impl + } type StubResponse = Either[Option[BadWebhookUrlError], WebhookHttpResponse] type StubResponses = Option[Queue[StubResponse]] @@ -44,7 +44,7 @@ final case class TestWebhookHttpClientImpl( ) extends WebhookHttpClient with TestWebhookHttpClient { - def requests: UManaged[Dequeue[WebhookHttpRequest]] = + def requests: URIO[Scope, Dequeue[WebhookHttpRequest]] = received.subscribe def post(request: WebhookHttpRequest): IO[HttpPostError, WebhookHttpResponse] = diff --git a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookRepo.scala b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookRepo.scala index 6b5eb20d..38890435 100644 --- a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookRepo.scala +++ b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookRepo.scala @@ -2,7 +2,7 @@ package zio.webhooks.testkit import zio._ import zio.prelude.NonEmptySet -import zio.stream.UStream +import zio.stream.{ UStream, ZStream } import zio.webhooks.WebhookUpdate._ import zio.webhooks.WebhooksProxy.UpdateMode import zio.webhooks._ @@ -22,30 +22,30 @@ trait TestWebhookRepo { /** * Subscribes to webhook updates. */ - def subscribeToWebhookUpdates: UManaged[Dequeue[WebhookUpdate]] + def subscribeToWebhookUpdates: URIO[Scope, Dequeue[WebhookUpdate]] } object TestWebhookRepo { - def removeWebhook(webhookId: WebhookId): URIO[Has[TestWebhookRepo], Unit] = - ZIO.serviceWith(_.removeWebhook(webhookId)) + def removeWebhook(webhookId: WebhookId): URIO[TestWebhookRepo, Unit] = + ZIO.serviceWithZIO(_.removeWebhook(webhookId)) - val test: ULayer[Has[TestWebhookRepo] with Has[WebhookRepo]] = { - for { - ref <- Ref.make(Map.empty[WebhookId, Webhook]) - hub <- Hub.bounded[WebhookUpdate](256) - impl = TestWebhookRepoImpl(ref, hub) - } yield Has.allOf[TestWebhookRepo, WebhookRepo](impl, impl) - }.toLayerMany + val test: ULayer[TestWebhookRepo with WebhookRepo] = + ZLayer.fromZIO { + for { + ref <- Ref.make(Map.empty[WebhookId, Webhook]) + hub <- Hub.bounded[WebhookUpdate](256) + } yield TestWebhookRepoImpl(ref, hub) + } - val subscriptionUpdateMode: URLayer[Has[TestWebhookRepo], Has[UpdateMode]] = - ZIO.service[TestWebhookRepo].map(repo => UpdateMode.Subscription(repo.getWebhookUpdates)).toLayer + val subscriptionUpdateMode: URLayer[TestWebhookRepo, UpdateMode] = + ZLayer.fromZIO(ZIO.service[TestWebhookRepo].map(repo => UpdateMode.Subscription(repo.getWebhookUpdates))) - def setWebhook(webhook: Webhook): URIO[Has[TestWebhookRepo], Unit] = - ZIO.serviceWith(_.setWebhook(webhook)) + def setWebhook(webhook: Webhook): URIO[TestWebhookRepo, Unit] = + ZIO.serviceWithZIO(_.setWebhook(webhook)) - def subscribeToWebhooks: URManaged[Has[TestWebhookRepo], Dequeue[WebhookUpdate]] = - ZManaged.service[TestWebhookRepo].flatMap(_.subscribeToWebhookUpdates) + def subscribeToWebhooks: URIO[Scope with TestWebhookRepo, Dequeue[WebhookUpdate]] = + ZIO.serviceWithZIO[TestWebhookRepo](_.subscribeToWebhookUpdates) } final private case class TestWebhookRepoImpl(ref: Ref[Map[WebhookId, Webhook]], hub: Hub[WebhookUpdate]) @@ -56,7 +56,7 @@ final private case class TestWebhookRepoImpl(ref: Ref[Map[WebhookId, Webhook]], ref.get.map(_(webhookId)) def getWebhookUpdates: UStream[WebhookUpdate] = - UStream.fromHub(hub) + ZStream.fromHub(hub) def pollWebhooksById(webhookIds: NonEmptySet[WebhookId]): UIO[Map[WebhookId, Webhook]] = ref.get.map(_.filter { case (id, _) => webhookIds.contains(id) }) @@ -77,6 +77,6 @@ final private case class TestWebhookRepoImpl(ref: Ref[Map[WebhookId, Webhook]], _ <- hub.publish(WebhookChanged(updatedWebhook)) } yield () - def subscribeToWebhookUpdates: UManaged[Dequeue[WebhookUpdate]] = + def subscribeToWebhookUpdates: URIO[Scope, Dequeue[WebhookUpdate]] = hub.subscribe } diff --git a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookStateRepo.scala b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookStateRepo.scala index 203b3c19..9155a5aa 100644 --- a/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookStateRepo.scala +++ b/webhooks-testkit/src/main/scala/zio/webhooks/testkit/TestWebhookStateRepo.scala @@ -11,6 +11,5 @@ final case class TestWebhookStateRepo(ref: Ref[Option[String]]) extends WebhookS } object TestWebhookStateRepo { - val test: ULayer[Has[WebhookStateRepo]] = - Ref.make(Option.empty[String]).map(TestWebhookStateRepo(_)).toLayer + val test: ULayer[WebhookStateRepo] = ZLayer.fromZIO(Ref.make(Option.empty[String]).map(TestWebhookStateRepo(_))) } diff --git a/webhooks/src/main/scala/zio/webhooks/WebhookEventRepo.scala b/webhooks/src/main/scala/zio/webhooks/WebhookEventRepo.scala index c542c097..09a12052 100644 --- a/webhooks/src/main/scala/zio/webhooks/WebhookEventRepo.scala +++ b/webhooks/src/main/scala/zio/webhooks/WebhookEventRepo.scala @@ -34,5 +34,5 @@ trait WebhookEventRepo { /** * Used by the server to subscribe to new webhook events. */ - def subscribeToNewEvents: UManaged[Dequeue[WebhookEvent]] + def subscribeToNewEvents: URIO[Scope, Dequeue[WebhookEvent]] } diff --git a/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala b/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala index db710c6a..9a97e27b 100644 --- a/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala +++ b/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala @@ -1,13 +1,11 @@ package zio.webhooks -import zio._ -import zio.clock.Clock -import zio.console.Console import zio.json._ -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks.WebhookDeliverySemantics._ import zio.webhooks.WebhookError._ import zio.webhooks.internal._ +import zio.{ Clock, Console, _ } /** * A [[WebhookServer]] is a stateful server that subscribes to [[WebhookEvent]]s and reliably @@ -16,16 +14,16 @@ import zio.webhooks.internal._ * [[WebhookStatus.Unavailable]] since some `Instant`. Dispatches are batched if and * only if a batching capacity is configured ''and'' a webhook's delivery batching is * [[WebhookDeliveryBatching.Batched]]. When `shutdown` is called, a `shutdownSignal` is sent - * which lets all dispatching work finish. Finally, the retry state is persisted, which allows + * which lets all dispatching work finish. A shutdownLatch Finally, the retry state is persisted, which allows * retries to resume after server restarts. * * A `live` server layer is provided in the companion object for convenience and proper resource * management, ensuring `shutdown` is called by the finalizer. */ final class WebhookServer private ( - private val clock: Clock.Service, + private val clock: Clock, private val config: WebhookServerConfig, - private val console: Console.Service, + private val console: Console, private val eventRepo: WebhookEventRepo, private val httpClient: WebhookHttpClient, private val stateRepo: WebhookStateRepo, @@ -37,7 +35,7 @@ final class WebhookServer private ( private val shutdownLatch: CountDownLatch, private val shutdownSignal: Promise[Nothing, Unit], private val webhooksProxy: WebhooksProxy, - private val webhookQueues: RefM[Map[WebhookId, Queue[WebhookEvent]]] + private val webhookQueues: Ref.Synchronized[Map[WebhookId, Queue[WebhookEvent]]] ) { /** @@ -80,14 +78,14 @@ final class WebhookServer private ( private def enqueueNewEvent(batchDispatcher: Option[BatchDispatcher], event: WebhookEvent): UIO[Unit] = { val webhookId = event.key.webhookId for { - webhookQueue <- webhookQueues.modify { map => + webhookQueue <- webhookQueues.modifyZIO { map => map.get(webhookId) match { case Some(queue) => - UIO((queue, map)) + ZIO.succeed((queue, map)) case None => for { queue <- Queue.dropping[WebhookEvent](config.webhookQueueCapacity) - _ <- UStream + _ <- ZStream .fromQueue(queue) .foreach(handleNewEvent(batchDispatcher, _)) .onError(fatalPromise.fail) @@ -97,7 +95,7 @@ final class WebhookServer private ( } accepted <- webhookQueue.offer(event) _ <- console - .putStrLn( + .printLine( s"""Slow webhook detected with id "${webhookId.value}"""" + " and event " + event ) @@ -111,11 +109,10 @@ final class WebhookServer private ( for { isRetrying <- retryController.isActive(webhookId) webhook <- webhooksProxy.getWebhookById(webhookId) - isShutDown <- shutdownSignal.isDone attemptRetryEnqueue = retryController.enqueueRetry(event).race(shutdownSignal.await) attemptDelivery = eventRepo.setEventStatus(event.key, WebhookEventStatus.Delivering) *> (if (isRetrying && webhook.deliveryMode.semantics == AtLeastOnce) - attemptRetryEnqueue.unless(isShutDown) + attemptRetryEnqueue.unlessZIO(shutdownSignal.isDone) else deliverEvent(batchDispatcher, event, webhook)) _ <- attemptDelivery.when(webhook.isEnabled) @@ -143,23 +140,23 @@ final class WebhookServer private ( * The server waits for event recovery and new event subscription to get ready, signalling that * the server is ready to accept events. */ - private def start: UManaged[Unit] = + private def start: URIO[Scope, Unit] = for { f1 <- startRetryMonitoring f2 <- startEventRecovery f3 <- startNewEventSubscription - f4 <- fatalPromise.await.forkManaged + f4 <- fatalPromise.await.forkScoped _ <- ZIO .raceAll(f1.await, List(f2.await, f3.await, f4.await)) .flatMap { case Exit.Failure(cause) => val flatCause = cause.flatten - errorHub.publish(FatalError(flatCause)).when(flatCause.died || flatCause.failed) + errorHub.publish(FatalError(flatCause)).when(flatCause.isDie || flatCause.isFailure) case _ => ZIO.unit } - .forkManaged - _ <- startupLatch.await.toManaged_ + .forkScoped + _ <- startupLatch.await } yield () /** @@ -174,7 +171,7 @@ final class WebhookServer private ( for { _ <- stateRepo.loadState.flatMap( ZIO - .foreach_(_)(jsonState => + .foreachDiscard(_)(jsonState => ZIO .fromEither(jsonState.fromJson[PersistentRetries]) .mapError(message => InvalidStateError(jsonState, message)) @@ -185,19 +182,19 @@ final class WebhookServer private ( f <- mergeShutdown(eventRepo.recoverEvents, shutdownSignal) .foreach(retryController.enqueueRetry) .ensuring(shutdownLatch.countDown) - .fork + .forkScoped } yield f - } <* startupLatch.countDown).toManaged_ + } <* startupLatch.countDown) /** * Starts server subscription to new [[WebhookEvent]]s. Counts down on the `startupLatch`, * signalling that it's ready to accept new events. */ private def startNewEventSubscription = - eventRepo.subscribeToNewEvents.mapM { eventDequeue => + eventRepo.subscribeToNewEvents.flatMap { eventDequeue => for { // signal that the server is ready to accept new webhook events - _ <- eventDequeue.poll *> startupLatch.countDown + _ <- eventDequeue.isEmpty *> startupLatch.countDown deliverFunc = (dispatch: WebhookDispatch, _: Queue[WebhookEvent]) => deliver(dispatch) batchDispatcher <- ZIO.foreach(config.batchingCapacity)( BatchDispatcher @@ -206,21 +203,20 @@ final class WebhookServer private ( ) handleEvent = for { event <- (shutdownSignal.await raceEither eventDequeue.take).map(_.toOption) - _ <- ZIO.foreach_(event)(enqueueNewEvent(batchDispatcher, _)) + _ <- ZIO.foreachDiscard(event)(enqueueNewEvent(batchDispatcher, _)) } yield () - isShutdown <- shutdownSignal.isDone _ <- handleEvent - .repeatUntilM(_ => shutdownSignal.isDone) - .unless(isShutdown) + .repeatUntilZIO(_ => shutdownSignal.isDone) + .unlessZIO(shutdownSignal.isDone) .ensuring(shutdownLatch.countDown) } yield () - }.fork + }.forkScoped /** * Listens for new retries and starts retrying delivers to a webhook. */ private def startRetryMonitoring = - retryController.start.ensuring(shutdownLatch.countDown).forkManaged + retryController.start.ensuring(shutdownLatch.countDown).forkScoped /** * Waits until all work in progress is finished, persists retries, then shuts down. @@ -237,7 +233,7 @@ final class WebhookServer private ( * Exposes a way to listen for [[WebhookError]]s. This provides clients a way to handle server * errors that would otherwise just fail silently. */ - def subscribeToErrors: UManaged[Dequeue[WebhookError]] = + def subscribeToErrors: URIO[Scope, Dequeue[WebhookError]] = errorHub.subscribe } @@ -246,25 +242,25 @@ object WebhookServer { /** * Creates a server, pulling dependencies from the environment then initializing internal state. */ - private def create: URManaged[Env, WebhookServer] = + private def create: URIO[Scope with Env, WebhookServer] = for { - clock <- ZManaged.service[Clock.Service] - config <- ZManaged.service[WebhookServerConfig] - console <- ZManaged.service[Console.Service] - eventRepo <- ZManaged.service[WebhookEventRepo] - httpClient <- ZManaged.service[WebhookHttpClient] - serializePayload <- ZManaged.service[SerializePayload] - webhookState <- ZManaged.service[WebhookStateRepo] - webhooksProxy <- ZManaged.service[WebhooksProxy] - errorHub <- Hub.sliding[WebhookError](config.errorSlidingCapacity).toManaged_ - fatalPromise <- Promise.makeManaged[Cause[Nothing], Nothing] - retryDispatchers <- RefM.makeManaged(Map.empty[WebhookId, RetryDispatcher]) - retryInputQueue <- Queue.bounded[WebhookEvent](1).toManaged_ - retryStates <- RefM.makeManaged(Map.empty[WebhookId, RetryState]) + clock <- ZIO.clock + config <- ZIO.service[WebhookServerConfig] + console <- ZIO.console + eventRepo <- ZIO.service[WebhookEventRepo] + httpClient <- ZIO.service[WebhookHttpClient] + serializePayload <- ZIO.service[SerializePayload] + webhookState <- ZIO.service[WebhookStateRepo] + webhooksProxy <- ZIO.service[WebhooksProxy] + errorHub <- Hub.sliding[WebhookError](config.errorSlidingCapacity) + fatalPromise <- Promise.make[Cause[Nothing], Nothing] + retryDispatchers <- Ref.Synchronized.make(Map.empty[WebhookId, RetryDispatcher]) + retryInputQueue <- Queue.bounded[WebhookEvent](1) + retryStates <- Ref.Synchronized.make(Map.empty[WebhookId, RetryState]) // startup & shutdown sync points: new event sub + event recovery + retrying - startupLatch <- CountDownLatch.make(3).toManaged_ - shutdownLatch <- CountDownLatch.make(3).toManaged_ - shutdownSignal <- Promise.makeManaged[Nothing, Unit] + startupLatch <- CountDownLatch.make(3) + shutdownLatch <- CountDownLatch.make(3) + shutdownSignal <- Promise.make[Nothing, Unit] retries = RetryController( clock, config, @@ -281,7 +277,7 @@ object WebhookServer { startupLatch, webhooksProxy ) - webhookQueues <- RefM.makeManaged(Map.empty[WebhookId, Queue[WebhookEvent]]) + webhookQueues <- Ref.Synchronized.make(Map.empty[WebhookId, Queue[WebhookEvent]]) } yield new WebhookServer( clock, config, @@ -300,31 +296,29 @@ object WebhookServer { webhookQueues ) - type Env = Has[SerializePayload] - with Has[WebhooksProxy] - with Has[WebhookStateRepo] - with Has[WebhookEventRepo] - with Has[WebhookHttpClient] - with Has[WebhookServerConfig] - with Clock - with Console + type Env = SerializePayload + with WebhooksProxy + with WebhookStateRepo + with WebhookEventRepo + with WebhookHttpClient + with WebhookServerConfig - def getErrors: URManaged[Has[WebhookServer], Dequeue[WebhookError]] = - ZManaged.service[WebhookServer].flatMap(_.subscribeToErrors) + def getErrors: URIO[Scope with WebhookServer, Dequeue[WebhookError]] = + ZIO.service[WebhookServer].flatMap(_.subscribeToErrors) /** * Creates and starts a managed server, ensuring shutdown on release. */ - val live: URLayer[WebhookServer.Env, Has[WebhookServer]] = - WebhookServer.start.toLayer + val live: URLayer[WebhookServer.Env, WebhookServer] = + ZLayer.scoped(WebhookServer.start) - def start: URManaged[Env, WebhookServer] = + def start: URIO[Scope with Env, WebhookServer] = for { server <- create _ <- server.start - _ <- ZManaged.finalizer(server.shutdown) + _ <- ZIO.addFinalizer(server.shutdown) } yield server - def subscribeToErrors: URManaged[Has[WebhookServer], Dequeue[WebhookError]] = - ZManaged.service[WebhookServer].flatMap(_.subscribeToErrors) + def subscribeToErrors: URIO[Scope with WebhookServer, Dequeue[WebhookError]] = + ZIO.service[WebhookServer].flatMap(_.subscribeToErrors) } diff --git a/webhooks/src/main/scala/zio/webhooks/WebhookServerConfig.scala b/webhooks/src/main/scala/zio/webhooks/WebhookServerConfig.scala index e7b1347b..057a4279 100644 --- a/webhooks/src/main/scala/zio/webhooks/WebhookServerConfig.scala +++ b/webhooks/src/main/scala/zio/webhooks/WebhookServerConfig.scala @@ -1,7 +1,6 @@ package zio.webhooks import zio._ -import zio.duration._ import java.time.Duration @@ -23,7 +22,7 @@ final case class WebhookServerConfig( ) object WebhookServerConfig { - val default: ULayer[Has[WebhookServerConfig]] = ZLayer.succeed( + val default: ULayer[WebhookServerConfig] = ZLayer.succeed( WebhookServerConfig( errorSlidingCapacity = 128, maxRequestsInFlight = 256, @@ -39,8 +38,8 @@ object WebhookServerConfig { ) ) - val defaultWithBatching: ULayer[Has[WebhookServerConfig]] = - default.map(serverConfig => Has(serverConfig.get.copy(batchingCapacity = Some(128)))) + val defaultWithBatching: ULayer[WebhookServerConfig] = + default.update(serverConfig => serverConfig.copy(batchingCapacity = Some(128))) /** * Retry configuration settings for each webhook. diff --git a/webhooks/src/main/scala/zio/webhooks/WebhookStateRepo.scala b/webhooks/src/main/scala/zio/webhooks/WebhookStateRepo.scala index ca41eb53..cdb16ac0 100644 --- a/webhooks/src/main/scala/zio/webhooks/WebhookStateRepo.scala +++ b/webhooks/src/main/scala/zio/webhooks/WebhookStateRepo.scala @@ -23,9 +23,9 @@ trait WebhookStateRepo { object WebhookStateRepo { // accessors - def loadState: URIO[Has[WebhookStateRepo], Option[String]] = - ZIO.serviceWith[WebhookStateRepo](_.loadState) + def loadState: URIO[WebhookStateRepo, Option[String]] = + ZIO.serviceWithZIO[WebhookStateRepo](_.loadState) - def saveState(state: String): URIO[Has[WebhookStateRepo], Unit] = - ZIO.serviceWith[WebhookStateRepo](_.setState(state)) + def saveState(state: String): URIO[WebhookStateRepo, Unit] = + ZIO.serviceWithZIO[WebhookStateRepo](_.setState(state)) } diff --git a/webhooks/src/main/scala/zio/webhooks/WebhooksProxy.scala b/webhooks/src/main/scala/zio/webhooks/WebhooksProxy.scala index da0093ca..4f8952a0 100644 --- a/webhooks/src/main/scala/zio/webhooks/WebhooksProxy.scala +++ b/webhooks/src/main/scala/zio/webhooks/WebhooksProxy.scala @@ -1,8 +1,7 @@ package zio.webhooks import zio._ -import zio.clock.Clock -import zio.duration.Duration +import zio.Duration import zio.prelude.NonEmptySet import zio.stream.UStream import zio.webhooks.WebhooksProxy.UpdateMode @@ -26,13 +25,13 @@ final case class WebhooksProxy private ( def getWebhookById(webhookId: WebhookId): UIO[Webhook] = for { option <- cache.get.map(_.get(webhookId)) - webhook <- option.map(UIO(_)).getOrElse(webhookRepo.getWebhookById(webhookId)) + webhook <- option.map(ZIO.succeed(_)).getOrElse(webhookRepo.getWebhookById(webhookId)) } yield webhook private def pollForUpdates(pollingFunction: PollingFunction): UIO[Unit] = for { keys <- cache.get.map(map => NonEmptySet.fromIterableOption(map.keys)) - _ <- ZIO.foreach_(keys)(pollingFunction(_).flatMap(cache.set)) + _ <- ZIO.foreachDiscard(keys)(pollingFunction(_).flatMap(cache.set)) } yield () def setWebhookStatus(webhookId: WebhookId, status: WebhookStatus): UIO[Unit] = @@ -43,7 +42,7 @@ final case class WebhooksProxy private ( _ <- webhookRepo.setWebhookStatus(webhookId, status) } yield () - private[webhooks] def start: URIO[Clock, Any] = + private[webhooks] def start: UIO[Any] = updateMode match { case UpdateMode.Polling(pollingInterval, pollingFunction) => pollForUpdates(pollingFunction).repeat(Schedule.fixed(pollingInterval)) @@ -59,12 +58,14 @@ final case class WebhooksProxy private ( } object WebhooksProxy { - type Env = Has[WebhookRepo] with Has[UpdateMode] with Clock + type Env = WebhookRepo with UpdateMode - val live: URLayer[Env, Has[WebhooksProxy]] = - ZIO.services[WebhookRepo, UpdateMode].flatMap((start _).tupled).toLayer + val live: URLayer[Env, WebhooksProxy] = + ZLayer.fromZIO { + ZIO.environmentWithZIO[Env](env => start(env.get[WebhookRepo], env.get[UpdateMode])) + } - private def start(webhookRepo: WebhookRepo, updateMode: UpdateMode): URIO[Clock, WebhooksProxy] = + private def start(webhookRepo: WebhookRepo, updateMode: UpdateMode): UIO[WebhooksProxy] = for { cache <- Ref.make(Map.empty[WebhookId, Webhook]) proxy = WebhooksProxy(cache, webhookRepo, updateMode) diff --git a/webhooks/src/main/scala/zio/webhooks/backends/InMemoryWebhookStateRepo.scala b/webhooks/src/main/scala/zio/webhooks/backends/InMemoryWebhookStateRepo.scala index 9f06b8e4..41b6e895 100644 --- a/webhooks/src/main/scala/zio/webhooks/backends/InMemoryWebhookStateRepo.scala +++ b/webhooks/src/main/scala/zio/webhooks/backends/InMemoryWebhookStateRepo.scala @@ -11,6 +11,6 @@ final case class InMemoryWebhookStateRepo private (ref: Ref[Option[String]]) ext } object InMemoryWebhookStateRepo { - val live: ULayer[Has[WebhookStateRepo]] = - Ref.make[Option[String]](Option.empty).map(InMemoryWebhookStateRepo(_)).toLayer + val live: ULayer[WebhookStateRepo] = + ZLayer.fromZIO(Ref.make[Option[String]](Option.empty).map(InMemoryWebhookStateRepo(_))) } diff --git a/webhooks/src/main/scala/zio/webhooks/backends/JsonPayloadSerialization.scala b/webhooks/src/main/scala/zio/webhooks/backends/JsonPayloadSerialization.scala index ea6b187e..52989267 100644 --- a/webhooks/src/main/scala/zio/webhooks/backends/JsonPayloadSerialization.scala +++ b/webhooks/src/main/scala/zio/webhooks/backends/JsonPayloadSerialization.scala @@ -1,11 +1,11 @@ package zio.webhooks.backends -import zio.{ Chunk, Has, ULayer, ZLayer } +import zio.{ Chunk, ULayer, ZLayer } import zio.webhooks._ object JsonPayloadSerialization { - val live: ULayer[Has[SerializePayload]] = + val live: ULayer[SerializePayload] = ZLayer.succeed { (webhookPayload: WebhookPayload, contentType: Option[WebhookContentMimeType]) => contentType match { case Some(WebhookContentMimeType(contentType)) if contentType.toLowerCase == "application/json" => diff --git a/webhooks/src/main/scala/zio/webhooks/backends/sttp/WebhookSttpClient.scala b/webhooks/src/main/scala/zio/webhooks/backends/sttp/WebhookSttpClient.scala index 6abe2c3f..494cab62 100644 --- a/webhooks/src/main/scala/zio/webhooks/backends/sttp/WebhookSttpClient.scala +++ b/webhooks/src/main/scala/zio/webhooks/backends/sttp/WebhookSttpClient.scala @@ -1,7 +1,9 @@ package zio.webhooks.backends.sttp import _root_.sttp.client3._ -import _root_.sttp.client3.httpclient.zio.{ HttpClientZioBackend, SttpClient } +import _root_.sttp.client3.httpclient.zio.HttpClientZioBackend +import sttp.capabilities +import sttp.capabilities.zio.ZioStreams import sttp.model.Uri import zio._ import zio.webhooks.WebhookError.BadWebhookUrlError @@ -14,7 +16,10 @@ import java.io.IOException * A [[WebhookSttpClient]] provides a [[WebhookHttpClient]] using sttp's ZIO backend, i.e. * `AsyncHttpClientZioBackend`. */ -final case class WebhookSttpClient(sttpClient: SttpClient.Service, permits: Semaphore) extends WebhookHttpClient { +final case class WebhookSttpClient( + sttpClient: SttpBackend[Task, ZioStreams with capabilities.WebSockets], + permits: Semaphore +) extends WebhookHttpClient { def post(webhookRequest: WebhookHttpRequest): IO[HttpPostError, WebhookHttpResponse] = permits.withPermit { @@ -40,11 +45,11 @@ object WebhookSttpClient { /** * A layer built with an STTP ZIO backend with the default settings */ - val live: RLayer[Has[WebhookServerConfig], Has[WebhookHttpClient]] = { + val live: RLayer[WebhookServerConfig, WebhookHttpClient] = ZLayer.scoped { for { - sttpBackend <- HttpClientZioBackend.managed() - capacity <- ZManaged.service[WebhookServerConfig].map(_.maxRequestsInFlight) - permits <- Semaphore.make(capacity.toLong).toManaged_ + sttpBackend <- HttpClientZioBackend.scoped() + capacity <- ZIO.service[WebhookServerConfig].map(_.maxRequestsInFlight) + permits <- Semaphore.make(capacity.toLong) } yield WebhookSttpClient(sttpBackend, permits) - }.toLayer + } } diff --git a/webhooks/src/main/scala/zio/webhooks/internal/BatchDispatcher.scala b/webhooks/src/main/scala/zio/webhooks/internal/BatchDispatcher.scala index 1ddfe3a6..8102f939 100644 --- a/webhooks/src/main/scala/zio/webhooks/internal/BatchDispatcher.scala +++ b/webhooks/src/main/scala/zio/webhooks/internal/BatchDispatcher.scala @@ -8,7 +8,7 @@ import zio.webhooks._ private[webhooks] final class BatchDispatcher private ( private val batchingCapacity: Int, - private val batchQueues: RefM[Map[BatchKey, Queue[WebhookEvent]]], + private val batchQueues: Ref.Synchronized[Map[BatchKey, Queue[WebhookEvent]]], private val deliver: (WebhookDispatch, Queue[WebhookEvent]) => UIO[Unit], private val fatalPromise: Promise[Cause[Nothing], Nothing], private val inputQueue: Queue[WebhookEvent], @@ -36,17 +36,17 @@ private[webhooks] final class BatchDispatcher private ( inputQueue.offer(event).unit def start: UIO[Any] = - mergeShutdown(UStream.fromQueue(inputQueue), shutdownSignal).groupByKey { ev => + mergeShutdown(ZStream.fromQueue(inputQueue), shutdownSignal).groupByKey { ev => val (webhookId, contentType) = ev.webhookIdAndContentType BatchKey(webhookId, contentType) } { case (batchKey, events) => - ZStream.fromEffect( + ZStream.fromZIO( for { - batchQueue <- batchQueues.modify { map => + batchQueue <- batchQueues.modifyZIO { map => map.get(batchKey) match { case Some(queue) => - UIO((queue, map)) + ZIO.succeed((queue, map)) case None => for (queue <- Queue.bounded[WebhookEvent](batchingCapacity)) yield (queue, map + (batchKey -> queue)) @@ -70,7 +70,7 @@ private[webhooks] object BatchDispatcher { webhooks: WebhooksProxy ): UIO[BatchDispatcher] = for { - batchQueue <- RefM.make(Map.empty[BatchKey, Queue[WebhookEvent]]) + batchQueue <- Ref.Synchronized.make(Map.empty[BatchKey, Queue[WebhookEvent]]) inputQueue <- Queue.bounded[WebhookEvent](1) dispatcher = new BatchDispatcher( capacity, diff --git a/webhooks/src/main/scala/zio/webhooks/internal/DequeueUtils.scala b/webhooks/src/main/scala/zio/webhooks/internal/DequeueUtils.scala new file mode 100644 index 00000000..03d5162f --- /dev/null +++ b/webhooks/src/main/scala/zio/webhooks/internal/DequeueUtils.scala @@ -0,0 +1,75 @@ +package zio.webhooks.internal +import zio.{ Chunk, Dequeue, Trace, UIO, ZIO } + +import scala.collection.mutable.ListBuffer + +private[webhooks] object DequeueUtils { + + final def filterOutput[A](d: Dequeue[A], f: A => Boolean): Dequeue[A] = + new Dequeue[A] { + override def awaitShutdown(implicit trace: Trace): UIO[Unit] = d.awaitShutdown + + override def capacity: Int = d.capacity + + override def isShutdown(implicit trace: Trace): UIO[Boolean] = d.isShutdown + + override def shutdown(implicit trace: Trace): UIO[Unit] = d.shutdown + + override def size(implicit trace: Trace): UIO[Int] = d.size + + override def take(implicit trace: Trace): UIO[A] = + d.take.flatMap { b => + if (f(b)) ZIO.succeedNow(b) + else take + } + + override def takeAll(implicit trace: Trace): UIO[Chunk[A]] = + d.takeAll.map(bs => bs.filter(f)) + + override def takeUpTo(max: Int)(implicit trace: Trace): UIO[Chunk[A]] = + ZIO.suspendSucceed { + val buffer = ListBuffer[A]() + def loop(max: Int): UIO[Unit] = + d.takeUpTo(max).flatMap { bs => + if (bs.isEmpty) ZIO.unit + else { + val filtered = bs.filter(f) + + buffer ++= filtered + + val length = filtered.length + if (length == max) ZIO.unit + else loop(max - length) + } + } + loop(max).as(Chunk.fromIterable(buffer)) + } + } + + final def map[A, B](d: Dequeue[A], f: A => B): Dequeue[B] = + new Dequeue[B] { + override def awaitShutdown(implicit trace: Trace): UIO[Unit] = d.awaitShutdown + + override def capacity: Int = d.capacity + + override def isShutdown(implicit trace: Trace): UIO[Boolean] = d.isShutdown + + override def shutdown(implicit trace: Trace): UIO[Unit] = d.shutdown + + override def size(implicit trace: Trace): UIO[Int] = d.size + + override def take(implicit trace: Trace): UIO[B] = d.take.map(f) + + override def takeAll(implicit trace: Trace): UIO[Chunk[B]] = d.takeAll.map(_.map(f)) + + override def takeUpTo(max: Int)(implicit trace: Trace): UIO[Chunk[B]] = d.takeUpTo(max).map(_.map(f)) + } + + implicit final class DequeueOps[A](d: Dequeue[A]) { + final def filterOutput(f: A => Boolean): Dequeue[A] = + DequeueUtils.filterOutput(d, f) + + final def map[B](f: A => B): Dequeue[B] = + DequeueUtils.map(d, f) + } +} diff --git a/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala b/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala index 009ca9cd..62c1c9c7 100644 --- a/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala +++ b/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala @@ -2,7 +2,7 @@ package zio.webhooks.internal import zio._ import zio.prelude.NonEmptySet -import zio.stream.UStream +import zio.stream.ZStream import zio.webhooks._ import java.time.Instant @@ -12,15 +12,15 @@ import java.time.Instant * dispatchers and state for each webhook. */ private[webhooks] final case class RetryController( - private val clock: zio.clock.Clock.Service, + private val clock: zio.Clock, private val config: WebhookServerConfig, private val errorHub: Hub[WebhookError], private val eventRepo: WebhookEventRepo, private val fatalPromise: Promise[Cause[Nothing], Nothing], private val httpClient: WebhookHttpClient, private val inputQueue: Queue[WebhookEvent], - private val retryDispatchers: RefM[Map[WebhookId, RetryDispatcher]], - private val retryStates: RefM[Map[WebhookId, RetryState]], + private val retryDispatchers: Ref.Synchronized[Map[WebhookId, RetryDispatcher]], + private val retryStates: Ref.Synchronized[Map[WebhookId, RetryState]], private val serializePayload: SerializePayload, private val shutdownLatch: CountDownLatch, private val shutdownSignal: Promise[Nothing, Unit], @@ -39,7 +39,7 @@ private[webhooks] final case class RetryController( _ <- retryDispatchers.set(loadedRetries.map { case (id, (dispatcher, _)) => (id, dispatcher) }) _ <- retryStates.set(loadedRetries.map { case (id, (_, retryState)) => (id, retryState) }) _ <- retryDispatchers.get.flatMap { - ZIO.foreach_(_) { + ZIO.foreachDiscard(_) { case (_, retryDispatcher) => retryDispatcher.start *> retryDispatcher.activateWithTimeout } @@ -100,52 +100,52 @@ private[webhooks] final case class RetryController( inputQueue.offerAll(events) def start: UIO[Any] = { - val handleRetryEvents = mergeShutdown(UStream.fromQueue(inputQueue), shutdownSignal).foreach { event => - val webhookId = event.key.webhookId - for { - retryQueue <- retryDispatchers.modify { map => - map.get(webhookId) match { - case Some(dispatcher) => - UIO((dispatcher.retryQueue, map)) - case None => - for { - retryQueue <- Queue.bounded[WebhookEvent](config.retry.capacity) - now <- clock.instant - retryDispatcher = RetryDispatcher( - clock, - config, - errorHub, - eventRepo, - fatalPromise, - httpClient, - retryStates, - retryQueue, - serializePayload, - shutdownSignal, - webhookId, - webhooksProxy - ) - _ <- retryStates.update(states => - UIO( - states + (webhookId -> RetryState( - activeSinceTime = now, - backoff = None, - failureCount = 0, - isActive = false, - lastRetryTime = now, - timeoutDuration = config.retry.timeout, - timerKillSwitch = None - )) + val handleRetryEvents = + mergeShutdown(ZStream.fromQueue(inputQueue), shutdownSignal).tap(ZIO.succeed(_)).foreach { event => + val webhookId = event.key.webhookId + for { + retryQueue <- retryDispatchers.modifyZIO { map => + map.get(webhookId) match { + case Some(dispatcher) => + ZIO.succeed((dispatcher.retryQueue, map)) + case None => + for { + retryQueue <- Queue.bounded[WebhookEvent](config.retry.capacity) + now <- clock.instant + retryDispatcher = RetryDispatcher( + clock, + config, + errorHub, + eventRepo, + fatalPromise, + httpClient, + retryStates, + retryQueue, + serializePayload, + shutdownSignal, + webhookId, + webhooksProxy ) - ) - _ <- retryDispatcher.start - } yield (retryQueue, map + (webhookId -> retryDispatcher)) + _ <- retryStates.updateZIO(states => + ZIO.succeed( + states + (webhookId -> RetryState( + activeSinceTime = now, + backoff = None, + failureCount = 0, + isActive = false, + lastRetryTime = now, + timeoutDuration = config.retry.timeout, + timerKillSwitch = None + )) + ) + ) + _ <- retryDispatcher.start + } yield (retryQueue, map + (webhookId -> retryDispatcher)) + } } - } - isShutDown <- shutdownSignal.isDone - _ <- retryQueue.offer(event).race(shutdownSignal.await).unless(isShutDown) - } yield () - } + _ <- retryQueue.offer(event).race(shutdownSignal.await).unlessZIO(shutdownSignal.isDone) + } yield () + } inputQueue.poll *> startupLatch.countDown *> handleRetryEvents } diff --git a/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala b/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala index bc50bedc..0df13d9a 100644 --- a/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala +++ b/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala @@ -1,20 +1,20 @@ package zio.webhooks.internal import zio._ -import zio.clock.Clock +import zio.Clock import zio.webhooks._ /** * A [[RetryDispatcher]] performs retry delivery for a single webhook. */ private[webhooks] final case class RetryDispatcher( - private val clock: Clock.Service, + private val clock: Clock, private val config: WebhookServerConfig, private val errorHub: Hub[WebhookError], private val eventRepo: WebhookEventRepo, private val fatalPromise: Promise[Cause[Nothing], Nothing], private val httpClient: WebhookHttpClient, - private val retryStates: RefM[Map[WebhookId, RetryState]], + private val retryStates: Ref.Synchronized[Map[WebhookId, RetryState]], retryQueue: Queue[WebhookEvent], private val serializePayload: SerializePayload, private val shutdownSignal: Promise[Nothing, Unit], @@ -27,11 +27,11 @@ private[webhooks] final case class RetryDispatcher( * the timeout duration. The timer is killed when retrying is deactivated. */ private[internal] def activateWithTimeout: UIO[Unit] = - retryStates.update { retryStates => + retryStates.updateZIO { retryStates => val currentState = retryStates(webhookId) for { nextState <- if (currentState.isActive) - UIO(currentState) + ZIO.succeed(currentState) else for { timerKillSwitch <- Promise.make[Nothing, Unit] @@ -83,7 +83,7 @@ private[webhooks] final case class RetryDispatcher( for { _ <- markDispatch(dispatch, WebhookEventStatus.Delivered) now <- clock.instant - _ <- retryStates.update { retryStates => + _ <- retryStates.updateZIO { retryStates => val newState = retryStates(dispatch.webhookId).resetBackoff(now) for { queueEmpty <- retryQueue.size.map(_ <= 0) @@ -91,7 +91,7 @@ private[webhooks] final case class RetryDispatcher( .foreach(batchQueue)(_.size.map(_ <= 0)) .map(_.getOrElse(true)) allEmpty = queueEmpty && batchExistsEmpty - newState <- if (allEmpty) newState.deactivate else UIO(newState) + newState <- if (allEmpty) newState.deactivate else ZIO.succeed(newState) } yield retryStates.updated(dispatch.webhookId, newState) } } yield () @@ -100,9 +100,9 @@ private[webhooks] final case class RetryDispatcher( case _ => for { timestamp <- clock.instant - _ <- retryStates.update { retryStates => + _ <- retryStates.updateZIO { retryStates => retryQueue.offerAll(dispatch.events).fork *> - UIO( + ZIO.succeed( retryStates.updated( webhookId, retryStates(webhookId).increaseBackoff(timestamp, config.retry) @@ -130,7 +130,7 @@ private[webhooks] final case class RetryDispatcher( retryState.backoff.map(retryQueue.take.delay(_)).getOrElse(retryQueue.take) } event <- shutdownSignal.await.disconnect.raceEither(take.disconnect).map(_.toOption) - _ <- ZIO.foreach_(event) { event => + _ <- ZIO.foreachDiscard(event) { event => val webhookId = event.key.webhookId for { _ <- activateWithTimeout @@ -150,7 +150,7 @@ private[webhooks] final case class RetryDispatcher( _ <- if (webhook.isEnabled) deliverEvent else - retryStates.update { retries => + retryStates.updateZIO { retries => retries(webhook.id).deactivate .map(retries.updated(webhook.id, _)) } @@ -159,7 +159,7 @@ private[webhooks] final case class RetryDispatcher( } yield () isShutdown <- shutdownSignal.isDone _ <- handleEvent - .repeatUntilM(_ => shutdownSignal.isDone) + .repeatUntilZIO(_ => shutdownSignal.isDone) .onError(fatalPromise.fail) .fork .unless(isShutdown) diff --git a/webhooks/src/main/scala/zio/webhooks/internal/RetryState.scala b/webhooks/src/main/scala/zio/webhooks/internal/RetryState.scala index d55316df..cc6b1f1f 100644 --- a/webhooks/src/main/scala/zio/webhooks/internal/RetryState.scala +++ b/webhooks/src/main/scala/zio/webhooks/internal/RetryState.scala @@ -1,7 +1,7 @@ package zio.webhooks.internal import zio._ -import zio.duration._ + import zio.webhooks.WebhookServerConfig import java.time.{ Instant, Duration => JDuration } @@ -23,7 +23,7 @@ private[webhooks] final case class RetryState( * Kills the current timeout timer, marking this retry state inactive. */ def deactivate: UIO[RetryState] = - ZIO.foreach_(timerKillSwitch)(_.succeed(())).as(copy(isActive = false, timerKillSwitch = None)) + ZIO.foreachDiscard(timerKillSwitch)(_.succeed(())).as(copy(isActive = false, timerKillSwitch = None)) /** * Progresses retrying to the next exponential backoff. diff --git a/webhooks/src/main/scala/zio/webhooks/package.scala b/webhooks/src/main/scala/zio/webhooks/package.scala index b17751d2..2e8b326a 100644 --- a/webhooks/src/main/scala/zio/webhooks/package.scala +++ b/webhooks/src/main/scala/zio/webhooks/package.scala @@ -1,6 +1,6 @@ package zio -import zio.stream.UStream +import zio.stream.{ UStream, ZStream } package object webhooks { @@ -29,7 +29,7 @@ package object webhooks { private[webhooks] def mergeShutdown[A](stream: UStream[A], shutdownSignal: Promise[Nothing, Unit]): UStream[A] = stream .map(Left(_)) - .mergeTerminateRight(UStream.fromEffect(shutdownSignal.await.map(Right(_)))) + .mergeHaltRight(ZStream.fromZIO(shutdownSignal.await.map(Right(_)))) .collectLeft type HttpHeader = (String, String) From f4e3de164e590b9e6bfda7278fc90090727ccb90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:13:51 +0800 Subject: [PATCH 02/18] Update dependency ch.epfl.scala:sbt-scalafix to v0.10.3 (#155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 4fc26596..c9df6660 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,4 +6,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.3") addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1032048a") addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.3") From a4c2454b8064075f7985d3ef76b24b8f0b1d3797 Mon Sep 17 00:00:00 2001 From: Paul Daniels Date: Fri, 4 Nov 2022 19:28:56 +0800 Subject: [PATCH 03/18] Use the companion objects for clock and use ZIO.log for printing messages (#160) --- .../scala/zio/webhooks/WebhookServer.scala | 50 ++++++++----------- .../webhooks/internal/RetryController.scala | 7 +-- .../webhooks/internal/RetryDispatcher.scala | 10 ++-- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala b/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala index 9a97e27b..a559da9b 100644 --- a/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala +++ b/webhooks/src/main/scala/zio/webhooks/WebhookServer.scala @@ -5,7 +5,7 @@ import zio.stream.ZStream import zio.webhooks.WebhookDeliverySemantics._ import zio.webhooks.WebhookError._ import zio.webhooks.internal._ -import zio.{ Clock, Console, _ } +import zio.{ Clock, _ } /** * A [[WebhookServer]] is a stateful server that subscribes to [[WebhookEvent]]s and reliably @@ -21,9 +21,7 @@ import zio.{ Clock, Console, _ } * management, ensuring `shutdown` is called by the finalizer. */ final class WebhookServer private ( - private val clock: Clock, private val config: WebhookServerConfig, - private val console: Console, private val eventRepo: WebhookEventRepo, private val httpClient: WebhookHttpClient, private val stateRepo: WebhookStateRepo, @@ -94,7 +92,7 @@ final class WebhookServer private ( } } accepted <- webhookQueue.offer(event) - _ <- console + _ <- Console .printLine( s"""Slow webhook detected with id "${webhookId.value}"""" + " and event " + event @@ -166,25 +164,24 @@ final class WebhookServer private ( * * This ensures retries are persistent with respect to server restarts. */ - private def startEventRecovery = - ({ - for { - _ <- stateRepo.loadState.flatMap( - ZIO - .foreachDiscard(_)(jsonState => - ZIO - .fromEither(jsonState.fromJson[PersistentRetries]) - .mapError(message => InvalidStateError(jsonState, message)) - .flatMap(retryController.loadRetries) - ) - .catchAll(errorHub.publish) - ) - f <- mergeShutdown(eventRepo.recoverEvents, shutdownSignal) - .foreach(retryController.enqueueRetry) - .ensuring(shutdownLatch.countDown) - .forkScoped - } yield f - } <* startupLatch.countDown) + private def startEventRecovery = { + for { + _ <- stateRepo.loadState.flatMap( + ZIO + .foreachDiscard(_)(jsonState => + ZIO + .fromEither(jsonState.fromJson[PersistentRetries]) + .mapError(message => InvalidStateError(jsonState, message)) + .flatMap(retryController.loadRetries) + ) + .catchAll(errorHub.publish) + ) + f <- mergeShutdown(eventRepo.recoverEvents, shutdownSignal) + .foreach(retryController.enqueueRetry) + .ensuring(shutdownLatch.countDown) + .forkScoped + } yield f + } <* startupLatch.countDown /** * Starts server subscription to new [[WebhookEvent]]s. Counts down on the `startupLatch`, @@ -225,7 +222,7 @@ final class WebhookServer private ( for { _ <- shutdownSignal.succeed(()) _ <- shutdownLatch.await - persistentState <- clock.instant.flatMap(retryController.persistRetries) + persistentState <- Clock.instant.flatMap(retryController.persistRetries) _ <- stateRepo.setState(persistentState.toJson) } yield () @@ -244,9 +241,7 @@ object WebhookServer { */ private def create: URIO[Scope with Env, WebhookServer] = for { - clock <- ZIO.clock config <- ZIO.service[WebhookServerConfig] - console <- ZIO.console eventRepo <- ZIO.service[WebhookEventRepo] httpClient <- ZIO.service[WebhookHttpClient] serializePayload <- ZIO.service[SerializePayload] @@ -262,7 +257,6 @@ object WebhookServer { shutdownLatch <- CountDownLatch.make(3) shutdownSignal <- Promise.make[Nothing, Unit] retries = RetryController( - clock, config, errorHub, eventRepo, @@ -279,9 +273,7 @@ object WebhookServer { ) webhookQueues <- Ref.Synchronized.make(Map.empty[WebhookId, Queue[WebhookEvent]]) } yield new WebhookServer( - clock, config, - console, eventRepo, httpClient, webhookState, diff --git a/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala b/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala index 62c1c9c7..93fe9704 100644 --- a/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala +++ b/webhooks/src/main/scala/zio/webhooks/internal/RetryController.scala @@ -12,7 +12,6 @@ import java.time.Instant * dispatchers and state for each webhook. */ private[webhooks] final case class RetryController( - private val clock: zio.Clock, private val config: WebhookServerConfig, private val errorHub: Hub[WebhookError], private val eventRepo: WebhookEventRepo, @@ -54,11 +53,10 @@ private[webhooks] final case class RetryController( loadedState: PersistentRetries.PersistentRetryState ): IO[WebhookError, (RetryDispatcher, RetryState)] = for { - now <- clock.instant + now <- Clock.instant retry <- Queue.bounded[WebhookEvent](config.retry.capacity).map { retryQueue => ( RetryDispatcher( - clock, config, errorHub, eventRepo, @@ -111,9 +109,8 @@ private[webhooks] final case class RetryController( case None => for { retryQueue <- Queue.bounded[WebhookEvent](config.retry.capacity) - now <- clock.instant + now <- Clock.instant retryDispatcher = RetryDispatcher( - clock, config, errorHub, eventRepo, diff --git a/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala b/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala index 0df13d9a..d5e55b45 100644 --- a/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala +++ b/webhooks/src/main/scala/zio/webhooks/internal/RetryDispatcher.scala @@ -8,7 +8,6 @@ import zio.webhooks._ * A [[RetryDispatcher]] performs retry delivery for a single webhook. */ private[webhooks] final case class RetryDispatcher( - private val clock: Clock, private val config: WebhookServerConfig, private val errorHub: Hub[WebhookError], private val eventRepo: WebhookEventRepo, @@ -38,7 +37,7 @@ private[webhooks] final case class RetryDispatcher( runTimer = timerKillSwitch.await .timeoutTo(false)(_ => true)(currentState.timeoutDuration) .flatMap(markWebhookUnavailable(webhookId).unless(_)) - _ <- runTimer.fork.provideLayer(ZLayer.succeed(clock)) + _ <- runTimer.fork } yield currentState.copy(isActive = true, timerKillSwitch = Some(timerKillSwitch)) } yield retryStates.updated(webhookId, nextState) } @@ -57,7 +56,7 @@ private[webhooks] final case class RetryDispatcher( private def markWebhookUnavailable(webhookId: WebhookId): IO[WebhookError, Unit] = for { _ <- eventRepo.setAllAsFailedByWebhookId(webhookId) - unavailableStatus <- clock.instant.map(WebhookStatus.Unavailable) + unavailableStatus <- Clock.instant.map(WebhookStatus.Unavailable) _ <- webhooksProxy.setWebhookStatus(webhookId, unavailableStatus) } yield () @@ -82,7 +81,7 @@ private[webhooks] final case class RetryDispatcher( case Right(WebhookHttpResponse(200)) => for { _ <- markDispatch(dispatch, WebhookEventStatus.Delivered) - now <- clock.instant + now <- Clock.instant _ <- retryStates.updateZIO { retryStates => val newState = retryStates(dispatch.webhookId).resetBackoff(now) for { @@ -99,7 +98,7 @@ private[webhooks] final case class RetryDispatcher( // move the retry state to the next backoff duration case _ => for { - timestamp <- clock.instant + timestamp <- Clock.instant _ <- retryStates.updateZIO { retryStates => retryQueue.offerAll(dispatch.events).fork *> ZIO.succeed( @@ -163,7 +162,6 @@ private[webhooks] final case class RetryDispatcher( .onError(fatalPromise.fail) .fork .unless(isShutdown) - .provideLayer(ZLayer.succeed(clock)) } yield () } } From b9d167f873ead811d1a5332f1bbe1f61b810bd30 Mon Sep 17 00:00:00 2001 From: Paul Daniels Date: Tue, 8 Nov 2022 10:05:48 +0800 Subject: [PATCH 04/18] Publish zio-webhooks-testkit (#161) --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index faffdab6..125ff685 100755 --- a/build.sbt +++ b/build.sbt @@ -78,7 +78,6 @@ lazy val zioWebhooksTest = module("zio-webhooks-test", "webhooks-test") lazy val webhooksTestkit = module("zio-webhooks-testkit", "webhooks-testkit") .settings( - publish / skip := true, libraryDependencies ++= Seq( "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" From 5f3f1b1686512206e0dd8cbdc06ee1d7d76dd3f3 Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Tue, 22 Nov 2022 15:24:03 +0330 Subject: [PATCH 05/18] Publish Docs to The NPM Registry (#163) * delete website. * install zio-sbt-website plugin. * prepare docs. * update ci. * generate site workflow. * ignore .idea files. * update homepage url. * remove extra workflow. --- .github/workflows/ci.yml | 15 +- .github/workflows/mdoc.yml | 18 -- .github/workflows/site.yml | 28 ++ .gitignore | 4 +- README.md | 6 +- build.sbt | 11 +- docs/about/code_of_conduct.md | 45 --- docs/about/contributing.md | 274 ------------------ docs/{about => }/index.md | 17 +- docs/overview/index.md | 20 -- docs/package.json | 5 + docs/sidebars.js | 7 + docs/usecases/example.md | 10 - docs/usecases/index.md | 8 - project/plugins.sbt | 21 +- website/core/Footer.js | 67 ----- website/package.json | 15 - website/pages/en/index.js | 140 --------- website/sidebars.json | 25 -- website/siteConfig.js | 118 -------- website/static/css/custom.css | 41 --- website/static/img/discord.png | Bin 4131 -> 0 bytes website/static/img/favicon.png | Bin 657 -> 0 bytes website/static/img/navbar_brand.png | Bin 13615 -> 0 bytes website/static/img/navbar_brand2x.png | Bin 16064 -> 0 bytes website/static/img/sidebar_brand.png | Bin 12777 -> 0 bytes website/static/img/sidebar_brand2x.png | Bin 17424 -> 0 bytes website/static/img/undraw_code_review.svg | 1 - website/static/img/undraw_online.svg | 0 website/static/img/undraw_open_source.svg | 1 - .../static/img/undraw_operating_system.svg | 1 - website/static/img/undraw_tweetstorm.svg | 1 - 32 files changed, 88 insertions(+), 811 deletions(-) delete mode 100755 .github/workflows/mdoc.yml create mode 100644 .github/workflows/site.yml delete mode 100755 docs/about/code_of_conduct.md delete mode 100755 docs/about/contributing.md rename docs/{about => }/index.md (85%) delete mode 100755 docs/overview/index.md create mode 100644 docs/package.json create mode 100644 docs/sidebars.js delete mode 100755 docs/usecases/example.md delete mode 100755 docs/usecases/index.md delete mode 100755 website/core/Footer.js delete mode 100755 website/pages/en/index.js delete mode 100755 website/sidebars.json delete mode 100755 website/siteConfig.js delete mode 100755 website/static/css/custom.css delete mode 100755 website/static/img/discord.png delete mode 100755 website/static/img/favicon.png delete mode 100755 website/static/img/navbar_brand.png delete mode 100755 website/static/img/navbar_brand2x.png delete mode 100755 website/static/img/sidebar_brand.png delete mode 100755 website/static/img/sidebar_brand2x.png delete mode 100755 website/static/img/undraw_code_review.svg delete mode 100755 website/static/img/undraw_online.svg delete mode 100755 website/static/img/undraw_open_source.svg delete mode 100755 website/static/img/undraw_operating_system.svg delete mode 100755 website/static/img/undraw_tweetstorm.svg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b41db2d..0d15d967 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,22 @@ jobs: - name: Run tests run: sbt ++${{ matrix.scala }}! test IntegrationTest/test + website: + runs-on: ubuntu-20.04 + timeout-minutes: 30 + steps: + - name: Checkout current branch + uses: actions/checkout@v3.1.0 + - name: Setup Scala and Java + uses: olafurpg/setup-scala@v13 + - name: Cache scala dependencies + uses: coursier/cache-action@v6 + - name: Check generation of ScalaDoc + run: sbt docs/compileDocs + publish: runs-on: ubuntu-20.04 - needs: [build] + needs: [build, website] if: github.event_name != 'pull_request' steps: - uses: actions/checkout@v3.0.2 diff --git a/.github/workflows/mdoc.yml b/.github/workflows/mdoc.yml deleted file mode 100755 index 5235582d..00000000 --- a/.github/workflows/mdoc.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Website - -on: - release: - types: - - published - -jobs: - publish: - runs-on: ubuntu-20.04 - if: github.event_name != 'pull_request' - steps: - - uses: actions/checkout@v3 - - uses: olafurpg/setup-scala@v13 - - uses: olafurpg/setup-gpg@v3 - - run: sbt docs/docusaurusPublishGhpages - env: - GIT_DEPLOY_KEY: ${{ secrets.GIT_DEPLOY_KEY }} diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml new file mode 100644 index 00000000..9a0cbf93 --- /dev/null +++ b/.github/workflows/site.yml @@ -0,0 +1,28 @@ +# This file was autogenerated using `zio-sbt` via `sbt generateGithubWorkflow` +# task and should be included in the git repository. Please do not edit +# it manually. + +name: website + +on: + release: + types: [ published ] + +jobs: + publish-docs: + runs-on: ubuntu-20.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3.1.0 + with: + fetch-depth: 0 + - name: Setup Scala and Java + uses: olafurpg/setup-scala@v13 + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://registry.npmjs.org' + - name: Publishing Docs to NPM Registry + run: sbt docs/publishToNpm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 105b094c..2db674e3 100755 --- a/.gitignore +++ b/.gitignore @@ -467,4 +467,6 @@ website/node_modules website/build website/i18n/en.json website/static/api -.idea + +# idea +.idea \ No newline at end of file diff --git a/README.md b/README.md index 5295f558..ebf0202b 100755 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ call `shutdown` on a server created manually. Either way, users will have to imp # Documentation -[ZIO Webhooks Microsite](https://zio.github.io/zio-webhooks/) +[ZIO Webhooks Microsite](https://zio.dev/zio-webhooks/) # Contributing -[Documentation for contributors](https://zio.github.io/zio-webhooks/docs/about/about_contributing) +[Documentation for contributors](https://zio.dev/about/contributing) ## Code of Conduct -See the [Code of Conduct](https://zio.github.io/zio-webhooks/docs/about/about_coc) +See the [Code of Conduct](https://zio.dev/about/code-of-conduct) ## Support diff --git a/build.sbt b/build.sbt index 125ff685..623673b4 100755 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,7 @@ import BuildHelper._ inThisBuild( List( organization := "dev.zio", - homepage := Some(url("https://zio.github.io/zio-webhooks/")), + homepage := Some(url("https://zio.dev/zio-webhooks/")), licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), developers := List( Developer( @@ -116,12 +116,7 @@ lazy val docs = project scalacOptions -= "-Xfatal-warnings", libraryDependencies ++= Seq( "dev.zio" %% "zio" % zioVersion - ), - ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(`zio-webhooks`), - ScalaUnidoc / unidoc / target := (LocalRootProject / baseDirectory).value / "website" / "static" / "api", - cleanFiles += (ScalaUnidoc / unidoc / target).value, - docusaurusCreateSite := docusaurusCreateSite.dependsOn(Compile / unidoc).value, - docusaurusPublishGhpages := docusaurusPublishGhpages.dependsOn(Compile / unidoc).value + ) ) .dependsOn(zioWebhooksCore) - .enablePlugins(MdocPlugin, DocusaurusPlugin, ScalaUnidocPlugin) + .enablePlugins(WebsitePlugin) diff --git a/docs/about/code_of_conduct.md b/docs/about/code_of_conduct.md deleted file mode 100755 index 7e9df263..00000000 --- a/docs/about/code_of_conduct.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -id: about_coc -title: "ZIO Code of Conduct" ---- - -We are committed to providing a friendly, safe and welcoming -environment for all, regardless of level of experience, gender, gender -identity and expression, sexual orientation, disability, personal -appearance, body size, race, ethnicity, age, religion, nationality, or -other such characteristics. - -The ZIO project follows the [Scala Code of Conduct](https://www.scala-lang.org/conduct/), with -an additional clause regarding moderation that is detailed below. All participants, contributors and -members are expected to follow the Scala Code of Conduct when discussing the project on the available -communication channels. If you are being harassed, please contact us immediately so that we can support you. - -## Moderation and Steering Committee - -The ZIO project is moderated by the Steering Committee team members: - -- Itamar Ravid [@iravid](https://github.com/iravid) -- John De Goes [@jdegoes](https://github.com/jdegoes) -- Kai [@neko-kai](https://github.com/neko-kai) -- Paul Shirshov [@pshirshov](https://github.com/pshirshov) -- Pierre Ricadat [@ghostdogpr](https://github.com/ghostdogpr) -- Wiem Zine El Abidine [@wi101](https://github.com/wi101) - -The ZIO project requires that drastic moderation actions detailed in the code of -conduct - for example, removing a user from the Gitter channel - be agreed upon -a group of over 2/3rds of the steering committee. - -For any questions, concerns, or moderation requests please contact any member of -the project, or the people listed above. - -## BDFL - -In addition to the above, the ZIO project's BDFL (benevolent dictator for life) is -John De Goes (@jdegoes), owing to his original founding of the project and continued -investments in it. While John adheres to the same code of conduct as everyone else, -he is entitled to override moderation decisions made by the steering committee. - -We do not take the BDFL position lightly, especially with regards to moderation. John -has consistently shown he is level-headed and able to handle conflict responsibly. Feel -free to reach out to any member of the steering committee, including John himself, -with any concern you might have. diff --git a/docs/about/contributing.md b/docs/about/contributing.md deleted file mode 100755 index 215d9ccb..00000000 --- a/docs/about/contributing.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -id: about_contributing -title: "ZIO-Webhooks Contributor Guidelines" ---- - -Thank you for your interest in contributing to ZIO-Webhooks, which is a small, zero-dependency library for doing type-safe, composable concurrent and asynchronous programming! - -We welcome contributions from all people! You will learn about functional programming, and you will add your own unique touch to the ZIO-Webhooks project. We are happy to help you to get started and to hear your suggestions and answer your questions. - -_You too can contribute to ZIO-Webhooks, we believe in you!_ - -# Contributing - -## Getting Started - -To begin contributing, please follow these steps: - -### Get The Project - -If you don't already have one, sign up for a free [GitHub Account](https://github.com/join?source=header-home). - -After you [log into](https://github.com/login) GitHub using your account, go to the [ZIO-Webhooks Project Page](https://github.com/zio/zio-webhooks), and click on [Fork](https://github.com/zio/zio-webhooks/fork) to fork the ZIO-Webhooks repository into your own account. - -You will make _all_ contributions from your own account. No one contributes _directly_ to the main repository. Contributors only ever merge code from other people's forks into the main repository. - -Once you have forked the repository, you can now clone your forked repository to your own machine, so you have a complete copy of the project and can begin safely making your modifications (even without an Internet connection). - -To clone your forked repository, first make sure you have installed [Git](https://git-scm.com/downloads), the version control system used by GitHub. Then open a Terminal and type the following commands: - -```bash -git clone git@github.com:your-user-name/zio-webhooks.git . -``` - -If these steps were successful, then congratulations, you now have a complete copy of the ZIO Webhooks project! - -The next step is to build the project on your machine, to ensure you know how to compile the project and run tests. - -### Build the Project - -The official way to build the project is with sbt. An sbt build file is included in the project, so if you choose to build the project this way, you won't have to do any additional configuration or setup (others choose to build the project using IntelliJ IDEA, Gradle, Maven, Mill, or Fury). - -We use a custom sbt script, which is included in the repository, in order to ensure settings are uniform across all development machines, and the continuous integration service (Circle CI). - -The sbt script is in the root of the repository. To launch this script from your Terminal window, simply type: - -```bash -./sbt -``` - -Sbt will launch, read the project build file, and download dependencies as required. - -You can now compile the production source code with the following sbt command: - -```bash -compile -``` - -You can compile the test source code with the following sbt command: - -```bash -test:compile -``` - -[Learn more](https://www.scala-sbt.org) about sbt to understand how you can list projects, switch projects, and otherwise manage an sbt project. - -### Find an Issue - -You may have your own idea about what contributions to make to ZIO-Webhooks, which is great! If you want to make sure the ZIO contributors are open to your idea, you can [open an issue](https://github.com/zio/zio-webhooks/issues/new) first on the ZIO project site. - -Otherwise, if you have no ideas about what to contribute, you can find a large supply of feature requests and bugs on the project's [issue tracker](https://github.com/zio/zio-webhooks/issues). - -Issues are tagged with various labels, such as `good first issue`, which help you find issues that are a fit for you. - -If some issue is confusing or you think you might need help, then just post a comment on the issue asking for help. Typically, the author of the issue will provide as much help as you need, and if the issue is critical, leading ZIO-Webhooks contributors will probably step in to mentor you and give you a hand, making sure you understand the issue thoroughly. - -Once you've decided on an issue and understand what is necessary to complete the issue, then it's a good idea to post a comment on the issue saying that you intend to work on it. Otherwise, someone else might work on it too! - -### Fix an Issue - -Once you have an issue, the next step is to fix the bug or implement the feature. Since ZIO-Webhooks is an open source project, there are no deadlines. Take your time! - -The only thing you have to worry about is if you take too long, especially for a critical issue, eventually someone else will come along and work on the issue. - -If you shoot for 2-3 weeks for most issues, this should give you plenty of time without having to worry about having your issue stolen. - -If you get stuck, please consider [opening a pull request](https://github.com/zio/zio-webhooks/compare) for your incomplete work, and asking for help (just prefix the pull request by _WIP_). In addition, you can comment on the original issue, pointing people to your own fork. Both of these are great ways to get outside help from people more familiar with the project. - -### Prepare Your Code - -If you've gotten this far, congratulations! You've implemented a new feature or fixed a bug. Now you're in the last mile, and the next step is submitting your code for review, so that other contributors can spot issues and help improve the quality of the code. - -To do this, you need to commit your changes locally. A good way to find out what you did locally is to use the `git status` command: - -```bash -git status -``` - -If you see new files, you will have to tell `git` to add them to the repository using `git add`: - -```bash -git add src/main/zio/zmx/NewFile.scala -``` - -Then you can commit all your changes at once with the following command: - -```bash -git commit -am "Fixed #94211 - Optimized race for lists of effects" -``` - -At this point, you have saved your work locally, to your machine, but you still need to push your changes to your fork of the repository. To do that, use the `git push` command: - -```bash -git push -``` - -Now while you were working on this great improvement, it's quite likely that other ZIO-Webhooks contributors were making their own improvements. You need to pull all those improvements into your own code base to resolve any conflicts and make sure the changes all work well together. - -To do that, use the `git pull` command: - -```bash -git pull git@github.com:zio/zio-webhooks.git master -``` - -You may get a warning from Git that some files conflicted. Don't worry! That just means you and another contributor edited the same parts of the same files. - -Using a text editor, open up the conflicted files, and try to merge them together, preserving your changes and the other changes (both are important!). - -Once you are done, you can commit again: - -```bash -git commit -am "merged upstream changes" -``` - -At this point, you should re-run all tests to make sure everything is passing: - -```bash -# If you are already in a SBT session you can type only 'test' - -sbt test -``` - -If all the tests are passing, then you can format your code: - -```bash -# If you are already in a SBT session you can type only 'fmt' - -sbt fmt -``` - -If your changes altered an API, then you may need to rebuild the microsite to make sure none of the (compiled) documentation breaks: - -```bash -# If you are already in a SBT session you can type only 'docs/docusaurusCreateSite' - -sbt docs/docusaurusCreateSite -``` - -Finally, if you are up-to-date with master, all your tests are passing, you have properly formatted your code, and the microsite builds properly, then it's time to submit your work for review! - -### Create a Pull Request - -To create a pull request, first push all your changes to your fork of the project repository: - -```bash -git push -``` - -Next, [open a new pull request](https://github.com/zio/zio-webhooks/compare) on GitHub, and select _Compare Across Forks_. On the right hand side, choose your own fork of the ZIO-Webhooks repository, in which you've been making your contribution. - -Provide a description for the pull request, which details the issue it is fixing, and has other information that may be helpful to developers reviewing the pull request. - -Finally, click _Create Pull Request_! - -### Get Your Pull Request Merged - -Once you have a pull request open, it's still your job to get it merged! To get it merged, you need at least one core ZIO-Webhooks contributor to approve the code. - -If you know someone who would be qualified to review your code, you can request that person, either in the comments of the pull request, or on the right-hand side, if you have appropriate permissions. - -Code reviews can sometimes take a few days, because open source projects are largely done outside of work, in people's leisure time. Be patient, but don't wait forever. If you haven't gotten a review within a few days, then consider gently reminding people that you need a review. - -Once you receive a review, you will probably have to go back and make minor changes that improve your contribution and make it follow existing conventions in the code base. This is normal, even for experienced contributors, and the rigorous reviews help ensure ZIO-Webhooks' code base stays high quality. - -After you make changes, you may need to remind reviewers to check out the code again. If they give a final approval, it means your code is ready for merge! Usually this will happen at the same time, though for controversial changes, a contributor may wait for someone more senior to merge. - -If you don't get a merge in a day after your review is successful, then please gently remind folks that your code is ready to be merged. - -Sit back, relax, and enjoy being a ZIO-Webhooks contributor! - -# ZIO-Webhooks Contributor License Agreement - -Thank you for your interest in contributing to the ZIO-Webhooks open source project. - -This contributor agreement ("Agreement") describes the terms and conditions under which you may Submit a Contribution to Us. By Submitting a Contribution to Us, you accept the terms and conditions in the Agreement. If you do not accept the terms and conditions in the Agreement, you must not Submit any Contribution to Us. - -This is a legally binding document, so please read it carefully before accepting the terms and conditions. If you accept this Agreement, the then-current version of this Agreement shall apply each time you Submit a Contribution. The Agreement may cover more than one software project managed by Us. - -## 1. Definitions - -"We" or "Us" means Ziverge, Inc., and its duly appointed and authorized representatives. - -"You" means the individual or entity who Submits a Contribution to Us. - -"Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. You may not Submit a Contribution if you do not own the Copyright in the entire work of authorship. - -"Copyright" means all rights protecting works of authorship owned or controlled by You, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You. - -"Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material. - -"Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, electronic mail, source code control systems, pull requests, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -"Submission Date" means the date on which You Submit a Contribution to Us. - -"Effective Date" means the earliest date You execute this Agreement by Submitting a Contribution to Us. - -## 2. Grant of Rights - -### 2.1 Copyright License - -2.1.1. You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement. - -2.1.2. To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3. - -### 2.2 Patent License - -For patent claims including, without limitation, method, process, and apparatus claims which You own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. - -### 2.3 Outbound License - -Based on the grant of rights in Sections 2.1 and 2.2, if We include Your Contribution in a Material, We may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also license the Contribution under the terms of the license or licenses which We are using for the Material on the Submission Date. - -### 2.4 Moral Rights - -If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect. - -### 2.5 Our Rights - -You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate. - -### 2.6 Reservation of Rights - -Any rights not expressly licensed under this section are expressly reserved by You. - -## 3. Agreement - -You confirm that: - -a. You have the legal authority to enter into this Agreement. - -b. You own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2. - -c. The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. - -d. You have followed the instructions in, if You do not own the Copyright in the entire work of authorship Submitted. - -## 4. Disclaimer - -EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW. - -## 5. Consequential Damage Waiver - -TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED. - -## 6. Miscellaneous - -6.1. This Agreement will be governed by and construed in accordance with the laws of the state of Maryland, in the United States of America, excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement. - -6.2. This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings. - -6.3. If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement. - -6.4. The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety. - -6.5. If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. diff --git a/docs/about/index.md b/docs/index.md similarity index 85% rename from docs/about/index.md rename to docs/index.md index ee5d36ad..65095d0b 100755 --- a/docs/about/index.md +++ b/docs/index.md @@ -1,9 +1,10 @@ --- -id: about_index -title: "About ZIO Webhooks" +id: index +title: "Introduction to ZIO Webhooks" +sidebar_label: "ZIO Webhooks" --- -A microlibrary for reliable and persistent webhook delivery. +ZIO Webhooks is a microlibrary for reliable and persistent webhook delivery. Below is a state diagram for each webhook handled by a server. Note that there aren't any initial or final states as the server doesn't manage the entire @@ -21,4 +22,12 @@ stateDiagram-v2 Retrying --> Unavailable : continuous failure for duration D Unavailable --> Enabled : enabled externally Unavailable --> Disabled : disabled externally -``` \ No newline at end of file +``` + +## Installation + +Include ZIO Webhooks in your project by adding the following to your `build.sbt`: + +```scala +libraryDependencies += "dev.zio" %% "zio-webhooks" % "@VERSION@" +``` diff --git a/docs/overview/index.md b/docs/overview/index.md deleted file mode 100755 index e659866a..00000000 --- a/docs/overview/index.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -id: overview_index -title: "Contents" ---- - -ZIO Webhooks - a microlibrary for reliable and persistent webhook delivery. - -## Installation - -Include ZIO Webhooks in your project by adding the following to your `build.sbt`: - -```scala mdoc:passthrough - -println(s"""```""") -if (zio.webhooks.BuildInfo.isSnapshot) - println(s"""resolvers += Resolver.sonatypeRepo("snapshots")""") -println(s"""libraryDependencies += "dev.zio" %% "zio-webhooks" % "${zio.webhooks.BuildInfo.version}"""") -println(s"""```""") - -``` diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..ed43cddf --- /dev/null +++ b/docs/package.json @@ -0,0 +1,5 @@ +{ + "name": "@zio.dev/zio-webhooks", + "description": "ZIO Webhooks Documentation", + "license": "Apache-2.0" +} diff --git a/docs/sidebars.js b/docs/sidebars.js new file mode 100644 index 00000000..b485cd2c --- /dev/null +++ b/docs/sidebars.js @@ -0,0 +1,7 @@ +const sidebars = { + sidebar: [ + "index" + ] +}; + +module.exports = sidebars; diff --git a/docs/usecases/example.md b/docs/usecases/example.md deleted file mode 100755 index a71b27a0..00000000 --- a/docs/usecases/example.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: usecases_example -title: "Example" ---- - -#### Program - -```scala mdoc:silent -// zio-webhooks example app here -``` diff --git a/docs/usecases/index.md b/docs/usecases/index.md deleted file mode 100755 index 7c89004f..00000000 --- a/docs/usecases/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: usecases_index -title: "Contents" ---- - -So now how to use it? Here you can find some examples to dive into: - - - **[Webhook example](example.md)** — Webhook example diff --git a/project/plugins.sbt b/project/plugins.sbt index c9df6660..7b7691d3 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,12 @@ -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.3") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.3") -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1032048a") -addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") -addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.3") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.3") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.3") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1032048a") +addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") +addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.3") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.0.0+84-6fd7d64e-SNAPSHOT") + +resolvers += Resolver.sonatypeRepo("public") diff --git a/website/core/Footer.js b/website/core/Footer.js deleted file mode 100755 index 83665881..00000000 --- a/website/core/Footer.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const React = require('react'); - -class Footer extends React.Component { - docUrl(doc, language) { - const baseUrl = this.props.config.baseUrl; - const docsUrl = this.props.config.docsUrl; - const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; - const langPart = `${language ? `${language}/` : ''}`; - return `${baseUrl}${docsPart}${langPart}${doc}`; - } - - pageUrl(doc, language) { - const baseUrl = this.props.config.baseUrl; - return baseUrl + (language ? `${language}/` : '') + doc; - } - - render() { - return ( - - ); - } -} - -module.exports = Footer; diff --git a/website/package.json b/website/package.json index fb609f1a..e69de29b 100755 --- a/website/package.json +++ b/website/package.json @@ -1,15 +0,0 @@ -{ - "scripts": { - "examples": "docusaurus-examples", - "start": "docusaurus-start", - "build": "docusaurus-build", - "publish-gh-pages": "docusaurus-publish", - "write-translations": "docusaurus-write-translations", - "version": "docusaurus-version", - "rename-version": "docusaurus-rename-version" - }, - "devDependencies": { - "docusaurus": "1.14.7", - "react-sidecar": "0.1.1" - } -} diff --git a/website/pages/en/index.js b/website/pages/en/index.js deleted file mode 100755 index 28cac3b7..00000000 --- a/website/pages/en/index.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const React = require('react'); - -const CompLibrary = require('../../core/CompLibrary.js'); - -const MarkdownBlock = CompLibrary.MarkdownBlock; /* Used to read markdown */ -const Container = CompLibrary.Container; -const GridBlock = CompLibrary.GridBlock; - -class HomeSplash extends React.Component { - render() { - const {siteConfig, language = ''} = this.props; - const {baseUrl, docsUrl} = siteConfig; - const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; - const langPart = `${language ? `${language}/` : ''}`; - const docUrl = doc => `${baseUrl}${docsPart}${langPart}${doc}`; - - const SplashContainer = props => ( -
-
-
{props.children}
-
-
- ); - - const Logo = props => ( -
- Project Logo -
- ); - - const ProjectTitle = () => ( -

- {siteConfig.title} - {siteConfig.tagline} -

- ); - - const PromoSection = props => ( -
-
-
{props.children}
-
-
- ); - - const Button = props => ( - - ); - - return ( - -
- - - - - - -
-
- ); - } -} - -class Index extends React.Component { - render() { - const {config: siteConfig, language = ''} = this.props; - const {baseUrl} = siteConfig; - - const Block = props => ( - - - - ); - - const FeatureCallout = () => ( -
-

Welcome to ZIO Webhooks

- - A microlibrary for reliable and persistent webhook delivery. - - - - Lorem ipsum - -
- ); - - const Features = () => ( - - {[ - { - content: 'Model webhooks\' communication without side effects', - image: `${baseUrl}img/undraw_tweetstorm.svg`, - imageAlign: 'top', - title: 'Effectful', - }, - { - content: 'Fully typed - with message, response and error type', - image: `${baseUrl}img/undraw_operating_system.svg`, - imageAlign: 'top', - title: 'Typed', - }, - ]} - - ); - - return ( -
- -
- - -
-
- ); - } -} - -module.exports = Index; diff --git a/website/sidebars.json b/website/sidebars.json deleted file mode 100755 index fdaadd2c..00000000 --- a/website/sidebars.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "overview-sidebar": { - "Overview": [ - "overview/overview_index", - "overview/overview_basics", - "overview/overview_supervision", - "overview/overview_remoting", - "overview/overview_persistence", - "overview/overview_akkainterop" - ] - }, - "usecases-sidebar": { - "Use Cases": [ - "usecases/usecases_index", - "usecases/usecases_pingpong" - ] - }, - "about-sidebar": { - "About": [ - "about/about_index", - "about/about_contributing", - "about/about_coc" - ] - } -} \ No newline at end of file diff --git a/website/siteConfig.js b/website/siteConfig.js deleted file mode 100755 index 8e5d1e94..00000000 --- a/website/siteConfig.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -// See https://docusaurus.io/docs/site-config for all the possible -// site configuration options. - -// List of projects/orgs using your project for the users page. -const users = [ - // { - // caption: 'User1', - // // You will need to prepend the image path with your baseUrl - // // if it is not '/', like: '/test-site/img/image.jpg'. - // image: '/img/undraw_open_source.svg', - // infoLink: 'https://www.facebook.com', - // pinned: true, - // }, -]; - -const siteConfig = { - title: 'ZIO Webhooks', - tagline: 'A microlibrary for reliable and persistent webhook delivery', - url: 'https://zio.github.io', - baseUrl: '/zio-webhooks/', - - projectName: 'zio-webhooks', - organizationName: 'zio', - - // For no header links in the top nav bar -> headerLinks: [], - headerLinks: [ - {doc: 'overview/overview_index', label: 'Overview'}, - {doc: 'usecases/usecases_index', label: 'Use Cases'}, - {doc: 'about/about_index', label: 'About'} - ], - - // by default Docusaurus combines CSS files in a way that doesn't play nicely with Scaladoc - separateCss: ["api"], - - // If you have users set above, you add it here: - users, - - /* path to images for header/footer */ - headerIcon: 'img/navbar_brand2x.png', - footerIcon: 'img/sidebar_brand2x.png', - favicon: 'img/favicon.png', - - /* Colors for website */ - colors: { - primaryColor: '#000000', - secondaryColor: '#000000', - }, - - /* Custom fonts for website */ - /* - fonts: { - myFont: [ - "Times New Roman", - "Serif" - ], - myOtherFont: [ - "-apple-system", - "system-ui" - ] - }, - */ - - // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. - copyright: `Copyright © ${new Date().getFullYear()} ZIO Maintainers`, - - highlight: { - // Highlight.js theme to use for syntax highlighting in code blocks. - theme: 'default', - }, - - // Add custom scripts here that would be placed in