{"id":16556776,"url":"https://github.com/alexklibisz/futil","last_synced_at":"2025-06-24T23:09:32.166Z","repository":{"id":53500091,"uuid":"341592538","full_name":"alexklibisz/futil","owner":"alexklibisz","description":"minimal utilities for Scala Futures","archived":false,"fork":false,"pushed_at":"2021-03-28T15:19:43.000Z","size":115,"stargazers_count":22,"open_issues_count":0,"forks_count":3,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-08T23:12:31.964Z","etag":null,"topics":["concurrent-programming","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexklibisz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-02-23T15:04:56.000Z","updated_at":"2022-03-24T21:07:01.000Z","dependencies_parsed_at":"2022-09-02T03:41:30.856Z","dependency_job_id":null,"html_url":"https://github.com/alexklibisz/futil","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/alexklibisz/futil","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexklibisz%2Ffutil","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexklibisz%2Ffutil/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexklibisz%2Ffutil/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexklibisz%2Ffutil/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexklibisz","download_url":"https://codeload.github.com/alexklibisz/futil/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexklibisz%2Ffutil/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261771193,"owners_count":23207223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrent-programming","scala"],"created_at":"2024-10-11T20:05:43.685Z","updated_at":"2025-06-24T23:09:32.139Z","avatar_url":"https://github.com/alexklibisz.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# futil\n\n[![Github CI Status][Badge-Github-CI]][Link-Github-CI]\n[![Sonatype Nexus (Releases)][Badge-Sonatype-Release]][Link-Sonatype-Release]\n[![Sonatype Nexus (Snapshot)][Badge-Sonatype-Snapshot]][Link-Sonatype-Snapshot]\n\nThis library aims to add some useful functionality to Scala's Futures without introducing a full effect system.\n\nScala's built-in [Futures](https://docs.scala-lang.org/overviews/core/futures.html) are a pretty good abstraction for \nconcurrent and asynchronous programming, but they have some quirks (e.g., lack of referential transparency).\nEffect systems and IO Monads like those provided by [cats-effect](https://typelevel.org/cats-effect/), \n[ZIO](https://zio.dev/), [monix](https://monix.io/), [akka](https://akka.io/), etc. have many useful features\nfor concurrent and asynchronous programming, but they can be difficult to introduce in an established codebase.\n\nIf you're starting a green-field project then you should totally learn and use a real effect system.\nIf you just need to limit the parallelism of some Futures or implement a simple Retry, you might give futil a try.\n\n**⚠ I consider futil feature-complete, but the interfaces could still change. ⚠️️**\n\n## Recipes\n\n### Setup\n\n```scala mdoc\n// Typical async stuff.\nimport scala.concurrent._\nimport duration._\nimport scala.util._\n\n// Futil imports.\nimport futil._\n\n// Most methods require an implicit ExecutionContext.\nimport ExecutionContext.Implicits.global\n\n// Some methods require an implicit ScheduledExecutorService.\nimport Futil.Implicits.scheduler\n\n// Let's pretend this is calling some external web service that does something useful. \ndef callService(i: Int): Future[Int] = Future(i + 1)\n```\n\n### Thunks\n\nScala Futures execute _eagerly_. This means when we define a `val foo: Future[Int] = ...`, it starts running _now_.\n\nTo account for this, some methods in `Futil` use a [_thunk_](https://en.wikipedia.org/wiki/Thunk). \nThunk is just a fancy word for a function that takes `Unit` and returns something.\n\nFor example, a thunk for a `Future[Int]`:\n\n```scala mdoc\ndef future(): Future[Int] = Future(42)\nval aThunk: () =\u003e Future[Int] = () =\u003e future()\n```\n\nFutil has a helper method for defining a thunk:\n\n```scala mdoc\nval alsoAThunk: () =\u003e Future[Int] = Futil.thunk(future())\n```\n\nA thunk of a Future is useful in two cases:\n\n1. When we need to delay the execution of a Future.\n2. When we need a way to re-run the Future on-demand.\n\nThunks are not fool-proof. For instance, if we define the Future as a `val`, and _then_ wrap it in a thunk, \nit will still execute eagerly and silently defeat the purpose of the whole exercise.\n\n### Timing\n\nNote that nanosecond precision is technically supported, but the overhead of scheduling, executing, etc.\nusually negates that level of precision.\n\nTime the execution of a Future.\n\n```scala mdoc\n// Times the service call, returning the value and the execution duration.\nval timed: Future[(Int, Duration)] = Futil.timed(callService(42))\n```\n\nLimit the time a Future spends executing.\n\n```scala mdoc\n// Returns a failed Future if the service call exceeds the given duration.\nval deadline: Future[Int] = Futil.deadline(1.seconds)(callService(42))\n```\n\nDelay the execution of a Future.\n\n```scala mdoc\n// Waits the given duration before executing the Future.\nval delayed: Future[Int] = Futil.delay(1.seconds)(callService(42))\n```\n\nSleep asynchronously.\n\n```scala mdoc\n// Sleeps the given duration before continuing.\nval slept: Future[Unit] = Futil.sleep(1.seconds)\n```\n\n### Parallelism\n\nRun a Future for every item in a Seq, limiting the number of Futures running in parallel at any given time.\nThis is a form of self rate-limiting, particularly useful when dealing with flaky or rate-limited external services.\n\n```scala mdoc\nval numInParallel = 16\nval inputs: Seq[Int] = 0 to 9999\ndef f(i: Int): Future[Double] = callService(i).map(_ * 3.14)\n\n// This has the same type signature as Future.traverse. \nval outputs: Future[Seq[Double]] = Futil.traverseParN(numInParallel)(inputs)(f)\n```\n\nRun a Future for every item in a Seq, exactly one at a time.\n\n```scala mdoc\nval outputsSerial: Future[Seq[Double]] = Futil.traverseSerial(inputs)(f)\n```\n\n### Retries\n\nRetry a fixed number of times.\n\n```scala mdoc\n// Retry on failure 3 times.\nFutil.retry(RetryPolicy.Repeat(3))(() =\u003e callService(42))\n```\n\nRetry a fixed number of times, or stop early based on the result of the previous call.\n\n```scala mdoc\n// Early stop if the last call returned a throwable containing the word \"please\".\ndef earlyStop(t: Try[Int]): Future[Boolean] = t match {\n  case Failure(t) =\u003e Future.successful(t.getMessage.contains(\"please\"))\n  case _          =\u003e Future.successful(false)\n}\nFutil.retry(RetryPolicy.Repeat(3), earlyStop)(() =\u003e callService(42))\n```\n\nRetry with a fixed delay between calls.\n\n```scala mdoc\n// Retry 3 times, waiting 3 seconds between each call.\nFutil.retry(RetryPolicy.FixedBackoff(3, 3.seconds))(() =\u003e callService(42))\n\n// Early stop if asked nicely.\nFutil.retry(RetryPolicy.FixedBackoff(3, 3.seconds), earlyStop)(() =\u003e callService(42))\n```\n\nRetry with exponential delay between calls.\n\n```scala mdoc\n// Retry 3 times, first delay is 2s, then 4s, then 8s.\nFutil.retry(RetryPolicy.ExponentialBackoff(3, 2.seconds))(() =\u003e callService(42))\n\n// Early stop if asked nicely.\nFutil.retry(RetryPolicy.ExponentialBackoff(3, 2.seconds), earlyStop)(() =\u003e callService(42))\n```\n\n### Asynchronous Semaphore (Advanced)\n\nA [semaphore](https://en.wikipedia.org/wiki/Semaphore_(programming)) lets us acquire and release a fixed number of \npermits in order to limit access to some resource.\nAn asynchronous semaphore lets us acquire and release asynchronously.\n\nAcquire and release permits:\n\n```scala mdoc\nval sem = Futil.semaphore(2)\nfor {\n  _ \u003c- sem.acquire()\n  _ \u003c- callService(42)\n  _ \u003c- sem.release()\n} yield ()\n```\n\nBe careful: if the method fails, the release method must still be called:\n\n```scala mdoc\nfor {\n  _ \u003c- sem.acquire()\n  _ \u003c- Future.failed(new Exception(\"uh oh!\"))\n  _ \u003c- sem.release() // This won't be called!\n} yield ()\n```\n\nUse the `withPermit` method to ensure the permit is released:\n\n```scala mdoc\nfor {\n  _ \u003c- sem.withPermit(() =\u003e callService(42))\n  _ \u003c- sem.withPermit(() =\u003e Future.failed(new Exception(\"uh oh!\"))) // Will still release the permit.\n} yield ()\n```\n\nHere's a real use-case: we have a singleton client to some service, and want to ensure the client makes at most 10 \nparallel calls to the service at any given time.\n\n```scala mdoc\n\nclass SomeServiceClient(parallelism: Int) {\n\n  private val sem = Futil.semaphore(parallelism)\n\n  def getFooById(id: Int): Future[String] = \n    sem.withPermit(() =\u003e callService(id).map(i =\u003e s\"Foo: $i\"))\n      \n  def getBarById(id: Int): Future[String] =\n    sem.withPermit(() =\u003e callService(id).map(i =\u003e s\"Bar: $i\"))\n} \n\n// The service can only handle 10 parallel calls.\nval client = new SomeServiceClient(10)\n\n// Get all the foos without making the service fall over.\nval foos = Future.sequence((0 to 999).map(client.getFooById(_)))\n```\n\n\u003c!-- Links --\u003e\n\n[Badge-Github-CI]: https://img.shields.io/github/workflow/status/alexklibisz/futil/CI/main\n[Link-Github-CI]: https://github.com/alexklibisz/futil/actions/workflows/pr.yml\n\n[Badge-Sonatype-Release]: https://img.shields.io/nexus/r/com.klibisz.futil/futil_2.13?server=https%3A%2F%2Foss.sonatype.org%2F\n[Link-Sonatype-Release]: https://search.maven.org/artifact/com.klibisz.futil/futil_2.13\n\n[Badge-Sonatype-Snapshot]: https://img.shields.io/nexus/s/com.klibisz.futil/futil_2.13?server=https%3A%2F%2Foss.sonatype.org\n[Link-Sonatype-Snapshot]: https://oss.sonatype.org/content/repositories/snapshots/com/klibisz/futil/futil_2.13/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexklibisz%2Ffutil","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexklibisz%2Ffutil","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexklibisz%2Ffutil/lists"}