├── project ├── build.properties └── plugins.sbt ├── .scalafix.conf ├── .gitignore ├── src └── main │ └── scala │ └── zionomicon │ ├── exercises │ ├── chap04 │ │ ├── 02-lepus-integration.sc │ │ └── 01-doobie-integration.sc │ ├── 06-parallelism-and-concurrency-operators.scala │ ├── 05-parallelism-and-concurrency-the-fiber-model.scala │ ├── 11-concurrent-structures-queue-work-distribution.scala │ ├── 15-scope-composable-resources.scala │ ├── 13-concurrent-structures-semaphore-work-limiting.scala │ ├── 10-concurrent-structures-promise-work-synchronization.scala │ ├── 14-acquire-release-safe-resource-handling-for-asynchronous-code.scala │ ├── 12-concurrent-structures-hub-broadcasting.scala │ ├── 08-parallelism-and-concurrency-interruption-in-depth.scala │ ├── 02-testing-zio-programs.scala │ ├── 09-concurrent-structures-ref-shared-state.scala │ ├── 03-the-zio-error-model.scala │ └── 01-first-steps-with-zio.scala │ └── solutions │ ├── chap04 │ ├── 02-lepus-integration.sc │ └── 01-doobie-integration.sc │ ├── 06-parallelism-and-concurrency-operators.scala │ ├── 08-parallelism-and-concurrency-interruption-in-depth.scala │ ├── 03-the-zio-error-model.scala │ ├── 05-parallelism-and-concurrency-the-fiber-model.scala │ ├── 15-scope-composable-resources.scala │ ├── 13-concurrent-structures-semaphore-work-limiting.scala │ ├── 01-first-steps-with-zio.scala │ ├── 10-concurrent-structures-promise-work-synchronization.scala │ ├── 09-concurrent-structures-ref-shared-state.scala │ ├── 14-acquire-release-safe-resource-handling-for-asynchronous-code.scala │ ├── 12-concurrent-structures-hub-broadcasting.scala │ ├── 02-testing-zio-programs.scala │ └── 11-concurrent-structures-queue-work-distribution.scala ├── .scalafmt.conf └── .github └── workflows └── ci.yml /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | OrganizeImports 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | metals.sbt 2 | sbt.json 3 | target 4 | .bloop 5 | .metals 6 | .vscode 7 | .idea 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("dev.zio" % "zio-sbt-ecosystem" % "0.4.0-alpha.32") 2 | addSbtPlugin("dev.zio" % "zio-sbt-ci" % "0.4.0-alpha.32") 3 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/chap04/02-lepus-integration.sc: -------------------------------------------------------------------------------- 1 | /** 2 | * 2. Write a ZIO program that uses lepus to connect to RabbitMQ server and 3 | * publish arbitrary messages to a queue. Lepus is a purely functional 4 | * scala client for RabbitMQ. You can find the library homepage 5 | * [here](http://lepus.hnaderi.dev/). 6 | */ 7 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/chap04/01-doobie-integration.sc: -------------------------------------------------------------------------------- 1 | /** 2 | * 1. Create a ZIO program that uses Doobie to perform a database operation. 3 | * Implement a function that inserts a user into a database and returns 4 | * the number of affected rows. Use the following table structure: 5 | * 6 | * ```sql 7 | * CREATE TABLE users ( 8 | * id SERIAL PRIMARY KEY, 9 | * name TEXT NOT NULL, 10 | * age INT NOT NULL 11 | * ) 12 | * ``` 13 | */ 14 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.6" 2 | runner.dialect = Scala213 3 | align.preset = most 4 | align.multiline = false 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings { 8 | style = asterisk 9 | } 10 | lineEndings = preserve 11 | includeCurlyBraceInSelectChains = false 12 | danglingParentheses.preset = true 13 | optIn.annotationNewlines = true 14 | newlines.alwaysBeforeMultilineDef = false 15 | 16 | rewrite.rules = [RedundantBraces] 17 | 18 | rewrite.redundantBraces.generalExpressions = false 19 | rewriteTokens = { 20 | "⇒": "=>" 21 | "→": "->" 22 | "←": "<-" 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/06-parallelism-and-concurrency-operators.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package zionomicon.exercises 4 | 5 | import zio._ 6 | 7 | import java.net.URL 8 | 9 | object ConcurrencyOperators { 10 | 11 | def foreachPar[R, E, A, B]( 12 | in: Iterable[A] 13 | )(f: A => ZIO[R, E, B]): ZIO[R, E, List[B]] = ??? 14 | 15 | /* 16 | 1. Implement the collectAllPar combinator using foreachPar. 17 | */ 18 | 19 | def collectAllPar[R, E, A]( 20 | in: Iterable[ZIO[R, E, A]] 21 | ): ZIO[R, E, List[A]] = ??? 22 | 23 | /* 24 | 2. Write a function that takes a collection of ZIO effects and collects all the successful 25 | and failed results as a tuple. 26 | */ 27 | 28 | def collectAllParResults[R, E, A]( 29 | in: Iterable[ZIO[R, E, A]] 30 | ): ZIO[R, Nothing, (List[A], List[E])] = ??? 31 | 32 | /* 33 | 3. Assume you have given the following fetchUrl function that fetches a URL and 34 | returns a ZIO effect: 35 | */ 36 | def fetchUrl(url: URL): ZIO[Any, Throwable, String] = ??? 37 | 38 | def fetchAllUrlsPar( 39 | urls: List[String] 40 | ): ZIO[Any, Nothing, (List[(URL, Throwable)], List[(URL, String)])] = ??? 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/05-parallelism-and-concurrency-the-fiber-model.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | object TheFiberModel { 4 | 5 | /* 6 | 1. Write a ZIO program that forks two effects, one that prints “Hello” after a two- 7 | second delay and another that prints “World” after a one-second delay. Ensure both 8 | effects run concurrently. 9 | */ 10 | object Question1 {} 11 | 12 | /* 13 | Modify the previous program to print “Done” only after both forked effects have completed. 14 | */ 15 | 16 | object Question2 {} 17 | 18 | /* 19 | Write a program that starts a long-running effect (e.g., printing numbers every second), then interrupts it after 5 seconds. 20 | */ 21 | object Question3 {} 22 | 23 | /* 24 | Create a program that forks an effect that might fail. Use await to handle both 25 | success and failure cases. 26 | */ 27 | 28 | object Question4 {} 29 | 30 | /* 31 | Create a program with an uninterruptible section that simulates a critical operation. 32 | Try to interrupt it and observe the behavior. 33 | */ 34 | 35 | object Question5 {} 36 | 37 | /* 38 | 6. Write a program demonstrating fiber supervision where a parent fiber forks two 39 | child fibers. Interrupt the parent and observe what happens to the children. 40 | 41 | */ 42 | 43 | object Question6 {} 44 | 45 | /* 46 | 7. Change one of the child fibers in the previous program to be a daemon fiber. Observe 47 | the difference in behavior when the parent is interrupted. 48 | */ 49 | 50 | object Question7 {} 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/11-concurrent-structures-queue-work-distribution.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package QueueWorkDistribution { 4 | 5 | /** 6 | * 1. Implement load balancer that distributes work across multiple worker 7 | * queues using a round-robin strategy: 8 | * 9 | * {{{ 10 | * trait LoadBalancer[A] { 11 | * def submit(work: A): Task[Unit] 12 | * def shutdown: Task[Unit] 13 | * } 14 | * object LoadBalancer { 15 | * def make[A](workerCount: Int, process: A => Task[A]) = ??? 16 | * } 17 | * }}} 18 | */ 19 | package LoadBalancerImpl {} 20 | 21 | /** 22 | * 2. Implement a rate limiter that limits the number of requests processed 23 | * in a given time frame. It takes the time interval and the maximum 24 | * number of calls that are allowed to be performed within the time 25 | * interval: 26 | * 27 | * {{{ 28 | * trait RateLimiter { 29 | * def acquire: UIO[Unit] 30 | * def apply[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] 31 | * } 32 | * 33 | * object RateLimiter { 34 | * def make(max: Int, interval: Duration): UIO[RateLimiter] = ??? 35 | * } 36 | * }}} 37 | */ 38 | package RateLimiterImpl {} 39 | 40 | /** 41 | * 3. Implement a circuit breaker that prevents calls to a service after a 42 | * certain number of failures: 43 | * 44 | * {{{ 45 | * trait CircuitBreaker { 46 | * def protect[A](operation: => Task[A]): Task[A] 47 | * } 48 | * }}} 49 | * 50 | * Hint: Use a sliding queue to store the results of the most recent 51 | * operations and track the number of failures. 52 | */ 53 | package CircuitBreakerImpl {} 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/15-scope-composable-resources.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package ScopeComposableResources { 4 | 5 | /** 6 | * 1. Assume we have written a worker as follows: 7 | * 8 | * {{{ 9 | * def worker(sem: Semaphore, id: Int): ZIO[Scope, Nothing, Unit] = 10 | * for { 11 | * _ <- sem.withPermitsScoped(2) 12 | * _ <- Console.printLine(s"Request $id: Starting processing").orDie 13 | * _ <- ZIO.sleep(5.seconds) 14 | * _ <- Console.printLine(s"Request $id: Completed processing").orDie 15 | * } yield () 16 | * }}} 17 | * 18 | * Please explain how and why these two applications have different behavior: 19 | * 20 | * Application 1: 21 | * 22 | * {{{ 23 | * object MainApp1 extends ZIOAppDefault { 24 | * def run = 25 | * for { 26 | * sem <- Semaphore.make(4) 27 | * _ <- ZIO.foreachParDiscard(1 to 10)(i => ZIO.scoped(worker(sem, i))) 28 | * } yield () 29 | * } 30 | * }}} 31 | * 32 | * Application 2: 33 | * 34 | * {{{ 35 | * object MainApp2 extends ZIOAppDefault { 36 | * def run = 37 | * for { 38 | * sem <- Semaphore.make(4) 39 | * _ <- ZIO.scoped(ZIO.foreachParDiscard(1 to 10)(i => worker(sem, i))) 40 | * } yield () 41 | * } 42 | * }}} 43 | */ 44 | package ComparingTwoWorkers {} 45 | 46 | /** 47 | * 2. Continuing from implementing the `Semaphore` data type from the 48 | * previous chapter, implement the `withPermits` operator, which takes 49 | * the number of permits to acquire and release within the lifetime of 50 | * the `Scope`: 51 | * 52 | * {{{ 53 | * trait Semaphore { 54 | * def withPermitsScoped(n: Long): ZIO[Scope, Nothing, Unit] 55 | * } 56 | * }}} 57 | */ 58 | package SemaphoreWithPermitsScopedImpl {} 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/13-concurrent-structures-semaphore-work-limiting.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package SemaphoreWorkLimiting { 4 | 5 | /** 6 | * 1. Implement a semaphore where the number of available permits can be 7 | * adjusted dynamically at runtime. This is useful for systems that need 8 | * to adapt their concurrency based on load or system resources. 9 | * 10 | * {{{ 11 | * trait DynamicSemaphore { 12 | * def withPermit[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] 13 | * def updatePermits(delta: Int): UIO[Unit] 14 | * def currentPermits: UIO[Int] 15 | * } 16 | * }}} 17 | * 18 | * Challenge: Ensure that reducing permits doesn't affect already-running 19 | * tasks, only future acquisitions. 20 | * 21 | * Hint: Please note that implementing the withPermit method will require 22 | * careful handling of resource acquisition and release to ensure that the 23 | * acquired permit is properly released after the task completes, regardless 24 | * of whether it succeeds, fails, or is interrupted. Consider using ZIO's 25 | * `ZIO.acquireRelease*` to manage this lifecycle effectively, which will be 26 | * discussed in the next chapter. 27 | */ 28 | package DynamicSemaphoreImpl {} 29 | 30 | /** 31 | * Solve the classic dining philosophers problem using Semaphores to prevent 32 | * deadlock. Five philosophers sit at a round table with five forks. Each 33 | * philosopher needs two adjacent forks to eat. 34 | * 35 | * {{{ 36 | * trait DiningPhilosophers { 37 | * def philosopherLifecycle(id: Int): ZIO[Any, Nothing, Unit] 38 | * def runDinner(duration: Duration): ZIO[Any, Nothing, Map[Int, Int]] // 39 | * philosopher -> meals eaten 40 | * } 41 | * }}} 42 | * 43 | * Must prevent both deadlock and starvation. 44 | */ 45 | package DiningPhilosophersImpl {} 46 | } 47 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/10-concurrent-structures-promise-work-synchronization.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package PromiseWorkSynchronization { 4 | 5 | /** 6 | * 1. Implement a countdown latch using `Ref` and `Promise`. A countdown 7 | * latch is a synchronization aid that allows one or more threads to wait 8 | * until a set of operations being performed in other threads completes. 9 | * The latch is initialized with a given count, and the count is 10 | * decremented each time an operation completes. When the count reaches 11 | * zero, all waiting threads are released: 12 | * 13 | * {{{ 14 | * trait CountDownLatch { 15 | * def countDown: UIO[Unit] 16 | * def await: UIO[Unit] 17 | * } 18 | * 19 | * object CountDownLatch { 20 | * def make(n: Int): UIO[CountDownLatch] = ??? 21 | * } 22 | * }}} 23 | */ 24 | 25 | package CountDownLatchImpl {} 26 | 27 | /** 28 | * 2. Similar to the previous exercise, you can implement `CyclicBarrier`. A 29 | * cyclic barrier is a synchronization aid that allows a set of threads 30 | * to all wait for each other to reach a common barrier point. Once all 31 | * threads have reached the barrier, they can proceed: 32 | * 33 | * {{{ 34 | * trait CyclicBarrier { 35 | * def await: UIO[Unit] 36 | * def reset: UIO[Unit] 37 | * } 38 | * 39 | * object CyclicBarrier { 40 | * def make(parties: Int): UIO[CyclicBarrier] = ??? 41 | * } 42 | * }}} 43 | */ 44 | package CyclicBarrierImpl {} 45 | 46 | /** 47 | * 3. Implement a concurrent bounded queue using `Ref` and `Promise`. It 48 | * should support enqueueing and dequeueing operations, blocking when the 49 | * queue is full or empty: 50 | * 51 | * {{{ 52 | * trait Queue[A] { 53 | * def offer(a: A): UIO[Unit] 54 | * def take: UIO[A] 55 | * } 56 | * 57 | * object Queue { 58 | * def make[A](capacity: Int): UIO[Queue[A]] = ??? 59 | * } 60 | * }}} 61 | */ 62 | 63 | package BoundedQueueImpl {} 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/14-acquire-release-safe-resource-handling-for-asynchronous-code.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package ResourceHanlding { 4 | 5 | /** 6 | * 1. Rewrite the following `sendData` function in terms of the 7 | * `ZIO.requireReleaseWith` operator: 8 | * 9 | * {{{ 10 | * import zio._ 11 | * import scala.util.Try 12 | * import java.net.Socket 13 | * 14 | * object LegacySendData { 15 | * def sendData( 16 | * host: String, 17 | * port: Int, 18 | * data: Array[Byte] 19 | * ): Try[Int] = { 20 | * var socket: Socket = null 21 | * try { 22 | * socket = new Socket(host, port) 23 | * val out = socket.getOutputStream 24 | * out.write(data) 25 | * Success(data.length) 26 | * } catch { 27 | * case e: Exception => Failure(e) 28 | * } finally { 29 | * if (socket != null) socket.close() 30 | * } 31 | * } 32 | * } 33 | * }}} 34 | * 35 | * Rewrite the function using ZIO 36 | * 37 | * {{{ 38 | * def sendData( 39 | * host: String, 40 | * port: Int, 41 | * data: Array[Byte] 42 | * ): Task[Int] = ??? 43 | * }}} 44 | */ 45 | package RewriteLegacySendData {} 46 | 47 | /** 48 | * 2. Implement `ZIO.acquireReleaseWith` using `ZIO.uninterruptibleMask`. 49 | * Write a test to ensure that `ZIO.acquireReleaseWith` guarantees the 50 | * three rules discussed in this chapter. 51 | */ 52 | package AcquireReleaseWithImpl {} 53 | 54 | /** 55 | * 3. Implement a simple semaphore using `Ref` and `Promise` and using 56 | * `ZIO.acquireReleaseWith` operator. A semaphore is a synchronization 57 | * primitive controlling access to a common resource by multiple fibers. 58 | * It is essentially a counter that tracks how many fibers can access a 59 | * resource at a time: 60 | * 61 | * {{{ 62 | * trait Semaphore { 63 | * def withPermits[R, E, A](n: Long)(task: ZIO[R, E, A]): ZIO[R, E, A] 64 | * } 65 | * 66 | * object Semaphore { 67 | * def make(permits: => Long): UIO[Semaphore] = ??? 68 | * } 69 | * }}} 70 | */ 71 | package SemaphoreImpl {} 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/chap04/02-lepus-integration.sc: -------------------------------------------------------------------------------- 1 | //> using scala 3.3.3 2 | //> using dep "io.circe::circe-generic:0.14.13" 3 | //> using dep "dev.hnaderi::named-codec-circe:0.3.0" 4 | //> using dep "dev.hnaderi::lepus-std:0.5.5" 5 | //> using dep "dev.hnaderi::lepus-circe:0.5.5" 6 | //> using dep "dev.zio::zio-streams:2.1.19" 7 | //> using dep "dev.zio::zio:2.1.19" 8 | //> using dep "dev.zio::zio-interop-cats:23.1.0.5" 9 | 10 | /** 11 | * 2. Write a ZIO program that uses lepus to connect to RabbitMQ server and 12 | * publish arbitrary messages to a queue. Lepus is a purely functional 13 | * scala client for RabbitMQ. You can find the library homepage 14 | * [here](http://lepus.hnaderi.dev/). 15 | */ 16 | 17 | // To run this script: 18 | // 1. Make sure RabbitMQ is running locally (or adjust the configuration) 19 | // docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management 20 | // 2. Run with scala-cli: 21 | // scala-cli run 02-lepus-integration.scala 22 | 23 | import lepus.client.* 24 | import lepus.protocol.domains.* 25 | import zio.interop.catz.* 26 | import zio.stream.* 27 | import zio.stream.interop.fs2z.* 28 | import zio.{Task, *} 29 | 30 | object HelloWorld extends ZIOAppDefault { 31 | implicit lazy val taskConsole: cats.effect.std.Console[Task] = 32 | cats.effect.std.Console.make[Task] 33 | 34 | private val exchange = ExchangeName.default 35 | 36 | def app(con: Connection[Task]) = con.channel.use(ch => 37 | for { 38 | _ <- ch.exchange.declare(ExchangeName("events"), ExchangeType.Topic) 39 | q <- ch.queue.declare(autoDelete = true) 40 | q <- 41 | ZIO.fromOption(q).orElseFail(new Exception("Queue declaration failed")) 42 | print = 43 | ch.messaging 44 | .consume[String](q.queue, mode = ConsumeMode.NackOnError) 45 | .toZStream() 46 | .tap { msg => 47 | Console.printLine(s"received message: ${msg.message.payload}") 48 | } 49 | publish = ZStream 50 | .fromZIO(Random.nextInt) 51 | .repeat(Schedule.spaced(1.second)) 52 | .tap(l => Console.printLine(s"publishing $l")) 53 | .map(l => Message[String](l.toString)) 54 | .mapZIO(msg => ch.messaging.publish(exchange, q.queue, msg)) 55 | _ <- print.merge(publish).interruptAfter(10.seconds).runDrain 56 | } yield () 57 | ) 58 | 59 | override def run = 60 | for { 61 | conn <- LepusClient[Task](debug = true).toScopedZIO 62 | _ <- app(conn) 63 | } yield () 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/12-concurrent-structures-hub-broadcasting.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package HubBroadcasting { 4 | 5 | /** 6 | * 1. Create a chatroom system using `Hub` where multiple users can join the 7 | * chat so each message sent is broadcast to all users. Each message sent 8 | * by a user is received by all other users. Users can leave the chat. 9 | * There is also a process that logs all messages sent to the chat room 10 | * in a file: 11 | * 12 | * {{{ 13 | * trait UserSession { 14 | * def sendMessage(message: String): UIO[Unit] 15 | * def receiveMessages: ZIO[Scope, Nothing, Dequeue[ChatEvent]] 16 | * def leave: UIO[Unit] 17 | * } 18 | * 19 | * trait ChatRoom { 20 | * def join(username: String): ZIO[Scope, Nothing, Dequeue[UserSession]] 21 | * def shutdown(username: String): UIO[Unit] 22 | * } 23 | * }}} 24 | */ 25 | package ChatRoomImpl {} 26 | 27 | /** 28 | * 2. Write a real-time auction system\index{Auction System} that allows 29 | * multiple bidders to practice in auctions simultaneously. The auction 30 | * system should broadcast bid updates to all participants while 31 | * maintaining the strict ordering of bids. Each participant should be 32 | * able to place bids and receive updates on the current highest bid: 33 | * 34 | * {{{ 35 | * trait AuctionSystem { 36 | * def placeBid( 37 | * auctionId: String, 38 | * bidderId: String, 39 | * amount: BigDecimal 40 | * ): UIO[Boolean] 41 | * 42 | * def createAuction( 43 | * id: String, 44 | * startPrice: BigDecimal, 45 | * duration: Duration 46 | * ): UIO[Unit] 47 | * 48 | * def subscribe: ZIO[Scope, Nothing, Dequeue[AuctionEvent]] 49 | * 50 | * def getAuction(id: String): UIO[Option[AuctionState]] 51 | * } 52 | * }}} 53 | * 54 | * The core models for the auction system could be as follows: 55 | * 56 | * {{{ 57 | * case class Bid( 58 | * auctionId: String, 59 | * bidderId: String, 60 | * amount: BigDecimal, 61 | * timestamp: Long 62 | * ) 63 | * 64 | * case class AuctionState( 65 | * id: String, 66 | * currentPrice: BigDecimal, 67 | * currentWinner: Option[String], 68 | * endTime: Long, 69 | * isActive: Boolean 70 | * ) 71 | * 72 | * // Events we'll broadcast 73 | * sealed trait AuctionEvent 74 | * case class BidPlaced(bid: Bid) extends AuctionEvent 75 | * case class AuctionEnded( 76 | * auctionId: String, 77 | * finalPrice: BigDecimal, 78 | * winner: Option[String] 79 | * ) extends AuctionEvent 80 | * }}} 81 | */ 82 | package AuctionSystemImpl {} 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/06-parallelism-and-concurrency-operators.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | import zio._ 4 | 5 | import java.net._ 6 | 7 | object ConcurrencyOperators { 8 | 9 | def foreachPar[R, E, A, B]( 10 | in: Iterable[A] 11 | )(f: A => ZIO[R, E, B]): ZIO[R, E, List[B]] = 12 | ZIO.foreachPar(in.toList)(f) 13 | 14 | /* 15 | 1. Implement the collectAllPar combinator using foreachPar. 16 | */ 17 | def collectAllPar[R, E, A]( 18 | in: Iterable[ZIO[R, E, A]] 19 | ): ZIO[R, E, List[A]] = 20 | foreachPar(in.toList)(identity) 21 | 22 | /* 23 | 2. Write a function that takes a collection of ZIO effects and collects all the successful 24 | and failed results as a tuple. 25 | */ 26 | def collectAllParResults[R, E, A]( 27 | in: Iterable[ZIO[R, E, A]] 28 | ): ZIO[R, Nothing, (List[A], List[E])] = 29 | ZIO.partitionPar(in.toList)(identity).map { case (errors, successes) => 30 | (successes.toList, errors.toList) 31 | } 32 | 33 | /* 34 | 3. Assume you have given the following fetchUrl function that fetches a URL and 35 | returns a ZIO effect: 36 | 37 | And you have a list of URLs you want to fetch in parallel. Implement a function that 38 | fetches all the URLs in parallel and collects both the successful and failed results. 39 | Both successful and failed results should be paired with the URL they correspond 40 | to. 41 | */ 42 | def fetchUrl(url: URL): ZIO[Any, Throwable, String] = ??? 43 | 44 | def fetchAllUrlsPar( 45 | urls: List[String] 46 | ): ZIO[Any, Nothing, (List[(URL, Throwable)], List[(URL, String)])] = 47 | for { 48 | parsedUrls <- foreachPar(urls)(url => 49 | ZIO 50 | .attempt(URI.create(url).toURL()) 51 | .fold( 52 | invalidUrl => Left(invalidUrl), 53 | validUrl => Right(validUrl) 54 | ) 55 | ) 56 | validAndInvalidUrls <- foreachPar(parsedUrls) { url => 57 | url match { 58 | case Left(invalid) => 59 | ZIO.succeed( 60 | Left( 61 | url.toOption.get -> invalid 62 | ) 63 | ) 64 | case Right(url) => 65 | fetchUrl(url).fold( 66 | fetchError => Left(url -> fetchError), 67 | content => Right(url -> content) 68 | ) 69 | } 70 | 71 | } 72 | } yield validAndInvalidUrls.partitionMap(identity) 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/08-parallelism-and-concurrency-interruption-in-depth.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | import zio._ 4 | import zio.test.TestAspect._ 5 | import zio.test._ 6 | 7 | package ParallelismAndConcurrencyInterruptionInDepth { 8 | 9 | /** 10 | * Find the right location to insert `ZIO.interruptible` to make the test 11 | * succeed. 12 | */ 13 | object Exercise1 extends ZIOSpecDefault { 14 | 15 | override def spec = 16 | test("interruptible") { 17 | for { 18 | ref <- Ref.make(0) 19 | latch <- Promise.make[Nothing, Unit] 20 | fiber <- ZIO 21 | .uninterruptible(latch.succeed(()) *> ZIO.never) 22 | .ensuring(ref.update(_ + 1)) 23 | .forkDaemon 24 | _ <- Live.live( 25 | latch.await *> fiber.interrupt.disconnect.timeout(1.second) 26 | ) 27 | value <- ref.get 28 | } yield assertTrue(value == 1) 29 | } @@ nonFlaky 30 | } 31 | 32 | /** 33 | * Find the right location to insert `ZIO.uninterruptible` to make the test 34 | * succeed. 35 | */ 36 | object Exercise2 extends ZIOSpecDefault { 37 | override def spec = 38 | test("uninterruptible") { 39 | for { 40 | ref <- Ref.make(0) 41 | latch <- Promise.make[Nothing, Unit] 42 | fiber <- { 43 | latch.succeed(()) *> 44 | Live.live(ZIO.sleep(10.millis)) *> 45 | ref.update(_ + 1) 46 | }.forkDaemon 47 | _ <- latch.await *> fiber.interrupt 48 | value <- ref.get 49 | } yield assertTrue(value == 1) 50 | } @@ nonFlaky 51 | 52 | } 53 | 54 | /** 55 | * Implement `withFinalizer` without using `ZIO#ensuring`. If the given zio 56 | * effect has not started, it can be interrupted. However, once it has 57 | * started, the finalizer must be executed regardless of whether the `zio` 58 | * effect succeeds, fails, or is interrupted: 59 | * 60 | * {{{ 61 | * def withFinalizer[R, E, A]( 62 | * zio: ZIO[R, E, A] 63 | * )(finalizer: UIO[Any]): ZIO[R, E, A] = ??? 64 | * }}} 65 | * 66 | * Write a test that checks the proper execution of the `finalizer` in the 67 | * case the given `zio` effect is interrupted. 68 | * 69 | * Hint: use the `uninterruptibleMask` primitive to implement `withFinalizer`. 70 | */ 71 | object Exercise3 extends ZIOSpecDefault { 72 | override def spec = ??? 73 | 74 | def withFinalizer[R, E, A]( 75 | zio: ZIO[R, E, A] 76 | )(finalizer: UIO[Any]): ZIO[R, E, A] = ??? 77 | } 78 | 79 | /** 80 | * Implement the `ZIO#disconnect` with the stuff you have learned in this 81 | * chapter, then compare your implementation with the one in ZIO. 82 | */ 83 | object Exercise4 { 84 | def disconnect[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = ??? 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated using `zio-sbt-ci` plugin via `sbt ciGenerateGithubWorkflow` 2 | # task and should be included in the git repository. Please do not edit it manually. 3 | 4 | name: CI 5 | env: 6 | JDK_JAVA_OPTIONS: -XX:+PrintCommandLineFlags 7 | 'on': 8 | workflow_dispatch: {} 9 | release: 10 | types: 11 | - published 12 | push: {} 13 | pull_request: 14 | branches-ignore: 15 | - gh-pages 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.run_id || github.ref }} 18 | cancel-in-progress: true 19 | jobs: 20 | build: 21 | name: Build 22 | runs-on: ubuntu-latest 23 | continue-on-error: true 24 | steps: 25 | - name: Git Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: '0' 29 | - name: Install libuv 30 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 31 | - name: Setup Scala 32 | uses: actions/setup-java@v4 33 | with: 34 | distribution: corretto 35 | java-version: '17' 36 | check-latest: true 37 | - name: Setup SBT 38 | uses: sbt/setup-sbt@v1 39 | - name: Cache Dependencies 40 | uses: coursier/cache-action@v6 41 | - name: Check all code compiles 42 | run: sbt +Test/compile 43 | - name: Check artifacts build process 44 | run: sbt +publishLocal 45 | lint: 46 | name: Lint 47 | runs-on: ubuntu-latest 48 | continue-on-error: false 49 | steps: 50 | - name: Git Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | fetch-depth: '0' 54 | - name: Install libuv 55 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 56 | - name: Setup Scala 57 | uses: actions/setup-java@v4 58 | with: 59 | distribution: corretto 60 | java-version: '17' 61 | check-latest: true 62 | - name: Setup SBT 63 | uses: sbt/setup-sbt@v1 64 | - name: Cache Dependencies 65 | uses: coursier/cache-action@v6 66 | - name: Check if the site workflow is up to date 67 | run: sbt ciCheckGithubWorkflow 68 | - name: Lint 69 | run: sbt lint 70 | test: 71 | name: Test 72 | runs-on: ubuntu-latest 73 | continue-on-error: false 74 | strategy: 75 | fail-fast: false 76 | matrix: 77 | java: 78 | - '11' 79 | - '17' 80 | - '21' 81 | steps: 82 | - name: Install libuv 83 | run: sudo apt-get update && sudo apt-get install -y libuv1-dev 84 | - name: Setup Scala 85 | uses: actions/setup-java@v4 86 | with: 87 | distribution: corretto 88 | java-version: ${{ matrix.java }} 89 | check-latest: true 90 | - name: Setup SBT 91 | uses: sbt/setup-sbt@v1 92 | - name: Cache Dependencies 93 | uses: coursier/cache-action@v6 94 | - name: Git Checkout 95 | uses: actions/checkout@v4 96 | with: 97 | fetch-depth: '0' 98 | - name: Test 99 | run: sbt +test 100 | ci: 101 | name: ci 102 | runs-on: ubuntu-latest 103 | continue-on-error: false 104 | needs: 105 | - lint 106 | - test 107 | - build 108 | steps: 109 | - name: Report Successful CI 110 | run: echo "ci passed" 111 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/02-testing-zio-programs.scala: -------------------------------------------------------------------------------- 1 | package TestingZIOPrograms { 2 | 3 | /** 4 | * 1. Write a ZIO program that simulates a countdown timer (e.g., prints 5 | * numbers from 5 to 1, with a 1-second delay between each). Test this 6 | * program using TestClock. 7 | */ 8 | 9 | import zio._ 10 | import zio.test._ 11 | 12 | object CountdownTimer extends ZIOSpecDefault { 13 | def countdown(n: Int): ZIO[Any, Nothing, Unit] = ??? 14 | 15 | override def spec = 16 | suite("Countdown Timer Spec")( 17 | test("should count down from 5 to 1") { 18 | ??? 19 | } 20 | ) 21 | } 22 | 23 | /** 24 | * 2. Create a simple cache that expires entries after a certain duration. 25 | * Implement a program that adds items to the cache and tries to retrieve 26 | * them. Write tests using `TestClock` to verify that items are available 27 | * before expiration and unavailable after expiration. 28 | */ 29 | 30 | object CacheWithExpiration extends ZIOSpecDefault { 31 | 32 | override def spec = 33 | suite("Cache With Expiration Spec")( 34 | test("should store and retrieve items before expiration") { 35 | ??? 36 | }, 37 | test("should not retrieve items after expiration") { 38 | ??? 39 | } 40 | ) 41 | } 42 | 43 | /** 44 | * 3. Create a rate limiter that allows a maximum of N operations per 45 | * minute. Implement a program that uses this rate limiter. Write tests 46 | * using `TestClock` to verify that the rate limiter correctly allows or 47 | * blocks operations based on the time window. 48 | */ 49 | 50 | object RateLimiterSpec extends ZIOSpecDefault { 51 | 52 | type RateLimiter 53 | 54 | override def spec = 55 | suite("Rate Limiter Spec")( 56 | test("should allow operations within rate limit") { 57 | ??? 58 | }, 59 | test("should block operations exceeding rate limit") { 60 | ??? 61 | } 62 | ) 63 | } 64 | 65 | /** 66 | * 4. Implement a function that reverses a list, then write a property-based 67 | * test to verify that reversing a list twice returns the original list. 68 | */ 69 | object ReverseListSpec extends ZIOSpecDefault { 70 | def reverseList[A](list: List[A]): List[A] = ??? 71 | 72 | override def spec = 73 | suite("Reverse List Spec")( 74 | test("reversing a list twice returns the original list") { 75 | ??? 76 | } 77 | ) 78 | } 79 | 80 | /** 81 | * 5. Implement an AVL tree (self-balancing binary search tree) with insert 82 | * and delete operations. Write property-based tests to verify that the 83 | * tree remains balanced after each operation. A balanced tree is one 84 | * where the height of every node's left and right subtrees differs by at 85 | * most one. 86 | */ 87 | object AVLTreeSpec extends ZIOSpecDefault { 88 | 89 | // A simple AVL Tree implementation here 90 | 91 | override def spec = 92 | suite("AVL Tree Spec")( 93 | suite("Balance Properties")( 94 | test("should maintain balance after insertions") { 95 | ??? 96 | }, 97 | test("should maintain balance after deletions") { 98 | ??? 99 | } 100 | ) 101 | ) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/08-parallelism-and-concurrency-interruption-in-depth.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | import zio._ 4 | import zio.test.TestAspect._ 5 | import zio.test._ 6 | 7 | package ParallelismAndConcurrencyInterruptionInDepth { 8 | 9 | /** 10 | * Find the right location to insert `ZIO.interruptible` to make the test 11 | * succeed. 12 | */ 13 | object Exercise1 extends ZIOSpecDefault { 14 | 15 | override def spec = 16 | test("interruptible") { 17 | for { 18 | ref <- Ref.make(0) 19 | latch <- Promise.make[Nothing, Unit] 20 | fiber <- ZIO 21 | .uninterruptible( 22 | latch.succeed(()) *> ZIO.interruptible(ZIO.never) 23 | ) 24 | .ensuring(ref.update(_ + 1)) 25 | .forkDaemon 26 | _ <- Live.live( 27 | latch.await *> fiber.interrupt.disconnect.timeout(1.second) 28 | ) 29 | value <- ref.get 30 | } yield assertTrue(value == 1) 31 | } @@ nonFlaky 32 | } 33 | 34 | /** 35 | * Find the right location to insert `ZIO.uninterruptible` to make the test 36 | * succeed. 37 | */ 38 | object Exercise2 extends ZIOSpecDefault { 39 | override def spec = 40 | test("uninterruptible") { 41 | for { 42 | ref <- Ref.make(0) 43 | latch <- Promise.make[Nothing, Unit] 44 | fiber <- ZIO.uninterruptible { 45 | latch.succeed(()) *> 46 | Live.live(ZIO.sleep(10.millis)) *> 47 | ref.update(_ + 1) 48 | }.forkDaemon 49 | _ <- latch.await *> fiber.interrupt 50 | value <- ref.get 51 | } yield assertTrue(value == 1) 52 | } @@ nonFlaky 53 | 54 | } 55 | 56 | /** 57 | * Implement `withFinalizer` without using `ZIO#ensuring`. If the given zio 58 | * effect has not started, it can be interrupted. However, once it has 59 | * started, the finalizer must be executed regardless of whether the `zio` 60 | * effect succeeds, fails, or is interrupted: 61 | * 62 | * {{{ 63 | * def withFinalizer[R, E, A]( 64 | * zio: ZIO[R, E, A] 65 | * )(finalizer: UIO[Any]): ZIO[R, E, A] = ??? 66 | * }}} 67 | * 68 | * Write a test that checks the proper execution of the `finalizer` in the 69 | * case the given `zio` effect is interrupted. 70 | * 71 | * Hint: use the `uninterruptibleMask` primitive to implement `withFinalizer`. 72 | */ 73 | object Exercise3 extends ZIOSpecDefault { 74 | override def spec = 75 | test("withFinalizer executes finalizer when zio is interrupted") { 76 | for { 77 | finalizerExecuted <- Ref.make(false) 78 | effectStarted <- Promise.make[Nothing, Unit] 79 | fiber <- withFinalizer(effectStarted.succeed(()) *> ZIO.never)( 80 | finalizerExecuted.set(true) 81 | ).fork 82 | _ <- effectStarted.await // Wait for the effect to start 83 | _ <- fiber.interrupt // Interrupt the fiber 84 | result <- finalizerExecuted.get 85 | } yield assertTrue(result) 86 | } @@ nonFlaky 87 | 88 | def withFinalizer[R, E, A]( 89 | zio: ZIO[R, E, A] 90 | )(finalizer: UIO[Any]): ZIO[R, E, A] = 91 | ZIO.uninterruptibleMask { restore => 92 | restore(zio).exit.flatMap { exit => 93 | finalizer *> ZIO.suspendSucceed(exit) 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Implement the `ZIO#disconnect` with the stuff you have learned in this 100 | * chapter, then compare your implementation with the one in ZIO. 101 | */ 102 | object Exercise4 { 103 | def disconnect[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = 104 | ZIO.uninterruptibleMask(restore => 105 | restore(zio).forkDaemon.flatMap(f => 106 | restore(f.join).onInterrupt(f.interruptFork) 107 | ) 108 | ) 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/09-concurrent-structures-ref-shared-state.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package RefSharedState { 4 | 5 | /** 6 | * 1. Write a simple `Counter` with the following interface that can be 7 | * incremented and decremented concurrently: 8 | * 9 | * {{{ 10 | * trait Counter { 11 | * def increment: UIO[Long] 12 | * def decrement: UIO[Long] 13 | * def get: UIO[Long] 14 | * def reset: UIO[Unit] 15 | * } 16 | * }}} 17 | */ 18 | object CounterImpl {} 19 | 20 | /** 21 | * 2. Implement a bounded queue using `Ref` that has a maximum capacity that 22 | * supports the following interface: 23 | * 24 | * {{{ 25 | * trait BoundedQueue[A] { 26 | * def enqueue(a: A): UIO[Boolean] // Returns false if queue is full 27 | * def dequeue: UIO[Option[A]] // Returns None if queue is empty 28 | * def size: UIO[Int] 29 | * def capacity: UIO[Int] 30 | * } 31 | * }}} 32 | */ 33 | object BoundedQueueImpl {} 34 | 35 | /** 36 | * 3. Write a `CounterManager` service that manages multiple counters with 37 | * the following interface: 38 | * 39 | * {{{ 40 | * type CounterId = String 41 | * 42 | * trait CounterManager { 43 | * def increment(id: CounterId): UIO[Int] 44 | * def decrement(id: CounterId): UIO[Int] 45 | * def get(id: CounterId): UIO[Int] 46 | * def reset(id: CounterId): UIO[Unit] 47 | * def remove(id: CounterId): UIO[Unit] 48 | * } 49 | * }}} 50 | */ 51 | 52 | object CounterManagerImpl {} 53 | 54 | /** 55 | * 4. Implement a basic log renderer for the `FiberRef[Log]` we have defined 56 | * through the chapter. It should show the hierarchical structure of 57 | * fiber logs using indentation: 58 | * 59 | * - Each level of nesting should be indented by two spaces from the 60 | * previous one. 61 | * - The log entries for each fiber should be shown on separate lines 62 | * - Child fiber logs should be shown under their parent fiber 63 | * 64 | * {{{ 65 | * trait Logger { 66 | * def log(message: String): UIO[Unit] 67 | * } 68 | * 69 | * object Logger { 70 | * def render(ref: Log): UIO[String] = ??? 71 | * } 72 | * }}} 73 | * 74 | * Example output: 75 | * 76 | * Got foo Got 1 Got 2 Got bar Got 3 Got 4 77 | */ 78 | object NestedLoggerRendererImpl {} 79 | 80 | /** 81 | * 5. Change the log model and use a more detailed one instead of just a 82 | * `String`, so that you can implement an advanced log renderer that adds 83 | * timestamps and fiber IDs, like the following output: 84 | * 85 | * {{{ 86 | * [2024-01-01 10:00:01][fiber-1] Child foo 87 | * [2024-01-01 10:00:02][fiber-2] Got 1 88 | * [2024-01-01 10:00:03][fiber-2] Got 2 89 | * [2024-01-01 10:00:01][fiber-1] Child bar 90 | * [2024-01-01 10:00:02][fiber-3] Got 3 91 | * [2024-01-01 10:00:03][fiber-3] Got 4 92 | * }}} 93 | * 94 | * Hint: You can use the following model for the log entry: 95 | * 96 | * {{{ 97 | * case class LogEntry( 98 | * timestamp: java.time.Instant, 99 | * fiberId: String, 100 | * message: String 101 | * ) 102 | * }}} 103 | */ 104 | object AdvancedNestedLoggerImpl {} 105 | 106 | /** 107 | * 6. Create a more advanced logging system that supports different log 108 | * levels. It also should support regional settings for log levels so 109 | * that the user can change the log level for a specific region of the 110 | * application: 111 | * 112 | * {{{ 113 | * trait Logger { 114 | * def log(message: String): UIO[Unit] 115 | * def withLogLevel[R, E, A](level: LogLevel)( 116 | * zio: ZIO[R, E, A] 117 | * ): ZIO[R, E, A] 118 | * } 119 | * }}} 120 | */ 121 | object AdvancedLoggingSystemWithLogLevel {} 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/03-the-zio-error-model.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | import zio._ 4 | 5 | object TheZIOErrorModel { 6 | 7 | /** 8 | * 1. Using the appropriate effect constructor, fix the following function 9 | * so that it no longer fails with defects when executed. Make a note of 10 | * how the inferred return type for the function changes. 11 | */ 12 | object Exercise1 { 13 | 14 | def failWithMessage(string: String) = 15 | ZIO.succeed(throw new Error(string)) 16 | } 17 | 18 | /** 19 | * 2. Using the `ZIO#foldCauseZIO` operator and the `Cause#defects` method, 20 | * implement the following function. This function should take the 21 | * effect, inspect defects, and if a suitable defect is found, it should 22 | * recover from the error with the help of the specified function, which 23 | * generates a new success value for such a defect. 24 | */ 25 | object Exercise2 { 26 | 27 | def recoverFromSomeDefects[R, E, A](zio: ZIO[R, E, A])( 28 | f: Throwable => Option[A] 29 | ): ZIO[R, E, A] = 30 | ??? 31 | } 32 | 33 | /** 34 | * 3. Using the `ZIO#foldCauseZIO` operator and the `Cause#prettyPrint` 35 | * method, implement an operator that takes an effect, and returns a new 36 | * effect that logs any failures of the original effect (including errors 37 | * and defects), without changing its failure or success value. 38 | */ 39 | object Exercise3 { 40 | 41 | def logFailures[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = 42 | ??? 43 | } 44 | 45 | /** 46 | * 4. Using the `ZIO#exit` method, which "runs" an effect to an `Exit` 47 | * value, implement the following function, which will execute the 48 | * specified effect on any failure at all: 49 | */ 50 | object Exercise4 { 51 | 52 | def onAnyFailure[R, E, A]( 53 | zio: ZIO[R, E, A], 54 | handler: ZIO[R, E, Any] 55 | ): ZIO[R, E, A] = 56 | ??? 57 | } 58 | 59 | /** 60 | * 5. Using the `ZIO#refineOrDie` method, implement the `ioException` 61 | * function, which refines the error channel to only include the 62 | * `IOException` error. 63 | */ 64 | object Exercise5 { 65 | 66 | def ioException[R, A]( 67 | zio: ZIO[R, Throwable, A] 68 | ): ZIO[R, java.io.IOException, A] = 69 | ??? 70 | } 71 | 72 | /** 73 | * 6. Using the `ZIO#refineToOrDie` method, narrow the error type of the 74 | * following effect to just `NumberFormatException`. 75 | */ 76 | object Exercise6 { 77 | 78 | val parseNumber: ZIO[Any, Throwable, Int] = 79 | ZIO.attempt("foo".toInt) 80 | } 81 | 82 | /** 83 | * 7. Using the `ZIO#foldZIO` method, implement the following two functions, 84 | * which make working with `Either` values easier, by shifting the 85 | * unexpected case into the error channel (and reversing this shifting). 86 | */ 87 | object Exercise7 { 88 | 89 | def left[R, E, A, B]( 90 | zio: ZIO[R, E, Either[A, B]] 91 | ): ZIO[R, Either[E, B], A] = 92 | ??? 93 | 94 | def unleft[R, E, A, B]( 95 | zio: ZIO[R, Either[E, B], A] 96 | ): ZIO[R, E, Either[A, B]] = 97 | ??? 98 | } 99 | 100 | /** 101 | * 8. Using the `ZIO#foldZIO` method, implement the following two functions, 102 | * which make working with `Either` values easier, by shifting the 103 | * unexpected case into the error channel (and reversing this shifting). 104 | */ 105 | object Exercise8 { 106 | 107 | def right[R, E, A, B]( 108 | zio: ZIO[R, E, Either[A, B]] 109 | ): ZIO[R, Either[E, A], B] = 110 | ??? 111 | 112 | def unright[R, E, A, B]( 113 | zio: ZIO[R, Either[E, A], B] 114 | ): ZIO[R, E, Either[A, B]] = 115 | ??? 116 | } 117 | 118 | /** 119 | * 9. Using the `ZIO#sandbox` method, implement the following function. 120 | */ 121 | object Exercise9 { 122 | 123 | def catchAllCause[R, E1, E2, A]( 124 | zio: ZIO[R, E1, A], 125 | handler: Cause[E1] => ZIO[R, E2, A] 126 | ): ZIO[R, E2, A] = ??? 127 | } 128 | 129 | /** 130 | * 10. Using the `ZIO#foldCauseZIO` method, implement the following 131 | * function. 132 | */ 133 | object Exercise10 { 134 | def catchAllCause[R, E1, E2, A]( 135 | zio: ZIO[R, E1, A], 136 | handler: Cause[E1] => ZIO[R, E2, A] 137 | ): ZIO[R, E2, A] = ??? 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/chap04/01-doobie-integration.sc: -------------------------------------------------------------------------------- 1 | /** 2 | * 1. Create a ZIO program that uses Doobie to perform a database operation. 3 | * Implement a function that inserts a user into a database and returns 4 | * the number of affected rows. Use the following table structure: 5 | * 6 | * ```sql 7 | * CREATE TABLE users ( 8 | * id SERIAL PRIMARY KEY, 9 | * name TEXT NOT NULL, 10 | * age INT NOT NULL 11 | * ) 12 | * ``` 13 | */ 14 | 15 | //> using scala "2.13.16" 16 | //> using dep "dev.zio::zio:2.1.18" 17 | //> using dep "dev.zio::zio-interop-cats:23.1.0.5" 18 | //> using dep "org.tpolecat::doobie-core:1.0.0-RC9" 19 | //> using dep "org.tpolecat::doobie-hikari:1.0.0-RC9" 20 | //> using dep "org.xerial:sqlite-jdbc:3.49.1.0" 21 | 22 | import com.zaxxer.hikari.HikariConfig 23 | import doobie._ 24 | import doobie.hikari._ 25 | import doobie.implicits._ 26 | import zio._ 27 | import zio.interop.catz._ 28 | 29 | // User case class 30 | case class User(name: String, age: Int) 31 | 32 | // Database operations 33 | object UserRepository { 34 | 35 | // Insert a user and return the number of affected rows 36 | def insertUser(user: User): ConnectionIO[Int] = 37 | sql""" 38 | INSERT INTO users (name, age) 39 | VALUES (${user.name}, ${user.age}) 40 | """.update.run 41 | 42 | // Insert a user and return the generated ID 43 | def insertUserWithId(user: User): ConnectionIO[Long] = 44 | sql""" 45 | INSERT INTO users (name, age) 46 | VALUES (${user.name}, ${user.age}) 47 | """.update.withUniqueGeneratedKeys[Long]("id") 48 | 49 | // Get all users (for verification) 50 | def getAllUsers: ConnectionIO[List[(Long, String, Int)]] = 51 | sql""" 52 | SELECT id, name, age FROM users 53 | """.query[(Long, String, Int)].to[List] 54 | 55 | // Create table if not exists 56 | def createTable: ConnectionIO[Int] = 57 | sql""" 58 | CREATE TABLE IF NOT EXISTS users ( 59 | id INTEGER PRIMARY KEY AUTOINCREMENT, 60 | name TEXT NOT NULL, 61 | age INTEGER NOT NULL 62 | ) 63 | """.update.run 64 | } 65 | 66 | // ZIO Layer for Database Transactor 67 | object Database { 68 | 69 | private def hikariConfig: HikariConfig = { 70 | val config = new HikariConfig() 71 | config.setDriverClassName("org.sqlite.JDBC") 72 | config.setJdbcUrl("jdbc:sqlite:users.db") 73 | config.setMaximumPoolSize(1) // SQLite doesn't support concurrent writes 74 | config 75 | } 76 | 77 | val transactorLive: ZLayer[Any, Throwable, HikariTransactor[Task]] = 78 | ZLayer.scoped { 79 | for { 80 | ec <- ZIO.executor.map(_.asExecutionContext) 81 | xa <- HikariTransactor 82 | .fromHikariConfigCustomEc[Task](hikariConfig, ec) 83 | .toScopedZIO 84 | } yield xa 85 | } 86 | } 87 | 88 | // Service layer 89 | trait UserService { 90 | def insertUser(user: User): Task[Int] 91 | def insertUserWithId(user: User): Task[Long] 92 | def getAllUsers: Task[List[(Long, String, Int)]] 93 | def initializeDatabase: Task[Unit] 94 | } 95 | 96 | object UserService { 97 | 98 | val live: ZLayer[HikariTransactor[Task], Nothing, UserService] = 99 | ZLayer.fromFunction { (xa: HikariTransactor[Task]) => 100 | new UserService { 101 | def insertUser(user: User): Task[Int] = 102 | UserRepository.insertUser(user).transact(xa) 103 | 104 | def insertUserWithId(user: User): Task[Long] = 105 | UserRepository.insertUserWithId(user).transact(xa) 106 | 107 | def getAllUsers: Task[List[(Long, String, Int)]] = 108 | UserRepository.getAllUsers.transact(xa) 109 | 110 | def initializeDatabase: Task[Unit] = 111 | UserRepository.createTable.transact(xa).unit 112 | } 113 | } 114 | } 115 | 116 | object Main extends ZIOAppDefault { 117 | 118 | val program: ZIO[UserService, Throwable, Unit] = for { 119 | service <- ZIO.service[UserService] 120 | 121 | // Initialize database 122 | _ <- service.initializeDatabase 123 | _ <- Console.printLine("Database initialized") 124 | 125 | // Insert users 126 | user1 = User("Alice", 25) 127 | user2 = User("Bob", 30) 128 | user3 = User("Charlie", 35) 129 | 130 | // Insert user and get affected rows 131 | affectedRows1 <- service.insertUser(user1) 132 | _ <- Console.printLine( 133 | s"Inserted ${user1.name}, affected rows: $affectedRows1" 134 | ) 135 | 136 | // Insert users and get generated IDs 137 | id2 <- service.insertUserWithId(user2) 138 | _ <- Console.printLine(s"Inserted ${user2.name} with ID: $id2") 139 | 140 | id3 <- service.insertUserWithId(user3) 141 | _ <- Console.printLine(s"Inserted ${user3.name} with ID: $id3") 142 | 143 | // Verify by getting all users 144 | _ <- Console.printLine("\nAll users in database:") 145 | users <- service.getAllUsers 146 | _ <- ZIO.foreach(users) { case (id, name, age) => 147 | Console.printLine(s" ID: $id, Name: $name, Age: $age") 148 | } 149 | 150 | } yield () 151 | 152 | def run = 153 | program 154 | .provide( 155 | Database.transactorLive, 156 | UserService.live 157 | ) 158 | .tapError(error => Console.printLineError(s"Error: ${error.getMessage}")) 159 | .exitCode 160 | } 161 | 162 | // To run this script: 163 | // 1. Run: scala-cli run zio-doobie-user.scala 164 | // 2. The SQLite database file 'users.db' will be created automatically 165 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/03-the-zio-error-model.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | import zio._ 4 | 5 | object TheZIOErrorModel { 6 | 7 | /** 8 | * 1. Using the appropriate effect constructor, fix the following function 9 | * so that it no longer fails with defects when executed. Make a note of 10 | * how the inferred return type for the function changes. 11 | */ 12 | object Exercise1 { 13 | 14 | def failWithMessage(string: String): ZIO[Any, Throwable, Nothing] = 15 | ZIO.attempt(throw new Error(string)) 16 | } 17 | 18 | /** 19 | * 2. Using the `ZIO#foldCauseZIO` operator and the `Cause#defects` method, 20 | * implement the following function. This function should take the 21 | * effect, inspect defects, and if a suitable defect is found, it should 22 | * recover from the error with the help of the specified function, which 23 | * generates a new success value for such a defect. 24 | */ 25 | object Exercise2 { 26 | 27 | def recoverFromSomeDefects[R, E, A](zio: ZIO[R, E, A])( 28 | f: Throwable => Option[A] 29 | ): ZIO[R, E, A] = 30 | zio.foldCauseZIO( 31 | cause => 32 | cause.defects 33 | .collectFirst(Function.unlift(f)) 34 | .fold[ZIO[R, E, A]](ZIO.failCause(cause))(a => ZIO.succeed(a)), 35 | a => ZIO.succeed(a) 36 | ) 37 | } 38 | 39 | /** 40 | * 3. Using the `ZIO#foldCauseZIO` operator and the `Cause#prettyPrint` 41 | * method, implement an operator that takes an effect, and returns a new 42 | * effect that logs any failures of the original effect (including errors 43 | * and defects), without changing its failure or success value. 44 | */ 45 | object Exercise3 { 46 | 47 | def logFailures[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = 48 | zio.foldCauseZIO( 49 | cause => 50 | ZIO.succeed(println(cause.prettyPrint)) *> ZIO.failCause(cause), 51 | a => ZIO.succeed(a) 52 | ) 53 | } 54 | 55 | /** 56 | * 4. Using the `ZIO#exit` method, which "runs" an effect to an `Exit` 57 | * value, implement the following function, which will execute the 58 | * specified effect on any failure at all: 59 | */ 60 | object Exercise4 { 61 | 62 | def onAnyFailure[R, E, A]( 63 | zio: ZIO[R, E, A], 64 | handler: ZIO[R, E, Any] 65 | ): ZIO[R, E, A] = 66 | zio.exit.flatMap { 67 | case Exit.Failure(cause) => handler *> ZIO.failCause(cause) 68 | case Exit.Success(a) => ZIO.succeed(a) 69 | } 70 | } 71 | 72 | /** 73 | * 5. Using the `ZIO#refineOrDie` method, implement the `ioException` 74 | * function, which refines the error channel to only include the 75 | * `IOException` error. 76 | */ 77 | object Exercise5 { 78 | 79 | def ioException[R, A]( 80 | zio: ZIO[R, Throwable, A] 81 | ): ZIO[R, java.io.IOException, A] = 82 | zio.refineToOrDie[java.io.IOException] 83 | } 84 | 85 | /** 86 | * 6. Using the `ZIO#refineToOrDie` method, narrow the error type of the 87 | * following effect to just `NumberFormatException`. 88 | */ 89 | object Exercise6 { 90 | 91 | val parseNumber: ZIO[Any, Throwable, Int] = 92 | ZIO.attempt("foo".toInt).refineToOrDie[NumberFormatException] 93 | } 94 | 95 | /** 96 | * 7. Using the `ZIO#foldZIO` method, implement the following two functions, 97 | * which make working with `Either` values easier, by shifting the 98 | * unexpected case into the error channel (and reversing this shifting). 99 | */ 100 | object Exercise7 { 101 | 102 | def left[R, E, A, B]( 103 | zio: ZIO[R, E, Either[A, B]] 104 | ): ZIO[R, Either[E, B], A] = 105 | zio.foldZIO( 106 | e => ZIO.fail(Left(e)), 107 | _.fold(a => ZIO.succeed(a), b => ZIO.fail(Right(b))) 108 | ) 109 | 110 | def unleft[R, E, A, B]( 111 | zio: ZIO[R, Either[E, B], A] 112 | ): ZIO[R, E, Either[A, B]] = 113 | zio.foldZIO( 114 | _.fold(e => ZIO.fail(e), b => ZIO.succeed(Right(b))), 115 | a => ZIO.succeed(Left(a)) 116 | ) 117 | } 118 | 119 | /** 120 | * 8. Using the `ZIO#foldZIO` method, implement the following two functions, 121 | * which make working with `Either` values easier, by shifting the 122 | * unexpected case into the error channel (and reversing this shifting). 123 | */ 124 | object Exercise8 { 125 | 126 | def right[R, E, A, B]( 127 | zio: ZIO[R, E, Either[A, B]] 128 | ): ZIO[R, Either[E, A], B] = 129 | zio.foldZIO( 130 | e => ZIO.fail(Left(e)), 131 | _.fold(a => ZIO.fail(Right(a)), b => ZIO.succeed(b)) 132 | ) 133 | 134 | def unright[R, E, A, B]( 135 | zio: ZIO[R, Either[E, A], B] 136 | ): ZIO[R, E, Either[A, B]] = 137 | zio.foldZIO( 138 | _.fold(e => ZIO.fail(e), a => ZIO.succeed(Left(a))), 139 | b => ZIO.succeed(Right(b)) 140 | ) 141 | } 142 | 143 | /** 144 | * 9. Using the `ZIO#sandbox` method, implement the following function. 145 | */ 146 | object Exercise9 { 147 | 148 | def catchAllCause[R, E1, E2, A]( 149 | zio: ZIO[R, E1, A], 150 | handler: Cause[E1] => ZIO[R, E2, A] 151 | ): ZIO[R, E2, A] = 152 | zio.sandbox.foldZIO(cause => handler(cause), a => ZIO.succeed(a)) 153 | } 154 | 155 | /** 156 | * 10. Using the `ZIO#foldCauseZIO` method, implement the following 157 | * function. 158 | */ 159 | object Exercise10 { 160 | def catchAllCause[R, E1, E2, A]( 161 | zio: ZIO[R, E1, A], 162 | handler: Cause[E1] => ZIO[R, E2, A] 163 | ): ZIO[R, E2, A] = 164 | zio.foldCauseZIO(cause => handler(cause), a => ZIO.succeed(a)) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/05-parallelism-and-concurrency-the-fiber-model.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | import zio._ 4 | 5 | object TheFiberModel_Solutions { 6 | 7 | /* 8 | 1. Write a ZIO program that forks two effects, one that prints “Hello” after a two- 9 | second delay and another that prints “World” after a one-second delay. Ensure both 10 | effects run concurrently. 11 | */ 12 | object Question1 { 13 | 14 | val first_effect = ZIO.sleep(2.seconds) *> Console.printLine("Hello") 15 | val second_effect = ZIO.sleep(1.second) *> Console.printLine("World") 16 | 17 | def run = for { 18 | first <- first_effect.fork 19 | second <- second_effect.fork 20 | _ <- first.join 21 | _ <- second.join 22 | } yield () 23 | 24 | } 25 | 26 | /* 27 | Modify the previous program to print “Done” only after both forked effects have completed. 28 | */ 29 | 30 | object Question2 { 31 | val first_effect = ZIO.sleep(2.seconds) *> Console.printLine("Hello") 32 | val second_effect = ZIO.sleep(1.second) *> Console.printLine("World") 33 | 34 | def run = for { 35 | first <- first_effect.fork 36 | second <- second_effect.fork 37 | _ <- first.join 38 | _ <- second.join 39 | _ <- Console.printLine("Done") 40 | } yield () 41 | } 42 | 43 | /* 44 | Write a program that starts a long-running effect (e.g., printing numbers every second), then interrupts it after 5 seconds. 45 | */ 46 | object Question3 { 47 | val printNumbers = 48 | ZIO.randomWith(_.nextInt).repeat(Schedule.fixed(1.second)) 49 | 50 | def run = 51 | for { 52 | fiber <- printNumbers.fork 53 | _ <- ZIO.sleep(5.seconds) 54 | _ <- fiber.interrupt 55 | _ <- Console.printLine("Done") 56 | } yield () 57 | } 58 | 59 | /* 60 | Create a program that forks an effect that might fail. Use await to handle both 61 | success and failure cases. 62 | */ 63 | 64 | object Question4 { 65 | val faillibleEffect = (n: Int) => 66 | ZIO.attempt(n / (n - 1)).refineOrDie { case _: ArithmeticException => 67 | "Division by zero" 68 | } 69 | 70 | def run = for { 71 | fibers <- 72 | ZIO.foreach((1 until 10).toList)(num => faillibleEffect(num).fork) 73 | results <- ZIO.foreach(fibers)(_.await) 74 | _ <- ZIO.foreach(results)(res => 75 | res.foldZIO( 76 | e => ZIO.debug("The fiber has failed with: " + e), 77 | s => ZIO.debug("The fiber has completed with: " + s) 78 | ) 79 | ) 80 | } yield () 81 | } 82 | 83 | /* 84 | Create a program with an uninterruptible section that simulates a critical operation. 85 | Try to interrupt it and observe the behavior. 86 | */ 87 | 88 | object Question5 { 89 | 90 | def criticalOperation: ZIO[Any, Throwable, Unit] = 91 | ZIO 92 | .attempt(while (true) { 93 | Thread.sleep(100) 94 | }) 95 | .uninterruptible 96 | 97 | def run = for { 98 | fiber <- criticalOperation.fork 99 | _ <- fiber.interrupt 100 | _ <- 101 | ZIO.debug( 102 | "Operation Finished!" 103 | ) // <- will never reach here and operation will never stop 104 | 105 | } yield () 106 | 107 | } 108 | 109 | /* 110 | 6. Write a program demonstrating fiber supervision where a parent fiber forks two 111 | child fibers. Interrupt the parent and observe what happens to the children. 112 | 113 | Below is the response 114 | 115 | Parent fiber beginning execution... 116 | Child fiber beginning execution... 117 | Child fiber 2 beginning execution... 118 | 119 | ---- Program complete ---- 120 | 121 | "Hello from a parent fiber" 122 | "Hello from a child fiber" 123 | "Hello from a child fiber 2" 124 | 125 | will never reach 126 | */ 127 | 128 | object Question6 { 129 | 130 | val child = 131 | (Console.printLine("Child fiber 1 beginning execution...").orDie *> 132 | ZIO.sleep(5.seconds) *> 133 | Console.printLine("Hello from a child fiber 1!").orDie) 134 | .onInterrupt(ZIO.debug("Child fiber 1 is interrupted")) 135 | 136 | val child2 = 137 | (Console.printLine("Child fiber 2 beginning execution...").orDie *> 138 | ZIO.sleep(5.seconds) *> 139 | Console.printLine("Hello from a child fiber 2!").orDie) 140 | .onInterrupt(ZIO.debug("Child fiber 2 is interrupted")) 141 | 142 | val parent = 143 | (Console.printLine("Parent fiber beginning execution...").orDie *> 144 | child.fork *> 145 | child2.fork *> 146 | ZIO.sleep(3.seconds) *> 147 | Console.printLine("Hello from a parent fiber!").orDie) 148 | .onInterrupt(ZIO.debug("Parent Fiber is interrupted")) 149 | 150 | def run = 151 | for { 152 | parentfiber <- parent.fork 153 | _ <- ZIO.sleep(1.second) 154 | _ <- parentfiber.interrupt 155 | _ <- ZIO.sleep(3.seconds) 156 | _ <- Console.printLine("---- Program complete ----").orDie 157 | } yield () 158 | 159 | } 160 | 161 | /* 162 | 7. Change one of the child fibers in the previous program to be a daemon fiber. Observe 163 | the difference in behavior when the parent is interrupted. 164 | */ 165 | 166 | object Question7 { 167 | 168 | val child = 169 | (Console.printLine("Child fiber 1 beginning execution...").orDie *> 170 | ZIO.sleep(5.seconds) *> 171 | Console.printLine("Hello from a child fiber 1!").orDie) 172 | .onInterrupt(ZIO.debug("Child fiber 1 is interrupted")) 173 | 174 | val child2 = 175 | (Console.printLine("Child fiber 2 beginning execution...").orDie *> 176 | ZIO.sleep(5.seconds) *> 177 | Console.printLine("Hello from a child fiber 2!").orDie) 178 | .onInterrupt(ZIO.debug("Child fiber 2 is interrupted")) 179 | 180 | val parent = 181 | (Console.printLine("Parent fiber beginning execution...").orDie *> 182 | child.fork *> 183 | child2.forkDaemon *> 184 | ZIO.sleep(3.seconds) *> 185 | Console.printLine("Hello from a parent fiber!").orDie) 186 | .onInterrupt(ZIO.debug("Parent Fiber is interrupted")) 187 | 188 | def run = 189 | for { 190 | fiber <- parent.fork 191 | _ <- ZIO.sleep(1.second) 192 | _ <- fiber.interrupt 193 | _ <- ZIO.sleep(3.seconds) 194 | _ <- Console.printLine("---- Program complete ----").orDie 195 | } yield () 196 | 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/15-scope-composable-resources.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package ScopeComposableResources { 4 | 5 | /** 6 | * 1. Assume we have written a worker as follows: 7 | * 8 | * {{{ 9 | * def worker(sem: Semaphore, id: Int): ZIO[Scope, Nothing, Unit] = 10 | * for { 11 | * _ <- sem.withPermitsScoped(2) 12 | * _ <- Console.printLine(s"Request $id: Starting processing").orDie 13 | * _ <- ZIO.sleep(5.seconds) 14 | * _ <- Console.printLine(s"Request $id: Completed processing").orDie 15 | * } yield () 16 | * }}} 17 | * 18 | * Please explain how and why these two applications have different behavior: 19 | * 20 | * Application 1: 21 | * 22 | * {{{ 23 | * object MainApp1 extends ZIOAppDefault { 24 | * def run = 25 | * for { 26 | * sem <- Semaphore.make(4) 27 | * _ <- ZIO.foreachParDiscard(1 to 10)(i => ZIO.scoped(worker(sem, i))) 28 | * } yield () 29 | * } 30 | * }}} 31 | * 32 | * Application 2: 33 | * 34 | * {{{ 35 | * object MainApp2 extends ZIOAppDefault { 36 | * def run = 37 | * for { 38 | * sem <- Semaphore.make(4) 39 | * _ <- ZIO.scoped(ZIO.foreachParDiscard(1 to 10)(i => worker(sem, i))) 40 | * } yield () 41 | * } 42 | * }}} 43 | */ 44 | package ComparingTwoWorkers { 45 | import zio._ 46 | 47 | object Worker { 48 | def worker(sem: Semaphore, id: Int): ZIO[Scope, Nothing, Unit] = 49 | for { 50 | _ <- sem.withPermitsScoped(2) 51 | _ <- Console.printLine(s"Request $id: Starting processing").orDie 52 | _ <- ZIO.sleep(5.seconds) 53 | _ <- Console.printLine(s"Request $id: Completed processing").orDie 54 | } yield () 55 | } 56 | 57 | // In Application 1, each worker invocation is wrapped in its own Scope 58 | // because of the ZIO.scoped call inside the foreachParDiscard. This means 59 | // that each worker will acquire and release its permits independently. 60 | // Since the semaphore has 4 permits and each worker requires 2 permits, 61 | // up to 2 workers can run concurrently. The remaining workers will wait 62 | // until permits are available. 63 | 64 | object MainApp1 extends ZIOAppDefault { 65 | import Worker._ 66 | 67 | def run = 68 | for { 69 | sem <- Semaphore.make(4) 70 | _ <- ZIO.foreachParDiscard(1 to 10)(i => ZIO.scoped(worker(sem, i))) 71 | } yield () 72 | } 73 | 74 | // In Application 2, there is a single Scope that encompasses all worker 75 | // invocations because of the outer ZIO.scoped call. This means that all 76 | // workers share the same lifetime for their acquired permits. As a result, 77 | // once the first 2 workers acquire their permits, they will hold onto 78 | // them until the entire scoped block completes. This blocks any other 79 | // workers from acquiring permits. They wait forever for permits that won't 80 | // be released which causes deadlock. 81 | 82 | object MainApp2 extends ZIOAppDefault { 83 | import Worker._ 84 | 85 | def run = 86 | for { 87 | sem <- Semaphore.make(4) 88 | _ <- ZIO.scoped(ZIO.foreachParDiscard(1 to 10)(i => worker(sem, i))) 89 | } yield () 90 | } 91 | } 92 | 93 | /** 94 | * 2. Continuing from implementing the `Semaphore` data type from the 95 | * previous chapter, implement the `withPermits` operator, which takes 96 | * the number of permits to acquire and release within the lifetime of 97 | * the `Scope`: 98 | * 99 | * {{{ 100 | * trait Semaphore { 101 | * def withPermitsScoped(n: Long): ZIO[Scope, Nothing, Unit] 102 | * } 103 | * }}} 104 | */ 105 | package SemaphoreWithPermitsScopedImpl { 106 | 107 | import zio._ 108 | 109 | import scala.collection.immutable.Queue 110 | 111 | trait Semaphore { 112 | def withPermits[R, E, A](n: Long)(task: ZIO[R, E, A]): ZIO[R, E, A] 113 | 114 | /** 115 | * Acquires n permits and registers their release with the Scope. The 116 | * permits will be automatically released when the Scope closes. 117 | * 118 | * @param n 119 | * the number of permits to acquire 120 | * @return 121 | * an effect that requires Scope and completes when permits are acquired 122 | */ 123 | def withPermitsScoped(n: Long): ZIO[Scope, Nothing, Unit] 124 | } 125 | 126 | object Semaphore { 127 | 128 | private case class State( 129 | permits: Long, 130 | waiting: Queue[(Long, Promise[Nothing, Unit])] 131 | ) 132 | 133 | def make(permits: => Long): UIO[Semaphore] = 134 | Ref.make(State(permits, Queue.empty)).map { ref => 135 | new Semaphore { 136 | 137 | private def acquire(n: Long): UIO[Unit] = 138 | ZIO.suspendSucceed { 139 | if (n <= 0) { 140 | ZIO.unit 141 | } else { 142 | Promise.make[Nothing, Unit].flatMap { promise => 143 | ref.modify { state => 144 | if (state.permits >= n) { 145 | ( 146 | ZIO.unit, 147 | state.copy(permits = state.permits - n) 148 | ) 149 | } else { 150 | ( 151 | promise.await, 152 | state 153 | .copy(waiting = state.waiting.enqueue((n, promise))) 154 | ) 155 | } 156 | }.flatten 157 | } 158 | } 159 | } 160 | 161 | private def release(n: Long): UIO[Unit] = { 162 | def satisfyWaiters(state: State): (UIO[Unit], State) = 163 | state.waiting.dequeueOption match { 164 | case Some(((needed, promise), rest)) 165 | if state.permits >= needed => 166 | val newState = state.copy( 167 | permits = state.permits - needed, 168 | waiting = rest 169 | ) 170 | val (moreEffects, finalState) = satisfyWaiters(newState) 171 | (promise.succeed(()) *> moreEffects, finalState) 172 | case _ => 173 | (ZIO.unit, state) 174 | } 175 | 176 | ZIO.suspendSucceed { 177 | if (n <= 0) { 178 | ZIO.unit 179 | } else { 180 | ref.modify { state => 181 | val stateWithPermits = 182 | state.copy(permits = state.permits + n) 183 | satisfyWaiters(stateWithPermits) 184 | }.flatten 185 | } 186 | } 187 | } 188 | 189 | def withPermits[R, E, A]( 190 | n: Long 191 | )(task: ZIO[R, E, A]): ZIO[R, E, A] = 192 | ZIO.acquireReleaseWith(acquire(n))(_ => release(n))(_ => task) 193 | 194 | /** 195 | * Acquires permits and registers their release with the ambient 196 | * Scope. The permits are held until the Scope closes, at which 197 | * point they're automatically released. 198 | */ 199 | def withPermitsScoped(n: Long): ZIO[Scope, Nothing, Unit] = 200 | ZIO.acquireRelease(acquire(n))(_ => release(n)) 201 | } 202 | } 203 | } 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/exercises/01-first-steps-with-zio.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | import zio._ 4 | 5 | object FirstStepsWithZIO { 6 | 7 | /** 8 | * Implement a ZIO version of the function `readFile` by using the 9 | * `ZIO.attempt` constructor. 10 | */ 11 | object Exercise1 { 12 | 13 | def readFile(file: String): String = { 14 | val source = scala.io.Source.fromFile(file) 15 | 16 | try source.getLines().mkString 17 | finally source.close() 18 | } 19 | 20 | def readFileZio(file: String) = 21 | ??? 22 | } 23 | 24 | /** 25 | * Implement a ZIO version of the function `writeFile` by using the 26 | * `ZIO.attempt` constructor. 27 | */ 28 | object Exercise2 { 29 | 30 | def writeFile(file: String, text: String): Unit = { 31 | import java.io._ 32 | val pw = new PrintWriter(new File(file)) 33 | try pw.write(text) 34 | finally pw.close 35 | } 36 | 37 | def writeFileZio(file: String, text: String) = 38 | ??? 39 | } 40 | 41 | /** 42 | * Using the `flatMap` method of ZIO effects, together with the `readFileZio` 43 | * and `writeFileZio` functions that you wrote, implement a ZIO version of the 44 | * function `copyFile`. 45 | */ 46 | object Exercise3 { 47 | 48 | import Exercise1._ 49 | import Exercise2._ 50 | 51 | def copyFile(source: String, dest: String): Unit = { 52 | val contents = readFile(source) 53 | writeFile(dest, contents) 54 | } 55 | 56 | def copyFileZio(source: String, dest: String) = 57 | ??? 58 | } 59 | 60 | /** 61 | * Rewrite the following ZIO code that uses `flatMap` into a _for 62 | * comprehension_. 63 | */ 64 | object Exercise4 { 65 | 66 | def printLine(line: String) = ZIO.attempt(println(line)) 67 | 68 | val readLine = ZIO.attempt(scala.io.StdIn.readLine()) 69 | 70 | printLine("What is your name?").flatMap { _ => 71 | readLine.flatMap { name => 72 | printLine(s"Hello, ${name}!") 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Rewrite the following ZIO code that uses `flatMap` into a _for 79 | * comprehension_. 80 | */ 81 | object Exercise5 { 82 | 83 | val random = ZIO.attempt(scala.util.Random.nextInt(3) + 1) 84 | 85 | def printLine(line: String) = ZIO.attempt(println(line)) 86 | 87 | val readLine = ZIO.attempt(scala.io.StdIn.readLine()) 88 | 89 | random.flatMap { int => 90 | printLine("Guess a number from 1 to 3:").flatMap { _ => 91 | readLine.flatMap { num => 92 | if (num == int.toString) printLine("You guessed right!") 93 | else printLine(s"You guessed wrong, the number was $int!") 94 | } 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Implement the `zipWith` function in terms of the toy model of a ZIO effect. 101 | * The function should return an effect that sequentially composes the 102 | * specified effects, merging their results with the specified user-defined 103 | * function. 104 | */ 105 | object Exercise6 { 106 | 107 | final case class ZIO[-R, +E, +A](run: R => Either[E, A]) 108 | 109 | def zipWith[R, E, A, B, C]( 110 | self: ZIO[R, E, A], 111 | that: ZIO[R, E, B] 112 | )(f: (A, B) => C): ZIO[R, E, C] = 113 | ??? 114 | } 115 | 116 | /** 117 | * Implement the `collectAll` function in terms of the toy model of a ZIO 118 | * effect. The function should return an effect that sequentially collects the 119 | * results of the specified collection of effects. 120 | */ 121 | object Exercise7 { 122 | 123 | import Exercise6._ 124 | 125 | def collectAll[R, E, A]( 126 | in: Iterable[ZIO[R, E, A]] 127 | ): ZIO[R, E, List[A]] = 128 | ??? 129 | } 130 | 131 | /** 132 | * Implement the `foreach` function in terms of the toy model of a ZIO effect. 133 | * The function should return an effect that sequentially runs the specified 134 | * function on every element of the specified collection. 135 | */ 136 | object Exercise8 { 137 | 138 | import Exercise6._ 139 | 140 | def foreach[R, E, A, B]( 141 | in: Iterable[A] 142 | )(f: A => ZIO[R, E, B]): ZIO[R, E, List[B]] = 143 | ??? 144 | } 145 | 146 | /** 147 | * Implement the `orElse` function in terms of the toy model of a ZIO effect. 148 | * The function should return an effect that tries the left hand side, but if 149 | * that effect fails, it will fallback to the effect on the right hand side. 150 | */ 151 | object Exercise9 { 152 | 153 | import Exercise6._ 154 | 155 | def orElse[R, E1, E2, A]( 156 | self: ZIO[R, E1, A], 157 | that: ZIO[R, E2, A] 158 | ): ZIO[R, E2, A] = 159 | ??? 160 | } 161 | 162 | /** 163 | * Using the following code as a foundation, write a ZIO application that 164 | * prints out the contents of whatever files are passed into the program as 165 | * command-line arguments. You should use the function `readFileZio` that you 166 | * developed in these exercises, as well as `ZIO.foreach`. 167 | */ 168 | object Exercise10 { 169 | 170 | import java.io.IOException 171 | 172 | object Cat extends ZIOAppDefault { 173 | 174 | val run = 175 | for { 176 | args <- ZIOAppArgs.getArgs 177 | _ <- cat(args) 178 | } yield () 179 | 180 | def cat(files: Chunk[String]): ZIO[Any, IOException, Unit] = 181 | ??? 182 | } 183 | } 184 | 185 | /** 186 | * Using `ZIO.fail` and `ZIO.succeed`, implement the following function, which 187 | * converts an `Either` into a ZIO effect: 188 | */ 189 | object Exercise11 { 190 | 191 | def eitherToZIO[E, A](either: Either[E, A]): ZIO[Any, E, A] = 192 | ??? 193 | } 194 | 195 | /** 196 | * Using `ZIO.fail` and `ZIO.succeed`, implement the following function, which 197 | * converts a `List` into a ZIO effect, by looking at the head element in the 198 | * list and ignoring the rest of the elements. 199 | */ 200 | object Exercise12 { 201 | 202 | def listToZIO[A](list: List[A]): ZIO[Any, None.type, A] = 203 | ??? 204 | } 205 | 206 | /** 207 | * Using `ZIO.succeed`, convert the following procedural function into a ZIO 208 | * function: 209 | */ 210 | object Exercise13 { 211 | 212 | def currentTime(): Long = java.lang.System.currentTimeMillis() 213 | 214 | lazy val currentTimeZIO: ZIO[Any, Nothing, Long] = 215 | ??? 216 | } 217 | 218 | /** 219 | * Using `ZIO.async`, convert the following asynchronous, callback-based 220 | * function into a ZIO function: 221 | */ 222 | object Exercise14 { 223 | 224 | def getCacheValue( 225 | key: String, 226 | onSuccess: String => Unit, 227 | onFailure: Throwable => Unit 228 | ): Unit = 229 | ??? 230 | 231 | def getCacheValueZio(key: String): ZIO[Any, Throwable, String] = 232 | ??? 233 | } 234 | 235 | /** 236 | * Using `ZIO.async`, convert the following asynchronous, callback-based 237 | * function into a ZIO function: 238 | */ 239 | object Exercise15 { 240 | 241 | trait User 242 | 243 | def saveUserRecord( 244 | user: User, 245 | onSuccess: () => Unit, 246 | onFailure: Throwable => Unit 247 | ): Unit = 248 | ??? 249 | 250 | def saveUserRecordZio(user: User): ZIO[Any, Throwable, Unit] = 251 | ??? 252 | } 253 | 254 | /** 255 | * Using `ZIO.fromFuture`, convert the following code to ZIO: 256 | */ 257 | object Exercise16 { 258 | 259 | import scala.concurrent.{ExecutionContext, Future} 260 | 261 | trait Query 262 | 263 | trait Result 264 | 265 | def doQuery(query: Query)(implicit 266 | ec: ExecutionContext 267 | ): Future[Result] = 268 | ??? 269 | 270 | def doQueryZio(query: Query): ZIO[Any, Throwable, Result] = 271 | ??? 272 | } 273 | 274 | /** 275 | * Using the `Console`, write a little program that asks the user what their 276 | * name is, and then prints it out to them with a greeting. 277 | */ 278 | object Exercise17 { 279 | 280 | object HelloHuman extends ZIOAppDefault { 281 | val run = 282 | ??? 283 | } 284 | } 285 | 286 | /** 287 | * Using the `Console` and `Random` services in ZIO, write a little program 288 | * that asks the user to guess a randomly chosen number between 1 and 3, and 289 | * prints out if they were correct or not. 290 | */ 291 | object Exercise18 { 292 | 293 | object NumberGuessing extends ZIOAppDefault { 294 | val run = 295 | ??? 296 | } 297 | } 298 | 299 | /** 300 | * Using the `Console` service and recursion, write a function that will 301 | * repeatedly read input from the console until the specified user-defined 302 | * function evaluates to `true` on the input. 303 | */ 304 | object Exercise19 { 305 | 306 | import java.io.IOException 307 | 308 | def readUntil( 309 | acceptInput: String => Boolean 310 | ): ZIO[Console, IOException, String] = 311 | ??? 312 | } 313 | 314 | /** 315 | * Using recursion, write a function that will continue evaluating the 316 | * specified effect, until the specified user-defined function evaluates to 317 | * `true` on the output of the effect. 318 | */ 319 | object Exercise20 { 320 | 321 | def doUntil[R, E, A]( 322 | body: ZIO[R, E, A] 323 | )(condition: A => Boolean): ZIO[R, E, A] = 324 | ??? 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/13-concurrent-structures-semaphore-work-limiting.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package SemaphoreWorkLimiting { 4 | 5 | /** 6 | * 1. Implement a semaphore where the number of available permits can be 7 | * adjusted dynamically at runtime. This is useful for systems that need 8 | * to adapt their concurrency based on load or system resources. 9 | * 10 | * {{{ 11 | * trait DynamicSemaphore { 12 | * def withPermit[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] 13 | * def updatePermits(delta: Int): UIO[Unit] 14 | * def currentPermits: UIO[Int] 15 | * } 16 | * }}} 17 | * 18 | * Challenge: Ensure that reducing permits doesn't affect already-running 19 | * tasks, only future acquisitions. 20 | * 21 | * Hint: Please note that implementing the withPermit method will require 22 | * careful handling of resource acquisition and release to ensure that the 23 | * acquired permit is properly released after the task completes, regardless 24 | * of whether it succeeds, fails, or is interrupted. Consider using ZIO's 25 | * `ZIO.acquireRelease*` to manage this lifecycle effectively, which will be 26 | * discussed in the next chapter. 27 | */ 28 | package DynamicSemaphoreImpl { 29 | 30 | import zio._ 31 | 32 | import scala.collection.immutable.Queue 33 | 34 | trait DynamicSemaphore { 35 | def withPermit[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] 36 | 37 | def updatePermits(delta: Int): UIO[Unit] 38 | 39 | def currentPermits: UIO[Int] 40 | } 41 | 42 | object DynamicSemaphore { 43 | def make(initialPermits: Int): UIO[DynamicSemaphore] = 44 | for { 45 | state <- 46 | Ref.make((initialPermits, 0, Queue.empty[Promise[Nothing, Unit]])) 47 | // (maxPermits, activeCount, waitQueue) 48 | } yield new DynamicSemaphore { 49 | def withPermit[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = { 50 | def acquire: UIO[Unit] = 51 | for { 52 | promise <- Promise.make[Nothing, Unit] 53 | shouldWait <- state.modify { case (max, active, queue) => 54 | if (active < max) 55 | (false, (max, active + 1, queue)) 56 | else 57 | (true, (max, active, queue.enqueue(promise))) 58 | } 59 | _ <- if (shouldWait) promise.await else ZIO.unit 60 | } yield () 61 | 62 | def release: UIO[Unit] = { 63 | state.modify { case (max, active, queue) => 64 | queue.dequeueOption match { 65 | case Some((head, tail)) => 66 | // Wake up next waiter, keep active count same 67 | (head.succeed(()), (max, active, tail)) 68 | case None => 69 | // No waiters, decrement active 70 | (ZIO.unit, (max, active - 1, Queue.empty)) 71 | } 72 | }.flatten 73 | }.unit 74 | 75 | ZIO.acquireReleaseWith(acquire)(_ => release)(_ => zio) 76 | } 77 | 78 | def updatePermits(delta: Int): UIO[Unit] = 79 | ZIO.uninterruptible { 80 | state.modify { case (max, active, queue) => 81 | val newMax = max + delta 82 | if (delta > 0) { 83 | // Increase permits - update max and wake up waiters 84 | val toWake = queue.take(delta.min(queue.size)) 85 | val remaining = queue.drop(delta.min(queue.size)) 86 | val wakeEffects = ZIO.foreachDiscard(toWake)(_.succeed(())) 87 | (wakeEffects, (newMax, active + toWake.size, remaining)) 88 | } else { 89 | // Decrease permits - just update max 90 | (ZIO.unit, (newMax, active, queue)) 91 | } 92 | }.flatten 93 | } 94 | 95 | def currentPermits: UIO[Int] = state.get.map(_._1) 96 | } 97 | } 98 | 99 | object DynamicSemaphoreExample extends ZIOAppDefault { 100 | 101 | def task(id: Int, duration: Duration): ZIO[Any, Nothing, Unit] = 102 | for { 103 | _ <- Console.printLine(s"Task $id: Started").orDie 104 | _ <- ZIO.sleep(duration) 105 | _ <- Console.printLine(s"Task $id: Completed").orDie 106 | } yield () 107 | 108 | def run = 109 | for { 110 | _ <- Console.printLine("=== Dynamic Semaphore Example ===\n").orDie 111 | 112 | // Create a semaphore with 2 initial permits 113 | semaphore <- DynamicSemaphore.make(2) 114 | 115 | _ <- Console 116 | .printLine(s"Initial permits: 2") 117 | .orDie 118 | _ <- semaphore.currentPermits 119 | .debug("Current Permits") 120 | .delay(1.seconds) 121 | .forever 122 | .forkDaemon 123 | 124 | _ <- Console 125 | .printLine("Example 1: Start running 50 tasks in background") 126 | .orDie 127 | fiber <- ZIO 128 | .foreachPar((1 to 20).toList) { i => 129 | semaphore.withPermit(task(i, 1.second)) 130 | } 131 | .fork 132 | _ <- Console.printLine("\n").orDie 133 | 134 | _ <- ZIO.sleep(2.seconds) 135 | _ <- Console 136 | .printLine("\n>>> Increasing permits from 2 to 4 <<<\n") 137 | .orDie 138 | _ <- semaphore.updatePermits(2) // Add 2 more permits 139 | 140 | _ <- fiber.join 141 | 142 | _ <- Console 143 | .printLine("\n>>> Decreasing permits from 4 to 3 <<<\n") 144 | .orDie 145 | _ <- semaphore.updatePermits(-1) // Remove 2 permits 146 | _ <- ZIO 147 | .foreachPar((21 to 40).toList) { i => 148 | semaphore.withPermit(task(i, 1.second)) 149 | } 150 | } yield () 151 | 152 | } 153 | } 154 | 155 | /** 156 | * Solve the classic dining philosophers problem using Semaphores to prevent 157 | * deadlock. Five philosophers sit at a round table with five forks. Each 158 | * philosopher needs two adjacent forks to eat. 159 | * 160 | * {{{ 161 | * trait DiningPhilosophers { 162 | * def philosopherLifecycle(id: Int): ZIO[Any, Nothing, Unit] 163 | * def runDinner(duration: Duration): ZIO[Any, Nothing, Map[Int, Int]] // 164 | * philosopher -> meals eaten 165 | * } 166 | * }}} 167 | * 168 | * Must prevent both deadlock and starvation. 169 | */ 170 | package DiningPhilosophersImpl { 171 | 172 | import zio._ 173 | 174 | trait DiningPhilosophers { 175 | def philosopherLifecycle(id: Int): ZIO[Any, Nothing, Unit] 176 | 177 | def runDinner(duration: Duration): ZIO[Any, Nothing, Map[Int, Int]] 178 | } 179 | 180 | object DiningPhilosophers { 181 | def make: UIO[DiningPhilosophers] = 182 | for { 183 | // 5 forks represented as individual semaphores 184 | forks <- ZIO.foreach((0 until 5).toList)(_ => Semaphore.make(1)) 185 | 186 | // Limit concurrent dining to prevent deadlock 187 | diningRoom <- Semaphore.make(4) // Max 4 philosophers can try to eat 188 | 189 | // Track meals eaten 190 | mealsEaten <- Ref.make(Map.empty[Int, Int].withDefaultValue(0)) 191 | 192 | } yield new DiningPhilosophers { 193 | 194 | def philosopherLifecycle(id: Int): ZIO[Any, Nothing, Unit] = { 195 | val leftFork = forks(id) 196 | val rightFork = forks((id + 1) % 5) 197 | 198 | def think: UIO[Unit] = 199 | Random.nextIntBetween(50, 150).flatMap(ms => ZIO.sleep(ms.millis)) 200 | 201 | def eat: UIO[Unit] = 202 | diningRoom.withPermit { 203 | leftFork.withPermit { 204 | rightFork.withPermit { 205 | for { 206 | _ <- 207 | Console.printLine(s"Philosopher $id is eating").orDie 208 | _ <- ZIO.sleep(100.millis) 209 | _ <- mealsEaten.update(m => m + (id -> (m(id) + 1))) 210 | _ <- Console 211 | .printLine(s"Philosopher $id finished eating") 212 | .orDie 213 | } yield () 214 | } 215 | } 216 | } 217 | 218 | (think *> eat).forever 219 | } 220 | 221 | def runDinner( 222 | duration: Duration 223 | ): ZIO[Any, Nothing, Map[Int, Int]] = 224 | for { 225 | fibers <- ZIO.foreach((0 until 5).toList)(id => 226 | philosopherLifecycle(id).fork 227 | ) 228 | _ <- ZIO.sleep(duration) 229 | _ <- ZIO.foreach(fibers)(_.interrupt) 230 | meals <- mealsEaten.get 231 | } yield meals 232 | } 233 | } 234 | 235 | object DiningPhilosophersExample extends ZIOAppDefault { 236 | 237 | def run: ZIO[Any, Any, Unit] = 238 | for { 239 | // Create the dining philosophers instance 240 | philosophers <- DiningPhilosophers.make 241 | 242 | // Example 1: Run for 5 seconds and show results 243 | _ <- Console.printLine("=== Starting 5-second dinner ===") 244 | meals1 <- philosophers.runDinner(5.seconds) 245 | _ <- printResults(meals1) 246 | 247 | // Example 2: Compare fairness across multiple runs 248 | _ <- Console.printLine( 249 | "\n=== Testing fairness (3 runs of 5 seconds) ===" 250 | ) 251 | _ <- ZIO.foreachDiscard(1 to 3) { runNum => 252 | for { 253 | philosophers <- DiningPhilosophers.make 254 | _ <- Console.printLine(s"\nRun $runNum:") 255 | meals <- philosophers.runDinner(5.seconds) 256 | _ <- printResults(meals) 257 | } yield () 258 | } 259 | 260 | } yield () 261 | 262 | def printResults(meals: Map[Int, Int]): UIO[Unit] = { 263 | for { 264 | _ <- Console.printLine("\nMeal counts:") 265 | _ <- ZIO.foreachDiscard(0 until 5) { id => 266 | Console.printLine(s" Philosopher $id: ${meals(id)} meals") 267 | } 268 | total = meals.values.sum 269 | avg = total / 5.0 270 | _ <- Console.printLine(s" Total: $total meals") 271 | _ <- Console.printLine(f" Average: $avg%.1f meals per philosopher") 272 | 273 | // Show fairness metric 274 | maxMeals = meals.values.max 275 | minMeals = meals.values.min 276 | fairness = 277 | if (maxMeals == 0) 100.0 else (minMeals.toDouble / maxMeals * 100) 278 | _ <- 279 | Console.printLine(f" Fairness: $fairness%.1f%% (min/max ratio)") 280 | } yield () 281 | }.orDie 282 | } 283 | } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/01-first-steps-with-zio.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | import zio._ 4 | 5 | object FirstStepsWithZIO { 6 | 7 | /** 8 | * Implement a ZIO version of the function `readFile` by using the 9 | * `ZIO.attempt` constructor. 10 | */ 11 | object Exercise1 { 12 | 13 | def readFile(file: String): String = { 14 | val source = scala.io.Source.fromFile(file) 15 | 16 | try source.getLines().mkString 17 | finally source.close() 18 | } 19 | 20 | def readFileZio(file: String): ZIO[Any, Throwable, String] = 21 | ZIO.attempt(readFile(file)) 22 | } 23 | 24 | /** 25 | * Implement a ZIO version of the function `writeFile` by using the 26 | * `ZIO.attempt` constructor. 27 | */ 28 | object Exercise2 { 29 | 30 | def writeFile(file: String, text: String): Unit = { 31 | import java.io._ 32 | val pw = new PrintWriter(new File(file)) 33 | try pw.write(text) 34 | finally pw.close 35 | } 36 | 37 | def writeFileZio(file: String, text: String) = 38 | ZIO.attempt(writeFile(file, text)) 39 | } 40 | 41 | /** 42 | * Using the `flatMap` method of ZIO effects, together with the `readFileZio` 43 | * and `writeFileZio` functions that you wrote, implement a ZIO version of the 44 | * function `copyFile`. 45 | */ 46 | object Exercise3 { 47 | import Exercise1._ 48 | import Exercise2._ 49 | 50 | def copyFile(source: String, dest: String): Unit = { 51 | val contents = readFile(source) 52 | writeFile(dest, contents) 53 | } 54 | 55 | def copyFileZio(source: String, dest: String) = 56 | readFileZio(source).flatMap(contents => writeFileZio(dest, contents)) 57 | } 58 | 59 | /** 60 | * Rewrite the following ZIO code that uses `flatMap` into a _for 61 | * comprehension_. 62 | */ 63 | object Exercise4 { 64 | 65 | def printLine(line: String) = ZIO.attempt(println(line)) 66 | val readLine: Task[String] = ZIO.attempt(scala.io.StdIn.readLine()) 67 | 68 | for { 69 | _ <- printLine("What is your name?") 70 | name <- readLine 71 | _ <- printLine(s"Hello, ${name}!") 72 | } yield () 73 | } 74 | 75 | /** 76 | * Rewrite the following ZIO code that uses `flatMap` into a _for 77 | * comprehension_. 78 | */ 79 | object Exercise5 { 80 | 81 | val random = ZIO.attempt(scala.util.Random.nextInt(3) + 1) 82 | def printLine(line: String) = ZIO.attempt(println(line)) 83 | val readLine = ZIO.attempt(scala.io.StdIn.readLine()) 84 | 85 | for { 86 | int <- random 87 | _ <- printLine("Guess a number from 1 to 3:") 88 | num <- readLine 89 | _ <- if (num == int.toString) printLine("You guessed right!") 90 | else printLine(s"You guessed wrong, the number was $int!") 91 | } yield () 92 | } 93 | 94 | /** 95 | * Implement the `zipWith` function in terms of the toy model of a ZIO effect. 96 | * The function should return an effect that sequentially composes the 97 | * specified effects, merging their results with the specified user-defined 98 | * function. 99 | */ 100 | object Exercise6 { 101 | 102 | final case class ZIO[-R, +E, +A](run: R => Either[E, A]) 103 | 104 | def zipWith[R, E, A, B, C]( 105 | self: ZIO[R, E, A], 106 | that: ZIO[R, E, B] 107 | )(f: (A, B) => C): ZIO[R, E, C] = 108 | ZIO(r => self.run(r).flatMap(a => that.run(r).map(b => f(a, b)))) 109 | } 110 | 111 | /** 112 | * Implement the `collectAll` function in terms of the toy model of a ZIO 113 | * effect. The function should return an effect that sequentially collects the 114 | * results of the specified collection of effects. 115 | */ 116 | object Exercise7 { 117 | import Exercise6._ 118 | 119 | def succeed[A](a: => A): ZIO[Any, Nothing, A] = 120 | ZIO(_ => Right(a)) 121 | 122 | def collectAll[R, E, A]( 123 | in: Iterable[ZIO[R, E, A]] 124 | ): ZIO[R, E, List[A]] = 125 | if (in.isEmpty) succeed(List.empty) 126 | else zipWith(in.head, collectAll(in.tail))(_ :: _) 127 | } 128 | 129 | /** 130 | * Implement the `foreach` function in terms of the toy model of a ZIO effect. 131 | * The function should return an effect that sequentially runs the specified 132 | * function on every element of the specified collection. 133 | */ 134 | object Exercise8 { 135 | import Exercise6._ 136 | import Exercise7._ 137 | 138 | def foreach[R, E, A, B]( 139 | in: Iterable[A] 140 | )(f: A => ZIO[R, E, B]): ZIO[R, E, List[B]] = 141 | collectAll(in.map(f)) 142 | } 143 | 144 | /** 145 | * Implement the `orElse` function in terms of the toy model of a ZIO effect. 146 | * The function should return an effect that tries the left hand side, but if 147 | * that effect fails, it will fallback to the effect on the right hand side. 148 | */ 149 | object Exercise9 { 150 | 151 | final case class ZIO[-R, +E, +A](run: R => Either[E, A]) 152 | 153 | def orElse[R, E1, E2, A]( 154 | self: ZIO[R, E1, A], 155 | that: ZIO[R, E2, A] 156 | ): ZIO[R, E2, A] = 157 | ZIO { r => 158 | self.run(r) match { 159 | case Left(e1) => that.run(r) 160 | case Right(a) => Right(a) 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Using the following code as a foundation, write a ZIO application that 167 | * prints out the contents of whatever files are passed into the program as 168 | * command-line arguments. You should use the function `readFileZio` that you 169 | * developed in these exercises, as well as `ZIO.foreach`. 170 | */ 171 | object Exercise10 { 172 | import Exercise1._ 173 | import Exercise5._ 174 | 175 | object Cat extends ZIOAppDefault { 176 | 177 | val run = 178 | for { 179 | args <- ZIOAppArgs.getArgs 180 | _ <- cat(args) 181 | } yield () 182 | 183 | def cat(files: Chunk[String]) = 184 | ZIO.foreach(files) { file => 185 | readFileZio(file).flatMap(printLine) 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Using `ZIO.fail` and `ZIO.succeed`, implement the following function, which 192 | * converts an `Either` into a ZIO effect: 193 | */ 194 | object Exercise11 { 195 | 196 | def eitherToZIO[E, A](either: Either[E, A]): ZIO[Any, E, A] = 197 | either.fold(e => ZIO.fail(e), a => ZIO.succeed(a)) 198 | } 199 | 200 | /** 201 | * Using `ZIO.fail` and `ZIO.succeed`, implement the following function, which 202 | * converts a `List` into a ZIO effect, by looking at the head element in the 203 | * list and ignoring the rest of the elements. 204 | */ 205 | object Exercise12 { 206 | 207 | def listToZIO[A](list: List[A]): ZIO[Any, None.type, A] = 208 | list match { 209 | case a :: _ => ZIO.succeed(a) 210 | case Nil => ZIO.fail(None) 211 | } 212 | } 213 | 214 | /** 215 | * Using `ZIO.succeed`, convert the following procedural function into a ZIO 216 | * function: 217 | */ 218 | object Exercise13 { 219 | 220 | def currentTime(): Long = java.lang.System.currentTimeMillis() 221 | 222 | lazy val currentTimeZIO: ZIO[Any, Nothing, Long] = 223 | ZIO.succeed(currentTime()) 224 | } 225 | 226 | /** 227 | * Using `ZIO.async`, convert the following asynchronous, callback-based 228 | * function into a ZIO function: 229 | */ 230 | object Exercise14 { 231 | 232 | def getCacheValue( 233 | key: String, 234 | onSuccess: String => Unit, 235 | onFailure: Throwable => Unit 236 | ): Unit = 237 | ??? 238 | 239 | def getCacheValueZio(key: String): ZIO[Any, Throwable, String] = 240 | ZIO.async { cb => 241 | getCacheValue( 242 | key, 243 | success => cb(ZIO.succeed(success)), 244 | failure => cb(ZIO.fail(failure)) 245 | ) 246 | } 247 | } 248 | 249 | /** 250 | * Using `ZIO.async`, convert the following asynchronous, callback-based 251 | * function into a ZIO function: 252 | */ 253 | object Exercise15 { 254 | 255 | trait User 256 | 257 | def saveUserRecord( 258 | user: User, 259 | onSuccess: () => Unit, 260 | onFailure: Throwable => Unit 261 | ): Unit = 262 | ??? 263 | 264 | def saveUserRecordZio(user: User): ZIO[Any, Throwable, Unit] = 265 | ZIO.async { cb => 266 | saveUserRecord( 267 | user, 268 | () => cb(ZIO.succeed(())), 269 | failure => cb(ZIO.fail(failure)) 270 | ) 271 | } 272 | } 273 | 274 | /** 275 | * Using `ZIO.fromFuture`, convert the following code to ZIO: 276 | */ 277 | object Exercise16 { 278 | 279 | import scala.concurrent.{ExecutionContext, Future} 280 | trait Query 281 | trait Result 282 | 283 | def doQuery(query: Query)(implicit 284 | ec: ExecutionContext 285 | ): Future[Result] = 286 | ??? 287 | 288 | def doQueryZio(query: Query): ZIO[Any, Throwable, Result] = 289 | ZIO.fromFuture(ec => doQuery(query)(ec)) 290 | } 291 | 292 | /** 293 | * Using the `Console`, write a little program that asks the user what their 294 | * name is, and then prints it out to them with a greeting. 295 | */ 296 | object Exercise17 { 297 | 298 | object HelloHuman extends ZIOAppDefault { 299 | val run = 300 | for { 301 | _ <- Console.printLine("What is your name?") 302 | name <- Console.readLine 303 | _ <- Console.printLine("Hello, " + name) 304 | } yield () 305 | } 306 | } 307 | 308 | /** 309 | * Using the `Console` and `Random` services in ZIO, write a little program 310 | * that asks the user to guess a randomly chosen number between 1 and 3, and 311 | * prints out if they were correct or not. 312 | */ 313 | object Exercise18 { 314 | 315 | object NumberGuessing extends ZIOAppDefault { 316 | val run = 317 | for { 318 | int <- Random.nextIntBounded(2).map(_ + 1) 319 | _ <- Console.printLine("Guess a number from 1 to 3:") 320 | num <- Console.readLine 321 | _ <- 322 | if (num == int.toString) Console.printLine("You guessed right!") 323 | else 324 | Console.printLine(s"You guessed wrong, the number was $int!") 325 | } yield () 326 | } 327 | } 328 | 329 | /** 330 | * Using the `Console` service and recursion, write a function that will 331 | * repeatedly read input from the console until the specified user-defined 332 | * function evaluates to `true` on the input. 333 | */ 334 | object Exercise19 { 335 | 336 | import java.io.IOException 337 | 338 | def readUntil( 339 | acceptInput: String => Boolean 340 | ): ZIO[Console, IOException, String] = 341 | Console.readLine.flatMap { input => 342 | if (acceptInput(input)) ZIO.succeed(input) 343 | else readUntil(acceptInput) 344 | } 345 | } 346 | 347 | /** 348 | * Using recursion, write a function that will continue evaluating the 349 | * specified effect, until the specified user-defined function evaluates to 350 | * `true` on the output of the effect. 351 | */ 352 | object Exercise20 { 353 | 354 | def doWhile[R, E, A]( 355 | body: ZIO[R, E, A] 356 | )(condition: A => Boolean): ZIO[R, E, A] = 357 | body.flatMap { a => 358 | if (condition(a)) doWhile(body)(condition) 359 | else ZIO.succeed(a) 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/10-concurrent-structures-promise-work-synchronization.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package PromiseWorkSynchronization { 4 | 5 | /** 6 | * 1. Implement a countdown latch using `Ref` and `Promise`. A countdown 7 | * latch is a synchronization aid that allows one or more threads to wait 8 | * until a set of operations being performed in other threads completes. 9 | * The latch is initialized with a given count, and the count is 10 | * decremented each time an operation completes. When the count reaches 11 | * zero, all waiting threads are released: 12 | * 13 | * {{{ 14 | * trait CountDownLatch { 15 | * def countDown: UIO[Unit] 16 | * def await: UIO[Unit] 17 | * } 18 | * 19 | * object CountDownLatch { 20 | * def make(n: Int): UIO[CountDownLatch] = ??? 21 | * } 22 | * }}} 23 | */ 24 | 25 | package CountDownLatchImpl { 26 | import zio._ 27 | 28 | final case class CountDownLatch( 29 | count: Ref[Int], 30 | promise: Promise[Nothing, Unit] 31 | ) { 32 | def countDown: UIO[Unit] = 33 | count.modify { current => 34 | if (current <= 0) { 35 | (ZIO.unit, 0) 36 | } else { 37 | val newCount = current - 1 38 | val effect = 39 | if (newCount == 0) 40 | promise.succeed(()).unit 41 | else 42 | ZIO.unit 43 | (effect, newCount) 44 | } 45 | }.flatten 46 | 47 | def await: UIO[Unit] = 48 | promise.await 49 | 50 | def getCount: UIO[Int] = 51 | count.get 52 | } 53 | 54 | object CountDownLatch { 55 | def make(n: Int): UIO[CountDownLatch] = 56 | if (n <= 0) 57 | ZIO.die(new IllegalArgumentException("n must be positive")) 58 | else 59 | Ref.make(n).zipWith(Promise.make[Nothing, Unit])(CountDownLatch(_, _)) 60 | } 61 | 62 | object CountDownLatchExample extends ZIOAppDefault { 63 | def run = 64 | for { 65 | latch <- CountDownLatch.make(3) 66 | 67 | // Start 3 fibers that will count down 68 | _ <- ZIO 69 | .foreachPar(1 to 3) { i => 70 | for { 71 | _ <- ZIO.debug(s"Fiber $i starting work...") 72 | _ <- ZIO.sleep(i.seconds) 73 | _ <- ZIO.debug(s"Fiber $i completed!") 74 | _ <- latch.countDown 75 | } yield () 76 | } 77 | .fork 78 | 79 | // Main fiber waits for all to complete 80 | _ <- ZIO.debug("Waiting for all fibers to complete...") 81 | _ <- latch.await 82 | _ <- ZIO.debug("All fibers completed!") 83 | } yield () 84 | } 85 | 86 | } 87 | 88 | /** 89 | * 2. Similar to the previous exercise, you can implement `CyclicBarrier`. A 90 | * cyclic barrier is a synchronization aid that allows a set of threads 91 | * to all wait for each other to reach a common barrier point. Once all 92 | * threads have reached the barrier, they can proceed: 93 | * 94 | * {{{ 95 | * trait CyclicBarrier { 96 | * def await: UIO[Unit] 97 | * def reset: UIO[Unit] 98 | * } 99 | * 100 | * object CyclicBarrier { 101 | * def make(parties: Int): UIO[CyclicBarrier] = ??? 102 | * } 103 | * }}} 104 | */ 105 | package CyclicBarrierImpl { 106 | 107 | import zio._ 108 | 109 | // Please note that this is an educational implementation and may not 110 | // be suitable for production use. If you want a well-tested and robust 111 | // implementation, consider using the `zio.concurrent.CyclicBarrier` 112 | // provided by ZIO. 113 | final case class CyclicBarrier( 114 | parties: Int, 115 | waiting: Ref[Int], 116 | promise: Ref[Promise[Nothing, Unit]] 117 | ) { 118 | def await: UIO[Unit] = 119 | for { 120 | currentPromise <- promise.get 121 | shouldRelease <- waiting.modify { current => 122 | val newWaiting = current + 1 123 | if (newWaiting == parties) { 124 | // Last thread to arrive - release everyone and reset 125 | (true, 0) 126 | } else { 127 | // Not the last thread - keep waiting 128 | (false, newWaiting) 129 | } 130 | } 131 | _ <- if (shouldRelease) { 132 | // Complete the current promise to release all waiting threads 133 | currentPromise.succeed(()).unit *> 134 | // Create a new promise for the next cycle 135 | Promise 136 | .make[Nothing, Unit] 137 | .flatMap(newPromise => promise.set(newPromise)) 138 | } else { 139 | // Wait for all threads to arrive 140 | currentPromise.await 141 | } 142 | } yield () 143 | 144 | def reset: UIO[Unit] = 145 | for { 146 | _ <- waiting.set(0) 147 | newPromise <- Promise.make[Nothing, Unit] 148 | _ <- promise.set(newPromise) 149 | } yield () 150 | } 151 | 152 | object CyclicBarrier { 153 | def make(parties: Int): UIO[CyclicBarrier] = 154 | if (parties <= 0) 155 | ZIO.die(new IllegalArgumentException("parties must be positive")) 156 | else 157 | for { 158 | waiting <- Ref.make(0) 159 | initialPromise <- Promise.make[Nothing, Unit] 160 | promiseRef <- Ref.make(initialPromise) 161 | } yield CyclicBarrier(parties, waiting, promiseRef) 162 | } 163 | 164 | object CyclicBarrierExample extends ZIOAppDefault { 165 | def run = 166 | for { 167 | barrier <- CyclicBarrier.make(3) 168 | _ <- ZIO.foreachPar(1 to 3) { i => 169 | for { 170 | _ <- ZIO.debug(s"Job $i: Starting work...") 171 | _ <- ZIO.sleep(i.seconds) 172 | _ <- ZIO.debug(s"Job $i: Reaching barrier...") 173 | _ <- barrier.await 174 | _ <- ZIO.debug(s"Job $i: Passed barrier!") 175 | } yield () 176 | } 177 | } yield () 178 | } 179 | 180 | } 181 | 182 | /** 183 | * 3. Implement a concurrent bounded queue using `Ref` and `Promise`. It 184 | * should support enqueueing and dequeueing operations, blocking when the 185 | * queue is full or empty: 186 | * 187 | * {{{ 188 | * trait Queue[A] { 189 | * def offer(a: A): UIO[Unit] 190 | * def take: UIO[A] 191 | * } 192 | * 193 | * object Queue { 194 | * def make[A](capacity: Int): UIO[Queue[A]] = ??? 195 | * } 196 | * }}} 197 | */ 198 | 199 | // Please note that this is an educational implementation and may not be 200 | // suitable for production use. If you want a well-tested and robust 201 | // implementation, consider using the `zio.Queue.bounded` provided by ZIO. 202 | package BoundedQueueImpl { 203 | import zio._ 204 | 205 | final case class BoundedQueue[A] private ( 206 | capacity: Int, 207 | state: Ref[BoundedQueue.State[A]] 208 | ) extends Queue[A] { 209 | import BoundedQueue._ 210 | 211 | def offer(a: A): UIO[Unit] = 212 | Promise.make[Nothing, Unit].flatMap { promise => 213 | state.modify { 214 | case State(queue, waitingConsumers, waitingProducers) => 215 | if (waitingConsumers.nonEmpty) { 216 | // There are waiting consumers, give the item directly to the first one 217 | val (consumer, remainingConsumers) = 218 | (waitingConsumers.head, waitingConsumers.tail) 219 | val effect = consumer.succeed(a).unit 220 | (effect, State(queue, remainingConsumers, waitingProducers)) 221 | } else if (queue.size < capacity) { 222 | // Queue has space, add the item 223 | ( 224 | ZIO.unit, 225 | State(queue.enqueue(a), waitingConsumers, waitingProducers) 226 | ) 227 | } else { 228 | // Queue is full, producer must wait 229 | ( 230 | promise.await, 231 | State( 232 | queue, 233 | waitingConsumers, 234 | waitingProducers.enqueue((a, promise)) 235 | ) 236 | ) 237 | } 238 | }.flatten 239 | } 240 | 241 | def take: UIO[A] = 242 | Promise.make[Nothing, A].flatMap { promise => 243 | state.modify { 244 | case State(queue, waitingConsumers, waitingProducers) => 245 | if (queue.nonEmpty) { 246 | // Queue has items 247 | val (item, remainingQueue) = queue.dequeue 248 | 249 | if (waitingProducers.nonEmpty) { 250 | // There are waiting producers, take their item and let them proceed 251 | val ((producerItem, producerPromise), remainingProducers) = 252 | waitingProducers.dequeue 253 | val effect = producerPromise.succeed(()).as(item) 254 | val newQueue = remainingQueue.enqueue(producerItem) 255 | ( 256 | effect, 257 | State(newQueue, waitingConsumers, remainingProducers) 258 | ) 259 | } else { 260 | // No waiting producers 261 | ( 262 | ZIO.succeed(item), 263 | State(remainingQueue, waitingConsumers, waitingProducers) 264 | ) 265 | } 266 | } else { 267 | // Queue is empty, consumer must wait 268 | val effect = promise.await 269 | ( 270 | effect, 271 | State( 272 | queue, 273 | waitingConsumers.enqueue(promise), 274 | waitingProducers 275 | ) 276 | ) 277 | } 278 | }.flatten 279 | } 280 | 281 | // Additional utility methods 282 | def size: UIO[Int] = 283 | state.get.map(_.queue.size) 284 | 285 | def isEmpty: UIO[Boolean] = 286 | state.get.map(_.queue.isEmpty) 287 | 288 | def isFull: UIO[Boolean] = 289 | state.get.map(_.queue.size >= capacity) 290 | } 291 | 292 | object BoundedQueue { 293 | 294 | case class State[A]( 295 | queue: scala.collection.immutable.Queue[A], 296 | waitingConsumers: scala.collection.immutable.Queue[Promise[Nothing, A]], 297 | waitingProducers: scala.collection.immutable.Queue[ 298 | (A, Promise[Nothing, Unit]) 299 | ] 300 | ) 301 | 302 | def make[A](capacity: Int): UIO[BoundedQueue[A]] = 303 | if (capacity <= 0) 304 | ZIO.die(new IllegalArgumentException("capacity must be positive")) 305 | else 306 | Ref 307 | .make( 308 | State[A]( 309 | scala.collection.immutable.Queue.empty, 310 | scala.collection.immutable.Queue.empty, 311 | scala.collection.immutable.Queue.empty 312 | ) 313 | ) 314 | .map(BoundedQueue(capacity, _)) 315 | } 316 | 317 | trait Queue[A] { 318 | def offer(a: A): UIO[Unit] 319 | 320 | def take: UIO[A] 321 | } 322 | 323 | object Queue { 324 | def make[A](capacity: Int): UIO[Queue[A]] = 325 | BoundedQueue.make(capacity) 326 | } 327 | 328 | // Example usage 329 | object BoundedQueueExample extends ZIOAppDefault { 330 | def run = 331 | for { 332 | queue <- BoundedQueue.make[Int](3) 333 | 334 | // Producer fiber that will block when the queue is full 335 | producer <- ZIO 336 | .foreachPar(1 to 5) { i => 337 | for { 338 | _ <- ZIO.debug(s"Offering $i") 339 | _ <- queue.offer(i) 340 | _ <- ZIO.debug(s"Offered $i successfully") 341 | } yield () 342 | } 343 | .fork 344 | 345 | consumer <- ZIO 346 | .foreachPar(1 to 5) { _ => 347 | for { 348 | _ <- ZIO.debug("Taking from queue...") 349 | v <- queue.take 350 | _ <- ZIO.debug(s"Took $v from queue") 351 | _ <- ZIO.sleep(500.millis) 352 | } yield () 353 | } 354 | .fork 355 | 356 | _ <- producer.join 357 | _ <- consumer.join 358 | 359 | _ <- ZIO.debug("All done!") 360 | } yield () 361 | } 362 | 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/09-concurrent-structures-ref-shared-state.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package RefSharedState { 4 | 5 | /** 6 | * 1. Write a simple `Counter` with the following interface that can be 7 | * incremented and decremented concurrently: 8 | * 9 | * {{{ 10 | * trait Counter { 11 | * def increment: UIO[Long] 12 | * def decrement: UIO[Long] 13 | * def get: UIO[Long] 14 | * def reset: UIO[Unit] 15 | * } 16 | * }}} 17 | */ 18 | object CounterImpl { 19 | 20 | import zio._ 21 | 22 | class Counter private (private val ref: Ref[Long]) { 23 | def increment: UIO[Long] = ref.updateAndGet(_ + 1) 24 | def decrement: UIO[Long] = ref.updateAndGet(_ - 1) 25 | def get: UIO[Long] = ref.get 26 | def reset: UIO[Unit] = ref.set(0L) 27 | } 28 | 29 | object Counter { 30 | def make: UIO[Counter] = Ref.make(0L).map(new Counter(_)) 31 | } 32 | 33 | } 34 | 35 | /** 36 | * 2. Implement a bounded queue using `Ref` that has a maximum capacity that 37 | * supports the following interface: 38 | * 39 | * {{{ 40 | * trait BoundedQueue[A] { 41 | * def enqueue(a: A): UIO[Boolean] // Returns false if queue is full 42 | * def dequeue: UIO[Option[A]] // Returns None if queue is empty 43 | * def size: UIO[Int] 44 | * def capacity: UIO[Int] 45 | * } 46 | * }}} 47 | */ 48 | object BoundedQueueImpl { 49 | import zio._ 50 | 51 | import scala.collection.immutable.Queue 52 | 53 | case class BoundedQueue[A]( 54 | enqueue: A => UIO[Boolean], 55 | dequeue: UIO[Option[A]], 56 | size: UIO[Int], 57 | capacity: UIO[Int] 58 | ) 59 | 60 | object BoundedQueue { 61 | def make[A](maxCapacity: Int): UIO[BoundedQueue[A]] = 62 | for { 63 | queueRef <- Ref.make(Queue.empty[A]) 64 | } yield BoundedQueue[A]( 65 | enqueue = (a: A) => 66 | queueRef.modify { queue => 67 | if (queue.size >= maxCapacity) { 68 | (false, queue) 69 | } else { 70 | (true, queue.enqueue(a)) 71 | } 72 | }, 73 | dequeue = queueRef.modify { queue => 74 | queue.dequeueOption match { 75 | case Some((element, newQueue)) => (Some(element), newQueue) 76 | case None => (None, queue) 77 | } 78 | }, 79 | size = queueRef.get.map(_.size), 80 | capacity = ZIO.succeed(maxCapacity) 81 | ) 82 | } 83 | } 84 | 85 | /** 86 | * 3. Write a `CounterManager` service that manages multiple counters with 87 | * the following interface: 88 | * 89 | * {{{ 90 | * type CounterId = String 91 | * 92 | * trait CounterManager { 93 | * def increment(id: CounterId): UIO[Int] 94 | * def decrement(id: CounterId): UIO[Int] 95 | * def get(id: CounterId): UIO[Int] 96 | * def reset(id: CounterId): UIO[Unit] 97 | * def remove(id: CounterId): UIO[Unit] 98 | * } 99 | * }}} 100 | */ 101 | 102 | object CounterManagerImpl { 103 | import zio._ 104 | 105 | type CounterId = String 106 | 107 | case class CounterManager private ( 108 | private val countersRef: Ref[Map[CounterId, Int]] 109 | ) { 110 | def increment(id: CounterId): UIO[Int] = 111 | countersRef.modify { counters => 112 | val newValue = counters.getOrElse(id, 0) + 1 113 | (newValue, counters.updated(id, newValue)) 114 | } 115 | 116 | def decrement(id: CounterId): UIO[Int] = 117 | countersRef.modify { counters => 118 | val newValue = counters.getOrElse(id, 0) - 1 119 | (newValue, counters.updated(id, newValue)) 120 | } 121 | 122 | def get(id: CounterId): UIO[Int] = 123 | countersRef.get.map(_.getOrElse(id, 0)) 124 | 125 | def reset(id: CounterId): UIO[Unit] = 126 | countersRef.update(_.updated(id, 0)) 127 | 128 | def remove(id: CounterId): UIO[Unit] = 129 | countersRef.update(_ - id) 130 | } 131 | 132 | object CounterManager { 133 | def make: UIO[CounterManager] = 134 | Ref.make(Map.empty[CounterId, Int]).map(CounterManager(_)) 135 | } 136 | } 137 | 138 | /** 139 | * 4. Implement a basic log renderer for the `FiberRef[Log]` we have defined 140 | * through the chapter. It should show the hierarchical structure of 141 | * fiber logs using indentation: 142 | * 143 | * - Each level of nesting should be indented by two spaces from the 144 | * previous one. 145 | * - The log entries for each fiber should be shown on separate lines 146 | * - Child fiber logs should be shown under their parent fiber 147 | * 148 | * {{{ 149 | * trait Logger { 150 | * def log(message: String): UIO[Unit] 151 | * } 152 | * 153 | * object Logger { 154 | * def render(ref: Log): UIO[String] = ??? 155 | * } 156 | * }}} 157 | * 158 | * Example output: 159 | * 160 | * {{{ 161 | * Got foo 162 | * Got 1 163 | * Got 2 164 | * Got bar 165 | * Got 3 166 | * Got 4 167 | * }}} 168 | */ 169 | import zio._ 170 | 171 | object NestedLoggerRendererImpl { 172 | 173 | sealed trait LogNode 174 | 175 | case class Message(content: String) extends LogNode 176 | 177 | case class Child(entries: Chunk[LogNode]) extends LogNode 178 | 179 | case class Logger private (private val logs: FiberRef[Chunk[LogNode]]) { 180 | def log(message: String): UIO[Unit] = 181 | logs.update(_ :+ Message(message)) 182 | 183 | def render: ZIO[Any, Nothing, String] = { 184 | def renderLog(log: Chunk[LogNode], indent: Int): Chunk[String] = { 185 | val indentStr = " " * indent 186 | log.flatMap { 187 | case Message(content) => Chunk(indentStr + content) 188 | case Child(childLog) => renderLog(childLog, indent + 2) 189 | } 190 | } 191 | 192 | logs.get.map(renderLog(_, 0).mkString("\n")) 193 | } 194 | } 195 | 196 | object Logger { 197 | def make: ZIO[Any, Nothing, Logger] = 198 | ZIO.scoped { 199 | FiberRef 200 | .make[Chunk[LogNode]]( 201 | initial = Chunk.empty, 202 | fork = _ => Chunk.empty, 203 | join = (parent, child) => parent ++ Chunk(Child(child)) 204 | ) 205 | .map(Logger(_)) 206 | } 207 | } 208 | } 209 | 210 | object Main extends ZIOAppDefault { 211 | import NestedLoggerRendererImpl._ 212 | 213 | def run = 214 | for { 215 | logger <- Logger.make 216 | _ <- logger.log("Starting application...") 217 | _ <- { 218 | for { 219 | _ <- logger.log("Initializing components...") 220 | _ <- logger.log("Configuring components...") 221 | _ <- logger.log("Setting up services...") 222 | } yield () 223 | }.fork.flatMap(_.join) 224 | _ <- logger.log("Application started successfully.") 225 | log <- logger.render 226 | _ <- Console.printLine(log) 227 | } yield () 228 | } 229 | 230 | /** 231 | * 5. Change the log model and use a more detailed one instead of just a 232 | * `String`, so that you can implement an advanced log renderer that adds 233 | * timestamps and fiber IDs, like the following output: 234 | * 235 | * {{{ 236 | * [2024-01-01 10:00:01][fiber-1] Child foo 237 | * [2024-01-01 10:00:02][fiber-2] Got 1 238 | * [2024-01-01 10:00:03][fiber-2] Got 2 239 | * [2024-01-01 10:00:01][fiber-1] Child bar 240 | * [2024-01-01 10:00:02][fiber-3] Got 3 241 | * [2024-01-01 10:00:03][fiber-3] Got 4 242 | * }}} 243 | * 244 | * Hint: You can use the following model for the log entry: 245 | * 246 | * {{{ 247 | * case class LogEntry( 248 | * timestamp: java.time.Instant, 249 | * fiberId: String, 250 | * message: String 251 | * ) 252 | * }}} 253 | */ 254 | object AdvancedNestedLoggerImpl { 255 | import java.time.{Instant, ZoneId} 256 | import java.time.format.DateTimeFormatter 257 | 258 | case class LogEntry(timestamp: Instant, fiberId: String, message: String) 259 | 260 | sealed trait LogNode 261 | 262 | case class Message(entry: LogEntry) extends LogNode 263 | 264 | case class Child(entries: Chunk[LogNode]) extends LogNode 265 | 266 | case class Logger private (private val logs: FiberRef[Chunk[LogNode]]) { 267 | def log(message: String): UIO[Unit] = 268 | for { 269 | timestamp <- Clock.instant 270 | fiberId <- ZIO.fiberId.map(_.id.toString).map(id => s"fiber-$id") 271 | entry = LogEntry(timestamp, fiberId, message) 272 | _ <- logs.update(_ :+ Message(entry)) 273 | } yield () 274 | 275 | def render: ZIO[Any, Nothing, String] = { 276 | val formatter = DateTimeFormatter 277 | .ofPattern("yyyy-MM-dd HH:mm:ss") 278 | .withZone(ZoneId.systemDefault()) 279 | 280 | def renderLog(log: Chunk[LogNode], indent: Int): Chunk[String] = { 281 | val indentStr = " " * indent 282 | log.flatMap { 283 | case Message(LogEntry(timestamp, fiberId, message)) => 284 | val timestampStr = formatter.format(timestamp) 285 | Chunk(s"$indentStr[$timestampStr][$fiberId] $message") 286 | case Child(childLog) => 287 | renderLog(childLog, indent + 2) 288 | } 289 | } 290 | 291 | logs.get.map(renderLog(_, 0).mkString("\n")) 292 | } 293 | } 294 | 295 | object Logger { 296 | def make: ZIO[Any, Nothing, Logger] = 297 | ZIO.scoped { 298 | FiberRef 299 | .make[Chunk[LogNode]]( 300 | initial = Chunk.empty, 301 | fork = _ => Chunk.empty, 302 | join = (parent, child) => parent ++ Chunk(Child(child)) 303 | ) 304 | .map(Logger(_)) 305 | } 306 | } 307 | } 308 | 309 | object AdvancedMain extends ZIOAppDefault { 310 | 311 | import AdvancedNestedLoggerImpl._ 312 | 313 | def run = 314 | for { 315 | logger <- Logger.make 316 | _ <- logger.log("Starting application...") 317 | _ <- { 318 | for { 319 | _ <- logger.log("Initializing components...") 320 | // Small delay to show different timestamps 321 | _ <- ZIO.sleep(100.millis) 322 | _ <- logger.log("Configuring components...") 323 | _ <- ZIO.sleep(100.millis) 324 | _ <- logger.log("Setting up services...") 325 | } yield () 326 | }.fork.flatMap(_.join) 327 | _ <- logger.log("Application started successfully.") 328 | log <- logger.render 329 | _ <- Console.printLine(log) 330 | } yield () 331 | } 332 | 333 | /** 334 | * 6. Create a more advanced logging system that supports different log 335 | * levels. It also should support regional settings for log levels so 336 | * that the user can change the log level for a specific region of the 337 | * application: 338 | * 339 | * {{{ 340 | * trait Logger { 341 | * def log(message: String): UIO[Unit] 342 | * def withLogLevel[R, E, A](level: LogLevel)( 343 | * zio: ZIO[R, E, A] 344 | * ): ZIO[R, E, A] 345 | * } 346 | * }}} 347 | */ 348 | 349 | import java.time.{Instant, ZoneId} 350 | import java.time.format.DateTimeFormatter 351 | 352 | object AdvancedLoggingSystemWithLogLevel { 353 | // Log levels with ordering 354 | sealed trait LogLevel extends Product with Serializable { 355 | def level: Int 356 | 357 | def name: String 358 | } 359 | 360 | object LogLevel { 361 | case object Debug extends LogLevel { 362 | val level = 0; 363 | val name = "DEBUG" 364 | } 365 | 366 | case object Info extends LogLevel { 367 | val level = 1; 368 | val name = "INFO" 369 | } 370 | 371 | case object Warn extends LogLevel { 372 | val level = 2; 373 | val name = "WARN" 374 | } 375 | 376 | case object Error extends LogLevel { 377 | val level = 3; 378 | val name = "ERROR" 379 | } 380 | 381 | implicit val ordering: Ordering[LogLevel] = Ordering.by(_.level) 382 | } 383 | 384 | // Enhanced log entry with log level 385 | case class LogEntry( 386 | timestamp: Instant, 387 | fiberId: String, 388 | level: LogLevel, 389 | message: String 390 | ) 391 | 392 | sealed trait LogNode 393 | case class Message(entry: LogEntry) extends LogNode 394 | case class Child(entries: Chunk[LogNode]) extends LogNode 395 | 396 | case class Logger private ( 397 | private val logs: FiberRef[Chunk[LogNode]], 398 | private val currentLevel: FiberRef[LogLevel] 399 | ) { 400 | 401 | private def logWithLevel(level: LogLevel, message: String): UIO[Unit] = 402 | for { 403 | threshold <- currentLevel.get 404 | _ <- ZIO.when(level.level >= threshold.level) { 405 | for { 406 | timestamp <- Clock.instant 407 | fiberId <- ZIO.fiberId.map(_.id).map(id => s"Fiber-$id") 408 | entry = LogEntry(timestamp, fiberId, level, message) 409 | _ <- logs.update(_ :+ Message(entry)) 410 | } yield () 411 | } 412 | } yield () 413 | 414 | def log(message: String): UIO[Unit] = info(message) 415 | 416 | def debug(message: String): UIO[Unit] = 417 | logWithLevel(LogLevel.Debug, message) 418 | 419 | def info(message: String): UIO[Unit] = 420 | logWithLevel(LogLevel.Info, message) 421 | 422 | def warn(message: String): UIO[Unit] = 423 | logWithLevel(LogLevel.Warn, message) 424 | 425 | def error(message: String): UIO[Unit] = 426 | logWithLevel(LogLevel.Error, message) 427 | 428 | def withLogLevel[R, E, A](level: LogLevel)( 429 | zio: ZIO[R, E, A] 430 | ): ZIO[R, E, A] = 431 | currentLevel.locally(level)(zio) 432 | 433 | def render: ZIO[Any, Nothing, String] = { 434 | val formatter = DateTimeFormatter 435 | .ofPattern("yyyy-MM-dd HH:mm:ss") 436 | .withZone(ZoneId.systemDefault()) 437 | 438 | def renderLog(log: Chunk[LogNode], indent: Int): Chunk[String] = { 439 | val indentStr = " " * indent 440 | log.flatMap { 441 | case Message(LogEntry(timestamp, fiberId, level, message)) => 442 | val timestampStr = formatter.format(timestamp) 443 | val levelStr = f"${level.name}%-5s" // Left-aligned, 5 chars wide 444 | Chunk( 445 | s"${indentStr}[$timestampStr][$fiberId][$levelStr] $message" 446 | ) 447 | case Child(childLog) => 448 | renderLog(childLog, indent + 2) 449 | } 450 | } 451 | 452 | logs.get.map(renderLog(_, 0).mkString("\n")) 453 | } 454 | } 455 | 456 | object Logger { 457 | def make( 458 | defaultLevel: LogLevel = LogLevel.Info 459 | ): ZIO[Any, Nothing, Logger] = 460 | ZIO.scoped { 461 | for { 462 | logsRef <- FiberRef.make[Chunk[LogNode]]( 463 | initial = Chunk.empty, 464 | fork = _ => Chunk.empty, 465 | join = (parent, child) => parent ++ Chunk(Child(child)) 466 | ) 467 | levelRef <- FiberRef.make(defaultLevel) 468 | } yield Logger(logsRef, levelRef) 469 | } 470 | } 471 | } 472 | 473 | } 474 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/14-acquire-release-safe-resource-handling-for-asynchronous-code.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.exercises 2 | 3 | package ResourceHanlding { 4 | 5 | /** 6 | * 1. Rewrite the following `sendData` function in terms of the 7 | * `ZIO.requireReleaseWith` operator: 8 | * 9 | * {{{ 10 | * import zio._ 11 | * import scala.util.Try 12 | * import java.net.Socket 13 | * 14 | * object LegacySendData { 15 | * def sendData( 16 | * host: String, 17 | * port: Int, 18 | * data: Array[Byte] 19 | * ): Try[Int] = { 20 | * var socket: Socket = null 21 | * try { 22 | * socket = new Socket(host, port) 23 | * val out = socket.getOutputStream 24 | * out.write(data) 25 | * Success(data.length) 26 | * } catch { 27 | * case e: Exception => Failure(e) 28 | * } finally { 29 | * if (socket != null) socket.close() 30 | * } 31 | * } 32 | * } 33 | * }}} 34 | * 35 | * Rewrite the function using ZIO 36 | * 37 | * {{{ 38 | * def sendData( 39 | * host: String, 40 | * port: Int, 41 | * data: Array[Byte] 42 | * ): Task[Int] = ??? 43 | * }}} 44 | */ 45 | package RewriteLegacySendData { 46 | import zio._ 47 | 48 | import java.net.{ServerSocket, Socket} 49 | 50 | object SocketClient extends ZIOAppDefault { 51 | def sendData( 52 | host: String, 53 | port: Int, 54 | data: Array[Byte] 55 | ): Task[Int] = 56 | ZIO.acquireReleaseWith( 57 | ZIO.attempt(new Socket(host, port)) 58 | )(socket => ZIO.succeed(socket.close()))(socket => 59 | ZIO.attempt { 60 | val out = socket.getOutputStream 61 | out.write(data) 62 | data.length 63 | } 64 | ) 65 | 66 | def run: Task[Unit] = { 67 | val messages = List( 68 | "Hello, Server!", 69 | "This is message 2", 70 | "ZIO rocks!", 71 | "Final message" 72 | ) 73 | 74 | ZIO.foreachParDiscard(messages) { message => 75 | for { 76 | bytesSent <- sendData("localhost", 8080, message.getBytes("UTF-8")) 77 | _ <- Console.printLine(s"✓ Sent: '$message' ($bytesSent bytes)") 78 | _ <- ZIO.sleep(1.second) 79 | } yield () 80 | } 81 | } 82 | } 83 | 84 | object SocketServer extends ZIOAppDefault { 85 | def readData(socket: Socket): Task[Array[Byte]] = 86 | ZIO.acquireReleaseWith( 87 | ZIO.attempt(socket.getInputStream) 88 | )(_ => ZIO.succeed(()))(inputStream => 89 | ZIO.attempt(inputStream.readAllBytes()) 90 | ) 91 | 92 | def handleClient(clientSocket: Socket, clientId: Int): Task[Unit] = 93 | ZIO 94 | .acquireReleaseWith( 95 | ZIO.succeed(clientSocket) 96 | )(socket => ZIO.attempt(socket.close()).orDie)(socket => 97 | for { 98 | _ <- 99 | Console.printLine( 100 | s"[Client $clientId] Connected from ${socket.getRemoteSocketAddress}" 101 | ) 102 | data <- readData(socket) 103 | message = new String(data, "UTF-8") 104 | _ <- 105 | Console.printLine( 106 | s"[Client $clientId] Received ${data.length} bytes: '$message'" 107 | ) 108 | _ <- Console.printLine(s"[Client $clientId] Connection closed") 109 | } yield () 110 | ) 111 | .catchAll { error => 112 | Console.printLine(s"[Client $clientId] Error: ${error.getMessage}") 113 | } 114 | 115 | def acceptLoop( 116 | serverSocket: ServerSocket, 117 | clientCounter: Ref[Int] 118 | ): Task[Unit] = { 119 | for { 120 | clientSocket <- ZIO.attempt(serverSocket.accept()) 121 | clientId <- clientCounter.updateAndGet(_ + 1) 122 | _ <- handleClient(clientSocket, clientId) 123 | } yield () 124 | }.forever 125 | 126 | def run: Task[Unit] = { 127 | val port = 8080 128 | 129 | ZIO.acquireReleaseWith( 130 | ZIO.attempt(new ServerSocket(port)) 131 | )(server => ZIO.attempt(server.close()).orDie)(server => 132 | for { 133 | _ <- Console.printLine(s"Server listening on port $port...") 134 | clientCounter <- Ref.make(0) 135 | _ <- acceptLoop(server, clientCounter) 136 | } yield () 137 | ) 138 | } 139 | } 140 | 141 | } 142 | 143 | /** 144 | * 2. Implement `ZIO.acquireReleaseWith` using `ZIO.uninterruptibleMask`. 145 | * Write a test to ensure that `ZIO.acquireReleaseWith` guarantees the 146 | * three rules discussed in this chapter. 147 | */ 148 | package AcquireReleaseWithImpl { 149 | 150 | import zio._ 151 | import zio.test._ 152 | 153 | /** 154 | * Implementation of ZIO.acquireReleaseWith using ZIO.uninterruptibleMask 155 | */ 156 | object AcquireReleaseImpl { 157 | 158 | def acquireReleaseWith[R, E, A, B]( 159 | acquire: ZIO[R, E, A] 160 | )( 161 | release: A => ZIO[R, Nothing, Any] 162 | )(use: A => ZIO[R, E, B]): ZIO[R, E, B] = 163 | ZIO.uninterruptibleMask { restore => 164 | // Step 1: Run acquire uninterruptibly (we're already in uninterruptible region) 165 | acquire.flatMap { resource => 166 | // Step 2: Restore interruptibility for the use action and capture its exit result 167 | restore(use(resource)).exit.flatMap { exit => 168 | // Step 3: Run release uninterruptibly, then complete with the original exit result 169 | release(resource).uninterruptible *> ZIO.suspendSucceed(exit) 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Comprehensive test suite to verify the three guarantees: 177 | * 1. The `acquire` action will be performed uninterruptibly. 178 | * 2. The `release` action will be performed uninterruptibly. 179 | * 3. If the `acquire` action successfully completes execution, then the 180 | * `release` action will be performed as soon as the `use` action 181 | * completes execution, regardless of how `use` completes execution. 182 | */ 183 | object AcquireReleaseWithSpec extends ZIOSpecDefault { 184 | import AcquireReleaseImpl._ 185 | 186 | def spec = suite("AcquireReleaseWith Implementation")( 187 | // Test 1: Acquire is uninterruptible 188 | test("acquire runs uninterruptibly even when interrupted") { 189 | for { 190 | acquireInterrupted <- Ref.make(false) 191 | latch1 <- Promise.make[Nothing, Unit] 192 | latch2 <- Promise.make[Nothing, Unit] 193 | 194 | acquire = 195 | (latch1.succeed(()) *> latch2.await) 196 | .map(_ => "resource") 197 | .onInterrupt( 198 | acquireInterrupted.set(true) 199 | ) 200 | 201 | release = (_: String) => ZIO.unit 202 | use = (_: String) => ZIO.unit 203 | 204 | fiber <- acquireReleaseWith(acquire)(release)(use).fork 205 | _ <- latch1.await *> fiber.interrupt *> latch2.succeed(()) 206 | _ <- fiber.join 207 | 208 | acquireInterrupted <- acquireInterrupted.get 209 | } yield assertTrue(!acquireInterrupted) 210 | }, 211 | 212 | // Test 2: Release is uninterruptible 213 | test("release runs uninterruptibly even when interrupted") { 214 | for { 215 | latch1 <- Promise.make[Nothing, Unit] 216 | latch2 <- Promise.make[Nothing, Unit] 217 | releaseInterrupted <- Ref.make(false) 218 | 219 | acquire = ZIO.succeed("resource") 220 | release = (_: String) => 221 | (latch1.succeed(()) *> latch2.await) 222 | .onInterrupt(releaseInterrupted.set(true)) 223 | use = (_: String) => ZIO.unit 224 | 225 | fiber <- acquireReleaseWith(acquire)(release)(use).fork 226 | _ <- latch1.await *> fiber.interrupt *> latch2.succeed(()) 227 | _ <- fiber.join 228 | releaseInterrupted <- releaseInterrupted.get 229 | } yield assertTrue(!releaseInterrupted) 230 | }, 231 | 232 | // Test 3a: Release is called when use succeeds 233 | test("release is called when use completes successfully") { 234 | for { 235 | released <- Ref.make(false) 236 | 237 | acquire = ZIO.succeed("resource") 238 | release = (_: String) => released.set(true) 239 | use = (_: String) => ZIO.succeed(42) 240 | 241 | result <- acquireReleaseWith(acquire)(release)(use) 242 | wasReleased <- released.get 243 | } yield assertTrue( 244 | result == 42, 245 | wasReleased 246 | ) 247 | }, 248 | 249 | // Test 3b: Release is called when use fails 250 | test("release is called when use fails") { 251 | for { 252 | released <- Ref.make(false) 253 | 254 | acquire = ZIO.succeed("resource") 255 | release = (_: String) => released.set(true) 256 | use = (_: String) => ZIO.fail("error") 257 | 258 | result <- acquireReleaseWith(acquire)(release)(use).exit 259 | wasReleased <- released.get 260 | } yield assertTrue( 261 | result.isFailure, 262 | wasReleased 263 | ) 264 | }, 265 | 266 | // Test 3c: Release is called when use is interrupted 267 | test("release is called when use is interrupted") { 268 | for { 269 | released <- Ref.make(false) 270 | useStarted <- Promise.make[Nothing, Unit] 271 | 272 | acquire = ZIO.succeed("resource") 273 | release = (_: String) => released.set(true) 274 | use = (_: String) => useStarted.succeed(()) *> ZIO.never 275 | 276 | fiber <- acquireReleaseWith(acquire)(release)(use).fork 277 | _ <- useStarted.await *> fiber.interrupt 278 | 279 | wasReleased <- released.get 280 | } yield assertTrue(wasReleased) 281 | }, 282 | 283 | // Test 4: If acquire fails, release is not called 284 | test("release is not called if acquire fails") { 285 | for { 286 | released <- Ref.make(false) 287 | 288 | acquire = ZIO.fail("acquire failed") 289 | release = (_: String) => released.set(true) 290 | use = (_: String) => ZIO.succeed(()) 291 | 292 | result <- acquireReleaseWith(acquire)(release)(use).exit 293 | wasReleased <- released.get 294 | } yield assertTrue( 295 | result.isFailure, 296 | !wasReleased 297 | ) 298 | }, 299 | 300 | // Test 5: Use action can be interruptible 301 | test("use action is interruptible by default") { 302 | for { 303 | useInterrupted <- Ref.make(false) 304 | released <- Ref.make(false) 305 | latch <- Promise.make[Nothing, Unit] 306 | 307 | acquire = ZIO.succeed("resource") 308 | release = (_: String) => released.set(true) 309 | use = (_: String) => 310 | (latch.succeed(()) *> ZIO.never) 311 | .onInterrupt(useInterrupted.set(true)) 312 | 313 | fiber <- acquireReleaseWith(acquire)(release)(use).fork 314 | _ <- latch.await *> fiber.interrupt 315 | 316 | interrupted <- useInterrupted.get 317 | wasReleased <- released.get 318 | } yield assertTrue( 319 | interrupted, // Use was interrupted 320 | wasReleased // But the release still ran 321 | ) 322 | } 323 | ) 324 | } 325 | 326 | } 327 | 328 | /** 329 | * 3. Implement a simple semaphore using `Ref` and `Promise` and using 330 | * `ZIO.acquireReleaseWith` operator. A semaphore is a synchronization 331 | * primitive controlling access to a common resource by multiple fibers. 332 | * It is essentially a counter that tracks how many fibers can access a 333 | * resource at a time: 334 | * 335 | * {{{ 336 | * trait Semaphore { 337 | * def withPermits[R, E, A](n: Long)(task: ZIO[R, E, A]): ZIO[R, E, A] 338 | * } 339 | * 340 | * object Semaphore { 341 | * def make(permits: => Long): UIO[Semaphore] = ??? 342 | * } 343 | * }}} 344 | */ 345 | package SemaphoreImpl { 346 | 347 | import zio._ 348 | import scala.collection.immutable.Queue 349 | 350 | /** 351 | * A semaphore is a synchronization primitive that controls access to a 352 | * common resource by multiple fibers using a counter that tracks available 353 | * permits. 354 | */ 355 | trait Semaphore { 356 | 357 | /** 358 | * Executes a task with the specified number of permits. Acquires n 359 | * permits before running the task and releases them afterwards, even if 360 | * the task fails or is interrupted. 361 | * 362 | * @param n 363 | * the number of permits to acquire 364 | * @param task 365 | * the task to execute with the acquired permits 366 | */ 367 | def withPermits[R, E, A](n: Long)(task: ZIO[R, E, A]): ZIO[R, E, A] 368 | } 369 | 370 | object Semaphore { 371 | 372 | /** 373 | * Internal state of the semaphore. 374 | * 375 | * @param permits 376 | * the number of currently available permits 377 | * @param waiting 378 | * queue of fibers waiting for permits, each with their required permit 379 | * count and a promise to complete when ready 380 | */ 381 | private case class State( 382 | permits: Long, 383 | waiting: Queue[(Long, Promise[Nothing, Unit])] 384 | ) 385 | 386 | /** 387 | * Creates a new semaphore with the specified number of permits. 388 | * 389 | * @param permits 390 | * the initial number of permits (must be non-negative) 391 | * @return 392 | * a new Semaphore instance 393 | */ 394 | def make(permits: => Long): UIO[Semaphore] = 395 | Ref.make(State(permits, Queue.empty)).map { ref => 396 | new Semaphore { 397 | 398 | /** 399 | * Acquires n permits from the semaphore. If not enough permits are 400 | * available, the fiber will wait. 401 | */ 402 | private def acquire(n: Long): UIO[Unit] = 403 | ZIO.suspendSucceed { 404 | if (n <= 0) { 405 | // No permits needed, proceed immediately 406 | ZIO.unit 407 | } else { 408 | Promise.make[Nothing, Unit].flatMap { promise => 409 | ref.modify { state => 410 | if (state.permits >= n) { 411 | // Enough permits available, acquire them immediately 412 | ( 413 | ZIO.unit, 414 | state.copy(permits = state.permits - n) 415 | ) 416 | } else { 417 | // Not enough permits, add to waiting queue 418 | ( 419 | promise.await, 420 | state 421 | .copy(waiting = state.waiting.enqueue((n, promise))) 422 | ) 423 | } 424 | }.flatten 425 | } 426 | } 427 | } 428 | 429 | /** 430 | * Releases n permits back to the semaphore. This may wake up 431 | * waiting fibers if they can now proceed. 432 | */ 433 | private def release(n: Long): UIO[Unit] = { 434 | 435 | def satisfyWaiters(state: State): (UIO[Unit], State) = 436 | state.waiting.dequeueOption match { 437 | case Some(((needed, promise), rest)) 438 | if state.permits >= needed => 439 | // This waiter can proceed - they have enough permits 440 | val newState = state.copy( 441 | permits = state.permits - needed, 442 | waiting = rest 443 | ) 444 | // Recursively check if we can satisfy more waiters 445 | val (moreEffects, finalState) = satisfyWaiters(newState) 446 | ( 447 | promise.succeed(()) *> moreEffects, 448 | finalState 449 | ) 450 | case _ => 451 | // Either queue is empty or next waiter needs more permits 452 | (ZIO.unit, state) 453 | } 454 | 455 | ZIO.suspendSucceed { 456 | if (n <= 0) { 457 | ZIO.unit 458 | } else { 459 | ref.modify { state => 460 | // First, add the released permits back 461 | val stateWithPermits = 462 | state.copy(permits = state.permits + n) 463 | // Then try to satisfy any waiting fibers 464 | satisfyWaiters(stateWithPermits) 465 | }.flatten 466 | } 467 | } 468 | } 469 | 470 | /** 471 | * Executes a task with n permits using acquireReleaseWith to ensure 472 | * permits are always released, even on failure or interruption. 473 | */ 474 | def withPermits[R, E, A]( 475 | n: Long 476 | )(task: ZIO[R, E, A]): ZIO[R, E, A] = 477 | ZIO.acquireReleaseWith(acquire(n))(_ => release(n))(_ => task) 478 | } 479 | } 480 | } 481 | 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/12-concurrent-structures-hub-broadcasting.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package HubBroadcasting { 4 | 5 | /** 6 | * 1. Create a chatroom system using `Hub` where multiple users can join the 7 | * chat so each message sent is broadcast to all users. Each message sent 8 | * by a user is received by all other users. Users can leave the chat. 9 | * There is also a process that logs all messages sent to the chat room 10 | * in a file: 11 | * 12 | * {{{ 13 | * trait UserSession { 14 | * def sendMessage(message: String): UIO[Unit] 15 | * def receiveMessages: ZIO[Scope, Nothing, Dequeue[ChatEvent]] 16 | * def leave: UIO[Unit] 17 | * } 18 | * 19 | * trait ChatRoom { 20 | * def join(username: String): ZIO[Scope, Nothing, Dequeue[UserSession]] 21 | * def shutdown(username: String): UIO[Unit] 22 | * } 23 | * }}} 24 | */ 25 | package ChatRoomImpl { 26 | 27 | import zio._ 28 | 29 | import java.time.LocalDateTime 30 | 31 | sealed trait ChatEvent { 32 | def username: String 33 | def timestamp: LocalDateTime 34 | } 35 | object ChatEvent { 36 | case class UserJoined(username: String, timestamp: LocalDateTime) 37 | extends ChatEvent 38 | case class UserLeft(username: String, timestamp: LocalDateTime) 39 | extends ChatEvent 40 | case class MessageSent( 41 | username: String, 42 | message: String, 43 | timestamp: LocalDateTime 44 | ) extends ChatEvent 45 | } 46 | 47 | trait UserSession { 48 | def sendMessage(message: String): UIO[Unit] 49 | def receiveMessages: ZIO[Scope, Nothing, Dequeue[ChatEvent]] 50 | def leave: UIO[Unit] 51 | } 52 | 53 | trait ChatRoom { 54 | def join(username: String): ZIO[Scope, Nothing, UserSession] 55 | def shutdown: UIO[Unit] 56 | } 57 | 58 | class UserSessionImpl( 59 | username: String, 60 | hub: Hub[ChatEvent], 61 | subscription: Dequeue[ChatEvent] 62 | ) extends UserSession { 63 | 64 | override def sendMessage(message: String): UIO[Unit] = 65 | hub 66 | .publish( 67 | ChatEvent.MessageSent(username, message, LocalDateTime.now()) 68 | ) 69 | .unit 70 | 71 | override def receiveMessages: ZIO[Scope, Nothing, Dequeue[ChatEvent]] = 72 | ZIO.succeed(subscription) 73 | 74 | override def leave: UIO[Unit] = 75 | hub.publish(ChatEvent.UserLeft(username, LocalDateTime.now())).unit 76 | } 77 | 78 | object ChatRoom { 79 | 80 | def make: ZIO[Scope, Nothing, ChatRoom] = 81 | for { 82 | hub <- Hub.unbounded[ChatEvent] 83 | activeUsersRef <- Ref.make(Set.empty[String]) 84 | chatRoom = new ChatRoomImpl(hub, activeUsersRef) 85 | _ <- ZIO.addFinalizer(chatRoom.shutdown) 86 | } yield chatRoom 87 | } 88 | 89 | class ChatRoomImpl( 90 | hub: Hub[ChatEvent], 91 | activeUsersRef: Ref[Set[String]] 92 | ) extends ChatRoom { 93 | 94 | override def join(username: String): ZIO[Scope, Nothing, UserSession] = 95 | for { 96 | _ <- activeUsersRef.update(_ + username) 97 | _ <- hub.publish(ChatEvent.UserJoined(username, LocalDateTime.now())) 98 | subscription <- hub.subscribe 99 | 100 | _ <- ZIO.addFinalizer { 101 | for { 102 | _ <- activeUsersRef.update(_ - username) 103 | _ <- subscription.shutdown 104 | } yield () 105 | } 106 | 107 | } yield new UserSessionImpl(username, hub, subscription) 108 | 109 | def shutdown: UIO[Unit] = 110 | hub.shutdown 111 | } 112 | 113 | object ChatRoomExample extends ZIOAppDefault { 114 | 115 | private def simulateUserActivity( 116 | chatRoom: ChatRoom, 117 | username: String, 118 | messages: List[String], 119 | stayDuration: Duration 120 | ): ZIO[Any, Nothing, Unit] = 121 | ZIO.scoped { 122 | for { 123 | session <- chatRoom.join(username) 124 | receiver <- session.receiveMessages 125 | 126 | _ <- receiver.take.flatMap { event => 127 | val message = event match { 128 | case ChatEvent.UserJoined(u, _) => 129 | s"[$username sees] $u joined" 130 | case ChatEvent.UserLeft(u, _) => 131 | s"[$username sees] $u left" 132 | case ChatEvent.MessageSent(u, msg, _) => 133 | s"[$username sees] $u: $msg" 134 | } 135 | Console.printLine(message).orDie 136 | } 137 | .repeatWhile(_ => true) 138 | .forkScoped // Fork a fiber to continuously take and print received messages 139 | 140 | _ <- ZIO.foreach(messages) { msg => 141 | for { 142 | _ <- session.sendMessage(msg) 143 | _ <- Clock.sleep(1.second) 144 | } yield () 145 | } 146 | 147 | _ <- Clock.sleep(stayDuration) 148 | _ <- session.leave // leave the chat 149 | 150 | } yield () 151 | } 152 | 153 | override def run = 154 | ZIO.scoped { 155 | for { 156 | chatRoom <- ChatRoom.make 157 | 158 | // Simulate multiple users 159 | _ <- ZIO.collectAllPar( 160 | List( 161 | simulateUserActivity( 162 | chatRoom, 163 | "Alice", 164 | List("Hello everyone!", "How's it going?"), 165 | 5.seconds 166 | ), 167 | simulateUserActivity( 168 | chatRoom, 169 | "Bob", 170 | List( 171 | "Hi Alice!", 172 | "I'm doing great!", 173 | "Anyone up for a game?" 174 | ), 175 | 7.seconds 176 | ).delay(1.second), 177 | simulateUserActivity( 178 | chatRoom, 179 | "Charlie", 180 | List("Hey folks!", "Count me in for the game!"), 181 | 6.seconds 182 | ).delay(2.seconds) 183 | ) 184 | ) 185 | 186 | _ <- 187 | Console.printLine( 188 | "\nChat session completed. Check chat_logs/chat.log for the full log." 189 | ) 190 | 191 | } yield () 192 | } 193 | } 194 | 195 | } 196 | 197 | /** 198 | * 2. Write a real-time auction system\index{Auction System} that allows 199 | * multiple bidders to practice in auctions simultaneously. The auction 200 | * system should broadcast bid updates to all participants while 201 | * maintaining the strict ordering of bids. Each participant should be 202 | * able to place bids and receive updates on the current highest bid: 203 | * 204 | * {{{ 205 | * trait AuctionSystem { 206 | * def placeBid( 207 | * auctionId: String, 208 | * bidderId: String, 209 | * amount: BigDecimal 210 | * ): UIO[Boolean] 211 | * 212 | * def createAuction( 213 | * id: String, 214 | * startPrice: BigDecimal, 215 | * duration: Duration 216 | * ): UIO[Unit] 217 | * 218 | * def subscribe: ZIO[Scope, Nothing, Dequeue[AuctionEvent]] 219 | * 220 | * def getAuction(id: String): UIO[Option[AuctionState]] 221 | * } 222 | * }}} 223 | * 224 | * The core models for the auction system could be as follows: 225 | * 226 | * {{{ 227 | * case class Bid( 228 | * auctionId: String, 229 | * bidderId: String, 230 | * amount: BigDecimal, 231 | * timestamp: Long 232 | * ) 233 | * 234 | * case class AuctionState( 235 | * id: String, 236 | * currentPrice: BigDecimal, 237 | * currentWinner: Option[String], 238 | * endTime: Long, 239 | * isActive: Boolean 240 | * ) 241 | * 242 | * // Events we'll broadcast 243 | * sealed trait AuctionEvent 244 | * case class BidPlaced(bid: Bid) extends AuctionEvent 245 | * case class AuctionEnded( 246 | * auctionId: String, 247 | * finalPrice: BigDecimal, 248 | * winner: Option[String] 249 | * ) extends AuctionEvent 250 | * }}} 251 | */ 252 | package AuctionSystemImpl { 253 | 254 | import zio._ 255 | 256 | case class Bid( 257 | auctionId: String, 258 | bidderId: String, 259 | amount: BigDecimal, 260 | timestamp: Long, 261 | sequence: Long 262 | ) 263 | 264 | case class AuctionState( 265 | id: String, 266 | currentPrice: BigDecimal, 267 | currentWinner: Option[String], 268 | endTime: Long, 269 | isActive: Boolean, 270 | bidHistory: List[Bid] = Nil 271 | ) 272 | 273 | sealed trait AuctionEvent { 274 | def sequence: Long 275 | } 276 | case class BidPlaced(bid: Bid) extends AuctionEvent { 277 | def sequence: Long = bid.sequence 278 | } 279 | case class AuctionEnded( 280 | auctionId: String, 281 | finalPrice: BigDecimal, 282 | winner: Option[String], 283 | sequence: Long 284 | ) extends AuctionEvent 285 | case class AuctionCreated( 286 | auctionId: String, 287 | startPrice: BigDecimal, 288 | endTime: Long, 289 | sequence: Long 290 | ) extends AuctionEvent 291 | 292 | trait AuctionSystem { 293 | def placeBid( 294 | auctionId: String, 295 | bidderId: String, 296 | amount: BigDecimal 297 | ): UIO[Boolean] 298 | 299 | def createAuction( 300 | id: String, 301 | startPrice: BigDecimal, 302 | duration: Duration 303 | ): UIO[Unit] 304 | 305 | def subscribe: ZIO[Scope, Nothing, Dequeue[AuctionEvent]] 306 | 307 | def getAuction(id: String): UIO[Option[AuctionState]] 308 | } 309 | 310 | case class AuctionSystemState( 311 | auctions: Map[String, AuctionState], 312 | eventSequence: Long 313 | ) 314 | 315 | class AuctionSystemLive( 316 | state: Ref.Synchronized[AuctionSystemState], 317 | eventHub: Hub[AuctionEvent] 318 | ) extends AuctionSystem { 319 | 320 | override def placeBid( 321 | auctionId: String, 322 | bidderId: String, 323 | amount: BigDecimal 324 | ): UIO[Boolean] = 325 | state.modifyZIO { currentState => 326 | val now = java.lang.System.currentTimeMillis() 327 | 328 | currentState.auctions.get(auctionId) match { 329 | case None => 330 | ZIO.succeed((false, currentState)) 331 | 332 | case Some(auction) if !auction.isActive => 333 | ZIO.succeed((false, currentState)) 334 | 335 | case Some(auction) if now > auction.endTime => 336 | val nextSeq = currentState.eventSequence + 1 337 | val endedAuction = auction.copy(isActive = false) 338 | val newState = currentState.copy( 339 | auctions = 340 | currentState.auctions.updated(auctionId, endedAuction), 341 | eventSequence = nextSeq 342 | ) 343 | val event = AuctionEnded( 344 | auctionId, 345 | auction.currentPrice, 346 | auction.currentWinner, 347 | nextSeq 348 | ) 349 | 350 | eventHub.publish(event).as((false, newState)) 351 | 352 | case Some(auction) if amount <= auction.currentPrice => 353 | ZIO.succeed((false, currentState)) 354 | 355 | case Some(auction) => 356 | val nextSeq = currentState.eventSequence + 1 357 | val bid = Bid(auctionId, bidderId, amount, now, nextSeq) 358 | 359 | val updatedAuction = auction.copy( 360 | currentPrice = amount, 361 | currentWinner = Some(bidderId), 362 | bidHistory = bid :: auction.bidHistory 363 | ) 364 | 365 | val newState = AuctionSystemState( 366 | auctions = 367 | currentState.auctions.updated(auctionId, updatedAuction), 368 | eventSequence = nextSeq 369 | ) 370 | 371 | val event = BidPlaced(bid) 372 | 373 | eventHub.publish(event).as((true, newState)) 374 | } 375 | } 376 | 377 | override def createAuction( 378 | id: String, 379 | startPrice: BigDecimal, 380 | duration: Duration 381 | ): UIO[Unit] = 382 | state.modifyZIO { currentState => 383 | val now = java.lang.System.currentTimeMillis() 384 | val endTime = now + duration.toMillis 385 | val nextSeq = currentState.eventSequence + 1 386 | 387 | val auction = AuctionState( 388 | id = id, 389 | currentPrice = startPrice, 390 | currentWinner = None, 391 | endTime = endTime, 392 | isActive = true, 393 | bidHistory = Nil 394 | ) 395 | 396 | val newState = currentState.copy( 397 | auctions = currentState.auctions.updated(id, auction), 398 | eventSequence = nextSeq 399 | ) 400 | 401 | val event = AuctionCreated(id, startPrice, endTime, nextSeq) 402 | 403 | eventHub.publish(event).as(((), newState)) 404 | } 405 | 406 | override def subscribe: ZIO[Scope, Nothing, Dequeue[AuctionEvent]] = 407 | ZIO.acquireRelease( 408 | eventHub.subscribe 409 | )(_.shutdown) 410 | 411 | override def getAuction(id: String): UIO[Option[AuctionState]] = 412 | state.modifyZIO { currentState => 413 | val now = java.lang.System.currentTimeMillis() 414 | 415 | currentState.auctions.get(id) match { 416 | case Some(auction) if auction.isActive && now > auction.endTime => 417 | val nextSeq = currentState.eventSequence + 1 418 | val endedAuction = auction.copy(isActive = false) 419 | val newState = currentState.copy( 420 | auctions = currentState.auctions.updated(id, endedAuction), 421 | eventSequence = nextSeq 422 | ) 423 | val event = AuctionEnded( 424 | id, 425 | auction.currentPrice, 426 | auction.currentWinner, 427 | nextSeq 428 | ) 429 | 430 | eventHub.publish(event).as((Some(endedAuction), newState)) 431 | 432 | case auctionOpt => 433 | ZIO.succeed((auctionOpt, currentState)) 434 | } 435 | } 436 | } 437 | 438 | object AuctionSystemLive { 439 | val layer: ZLayer[Any, Nothing, AuctionSystem] = 440 | ZLayer { 441 | for { 442 | state <- Ref.Synchronized.make(AuctionSystemState(Map.empty, 0L)) 443 | eventHub <- Hub.unbounded[AuctionEvent] 444 | } yield new AuctionSystemLive(state, eventHub) 445 | } 446 | } 447 | 448 | object AuctionSystemExample extends ZIOAppDefault { 449 | 450 | override def run = { 451 | ZIO.scoped { 452 | for { 453 | _ <- Console.printLine("Starting Fixed Auction System").orDie 454 | _ <- for { 455 | system <- ZIO.service[AuctionSystem] 456 | events <- system.subscribe 457 | 458 | _ <- events.take.flatMap { event => 459 | Console 460 | .printLine( 461 | s"Event received (seq=${event.sequence}): $event" 462 | ) 463 | .orDie 464 | }.forever.forkScoped 465 | 466 | _ <- system.createAuction( 467 | "item-001", 468 | BigDecimal(100), 469 | Duration.fromSeconds(60) 470 | ) 471 | _ <- Console.printLine("Auction created for item-001").orDie 472 | 473 | // Simulate multiple bidders with distinct bid amounts 474 | _ <- ZIO.foreachParDiscard(1 to 3) { bidderId => 475 | ZIO.foreachDiscard(1 to 3) { bidNum => 476 | // Ensure unique bid amounts with more variation 477 | val amount = 478 | BigDecimal(100 + bidderId * 20 + bidNum * 7) 479 | for { 480 | success <- system.placeBid( 481 | s"item-001", 482 | s"bidder-$bidderId", 483 | amount 484 | ) 485 | _ <- Console 486 | .printLine( 487 | s"Bidder $bidderId bid $amount: $success" 488 | ) 489 | .orDie 490 | _ <- ZIO.sleep( 491 | Duration.fromMillis( 492 | 100L + (scala.util.Random.nextInt(200)) 493 | ) 494 | ) 495 | } yield () 496 | } 497 | } 498 | 499 | _ <- ZIO.sleep(Duration.fromSeconds(2)) 500 | 501 | finalState <- system.getAuction("item-001") 502 | _ <- 503 | Console.printLine(s"\n=== Final auction state ===").orDie 504 | _ <- Console.printLine(s"Final state: $finalState").orDie 505 | } yield () 506 | } yield () 507 | } 508 | }.provide(AuctionSystemLive.layer) 509 | } 510 | } 511 | 512 | } 513 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/02-testing-zio-programs.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package TestingZIOPrograms { 4 | 5 | import zio._ 6 | import zio.test._ 7 | 8 | import scala.annotation.tailrec 9 | 10 | /** 11 | * 1. Write a ZIO program that simulates a countdown timer (e.g., prints 12 | * numbers from 5 to 1, with a 1-second delay between each). Test this 13 | * program using TestClock. 14 | */ 15 | object CountdownTimer extends ZIOSpecDefault { 16 | def countdown(n: Int): ZIO[Any, Nothing, Unit] = 17 | if (n <= 0) ZIO.unit 18 | else 19 | for { 20 | _ <- Console.printLine(s"Countdown: $n").orDie 21 | _ <- ZIO.sleep(1.second) 22 | _ <- countdown(n - 1) 23 | } yield () 24 | 25 | override def spec = 26 | suite("Countdown Timer Spec")( 27 | test("should count down from 5 to 1") { 28 | for { 29 | f <- countdown(5).fork 30 | _ <- TestClock.adjust( 31 | 5.seconds 32 | ) // Adjust the clock to simulate time passing 33 | _ <- f.join 34 | o <- TestConsole.output 35 | } yield assertTrue( 36 | o == Vector( 37 | "Countdown: 5\n", 38 | "Countdown: 4\n", 39 | "Countdown: 3\n", 40 | "Countdown: 2\n", 41 | "Countdown: 1\n" 42 | ) 43 | ) 44 | } 45 | ) 46 | } 47 | 48 | /** 49 | * 2. Create a simple cache that expires entries after a certain duration. 50 | * Implement a program that adds items to the cache and tries to retrieve 51 | * them. Write tests using `TestClock` to verify that items are available 52 | * before expiration and unavailable after expiration. 53 | */ 54 | object CacheWithExpiration extends ZIOSpecDefault { 55 | 56 | import java.util.concurrent.TimeUnit 57 | 58 | case class CacheEntry[V](value: V, expirationTime: Long) 59 | 60 | case class Cache[K, V]( 61 | private val storage: Ref[Map[K, CacheEntry[V]]], 62 | expiration: Long 63 | ) { 64 | 65 | def put(key: K, value: V): ZIO[Any, Nothing, Unit] = 66 | for { 67 | currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) 68 | expirationTime = currentTime + expiration 69 | _ <- 70 | storage.update(_.updated(key, CacheEntry(value, expirationTime))) 71 | } yield () 72 | 73 | def get(key: K): ZIO[Any, Nothing, Option[V]] = 74 | for { 75 | currentTime <- Clock.currentTime(TimeUnit.MILLISECONDS) 76 | storageMap <- storage.get 77 | result = storageMap.get(key) match { 78 | case Some(entry) if entry.expirationTime > currentTime => 79 | Some(entry.value) 80 | case _ => 81 | None 82 | } 83 | // Optionally clean up expired entries 84 | _ <- storage.update(_.filter { case (_, entry) => 85 | entry.expirationTime > currentTime 86 | }) 87 | } yield result 88 | } 89 | 90 | object Cache { 91 | def make[K, V](expiration: Long): UIO[Cache[K, V]] = 92 | for { 93 | ref <- Ref.make(Map.empty[K, CacheEntry[V]]) 94 | } yield Cache(ref, expiration) 95 | } 96 | 97 | override def spec = 98 | suite("Cache With Expiration Spec")( 99 | test("should store and retrieve items before expiration") { 100 | for { 101 | cache <- Cache.make[String, String](5000) // 5 seconds expiration 102 | _ <- cache.put("key1", "value1") 103 | value <- cache.get("key1") 104 | } yield assertTrue(value.contains("value1")) 105 | }, 106 | test("should not retrieve items after expiration") { 107 | for { 108 | cache <- Cache.make[String, String](1000) // 1 second expiration 109 | _ <- cache.put("key2", "value2") 110 | _ <- TestClock.adjust( 111 | 1500.millis 112 | ) // Adjust the clock to simulate time passing 113 | value <- cache.get("key2") 114 | } yield assertTrue(value.isEmpty) 115 | }, 116 | test("should handle multiple items with different expiration times") { 117 | for { 118 | cache <- Cache.make[String, String](2000) // 2 seconds expiration 119 | _ <- cache.put("key1", "value1") 120 | _ <- TestClock.adjust(1000.millis) // 1 second passed 121 | _ <- cache.put( 122 | "key2", 123 | "value2" 124 | ) // This will expire 1 second after key1 125 | _ <- TestClock.adjust(1500.millis) // Total 2.5 seconds passed 126 | value1 <- cache.get("key1") // Should be expired 127 | value2 <- cache.get("key2") // Should still be valid 128 | } yield assertTrue(value1.isEmpty && value2.contains("value2")) 129 | }, 130 | test("should overwrite existing keys with new expiration time") { 131 | for { 132 | cache <- Cache.make[String, String](1000) // 1 second expiration 133 | _ <- cache.put("key1", "value1") 134 | _ <- TestClock.adjust(900.millis) // Almost expired 135 | _ <- cache.put("key1", "updated") // Reset expiration 136 | _ <- 137 | TestClock.adjust(500.millis) // Total 1.4 seconds from first put 138 | value <- cache.get("key1") 139 | } yield assertTrue(value.contains("updated")) 140 | } 141 | ) 142 | 143 | } 144 | 145 | /** 146 | * 3. Create a rate limiter that allows a maximum of N operations per 147 | * minute. Implement a program that uses this rate limiter. Write tests 148 | * using `TestClock` to verify that the rate limiter correctly allows or 149 | * blocks operations based on the time window. 150 | */ 151 | object RateLimiterSpec extends ZIOSpecDefault { 152 | 153 | import zio.test.Assertion._ 154 | 155 | import java.time.Instant 156 | 157 | /** 158 | * Rate limiter that allows a maximum of N operations per minute. Uses a 159 | * sliding window approach to track operations. 160 | */ 161 | trait RateLimiter { 162 | def tryAcquire: UIO[Boolean] 163 | } 164 | 165 | object RateLimiter { 166 | 167 | /** 168 | * Creates a rate limiter that allows maxOps operations per minute 169 | */ 170 | def make(maxOps: Int): UIO[RateLimiter] = 171 | for { 172 | // Store timestamps of operations within the current window 173 | operationTimestamps <- Ref.make[List[Instant]](List.empty) 174 | } yield new RateLimiter { 175 | 176 | def tryAcquire: UIO[Boolean] = 177 | for { 178 | now <- Clock.instant 179 | oneMinuteAgo = now.minusSeconds(60) 180 | 181 | acquired <- operationTimestamps.modify { timestamps => 182 | // Remove timestamps older than 1 minute 183 | val validTimestamps = 184 | timestamps.filter(_.isAfter(oneMinuteAgo)) 185 | 186 | // Check if we can add a new operation 187 | if (validTimestamps.size < maxOps) { 188 | // Add current timestamp and allow operation 189 | (true, now :: validTimestamps) 190 | } else { 191 | // Rate limit exceeded 192 | (false, validTimestamps) 193 | } 194 | } 195 | } yield acquired 196 | } 197 | } 198 | 199 | override def spec = 200 | suite("Rate Limiter Spec")( 201 | test("should allow operations within rate limit") { 202 | for { 203 | rateLimiter <- RateLimiter.make(5) // Allow 5 ops per minute 204 | 205 | // Try 5 operations - all should be allowed 206 | results <- ZIO.foreach(1 to 5)(_ => rateLimiter.tryAcquire) 207 | 208 | } yield assert(results)(forall(equalTo(true))) 209 | }, 210 | test("should block operations exceeding rate limit") { 211 | for { 212 | rateLimiter <- RateLimiter.make(3) // Allow 3 ops per minute 213 | 214 | // First 3 operations should be allowed 215 | firstBatch <- ZIO.foreach(1 to 3)(_ => rateLimiter.tryAcquire) 216 | 217 | // Next 2 operations should be blocked 218 | secondBatch <- ZIO.foreach(1 to 2)(_ => rateLimiter.tryAcquire) 219 | 220 | } yield { 221 | assert(firstBatch)(forall(equalTo(true))) && 222 | assert(secondBatch)(forall(equalTo(false))) 223 | } 224 | }, 225 | test("should reset after time window passes") { 226 | for { 227 | rateLimiter <- RateLimiter.make(2) // Allow 2 ops per minute 228 | 229 | // Use up the limit 230 | _ <- rateLimiter.tryAcquire 231 | _ <- rateLimiter.tryAcquire 232 | 233 | // This should be blocked 234 | blockedResult <- rateLimiter.tryAcquire 235 | 236 | // Advance time by 61 seconds 237 | _ <- TestClock.adjust(61.seconds) 238 | 239 | // Now operations should be allowed again 240 | allowedResult1 <- rateLimiter.tryAcquire 241 | allowedResult2 <- rateLimiter.tryAcquire 242 | 243 | } yield { 244 | assert(blockedResult)(equalTo(false)) && 245 | assert(allowedResult1)(equalTo(true)) && 246 | assert(allowedResult2)(equalTo(true)) 247 | } 248 | }, 249 | test("should use sliding window for rate limiting") { 250 | for { 251 | rateLimiter <- RateLimiter.make(3) // Allow 3 ops per minute 252 | 253 | // Perform operations at different times 254 | op1 <- rateLimiter.tryAcquire // t=0 255 | _ <- TestClock.adjust(20.seconds) 256 | 257 | op2 <- rateLimiter.tryAcquire // t=20 258 | _ <- TestClock.adjust(20.seconds) 259 | 260 | op3 <- rateLimiter.tryAcquire // t=40 261 | 262 | // Should be blocked (3 ops in last 60 seconds) 263 | op4 <- rateLimiter.tryAcquire // t=40 264 | 265 | _ <- TestClock.adjust(21.seconds) // t=61 266 | 267 | // First operation is now outside the window, so this should be allowed 268 | op5 <- rateLimiter.tryAcquire // t=61 269 | 270 | } yield { 271 | assert(op1)(equalTo(true)) && 272 | assert(op2)(equalTo(true)) && 273 | assert(op3)(equalTo(true)) && 274 | assert(op4)(equalTo(false)) && 275 | assert(op5)(equalTo(true)) 276 | } 277 | }, 278 | test("should handle burst of operations correctly") { 279 | for { 280 | rateLimiter <- RateLimiter.make(5) // Allow 5 ops per minute 281 | 282 | // Burst of 10 operations at once 283 | results <- ZIO.foreach(1 to 10)(_ => rateLimiter.tryAcquire) 284 | 285 | allowed = results.take(5) 286 | blocked = results.drop(5) 287 | 288 | } yield { 289 | assert(allowed)(forall(equalTo(true))) && 290 | assert(blocked)(forall(equalTo(false))) 291 | } 292 | } 293 | ) 294 | } 295 | 296 | /** 297 | * 4. Implement a function that reverses a list, then write a property-based 298 | * test to verify that reversing a list twice returns the original list. 299 | */ 300 | object ReverseListSpec extends ZIOSpecDefault { 301 | def reverseList[A](list: List[A]): List[A] = { 302 | @annotation.tailrec 303 | def loop(remaining: List[A], reversed: List[A]): List[A] = 304 | remaining match { 305 | case Nil => reversed 306 | case head :: tail => loop(tail, head :: reversed) 307 | } 308 | 309 | loop(list, Nil) 310 | } 311 | 312 | override def spec = 313 | suite("Reverse List Spec")( 314 | test("reversing a list twice returns the original list") { 315 | check(Gen.listOf(Gen.int)) { list => 316 | assertTrue(reverseList(reverseList(list)) == list) 317 | } 318 | } 319 | ) 320 | } 321 | 322 | /** 323 | * 5. Implement an AVL tree (self-balancing binary search tree) with insert 324 | * and delete operations. Write property-based tests to verify that the 325 | * tree remains balanced after each operation. A balanced tree is one 326 | * where the height of every node's left and right subtrees differs by at 327 | * most one. 328 | */ 329 | object AVLTreeSpec extends ZIOSpecDefault { 330 | 331 | import zio.test.Gen 332 | 333 | sealed trait AVLNode[+A] { 334 | def height: Int 335 | 336 | def balanceFactor: Int 337 | 338 | def toList: List[A] 339 | 340 | def isBalanced: Boolean 341 | 342 | def contains[B >: A](value: B)(implicit ord: Ordering[B]): Boolean 343 | 344 | def min: Option[A] 345 | 346 | def max: Option[A] 347 | } 348 | 349 | case object Empty extends AVLNode[Nothing] { 350 | val height = 0 351 | val balanceFactor = 0 352 | val toList = List.empty 353 | val isBalanced = true 354 | 355 | def contains[B](value: B)(implicit ord: Ordering[B]) = false 356 | 357 | val min = None 358 | val max = None 359 | } 360 | 361 | case class Node[A]( 362 | value: A, 363 | left: AVLNode[A], 364 | right: AVLNode[A], 365 | height: Int 366 | ) extends AVLNode[A] { 367 | 368 | def balanceFactor: Int = left.height - right.height 369 | 370 | def contains[B >: A](needle: B)(implicit ord: Ordering[B]): Boolean = { 371 | import ord._ 372 | if (needle < value) left.contains(needle) 373 | else if (needle > value) right.contains(needle) 374 | else true 375 | } 376 | 377 | def toList: List[A] = left.toList ++ List(value) ++ right.toList 378 | 379 | def isBalanced: Boolean = { 380 | val bf = balanceFactor 381 | bf >= -1 && bf <= 1 && left.isBalanced && right.isBalanced 382 | } 383 | 384 | @tailrec 385 | final def min: Option[A] = left match { 386 | case Empty => Some(value) 387 | case node: Node[A] => node.min 388 | } 389 | 390 | @tailrec 391 | final def max: Option[A] = right match { 392 | case Empty => Some(value) 393 | case node: Node[A] => node.max 394 | } 395 | } 396 | 397 | object Node { 398 | def apply[A](value: A, left: AVLNode[A], right: AVLNode[A]): Node[A] = { 399 | val h = 1 + math.max(left.height, right.height) 400 | new Node(value, left, right, h) 401 | } 402 | } 403 | 404 | // AVL Tree operations 405 | object AVLTree { 406 | 407 | def empty[A]: AVLNode[A] = Empty 408 | 409 | private def rotateLeft[A](node: Node[A]): Node[A] = node.right match { 410 | case Node(rv, rl, rr, _) => 411 | Node(rv, Node(node.value, node.left, rl), rr) 412 | case Empty => node // Should not happen in a valid AVL tree 413 | } 414 | 415 | private def rotateRight[A](node: Node[A]): Node[A] = node.left match { 416 | case Node(lv, ll, lr, _) => 417 | Node(lv, ll, Node(node.value, lr, node.right)) 418 | case Empty => node // Should not happen in a valid AVL tree 419 | } 420 | 421 | private def balance[A](node: Node[A]): AVLNode[A] = { 422 | val bf = node.balanceFactor 423 | 424 | if (bf > 1) { 425 | // Left-heavy 426 | node.left match { 427 | case ln: Node[A] if ln.balanceFactor < 0 => 428 | // Left-Right case 429 | rotateRight(Node(node.value, rotateLeft(ln), node.right)) 430 | case _: Node[A] => 431 | // Left-Left case 432 | rotateRight(node) 433 | case Empty => node 434 | } 435 | } else if (bf < -1) { 436 | // Right-heavy 437 | node.right match { 438 | case rn: Node[A] if rn.balanceFactor > 0 => 439 | // Right-Left case 440 | rotateLeft(Node(node.value, node.left, rotateRight(rn))) 441 | case _: Node[A] => 442 | // Right-Right case 443 | rotateLeft(node) 444 | case Empty => node 445 | } 446 | } else node 447 | } 448 | 449 | def insert[A](tree: AVLNode[A], value: A)(implicit 450 | ord: Ordering[A] 451 | ): AVLNode[A] = { 452 | import ord._ 453 | tree match { 454 | case Empty => Node(value, Empty, Empty) 455 | case node @ Node(v, l, r, _) => 456 | if (value < v) { 457 | balance(Node(v, insert(l, value), r)) 458 | } else if (value > v) { 459 | balance(Node(v, l, insert(r, value))) 460 | } else node // Value already exists 461 | } 462 | } 463 | 464 | def delete[A](tree: AVLNode[A], value: A)(implicit 465 | ord: Ordering[A] 466 | ): AVLNode[A] = { 467 | import ord._ 468 | tree match { 469 | case Empty => Empty 470 | case node @ Node(v, l, r, _) => 471 | if (value < v) { 472 | balance(Node(v, delete(l, value), r)) 473 | } else if (value > v) { 474 | balance(Node(v, l, delete(r, value))) 475 | } else { 476 | // Found the node to delete 477 | (l, r) match { 478 | case (Empty, Empty) => Empty 479 | case (Empty, right) => right 480 | case (left, Empty) => left 481 | case (left: Node[A], right: Node[A]) => 482 | // Find the inorder successor (minimum in right subtree) 483 | right.min match { 484 | case Some(successor) => 485 | balance(Node(successor, left, delete(right, successor))) 486 | case None => left // Should not happen 487 | } 488 | } 489 | } 490 | } 491 | } 492 | 493 | def size[A](tree: AVLNode[A]): Int = tree match { 494 | case Empty => 0 495 | case Node(_, l, r, _) => 1 + size(l) + size(r) 496 | } 497 | 498 | def isBST[A: Ordering](tree: AVLNode[A]): Boolean = { 499 | def check(node: AVLNode[A], min: Option[A], max: Option[A]): Boolean = 500 | node match { 501 | case Empty => true 502 | case Node(v, l, r, _) => 503 | val ord = implicitly[Ordering[A]] 504 | val validMin = min.forall(ord.lt(_, v)) 505 | val validMax = max.forall(ord.gt(_, v)) 506 | validMin && validMax && 507 | check(l, min, Some(v)) && 508 | check(r, Some(v), max) 509 | } 510 | 511 | check(tree, None, None) 512 | } 513 | } 514 | 515 | override def spec = 516 | suite("AVL Tree Spec")( 517 | suite("Balance Properties")( 518 | test("should maintain balance after insertions") { 519 | check(Gen.listOf(Gen.int(-1000, 1000))) { list => 520 | val tree = list.foldLeft(AVLTree.empty[Int])(AVLTree.insert) 521 | assertTrue( 522 | tree.isBalanced, 523 | AVLTree.isBST(tree), 524 | tree.toList.sorted == list.distinct.sorted 525 | ) 526 | } 527 | }, 528 | test("should maintain balance after deletions") { 529 | check( 530 | Gen.listOf(Gen.int(-100, 100)), 531 | Gen.listOf(Gen.int(-100, 100)) 532 | ) { (insertList, deleteList) => 533 | val tree = 534 | insertList.foldLeft(AVLTree.empty[Int])(AVLTree.insert) 535 | val afterDeletions = deleteList.foldLeft(tree)(AVLTree.delete) 536 | 537 | assertTrue( 538 | afterDeletions.isBalanced, 539 | AVLTree.isBST(afterDeletions), 540 | afterDeletions.toList.sorted == insertList.distinct 541 | .filterNot(deleteList.contains) 542 | .sorted 543 | ) 544 | } 545 | } 546 | ) 547 | ) 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /src/main/scala/zionomicon/solutions/11-concurrent-structures-queue-work-distribution.scala: -------------------------------------------------------------------------------- 1 | package zionomicon.solutions 2 | 3 | package QueueWorkDistribution { 4 | 5 | /** 6 | * 1. Implement load balancer that distributes work across multiple worker 7 | * queues using a round-robin strategy: 8 | * 9 | * {{{ 10 | * trait LoadBalancer[A] { 11 | * def submit(work: A): Task[Unit] 12 | * def shutdown: Task[Unit] 13 | * } 14 | * object LoadBalancer { 15 | * def make[A](workerCount: Int, process: A => Task[A]) = ??? 16 | * } 17 | * }}} 18 | */ 19 | package LoadBalancerImpl { 20 | 21 | import zio._ 22 | 23 | trait LoadBalancer[A] { 24 | def submit(work: A): Task[Unit] 25 | 26 | def shutdown: Task[Unit] 27 | 28 | def isShutdown: UIO[Boolean] 29 | } 30 | 31 | object LoadBalancer { 32 | 33 | def make[A]( 34 | workerCount: Int, 35 | process: A => Task[A] 36 | ): Task[LoadBalancer[A]] = 37 | for { 38 | workers <- ZIO.replicateZIO(workerCount)(Queue.unbounded[A]) 39 | 40 | roundRobinCounter <- Ref.make(0) 41 | isShutdownFlag <- Ref.make(false) 42 | 43 | totalSubmitted <- Ref.make(0L) 44 | totalProcessed <- Ref.make(0L) 45 | processingCounters <- ZIO.replicateZIO(workerCount)(Ref.make(0L)) 46 | 47 | // Worker completion promises 48 | workerCompletions <- 49 | ZIO.replicateZIO(workerCount)(Promise.make[Nothing, Unit]) 50 | 51 | // Start worker fibers with processing tracking 52 | workerFibers <- 53 | ZIO.foreach( 54 | workers 55 | .zip(processingCounters) 56 | .zip(workerCompletions) 57 | .zipWithIndex 58 | ) { case (((queue, counter), completion), idx) => 59 | def workerLoop: UIO[Unit] = 60 | queue.poll.flatMap { 61 | case Some(work) => 62 | for { 63 | _ <- counter.update(_ + 1) 64 | _ <- 65 | process(work) 66 | .tapError(err => 67 | ZIO.debug(s"Worker $idx failed: ${err.getMessage}") 68 | ) 69 | .ignore 70 | _ <- totalProcessed.update(_ + 1) 71 | _ <- workerLoop 72 | } yield () 73 | 74 | case None => 75 | isShutdownFlag.get.flatMap { shuttingDown => 76 | if (shuttingDown) 77 | ZIO.debug(s"Worker $idx exiting gracefully") *> 78 | completion.succeed(()).unit 79 | else workerLoop 80 | } 81 | } 82 | 83 | workerLoop 84 | .onError(cause => 85 | ZIO.debug(s"Worker $idx error: $cause") *> 86 | completion.failCause(cause) 87 | ) 88 | .forkDaemon 89 | } 90 | 91 | } yield new LoadBalancer[A] { 92 | 93 | override def submit(work: A): Task[Unit] = 94 | for { 95 | shutdown <- isShutdownFlag.get.debug("Checking shutdown status") 96 | _ <- ZIO.when(shutdown)( 97 | ZIO.fail( 98 | new IllegalStateException( 99 | "LoadBalancer is shutting down" 100 | ) 101 | ) 102 | ) 103 | 104 | queueIndex <- 105 | roundRobinCounter.getAndUpdate(i => (i + 1) % workerCount) 106 | _ <- totalSubmitted.update(_ + 1) 107 | _ <- workers.toList(queueIndex).offer(work) 108 | } yield () 109 | 110 | override def shutdown: Task[Unit] = 111 | ZIO.uninterruptibleMask { restore => 112 | for { 113 | alreadyShutdown <- isShutdownFlag.getAndSet(true) 114 | _ <- ZIO.unless(alreadyShutdown) { 115 | for { 116 | submitted <- totalSubmitted.get 117 | processed <- totalProcessed.get 118 | remaining = submitted - processed 119 | 120 | _ <- 121 | ZIO.debug( 122 | s"Starting graceful shutdown: $remaining items remaining to process" 123 | ) 124 | 125 | // Wait for all workers to complete processing 126 | _ <- restore(ZIO.foreach(workerCompletions)(_.await)) 127 | 128 | // Wait for all worker fibers to complete 129 | _ <- restore(ZIO.foreach(workerFibers)(_.join)) 130 | 131 | } yield () 132 | } 133 | } yield () 134 | } 135 | 136 | override def isShutdown: UIO[Boolean] = 137 | isShutdownFlag.get 138 | } 139 | } 140 | 141 | // Example usage with graceful shutdown 142 | object LoadBalancerGracefulExample extends ZIOAppDefault { 143 | 144 | def run = 145 | for { 146 | // Create load balancer with metrics 147 | balancer <- LoadBalancer.make[String]( 148 | workerCount = 3, 149 | process = (work: String) => 150 | for { 151 | _ <- ZIO.debug(s"Processing: $work") 152 | 153 | // Simulate variable processing time 154 | delay <- Random.nextIntBetween(1000, 5000) 155 | _ <- ZIO.sleep(delay.milliseconds) 156 | 157 | processed = s"Completed: $work" 158 | _ <- ZIO.debug(processed) 159 | } yield processed 160 | ) 161 | 162 | // Submit a batch of work 163 | _ <- ZIO 164 | .foreach(1 to 20) { i => 165 | balancer.submit(s"Task-$i").delay(100.milliseconds) 166 | } 167 | .fork // Submit asynchronously 168 | 169 | // Let some work get processed 170 | _ <- ZIO.sleep(500.milliseconds) 171 | 172 | // Try graceful shutdown 173 | _ <- ZIO.debug("Initiating graceful shutdown...") 174 | shutdownFiber <- balancer.shutdown.fork 175 | 176 | // Wait until shutdown starts 177 | _ <- balancer.isShutdown.repeatUntil(identity) 178 | 179 | // Try to submit more work (should fail) 180 | _ <- balancer.submit("Late-Task").either.flatMap { 181 | case Left(e) => 182 | ZIO.debug( 183 | s"Expected: Cannot submit during shutdown - ${e.getMessage}" 184 | ) 185 | case Right(_) => 186 | ZIO.debug( 187 | "Unexpected: Submission succeeded during shutdown" 188 | ) 189 | } 190 | 191 | // Wait for shutdown to complete 192 | _ <- shutdownFiber.join 193 | 194 | _ <- ZIO.debug("Application complete") 195 | 196 | } yield () 197 | } 198 | 199 | } 200 | 201 | /** 202 | * 2. Implement a rate limiter that limits the number of requests processed 203 | * in a given time frame. It takes the time interval and the maximum 204 | * number of calls that are allowed to be performed within the time 205 | * interval: 206 | * 207 | * {{{ 208 | * trait RateLimiter { 209 | * def acquire: UIO[Unit] 210 | * def apply[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] 211 | * } 212 | * 213 | * object RateLimiter { 214 | * def make(max: Int, interval: Duration): UIO[RateLimiter] = ??? 215 | * } 216 | * }}} 217 | */ 218 | package RateLimiterImpl { 219 | 220 | import zio._ 221 | 222 | import java.util.concurrent.TimeUnit 223 | 224 | trait RateLimiter { 225 | def acquire: UIO[Unit] 226 | 227 | def apply[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] 228 | } 229 | 230 | object RateLimiter { 231 | def make(max: Int, interval: Duration): UIO[RateLimiter] = 232 | for { 233 | // Create a bounded queue to hold permits (tokens) 234 | permits <- Queue.bounded[Unit](max) 235 | // Initially fill the queue with max permits 236 | _ <- permits.offerAll(List.fill(max)(())) 237 | } yield new RateLimiter { 238 | 239 | def acquire: UIO[Unit] = 240 | for { 241 | // Take a permit from the queue (blocks if none available) 242 | _ <- permits.take 243 | // Schedule returning the permit after the interval expires 244 | _ <- 245 | (ZIO.sleep(interval) *> permits.offer(())).fork.uninterruptible 246 | } yield () 247 | 248 | def apply[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = 249 | acquire *> zio 250 | } 251 | } 252 | 253 | object RateLimiterExample extends ZIOAppDefault { 254 | 255 | def run = 256 | for { 257 | // Create rate limiter: max 5 requests per 10 seconds 258 | rateLimiter <- RateLimiter.make(max = 5, interval = 10.seconds) 259 | 260 | startTime <- Clock.currentTime(TimeUnit.MILLISECONDS) 261 | 262 | // Submit 15 requests 263 | _ <- ZIO.foreach(1 to 15) { i => 264 | rateLimiter { 265 | for { 266 | now <- Clock.currentTime(TimeUnit.MILLISECONDS) 267 | elapsed = now - startTime 268 | random <- Random.nextLongBetween(100, 500) 269 | 270 | _ <- ZIO.sleep(random.milliseconds) 271 | _ <- ZIO.debug(s"Request $i processed at ${elapsed}ms") 272 | } yield () 273 | } 274 | } 275 | 276 | _ <- ZIO.debug("All requests completed") 277 | } yield () 278 | } 279 | 280 | object RateLimiterConcurrentExample extends ZIOAppDefault { 281 | 282 | def run = 283 | for { 284 | // Rate limiter: 5 requests per 10 seconds 285 | rateLimiter <- RateLimiter.make(max = 5, interval = 10.seconds) 286 | 287 | startTime <- Clock.currentTime(TimeUnit.MILLISECONDS) 288 | 289 | // Launch 15 concurrent requests 290 | _ <- ZIO.foreachPar(1 to 15) { i => 291 | rateLimiter { 292 | for { 293 | now <- Clock.currentTime(TimeUnit.MILLISECONDS) 294 | elapsed = now - startTime 295 | random <- Random.nextLongBetween(100, 500) 296 | 297 | _ <- ZIO.sleep(random.milliseconds) 298 | _ <- ZIO.debug(s"Request $i processed at ${elapsed}ms") 299 | } yield () 300 | } 301 | } 302 | 303 | _ <- ZIO.debug("All requests completed") 304 | } yield () 305 | } 306 | } 307 | 308 | /** 309 | * 3. Implement a circuit breaker that prevents calls to a service after a 310 | * certain number of failures: 311 | * 312 | * {{{ 313 | * trait CircuitBreaker { 314 | * def protect[A](operation: => Task[A]): Task[A] 315 | * } 316 | * }}} 317 | */ 318 | package CircuitBreakerImpl { 319 | import zio._ 320 | 321 | import java.util.concurrent.TimeUnit 322 | 323 | trait CircuitBreaker { 324 | def protect[A](operation: Task[A]): Task[A] 325 | def currentState: UIO[CircuitBreaker.State] 326 | } 327 | 328 | case class CircuitBreakerOpen() extends Exception("Circuit breaker is open") 329 | 330 | object CircuitBreaker { 331 | sealed trait State { 332 | def failureCount: Int 333 | } 334 | object State { 335 | case class Closed(failureCount: Int) extends State { 336 | override def toString: String = "Closed" 337 | } 338 | case class Open(failureCount: Int, openedAt: Long) extends State { 339 | override def toString: String = "Open" 340 | } 341 | } 342 | 343 | def make( 344 | maxFailures: Int, 345 | resetTimeout: Duration 346 | ): ZIO[Any, Nothing, CircuitBreaker] = 347 | Ref.Synchronized 348 | .make[State](State.Closed(failureCount = 0)) 349 | .map { stateRef => 350 | new CircuitBreaker { 351 | override def currentState: UIO[State] = stateRef.get 352 | 353 | override def protect[A](operation: Task[A]): Task[A] = 354 | stateRef.modifyZIO { 355 | case State.Closed(failureCount) => 356 | // Execute operation and capture result as Either 357 | operation.either.flatMap { 358 | case Left(error) => 359 | val newFailureCount = failureCount + 1 360 | if (newFailureCount >= maxFailures) { 361 | // Transition to Open state with current timestamp 362 | Clock.currentTime(TimeUnit.MILLISECONDS).map { now => 363 | val newState = State.Open( 364 | failureCount = newFailureCount, 365 | openedAt = now 366 | ) 367 | (Left(error), newState) 368 | } 369 | } else { 370 | // Stay in Closed state 371 | val newState = 372 | State.Closed(failureCount = newFailureCount) 373 | ZIO.succeed((Left(error), newState)) 374 | } 375 | case Right(success) => 376 | // Reset failure count and stay in Closed state 377 | val newState = State.Closed(failureCount = 0) 378 | ZIO.succeed((Right(success), newState)) 379 | }.uninterruptible 380 | 381 | case state @ State.Open(failureCount, openedAt) => 382 | // Check if enough time has passed to transition to implicit half-open state 383 | Clock.currentTime(TimeUnit.MILLISECONDS).flatMap { now => 384 | val elapsed = now - openedAt 385 | if (elapsed >= resetTimeout.toMillis) { 386 | // Transition to implicit half-open state and try the operation 387 | ZIO.uninterruptibleMask { restore => 388 | restore(operation).either.flatMap { 389 | case Left(error) => 390 | // Transition back to Open with new timestamp 391 | Clock.currentTime(TimeUnit.MILLISECONDS).map { 392 | newNow => 393 | val newState = State.Open( 394 | failureCount = failureCount, 395 | openedAt = newNow 396 | ) 397 | (Left(error), newState) 398 | } 399 | case Right(success) => 400 | // Transition to Closed 401 | val newState = State.Closed(failureCount = 0) 402 | ZIO.succeed((Right(success), newState)) 403 | } 404 | } 405 | } else { 406 | // Still in the timeout period, reject immediately 407 | ZIO.succeed((Left(CircuitBreakerOpen()), state)) 408 | } 409 | } 410 | 411 | }.absolve 412 | } 413 | } 414 | 415 | } 416 | 417 | object CircuitBreakerExample extends ZIOAppDefault { 418 | 419 | // Service that responds based on predetermined outcomes (true = success, false = failure) 420 | class TestService(outcomes: List[Boolean]) { 421 | private val index = new java.util.concurrent.atomic.AtomicInteger(0) 422 | 423 | def call: Task[String] = { 424 | val i = index.getAndIncrement() 425 | outcomes.lift(i) match { 426 | case Some(true) | None => ZIO.succeed("Success") 427 | case Some(false) => ZIO.fail(new Exception("Service failure")) 428 | } 429 | } 430 | } 431 | 432 | // Example 1: Basic circuit breaker behavior 433 | def example1Basic: ZIO[Any, Nothing, Unit] = 434 | for { 435 | _ <- ZIO.debug("\n=== Example 1: Basic Behavior ===") 436 | _ <- ZIO.debug("Scenario: 3 consecutive failures open the circuit\n") 437 | cb <- CircuitBreaker.make(maxFailures = 3, resetTimeout = 1.second) 438 | service = new TestService(List.fill(6)(false)) // 6 failures 439 | 440 | _ <- ZIO.foreach(1 to 6) { i => 441 | for { 442 | state <- cb.currentState 443 | _ <- ZIO.debug(s"Request $i [Circuit: $state]") 444 | _ <- (cb.protect(service.call): Task[String]) 445 | .tap(_ => ZIO.debug(s" ✓ Success")) 446 | .tapError(_ => ZIO.debug(s" ✗ Failed")) 447 | .either 448 | .flatMap { 449 | case Left(_: CircuitBreakerOpen) => 450 | ZIO.debug(s" ⊗ Rejected - circuit is open") 451 | case _ => 452 | ZIO.unit 453 | } 454 | } yield () 455 | } 456 | 457 | finalState <- cb.currentState 458 | _ <- ZIO.debug(s"\nFinal state: $finalState") 459 | } yield () 460 | 461 | // Example 2a: Successful recovery 462 | def example2aRecoverySuccess: ZIO[Any, Nothing, Unit] = 463 | for { 464 | _ <- ZIO.debug("\n=== Example 2a: Successful Recovery ===") 465 | _ <- 466 | ZIO.debug( 467 | "Scenario: Circuit opens, then successfully recovers after timeout\n" 468 | ) 469 | cb <- CircuitBreaker.make(maxFailures = 2, resetTimeout = 1.second) 470 | service = new TestService(List(false, false, true)) 471 | 472 | _ <- ZIO.debug("Opening circuit with 2 failures...") 473 | _ <- ZIO.foreach(1 to 2) { i => 474 | cb.protect(service.call) 475 | .tapError(_ => ZIO.debug(s"Request $i: ✗ Failed")) 476 | .ignore 477 | } 478 | 479 | state1 <- cb.currentState 480 | _ <- ZIO.debug(s"Circuit state: $state1\n") 481 | 482 | _ <- ZIO.debug("Waiting for reset timeout (1 second)...") 483 | _ <- ZIO.sleep(1100.millis) 484 | 485 | _ <- ZIO.debug("Attempting recovery request...") 486 | _ <- cb.protect(service.call) 487 | .tap(_ => ZIO.debug("Request 3: ✓ Success - circuit closed")) 488 | .ignore 489 | 490 | finalState <- cb.currentState 491 | _ <- ZIO.debug(s"Final state: $finalState") 492 | } yield () 493 | 494 | // Example 2b: Failed recovery then successful recovery 495 | def example2bRecoveryFailure: ZIO[Any, Nothing, Unit] = 496 | for { 497 | _ <- ZIO.debug("\n=== Example 2b: Failed Recovery, Then Success ===") 498 | _ <- 499 | ZIO.debug( 500 | "Scenario: Circuit opens, first recovery fails, second recovery succeeds\n" 501 | ) 502 | cb <- CircuitBreaker.make(maxFailures = 2, resetTimeout = 500.millis) 503 | service = new TestService( 504 | List( 505 | false, // Failure 1 506 | false, // Failure 2 - opens circuit 507 | false, // Recovery attempt 1 fails - reopens circuit 508 | true // Recovery attempt 2 succeeds - closes circuit 509 | ) 510 | ) 511 | 512 | _ <- ZIO.debug("Opening circuit with 2 failures...") 513 | _ <- ZIO.foreach(1 to 2) { i => 514 | cb.protect(service.call) 515 | .tapError(_ => ZIO.debug(s"Request $i: ✗ Failed")) 516 | .ignore 517 | } 518 | 519 | state1 <- cb.currentState 520 | _ <- ZIO.debug(s"Circuit state: $state1\n") 521 | 522 | _ <- ZIO.debug("Waiting for reset timeout (500ms)...") 523 | _ <- ZIO.sleep(600.millis) 524 | 525 | _ <- ZIO.debug("First recovery attempt...") 526 | _ <- 527 | cb.protect(service.call) 528 | .tapError(_ => ZIO.debug("Request 3: ✗ Failed - circuit reopens")) 529 | .ignore 530 | 531 | state2 <- cb.currentState 532 | _ <- ZIO.debug(s"Circuit state: $state2\n") 533 | 534 | _ <- ZIO.debug("Waiting for reset timeout again (500ms)...") 535 | _ <- ZIO.sleep(600.millis) 536 | 537 | _ <- ZIO.debug("Second recovery attempt...") 538 | _ <- cb.protect(service.call) 539 | .tap(_ => ZIO.debug("Request 4: ✓ Success - circuit closed")) 540 | .ignore 541 | 542 | finalState <- cb.currentState 543 | _ <- ZIO.debug(s"Final state: $finalState") 544 | } yield () 545 | 546 | // Example 3: Parallel requests with mixed outcomes 547 | def example3Parallel: ZIO[Any, Nothing, Unit] = 548 | for { 549 | _ <- ZIO.debug( 550 | "\n=== Example 3: Parallel Requests with Mixed Outcomes ===" 551 | ) 552 | _ <- 553 | ZIO.debug( 554 | "Scenario: 5 concurrent requests with random success/failure patterns\n" 555 | ) 556 | 557 | cb <- CircuitBreaker.make(maxFailures = 3, resetTimeout = 2.seconds) 558 | // Mix of outcomes: some succeed, some fail 559 | service = 560 | new TestService( 561 | scala.util.Random.shuffle(List(false, true, false, false, false)) 562 | ) 563 | 564 | stateBefore <- cb.currentState 565 | _ <- ZIO.debug(s"State before: $stateBefore") 566 | _ <- ZIO.debug("Sending 5 parallel requests...\n") 567 | 568 | results <- 569 | ZIO.foreachPar(1 to 5) { requestNum => 570 | cb.protect(service.call) 571 | .as((requestNum, "success")) 572 | .tap(_ => ZIO.debug(s"[Request #$requestNum] ✓ Success")) 573 | .tapError(_ => ZIO.debug(s"[Request #$requestNum] ✗ Failed")) 574 | .catchAll { 575 | case _: CircuitBreakerOpen => 576 | ZIO.debug(s"[Request #$requestNum] ⊗ Rejected") *> 577 | ZIO.succeed((requestNum, "rejected")) 578 | case _ => 579 | ZIO.succeed((requestNum, "failed")) 580 | } 581 | } 582 | 583 | stateAfter <- cb.currentState 584 | _ <- ZIO.debug(s"\nState after: $stateAfter") 585 | 586 | successCount = results.count(_._2 == "success") 587 | rejectedCount = results.count(_._2 == "rejected") 588 | failedCount = results.count(_._2 == "failed") 589 | 590 | _ <- 591 | ZIO.debug( 592 | s"\nResults: $successCount success, $failedCount failed, $rejectedCount rejected" 593 | ) 594 | } yield () 595 | 596 | // Example 4: Success resets counter 597 | def example4Reset: ZIO[Any, Nothing, Unit] = 598 | for { 599 | _ <- ZIO.debug("\n=== Example 4: Failure Counter Reset ===") 600 | _ <- ZIO.debug( 601 | "Scenario: Successful request resets the failure counter\n" 602 | ) 603 | cb <- CircuitBreaker.make(maxFailures = 3, resetTimeout = 1.second) 604 | service = new TestService( 605 | List( 606 | false, // Failure 1 607 | false, // Failure 2 608 | true, // Success - resets counter to 0 609 | false, // Failure 1 (counter reset) 610 | false, // Failure 2 611 | false // Failure 3 - opens circuit 612 | ) 613 | ) 614 | 615 | _ <- ZIO.foreach(1 to 7) { i => 616 | for { 617 | state <- cb.currentState 618 | _ <- ZIO.debug(s"Request $i [Circuit: $state]") 619 | _ <- 620 | cb.protect(service.call) 621 | .tap(_ => 622 | ZIO.debug(s" ✓ Success (failure counter reset to 0)") 623 | ) 624 | .tapError(_ => ZIO.debug(s" ✗ Failed")) 625 | .either 626 | .flatMap { 627 | case Left(_: CircuitBreakerOpen) => 628 | ZIO.debug(s" ⊗ Rejected - circuit is open") 629 | case _ => 630 | ZIO.unit 631 | } 632 | } yield () 633 | } 634 | 635 | finalState <- cb.currentState 636 | _ <- ZIO.debug(s"\nFinal state: $finalState") 637 | _ <- 638 | ZIO.debug( 639 | "Note: Request 3 succeeded and reset the counter, so it took 3 more" 640 | ) 641 | _ <- ZIO.debug( 642 | " consecutive failures (requests 4-6) to open the circuit" 643 | ) 644 | } yield () 645 | 646 | def run = 647 | for { 648 | _ <- example1Basic 649 | _ <- example2aRecoverySuccess 650 | _ <- example2bRecoveryFailure 651 | _ <- example3Parallel.repeatN(5) 652 | _ <- example4Reset 653 | _ <- ZIO.debug("\n=== All examples completed ===") 654 | } yield () 655 | } 656 | } 657 | 658 | } 659 | --------------------------------------------------------------------------------