Piotr Gawryś
https://github.com/Avasil
twitter.com/p_gawrys
twitter.com/p_gawrys
twitter.com/p_gawrys
twitter.com/p_gawrys
twitter.com/p_gawrys
twitter.com/p_gawrys
twitter.com/p_gawrys
object SharedResource {
private val counter = AtomicInt(2)
def access(i: Int): Unit = {
if (counter.decrementAndGet() < 0)
throw new IllegalStateException("counter less than 0")
Thread.sleep(100)
counter.increment()
}
}
implicit val ec =
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
val f: Int => Future[Unit] = i => Future {
SharedResource.access(i)
}
Await.result(Future.traverse(List(1, 2, 3, 4, 5))(f), 60.second)
Exception in thread "main" java.lang.IllegalStateException: counter less than 0
twitter.com/p_gawrys
implicit val ec =
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
def traverseN(n: Int, list: List[Int])(
f: Int => Future[Unit]
): Future[List[Unit]] = {
// java.util.concurrent.Semaphore
val semaphore = new Semaphore(n)
Future.traverse(list) { i =>
val future = Future(semaphore.acquire()).flatMap(_ => f(i))
future.onComplete(_ => semaphore.release())
future
}
}
val f: Int => Future[Unit] = i => Future {
SharedResource.access(i)
}
Await.result(traverseN(2, List.range(1, 5))(f), Duration.Inf) // works!
Await.result(traverseN(2, List.range(1, 10))(f), Duration.Inf) // hangs forever...
twitter.com/p_gawrys
type Listener[A] = Either[Throwable, A] => Unit
private final case class State(
available: Long,
awaitPermits: Queue[(Long, Listener[Unit])]
)
def unsafeAcquireN(n: Long, await: Listener[Unit]): Cancelable
type Listener[A] = Either[Throwable, A] => Unit
private final case class State(
available: Long,
awaitPermits: Queue[(Long, Listener[Unit])]
)
def cancelAcquisition(n: Long, isAsync: Boolean): (Listener[Unit] => Unit)
type Listener[A] = Either[Throwable, A] => Unit
private final case class State(
available: Long,
awaitPermits: Queue[(Long, Listener[Unit])]
)
def unsafeReleaseN(n: Long): Unit
type Listener[A] = Either[Throwable, A] => Unit
private final case class State(
available: Long,
awaitPermits: Queue[(Long, Listener[Unit])]
)
def acquireN(n: Long): CancelableFuture[Unit] = {
if (unsafeTryAcquireN(n)) {
CancelableFuture.unit
} else {
val p = Promise[Unit]()
unsafeAcquireN(n, Callback.fromPromise(p)) match {
case Cancelable.empty => CancelableFuture.unit
case c => CancelableFuture(p.future, c)
}
}
}
implicit val ec =
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4))
def traverseN(n: Int, list: List[Int])(
f: Int => Future[Unit]
): Future[List[Unit]] = {
// monix.execution.AsyncSemaphore
val semaphore = AsyncSemaphore(n)
Future.traverse(list) { i =>
semaphore.withPermit(() => f(i))
}
}
val f: Int => Future[Unit] = i => Future {
SharedResource.access(i)
}
Await.result(traverseN(2, List.range(1, 10))(f), Duration.Inf) // works!
object LocalExample extends App with StrictLogging {
implicit val ec = ExecutionContext.global
def req(requestId: String, userName: String): Future[Unit] = Future {
logger.info(s"Received a request to create a user $userName")
// do sth
}.flatMap(_ => registerUser(userName))
def registerUser(name: String): Future[Unit] = {
// business logic
logger.info(s"Registering a new user named $name")
Future.unit
}
val requests = List(req("1", "Clark"), req("2", "Bruce"), req("3", "Diana"))
Await.result(Future.sequence(requests), Duration.Inf)
}
Received a request to create a user Bruce
Registering a new user named Bruce
Received a request to create a user Diana
Registering a new user named Diana
Received a request to create a user Clark
Registering a new user named Clark
def req(requestId: String, userName: String): Future[Unit] = Future {
logger.info(s"$requestId: Received a request to create a user $userName")
// do sth
}.flatMap(_ => registerUser(requestId, userName))
def registerUser(requestId: String, name: String): Future[Unit] = {
// business logic
logger.info(s"$requestId: Registering a new user named $name")
Future.unit
}
3: Received a request to create a user Diana
3: Registering a new user named Diana
1: Received a request to create a user Clark
1: Registering a new user named Clark
2: Received a request to create a user Bruce
2: Registering a new user named Bruce
logger.info("Logging something.")
MDC.put("requestId", "1")
logger.info("Logging something with MDC.")
: Logging something.
1: Logging something with MDC.
def req(requestId: String, userName: String): Future[Unit] = Future {
MDC.put("requestId", requestId)
logger.info(s"Received a request to create a user $userName")
// more flatmaps to add async boundaries
}.flatMap(_ => Future(()).flatMap(_ => Future())).flatMap(_ => registerUser(userName))
def registerUser(name: String): Future[Unit] = {
// business logic
logger.info(s"Registering a new user named $name")
Future.unit
}
3: Received a request to create a user Diana
2: Received a request to create a user Bruce
1: Received a request to create a user Clark
1: Registering a new user named Clark
2: Registering a new user named Bruce
2: Registering a new user named Diana
twitter.com/p_gawrys
implicit val s = Scheduler.traced
// from https://github.com/mdedetrich/monix-mdc
MonixMDCAdapter.initialize()
def req(requestId: String, userName: String): Future[Unit] = Local.isolate {
Future {
MDC.put("requestId", requestId)
logger.info(s"Received a request to create a user $userName")
// more flatmaps to add async boundaries
}.flatMap(_ => Future(()).flatMap(_ => Future())).flatMap(_ => registerUser(userName))
}
1: Received a request to create a user Clark
3: Received a request to create a user Diana
2: Received a request to create a user Bruce
3: Registering a new user named Diana
1: Registering a new user named Clark
2: Registering a new user named Bruce
implicit val ec = Scheduler.traced
val local = Local(0)
def blackbox: Future[Unit] = {
val p = Promise[Unit]()
new Thread {
override def run(): Unit = {
Thread.sleep(100)
p.success(())
}
}.start()
p.future
}
val f = Local.isolate {
for {
_ <- Future { local := local.get + 100 }
_ <- blackbox
_ <- Future { local := local.get + 100 }
// can print 100 if blackbox is not isolated!
} yield println(local.get)
}
Await.result(f, Duration.Inf)
COURIER
LOCATION UPDATE
SHIPPING SYSTEM
REGISTER PARCEL
PARCEL
CHECK STATUS
UPDATE STATUS
case class ParcelStatus(estimatedDelivery: OffsetDateTime, route: String)
case class ParcelDelivery(id: Long, getLatestStatus: Task[Option[ParcelStatus]])
class ShippingSystem {
// adds new parcel to the system
def registerParcel(
id: Long,
destination: String
): Task[ParcelDelivery] = ???
// sends a new location of the parcel
def updateLocation(id: Long, location: String): Task[Unit] = ???
}
case class ParcelStatus(estimatedDelivery: OffsetDateTime, route: String)
case class ParcelDelivery(id: Long, getLatestStatus: Task[Option[ParcelStatus]])
class ShippingSystem {
// adds new parcel to the system
def registerParcel(
id: Long,
destination: String
): Task[ParcelDelivery] = ???
// sends a new location of the parcel
def updateLocation(id: Long, location: String): Task[Unit] = ???
}
could use synchronization and/or back-pressure
COURIER
LOCATION UPDATE
QUEUE
SHIPPING SYSTEM
REGISTER PARCEL
PARCEL
CHECK STATUS
UPDATE STATUS
twitter.com/p_gawrys
twitter.com/p_gawrys
case class ParcelLocation(id: Long, location: String)
type ParcelLocationQueue = ConcurrentQueue[Task, ParcelLocation]
class ShippingSystem private[example] (
queue: ParcelLocationQueue,
// impure data structure to cut boilerplate for the sake of example
deliveries: TrieMap[Long, ParcelStatus]
) {
def registerParcel(id: Long, destination: String): Task[ParcelDelivery] =
Task {
val parcelStatus = ParcelStatus(OffsetDateTime.now().plusYears(1L), s"route-to-$destination")
deliveries.addOne(id -> parcelStatus)
ParcelDelivery(id, Task(deliveries.get(id)))
}
def updateLocation(id: Long, location: String): Task[Unit] =
queue.offer(ParcelLocation(id, location))
def run(): Task[Unit] =
queue.poll.flatMap(updateStatus).loopForever
private def updateStatus(parcel: ParcelLocation): Task[Unit] = {
Task(deliveries.get(parcel.id)).flatMap {
case Some(lastStatus) =>
// imagine some calculations here
val newStatus = ParcelStatus(
OffsetDateTime.now().plusHours(4L), lastStatus.route + s"-at-${parcel.location}")
Task(deliveries.update(parcel.id, newStatus))
.flatMap(_ => Task(println(s"Updated parcel ${parcel.id} with new status $newStatus")))
case None =>
Task(println(s"Received missing parcel ${parcel.id}"))
}
}
}
override def run(args: List[String]): Task[ExitCode] =
for {
// will back-pressure updateLocation if it's full
queue <- ConcurrentQueue.bounded[Task, ParcelLocation](1024)
shippingSystem = new ShippingSystem(
queue, TrieMap.empty[Long, ParcelStatus])
// run shippingSystem in the background
_ <- shippingSystem.run().startAndForget
parcel <- shippingSystem.registerParcel(0L, "NYC")
_ <- shippingSystem.updateLocation(0L, "WAW")
_ <- Task.sleep(100.millis)
latestStatus <- parcel.getLatestStatus
} yield {
ExitCode.Success
}
twitter.com/p_gawrys
COURIER
LOCATION UPDATE
???
SHIPPING SYSTEM
REGISTER PARCEL
PARCEL
CHECK STATUS
UPDATE STATUS
ANALYTICS SYSTEM
twitter.com/p_gawrys
twitter.com/p_gawrys
final class ConcurrentChannel[F[_], E, A] {
def push(a: A): F[Boolean]
def pushMany(seq: Iterable[A]): F[Boolean]
def halt(e: E): F[Unit]
def consume: Resource[F, ConsumerF[F, E, A]]
def consumeWithConfig(config: ConsumerF.Config): Resource[F, ConsumerF[F, E, A]]
def awaitConsumers(n: Int): F[Boolean]
}
trait ConsumerF[F[_], E, A] {
def pull: F[Either[E, A]]
def pullMany(minLength: Int, maxLength: Int): F[Either[E, Seq[A]]]
}
type ParcelLocationChannel = ConcurrentChannel[Task, Unit, ParcelLocation]
class ShippingSystem private[example] (
channel: ParcelLocationChannel,
// impure data structure to cut boilerplate for the sake of example
deliveries: TrieMap[Long, ParcelStatus]
) {
// the same as queue version
def registerParcel(id: Long, destination: String): Task[ParcelDelivery]
def updateLocation(id: Long, location: String): Task[Unit] =
channel.push(ParcelLocation(id, location)).map(_ => ())
def run(): Task[Unit] = channel.consume.use(consumeChannel)
private def consumeChannel(
consumer: ConsumerF[Task, Unit, ParcelLocation]): Task[Unit] = {
consumer.pull.flatMap {
case Left(halt) =>
Task(println(s"Closing ShippingSystem"))
case Right(parcel) =>
updateStatus(parcel).flatMap(_ => consumeChannel(consumer))
}
}
// the same as queue version
private def updateStatus(parcel: ParcelLocation): Task[Unit]
}
twitter.com/p_gawrys
override def run(args: List[String]): Task[ExitCode] =
for {
channel <- ConcurrentChannel.of[Task, Unit, ParcelLocation]
shippingSystem = new ShippingSystem(
channel, TrieMap.empty[Long, ParcelStatus])
analytics = new AnalyticsSystem(channel)
// run both systems in the background
_ <- Task.parZip2(shippingSystem.run(), analytics.run()).startAndForget
// wait until we have 2 subscribers
_ <- channel.awaitConsumers(2)
parcel <- shippingSystem.registerParcel(0L, "NYC")
_ <- shippingSystem.updateLocation(0L, "new location")
_ <- Task.sleep(100.millis)
latestStatus <- parcel.getLatestStatus
} yield {
println(latestStatus)
ExitCode.Success
}
twitter.com/p_gawrys
twitter.com/p_gawrys
https://github.com/typelevel/cats-effect
https://github.com/functional-streams-for-scala/fs2
https://github.com/monix/monix
https://github.com/zio/zio
Some of the projects worth checking out:
twitter.com/p_gawrys