Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/alexklibisz/futil
minimal utilities for Scala Futures
https://github.com/alexklibisz/futil
concurrent-programming scala
Last synced: 3 months ago
JSON representation
minimal utilities for Scala Futures
- Host: GitHub
- URL: https://github.com/alexklibisz/futil
- Owner: alexklibisz
- License: apache-2.0
- Created: 2021-02-23T15:04:56.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2021-03-28T15:19:43.000Z (almost 4 years ago)
- Last Synced: 2023-07-02T09:04:23.235Z (over 1 year ago)
- Topics: concurrent-programming, scala
- Language: Scala
- Homepage:
- Size: 112 KB
- Stars: 22
- Watchers: 4
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# futil
[![Github CI Status][Badge-Github-CI]][Link-Github-CI]
[![Sonatype Nexus (Releases)][Badge-Sonatype-Release]][Link-Sonatype-Release]
[![Sonatype Nexus (Snapshot)][Badge-Sonatype-Snapshot]][Link-Sonatype-Snapshot]This library aims to add some useful functionality to Scala's Futures without introducing a full effect system.
Scala's built-in [Futures](https://docs.scala-lang.org/overviews/core/futures.html) are a pretty good abstraction for
concurrent and asynchronous programming, but they have some quirks (e.g., lack of referential transparency).
Effect systems and IO Monads like those provided by [cats-effect](https://typelevel.org/cats-effect/),
[ZIO](https://zio.dev/), [monix](https://monix.io/), [akka](https://akka.io/), etc. have many useful features
for concurrent and asynchronous programming, but they can be difficult to introduce in an established codebase.If you're starting a green-field project then you should totally learn and use a real effect system.
If you just need to limit the parallelism of some Futures or implement a simple Retry, you might give futil a try.**⚠ I consider futil feature-complete, but the interfaces could still change. ⚠️️**
## Recipes
### Setup
```scala mdoc
// Typical async stuff.
import scala.concurrent._
import duration._
import scala.util._// Futil imports.
import futil._// Most methods require an implicit ExecutionContext.
import ExecutionContext.Implicits.global// Some methods require an implicit ScheduledExecutorService.
import Futil.Implicits.scheduler// Let's pretend this is calling some external web service that does something useful.
def callService(i: Int): Future[Int] = Future(i + 1)
```### Thunks
Scala Futures execute _eagerly_. This means when we define a `val foo: Future[Int] = ...`, it starts running _now_.
To account for this, some methods in `Futil` use a [_thunk_](https://en.wikipedia.org/wiki/Thunk).
Thunk is just a fancy word for a function that takes `Unit` and returns something.For example, a thunk for a `Future[Int]`:
```scala mdoc
def future(): Future[Int] = Future(42)
val aThunk: () => Future[Int] = () => future()
```Futil has a helper method for defining a thunk:
```scala mdoc
val alsoAThunk: () => Future[Int] = Futil.thunk(future())
```A thunk of a Future is useful in two cases:
1. When we need to delay the execution of a Future.
2. When we need a way to re-run the Future on-demand.Thunks are not fool-proof. For instance, if we define the Future as a `val`, and _then_ wrap it in a thunk,
it will still execute eagerly and silently defeat the purpose of the whole exercise.### Timing
Note that nanosecond precision is technically supported, but the overhead of scheduling, executing, etc.
usually negates that level of precision.Time the execution of a Future.
```scala mdoc
// Times the service call, returning the value and the execution duration.
val timed: Future[(Int, Duration)] = Futil.timed(callService(42))
```Limit the time a Future spends executing.
```scala mdoc
// Returns a failed Future if the service call exceeds the given duration.
val deadline: Future[Int] = Futil.deadline(1.seconds)(callService(42))
```Delay the execution of a Future.
```scala mdoc
// Waits the given duration before executing the Future.
val delayed: Future[Int] = Futil.delay(1.seconds)(callService(42))
```Sleep asynchronously.
```scala mdoc
// Sleeps the given duration before continuing.
val slept: Future[Unit] = Futil.sleep(1.seconds)
```### Parallelism
Run a Future for every item in a Seq, limiting the number of Futures running in parallel at any given time.
This is a form of self rate-limiting, particularly useful when dealing with flaky or rate-limited external services.```scala mdoc
val numInParallel = 16
val inputs: Seq[Int] = 0 to 9999
def f(i: Int): Future[Double] = callService(i).map(_ * 3.14)// This has the same type signature as Future.traverse.
val outputs: Future[Seq[Double]] = Futil.traverseParN(numInParallel)(inputs)(f)
```Run a Future for every item in a Seq, exactly one at a time.
```scala mdoc
val outputsSerial: Future[Seq[Double]] = Futil.traverseSerial(inputs)(f)
```### Retries
Retry a fixed number of times.
```scala mdoc
// Retry on failure 3 times.
Futil.retry(RetryPolicy.Repeat(3))(() => callService(42))
```Retry a fixed number of times, or stop early based on the result of the previous call.
```scala mdoc
// Early stop if the last call returned a throwable containing the word "please".
def earlyStop(t: Try[Int]): Future[Boolean] = t match {
case Failure(t) => Future.successful(t.getMessage.contains("please"))
case _ => Future.successful(false)
}
Futil.retry(RetryPolicy.Repeat(3), earlyStop)(() => callService(42))
```Retry with a fixed delay between calls.
```scala mdoc
// Retry 3 times, waiting 3 seconds between each call.
Futil.retry(RetryPolicy.FixedBackoff(3, 3.seconds))(() => callService(42))// Early stop if asked nicely.
Futil.retry(RetryPolicy.FixedBackoff(3, 3.seconds), earlyStop)(() => callService(42))
```Retry with exponential delay between calls.
```scala mdoc
// Retry 3 times, first delay is 2s, then 4s, then 8s.
Futil.retry(RetryPolicy.ExponentialBackoff(3, 2.seconds))(() => callService(42))// Early stop if asked nicely.
Futil.retry(RetryPolicy.ExponentialBackoff(3, 2.seconds), earlyStop)(() => callService(42))
```### Asynchronous Semaphore (Advanced)
A [semaphore](https://en.wikipedia.org/wiki/Semaphore_(programming)) lets us acquire and release a fixed number of
permits in order to limit access to some resource.
An asynchronous semaphore lets us acquire and release asynchronously.Acquire and release permits:
```scala mdoc
val sem = Futil.semaphore(2)
for {
_ <- sem.acquire()
_ <- callService(42)
_ <- sem.release()
} yield ()
```Be careful: if the method fails, the release method must still be called:
```scala mdoc
for {
_ <- sem.acquire()
_ <- Future.failed(new Exception("uh oh!"))
_ <- sem.release() // This won't be called!
} yield ()
```Use the `withPermit` method to ensure the permit is released:
```scala mdoc
for {
_ <- sem.withPermit(() => callService(42))
_ <- sem.withPermit(() => Future.failed(new Exception("uh oh!"))) // Will still release the permit.
} yield ()
```Here's a real use-case: we have a singleton client to some service, and want to ensure the client makes at most 10
parallel calls to the service at any given time.```scala mdoc
class SomeServiceClient(parallelism: Int) {
private val sem = Futil.semaphore(parallelism)
def getFooById(id: Int): Future[String] =
sem.withPermit(() => callService(id).map(i => s"Foo: $i"))
def getBarById(id: Int): Future[String] =
sem.withPermit(() => callService(id).map(i => s"Bar: $i"))
}// The service can only handle 10 parallel calls.
val client = new SomeServiceClient(10)// Get all the foos without making the service fall over.
val foos = Future.sequence((0 to 999).map(client.getFooById(_)))
```[Badge-Github-CI]: https://img.shields.io/github/workflow/status/alexklibisz/futil/CI/main
[Link-Github-CI]: https://github.com/alexklibisz/futil/actions/workflows/pr.yml[Badge-Sonatype-Release]: https://img.shields.io/nexus/r/com.klibisz.futil/futil_2.13?server=https%3A%2F%2Foss.sonatype.org%2F
[Link-Sonatype-Release]: https://search.maven.org/artifact/com.klibisz.futil/futil_2.13[Badge-Sonatype-Snapshot]: https://img.shields.io/nexus/s/com.klibisz.futil/futil_2.13?server=https%3A%2F%2Foss.sonatype.org
[Link-Sonatype-Snapshot]: https://oss.sonatype.org/content/repositories/snapshots/com/klibisz/futil/futil_2.13/