README.md

# Tasque

<p>
  <img src="branding/logo.png" alt="Tasque Logo" width="180" align="left" />
</p>

Tasque is an asynchronous, bounded-concurrency task queue for Elixir.
It lets you enqueue anonymous functions or MFA tuples, run them under a
supervised `Task.Supervisor`, and receive results back via standard OTP
messages.

It is a good fit when you need bounded parallelism, per-task timeouts,
and OTP-friendly result delivery without introducing a separate job
system.

<br clear="left" />

Good use cases for this library include:

- Database queries
- Communications with external APIs or services

## Why Tasque?

- Bounded concurrency with a simple FIFO queue
- Anonymous function and MFA task support
- Result delivery through regular OTP messages
- Per-task timeouts that cover both queue wait time and execution time
- Support for atom, `{:global, term}`, and `{:via, module, term}` queue names

## Quick Start

Add a queue to your supervision tree:

```elixir
children = [
  {Tasque, name: MyApp.Queue, max_concurrency: 10}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

Then enqueue work and await the result:

```elixir
{:ok, ref} = Tasque.queue_task(MyApp.Queue, fn -> expensive_work() end)
{:ok, result} = Tasque.await(ref)
```

You can also consume results directly from the caller mailbox:

```elixir
{:ok, ref} = Tasque.queue_task(MyApp.Queue, fn -> String.upcase("hello") end)

receive do
  {:tasque_result, ^ref, {:ok, result}} -> result
  {:tasque_result, ^ref, {:exit, reason}} -> {:error, reason}
end
```

## Task Formats

Tasks can be provided as either:

- A zero-arity function: `fn -> :work end`
- An MFA tuple: `{String, :upcase, ["hello"]}`

Invalid task formats return `{:error, :invalid_task}`.

## Result Format

Every task result is delivered to the calling process as:

```elixir
{:tasque_result, ref, outcome}
```

Where `outcome` is one of:

- `{:ok, result}` when the task completes successfully
- `{:exit, reason}` when the task crashes, exits, or times out

## Timeouts

Per-task timeouts start when a task is enqueued, so they include both queue
wait time and execution time. If a timeout fires before dispatch, the task is
dropped from the queue and the caller receives `{:exit, :timeout}`. If it
fires while the task is running, the task process is terminated and the caller
receives the same timeout result.

This is separate from `Tasque.await/2`, whose timeout only controls how long
the caller waits for a result. An `await/2` timeout does not cancel the task.

## Naming

The queue `:name` supports the standard OTP naming forms:

- An atom such as `MyApp.Queue`
- A global name such as `{:global, :my_queue}`
- A via tuple such as `{:via, Registry, {MyApp.Registry, "queue"}}`

Tasque derives matching companion names for its supervisor processes using the
same naming strategy.

## Examples

Different workloads often benefit from different concurrency limits. For
example, CPU-bound work is usually best capped near the number of schedulers,
while I/O-heavy work can often tolerate a higher limit:

```elixir
children = [
  {Tasque, name: MyApp.CpuQueue, max_concurrency: System.schedulers_online()},
  {Tasque, name: MyApp.IoQueue, max_concurrency: 50}
]
```

You can then route work to the appropriate queue:

```elixir
{:ok, image_ref} =
  Tasque.queue_task(MyApp.CpuQueue, fn -> render_thumbnail(image) end)

{:ok, api_ref} =
  Tasque.queue_task(MyApp.IoQueue, fn -> fetch_remote_profile(user_id) end, timeout: 5_000)

{:ok, thumbnail} = Tasque.await(image_ref)
{:ok, profile} = Tasque.await(api_ref)
```

MFA tasks work the same way:

```elixir
{:ok, ref} = Tasque.queue_task(MyApp.IoQueue, {String, :upcase, ["hello"]})
{:ok, "HELLO"} = Tasque.await(ref)
```

## Caveats

- Task results are delivered to the process that called `Tasque.queue_task/3`.
  If that caller exits before the task finishes, the result message is sent to
  a dead PID and is effectively lost.
- `Tasque.await/2` only controls how long the caller waits for a result. If it
  returns `{:error, :timeout}`, the task may still be queued or running, and
  its eventual `{:tasque_result, ref, outcome}` message will still arrive in
  the caller's mailbox.
- Per-task `:timeout` is different from `await/2` timeout. The queue-enforced
  timeout starts at enqueue time, includes queue wait time, and may expire
  before the task is ever dispatched.

## Installation

The package can be installed by adding `tasque` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:tasque, "~> 1.0.0"}
  ]
end
```

Documentation can be found at <https://hexdocs.pm/tasque>.