%{ title: "Oban Starts Where Tasks End", description: "Tasks are a core Elixir concurrency abstraction, but are they enough for critical work?" } --- Burgeoning Elixirists frequently ask, "Who needs background jobs in Elixir? Isn't that what [`Task.start/1`][task] is for?" Not quite. Let's examine why a `Task` is the wrong level of abstraction for critical background work. ## Layering Abstractions Erlang, and therefore Elixir, provides a legendary concurrency story through lightweight processes (`spawn`) and message passing (`send`). Those two functions are technically all you need to build actor-model-based concurrency. If you _really_ wanted to, you could build an entire application purely with `spawn` and `send`. Presumably, it would be tedious, and you'd slowly reimplement an ad-hoc, informally-specified, [bug-ridden version of half of OTP][vird]. While Erlang provides basic concurrency primitives, decades of [in-the-field][field] experience has guided the creation of elegant concurrency abstractions such as `GenServer` for long-lived generic processes, and `Supervisor` for maintaining trees of processes. So, rather than stitching systems together with `spawn` and `send`, applications are composed of standard, well-behaved GenServers and Supervisors. Elixir provides ergonomic abstractions that simplify advanced patterns based on OTP's abstractions. For instance, `Registry` adds a process-aware wrapper around ETS tables, and `Agent` brings a GenServer tailored for state management. Then there is the `Task` module for one-off OTP-friendly processes. ## What Tasks Are and What They Aren't Tasks are a more powerful abstraction than bare processes, making it simple to convert sequential code into concurrent code with varying guarantees. Depending on how tasks are initialized, they have a spectrum of responsibility between best-effort and loosely supervised: 1. **Best-Effort (`Task.start/1`)**—Tasks without process linking, supervision, concurrency controls, and no shutdown guarantees. 2. **Supervised (`Task.Supervisor.async/2`)**—Tasks with process linking, simple enumerability, hard concurrency limits, and configurable shutdown periods. Supervised tasks improve observability, constraine resources, and provide shutdown guarantees. But, tasks lack essential functionality for mission-critical work. Consider the following: * **Enqueueing**—How can I wait to execute a task when the supervisor hits a concurrency limit? How do I separate fast and slow tasks to prevent bottlenecks? * **Scheduling**—How do I run a task at a specific time in the future? What if too many scheduled jobs all need to start simultaneously? How do I reschedule them? * **Retries**—How do I restart tasks with transient failures? How do I delay and stagger retries with some backoff to prevent concurrent access problems? * **Uniqueness**—How do I prevent the same task from executing concurrently on the same node? What if I ran a task a few seconds ago, and the result is still usable? * **Distribution**—How do I distribute tasks evenly between every node in my cluster? What if I only need to run tasks on _some_ nodes? * **Instrumentation**—How can I measure the run time for various tasks and integrate them with my other application metrics? * **Runtime Visibility**—What function and arguments is each task currently doing? How long has it been doing it? * **Historical Observability**—What tasks are complete? When did they start and how long did they take? That's a lot of missing functionality, and there's a more significant issue. Once you've [implemented a solution][oban] for all of those missing pieces (or at least the parts you need right now) there is an essential component missing. ## Persistence is Crucial What happens when your application inevitably restarts, whether intentionally or from cascading failures? To retain tasks between restarts, you need persistent storage. There are abundant persistence options from the Erlang native [RabbitMQ][rmq] to the inescapable [Redis][red]. Any persistent store could work with enough effort. However, the best fit, in our opinion, is [PostgreSQL][pgs] (surely not a surprise, as Oban says, "powered by modern PostgreSQL" right on the tin). Aside from a well-earned reputation as a flexible, reliable, and highly performant relational database, PostgreSQL's killer feature is that it's probably _already_ in your application. Persisting tasks, or at least a task-like wrapper, in a database neatly solves many of the problems we identified earlier: * **Enqueueing**—With atomic operations, a SQL table can behave like a queue. * **Scheduling**—With timestamps, we can defer execution until a specific time. * **Uniqueness**—With persistence, we can query for duplicate tasks. * **Distribution**—With a central database, nodes can pull tasks when ready. * **Historical Observability**—With retention, we can look at completed tasks. Coordinating with a database is heavier weight than a BEAM process, but the upside of persistence is immense. Tasks can be enqueued atomically within the same transaction as your other application code. More importantly, you're assured that critical tasks won't disappear unexpectedly during a routine application restart. ## Picking Up Where Tasks Leave Off Something right here to transition from persistence back to layers. You _could_ rebuild GenServers, Supervisors, Agents, Tasks, or a Registry, but they already exist in Elixir as a springboard for you to build on. As Elixir builds on top of OTP, Oban builds on those primitives (and some phenomenal packages) to formalize how well behaved, observable, reliable, and persistent tasks should operate. In fact, Oban links processes through a Registry, manages queues with a DynamicSupervisor, and executes every job within a supervised Task! Even in an environment with the subjectively _best_ concurrency story of any runtime, you still need additional functionality for mission-critical ~~tasks~~ [jobs][job]. That's where Oban starts. Learn more about how Oban handles persistence, and solves the smattering of functionality outlined earlier [in the thing][start]. [field]: https://www.ericsson.com/4ab333/assets/global/qbank/2019/10/29/101210-0722-29729crop032956163160resize1500844autoorientbackground23ffffffquality90stripextensionjpgid8.jpg [job]: https://hexdocs.pm/oban/Oban.Job.html [oban]: https://github.com/sorentwo/oban [pgs]: https://www.postgresql.org/ [red]: https://redis.io/ [rmq]: https://www.rabbitmq.com/ [start]: https://hexdocs.pm/oban/installation.html [task]: https://hexdocs.pm/elixir/Task.html#start/1 [vird]: http://rvirding.blogspot.com/2008/01/virdings-first-rule-of-programming.html