Full Code of fredwu/opq for AI

master ad47da36977d cached
27 files
23.5 KB
8.0k tokens
76 symbols
1 requests
Download .txt
Repository: fredwu/opq
Branch: master
Commit: ad47da36977d
Files: 27
Total size: 23.5 KB

Directory structure:
gitextract_hr9hujaw/

├── .formatter.exs
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .recode.exs
├── .tool-versions
├── CHANGELOG.md
├── README.md
├── config/
│   └── config.exs
├── lib/
│   ├── opq/
│   │   ├── feeder.ex
│   │   ├── options.ex
│   │   ├── options_handler.ex
│   │   ├── queue/
│   │   │   ├── enumerable.ex
│   │   │   └── inspect.ex
│   │   ├── queue.ex
│   │   ├── rate_limiter.ex
│   │   ├── worker.ex
│   │   └── worker_supervisor.ex
│   └── opq.ex
├── mix.exs
└── test/
    ├── lib/
    │   ├── opq/
    │   │   ├── feeder_test.exs
    │   │   ├── options_test.exs
    │   │   ├── worker_supervisor_test.exs
    │   │   └── worker_test.exs
    │   └── opq_test.exs
    ├── support/
    │   ├── test_case.ex
    │   └── test_helpers.ex
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
  plugins: [Recode.FormatterPlugin]
]


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Elixir
        uses: erlef/setup-beam@v1
        with:
          version-type: strict
          version-file: .tool-versions
      - name: Restore dependencies cache
        id: mix-cache
        uses: actions/cache@v3
        with:
          path: |
            deps
            _build
          key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
          restore-keys: ${{ runner.os }}-mix-
      - name: Install dependencies
        if: steps.mix-cache.outputs.cache-hit != 'true'
        run: |
          mix local.rebar --force
          mix local.hex --force
          mix deps.get
      - name: Run tests
        run: mix test


================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez


================================================
FILE: .recode.exs
================================================
[
  version: "0.6.4",
  # Can also be set/reset with `--autocorrect`/`--no-autocorrect`.
  autocorrect: true,
  # With "--dry" no changes will be written to the files.
  # Can also be set/reset with `--dry`/`--no-dry`.
  # If dry is true then verbose is also active.
  dry: false,
  # Can also be set/reset with `--verbose`/`--no-verbose`.
  verbose: false,
  # Can be overwritten by calling `mix recode "lib/**/*.ex"`.
  inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"],
  formatter: {Recode.Formatter, []},
  tasks: [
    # Tasks could be added by a tuple of the tasks module name and an options
    # keyword list. A task can be deactivated by `active: false`. The execution of
    # a deactivated task can be forced by calling `mix recode --task ModuleName`.
    {Recode.Task.AliasExpansion, []},
    {Recode.Task.AliasOrder, []},
    {Recode.Task.Dbg, [autocorrect: false]},
    {Recode.Task.EnforceLineLength, [active: false]},
    {Recode.Task.FilterCount, []},
    {Recode.Task.IOInspect, [autocorrect: false]},
    {Recode.Task.Nesting, []},
    {Recode.Task.PipeFunOne, []},
    {Recode.Task.SinglePipe, []},
    {Recode.Task.Specs, [active: false, exclude: "test/**/*.{ex,exs}", config: [only: :visible]]},
    {Recode.Task.TagFIXME, [exit_code: 2]},
    {Recode.Task.TagTODO, [exit_code: 4]},
    {Recode.Task.TestFileExt, []},
    {Recode.Task.UnusedVariable, [active: false]}
  ]
]


================================================
FILE: .tool-versions
================================================
erlang 26.1.1
elixir 1.15.6


================================================
FILE: CHANGELOG.md
================================================
# OPQ Changelog

## master

## v4.0.4 [2023-09-29]

- [Improved] Updated all the dependencies
- [Improved] Varies small fixes and improvements

## v4.0.3 [2023-07-02]

- [Added] Graceful handling of exit signals in the default worker

## v4.0.2 [2023-06-13]

- [Improved] Updated all the dependencies

## v4.0.1 [2021-10-14]

- [Fixed] Wrong link to the project

## v4.0.0 [2021-10-14]

- [Added] `OPQ.Queue` wraps `:queue` and implements `Enumerable`

## v3.3.0 [2021-10-12]

- [Added] The ability to start as part of a supervision tree

## v3.2.0 [2021-10-11]

- [Improved] Updated to the new `ConsumerSupervisor` syntax
- [Improved] Updated all the dependencies

## v3.1.1 [2018-10-15]

- [Fixed] Infinite loop without rate limiting (thanks @Harrisonl)

## v3.1.0 [2018-07-30]

- [Improved] Varies small fixes and improvements
- [Improved] Use `cast` instead of `call` to avoid timeouts

## v3.0.1 [2017-09-02]

- [Fixed] Agent should be stopped too when `OPQ.stop/1` is called
- [Improved] Varies small fixes and improvements

## v3.0.0 [2017-08-31]

- [Added] Added support for enqueueing MFAs
- [Improved] Simplified named queue API by storing `opts`
- [Improved] Varies small fixes and improvements

## v2.0.1 [2017-08-30]

- [Improved] Event dispatching should immediately be paused
- [Improved] Varies small fixes and improvements

## v2.0.0 [2017-08-29]

- [Added] Pause / resume / stop the queue
- [Improved] Varies small fixes and improvements

## v1.0.1 [2017-08-14]

- [Improved] Varies small fixes and improvements

## v1.0.0 [2017-08-14]

- [Added] A fast, in-memory FIFO queue
- [Added] Worker pool
- [Added] Rate limit
- [Added] Timeouts
- [Improved] Varies small fixes and improvements


================================================
FILE: README.md
================================================
# OPQ: One Pooled Queue

[![Build Status](https://github.com/fredwu/opq/actions/workflows/ci.yml/badge.svg)](https://github.com/fredwu/opq/actions)
[![CodeBeat](https://codebeat.co/badges/76916047-5b66-466d-91d3-7131a269899a)](https://codebeat.co/projects/github-com-fredwu-opq-master)
[![Coverage](https://img.shields.io/coveralls/fredwu/opq.svg)](https://coveralls.io/github/fredwu/opq?branch=master)
[![Hex Version](https://img.shields.io/hexpm/v/opq.svg)](https://hex.pm/packages/opq)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/opq/)
[![Total Download](https://img.shields.io/hexpm/dt/opq.svg)](https://hex.pm/packages/opq)
[![License](https://img.shields.io/hexpm/l/opq.svg)](https://github.com/fredwu/opq/blob/master/LICENSE.md)
[![Last Updated](https://img.shields.io/github/last-commit/fredwu/opq.svg)](https://github.com/fredwu/opq/commits/master)

## Elixir Queue!

A simple, in-memory queue with worker pooling and rate limiting in Elixir. OPQ leverages Erlang's [queue](http://erlang.org/doc/man/queue.html) module and Elixir's [GenStage](https://github.com/elixir-lang/gen_stage).

Originally built to support [Crawler](https://github.com/fredwu/crawler).

## Features

- A fast, in-memory FIFO queue.
- Worker pool.
- Rate limit.
- Timeouts.
- Pause / resume / stop the queue.

See [Hex documentation](https://hexdocs.pm/opq/).

## Installation

```Elixir
def deps do
  [{:opq, "~> 4.0"}]
end
```

## Usage

### A simple example:

```elixir
{:ok, opq} = OPQ.init()

OPQ.enqueue(opq, fn -> IO.inspect("hello") end)
OPQ.enqueue(opq, fn -> IO.inspect("world") end)
```

### Specify module, function and arguments:

```elixir
{:ok, opq} = OPQ.init()

OPQ.enqueue(opq, IO, :inspect, ["hello"])
OPQ.enqueue(opq, IO, :inspect, ["world"])
```

### Specify a custom name for the queue:

```elixir
OPQ.init(name: :items)

OPQ.enqueue(:items, fn -> IO.inspect("hello") end)
OPQ.enqueue(:items, fn -> IO.inspect("world") end)
```

### Start as part of a supervision tree:

Note, when starting as part of a supervision tree, the `:name` option must be provided.

```elixir
children = [
  {OPQ, name: :items}
]
```

### Specify a custom worker to process items in the queue:

```elixir
defmodule CustomWorker do
  def start_link(item) do
    Task.start_link(fn ->
      Agent.update(:bucket, &[item | &1])
    end)
  end
end

Agent.start_link(fn -> [] end, name: :bucket)

{:ok, opq} = OPQ.init(worker: CustomWorker)

OPQ.enqueue(opq, "hello")
OPQ.enqueue(opq, "world")

Agent.get(:bucket, & &1) # => ["world", "hello"]
```

### Rate limit:

```elixir
{:ok, opq} = OPQ.init(workers: 1, interval: 1000)

Task.async(fn ->
  OPQ.enqueue(opq, fn -> IO.inspect("hello") end)
  OPQ.enqueue(opq, fn -> IO.inspect("world") end)
end)
```

If no interval is supplied, the ratelimiter will be bypassed.

### Check the queue and number of available workers:

```elixir
{:ok, opq} = OPQ.init()

OPQ.enqueue(opq, fn -> Process.sleep(1000) end)

{status, queue, available_workers} = OPQ.info(opq) # => {:normal, #OPQ.Queue<[]>, 9}

Process.sleep(1200)

{status, queue, available_workers} = OPQ.info(opq) # => {:normal, #OPQ.Queue<[]>, 10}
```

If you just need to get the queue itself:

```elixir
OPQ.queue(opq) # => #OPQ.Queue<[]>
```

### Queue

OPQ implements `Enumerable`, so you can perform enumerable functions on the queue:

```elixir
{:ok, opq} = OPQ.init()

queue = OPQ.queue(opq)

Enum.count(queue) # => 0
Enum.empty?(queue) # => true
```

### Stop the queue:

```elixir
{:ok, opq} = OPQ.init()

OPQ.enqueue(opq, fn -> IO.inspect("hello") end)
OPQ.stop(opq)
OPQ.enqueue(opq, fn -> IO.inspect("world") end) # => (EXIT) no process...
```

### Pause and resume the queue:

```elixir
{:ok, opq} = OPQ.init()

OPQ.enqueue(opq, fn -> IO.inspect("hello") end) # => "hello"
OPQ.pause(opq)
OPQ.info(opq) # => {:paused, {[], []}, 10}
OPQ.enqueue(opq, fn -> IO.inspect("world") end)
OPQ.resume(opq) # => "world"
OPQ.info(opq) # => {:normal, {[], []}, 10}
```

## Configurations

| Option       | Type        | Default Value  | Description |
|--------------|-------------|----------------|-------------|
| `:name`      | atom/module | pid            | The name of the queue.
| `:worker`    | module      | `OPQ.Worker`   | The worker that processes each item from the queue.
| `:workers`   | integer     | `10`           | Maximum number of workers.
| `:interval`  | integer     | `0`            | Rate limit control - number of milliseconds before asking for more items to process, defaults to `0` which is effectively no rate limit.
| `:timeout`   | integer     | `5000`         | Number of milliseconds allowed to perform the work, it should always be set to higher than `:interval`.

## Changelog

Please see [CHANGELOG.md](CHANGELOG.md).

## License

Licensed under [MIT](http://fredwu.mit-license.org/).


================================================
FILE: config/config.exs
================================================
import Config


================================================
FILE: lib/opq/feeder.ex
================================================
defmodule OPQ.Feeder do
  @moduledoc """
  A GenStage producer that feeds items in a buffered queue to the consumers.
  """

  use GenStage

  def start_link(nil), do: GenStage.start_link(__MODULE__, :ok)
  def start_link(name), do: GenStage.start_link(__MODULE__, :ok, name: name)

  def init(:ok) do
    {:producer, {:normal, %OPQ.Queue{}, 0}}
  end

  def handle_cast(:stop, state) do
    {:stop, :shutdown, state}
  end

  def handle_cast(:pause, {_status, queue, demand}) do
    dispatch_or_pause(:paused, queue, demand)
  end

  def handle_cast(:resume, {_status, queue, demand}) do
    dispatch_events(:normal, queue, demand, [])
  end

  def handle_cast({:enqueue, event}, {status, %OPQ.Queue{data: data}, pending_demand}) do
    data = :queue.in(event, data)

    dispatch_or_pause(status, %OPQ.Queue{data: data}, pending_demand)
  end

  def handle_call(:info, _from, state) do
    {:reply, state, [], state}
  end

  def handle_call(:queue, _from, {_status, queue, _demand} = state) do
    {:reply, queue, [], state}
  end

  defp dispatch_or_pause(:normal, queue, demand) do
    dispatch_events(:normal, queue, demand, [])
  end

  defp dispatch_or_pause(:paused, queue, demand) do
    {:noreply, [], {:paused, queue, demand}}
  end

  def handle_demand(demand, {status, queue, pending_demand}) do
    dispatch_events(status, queue, demand + pending_demand, [])
  end

  defp dispatch_events(:paused, queue, demand, events) do
    {:noreply, Enum.reverse(events), {:paused, queue, demand}}
  end

  defp dispatch_events(status, queue, 0, events) do
    {:noreply, Enum.reverse(events), {status, queue, 0}}
  end

  defp dispatch_events(status, %OPQ.Queue{data: data}, demand, events) do
    case :queue.out(data) do
      {{:value, event}, data} ->
        dispatch_events(status, %OPQ.Queue{data: data}, demand - 1, [event | events])

      {:empty, data} ->
        {:noreply, Enum.reverse(events), {status, %OPQ.Queue{data: data}, demand}}
    end
  end
end


================================================
FILE: lib/opq/options.ex
================================================
defmodule OPQ.Options do
  @moduledoc """
  Options for configuring OPQ.
  """

  @worker OPQ.Worker
  @workers 10
  @interval 0
  @timeout 5_000

  @doc """
  ## Examples

      iex> Options.assign_defaults([]) |> Keyword.get(:workers)
      10

      iex> Options.assign_defaults([workers: 4]) |> Keyword.get(:workers)
      4
  """
  def assign_defaults(opts) do
    Keyword.merge(
      [
        worker: worker(),
        workers: workers(),
        interval: interval(),
        timeout: timeout()
      ],
      opts
    )
  end

  defp worker(), do: Application.get_env(:opq, :worker, @worker)
  defp workers(), do: Application.get_env(:opq, :workers, @workers)
  defp interval(), do: Application.get_env(:opq, :interval, @interval)
  defp timeout(), do: Application.get_env(:opq, :timeout, @timeout)
end


================================================
FILE: lib/opq/options_handler.ex
================================================
defmodule OPQ.OptionsHandler do
  @moduledoc """
  Saves and loads options to pass around.
  """

  def save_opts(feeder, opts) do
    Agent.start_link(fn -> opts end, name: name(feeder))
  end

  def timeout(feeder), do: load_opts(feeder)[:timeout]

  def stop(feeder), do: Agent.stop(name(feeder))

  defp load_opts(feeder), do: Agent.get(name(feeder), & &1)
  defp name(feeder), do: :"opq-#{Kernel.inspect(feeder)}"
end


================================================
FILE: lib/opq/queue/enumerable.ex
================================================
defimpl Enumerable, for: OPQ.Queue do
  @moduledoc """
  Implementation based on https://github.com/princemaple/elixir-queue
  """

  def count(%OPQ.Queue{data: q}), do: {:ok, :queue.len(q)}

  def member?(%OPQ.Queue{data: q}, item) do
    {:ok, :queue.member(item, q)}
  end

  def reduce(%OPQ.Queue{data: q}, acc, fun) do
    Enumerable.List.reduce(:queue.to_list(q), acc, fun)
  end

  def slice(%OPQ.Queue{}), do: {:error, __MODULE__}
end


================================================
FILE: lib/opq/queue/inspect.ex
================================================
defimpl Inspect, for: OPQ.Queue do
  @moduledoc """
  Implementation based on https://github.com/princemaple/elixir-queue
  """

  import Inspect.Algebra

  def inspect(%OPQ.Queue{} = q, opts) do
    concat(["#OPQ.Queue<", to_doc(Enum.to_list(q), opts), ">"])
  end
end


================================================
FILE: lib/opq/queue.ex
================================================
defmodule OPQ.Queue do
  @moduledoc """
  A `:queue` wrapper so that protocols like `Enumerable` can be implemented.
  """

  @opaque t() :: %__MODULE__{data: :queue.queue()}

  defstruct data: :queue.new()
end


================================================
FILE: lib/opq/rate_limiter.ex
================================================
defmodule OPQ.RateLimiter do
  @moduledoc """
  Provides rate limit.
  """

  use GenStage

  def start_link(opts) do
    GenStage.start_link(__MODULE__, opts)
  end

  def init(opts) do
    {:producer_consumer, {}, subscribe_to: [{opts[:name], opts}]}
  end

  def handle_subscribe(:producer, opts, from, _state) do
    {:manual, ask_and_schedule(from, {opts[:workers], opts[:interval]})}
  end

  def handle_subscribe(:consumer, _opts, _from, state) do
    {:automatic, state}
  end

  def handle_events(events, _from, {pending, interval}) do
    {:noreply, events, {pending + length(events), interval}}
  end

  def handle_info({:ask, from}, state) do
    {:noreply, [], ask_and_schedule(from, state)}
  end

  defp ask_and_schedule(from, {pending, interval}) do
    GenStage.ask(from, pending)

    Process.send_after(self(), {:ask, from}, interval)

    {0, interval}
  end
end


================================================
FILE: lib/opq/worker.ex
================================================
defmodule OPQ.Worker do
  @moduledoc """
  A default worker that simply executes an item if it's a function.
  """

  def start_link(item) do
    Task.start_link(fn ->
      Process.flag(:trap_exit, true)
      process_item(item)
    end)
  end

  defp process_item({mod, fun, args}), do: apply(mod, fun, args)
  defp process_item(item) when is_function(item), do: item.()
  defp process_item(item), do: item
end


================================================
FILE: lib/opq/worker_supervisor.ex
================================================
defmodule OPQ.WorkerSupervisor do
  @moduledoc """
  A supervisor that subscribes to `Feeder` and spins up the worker pool.
  """

  use ConsumerSupervisor

  def start_link(opts) do
    ConsumerSupervisor.start_link(__MODULE__, opts)
  end

  def init(opts) do
    children = [
      %{id: opts[:worker], start: {opts[:worker], :start_link, []}, restart: :temporary}
    ]

    cs_opts = [
      strategy: :one_for_one,
      subscribe_to: [
        {
          opts[:producer_consumer],
          min_demand: 0, max_demand: opts[:workers], timeout: opts[:timeout]
        }
      ]
    ]

    ConsumerSupervisor.init(children, cs_opts)
  end
end


================================================
FILE: lib/opq.ex
================================================
defmodule OPQ do
  @moduledoc """
  A simple, in-memory queue with worker pooling and rate limiting in Elixir.
  """

  use GenServer

  alias OPQ.Feeder
  alias OPQ.Options
  alias OPQ.OptionsHandler, as: Opt
  alias OPQ.RateLimiter
  alias OPQ.WorkerSupervisor

  def start_link(opts \\ []), do: init(opts)

  def init(opts \\ []) do
    opts
    |> Options.assign_defaults()
    |> start_links()
  end

  def enqueue(feeder, event) do
    GenStage.cast(feeder, {:enqueue, event})
  end

  def enqueue(feeder, mod, fun, args)
      when is_atom(mod) and
             is_atom(fun) and
             is_list(args) do
    enqueue(feeder, {mod, fun, args})
  end

  def stop(feeder) do
    Process.flag(:trap_exit, true)
    GenStage.cast(feeder, :stop)
    Opt.stop(feeder)
  end

  def pause(feeder), do: GenStage.cast(feeder, :pause)
  def resume(feeder), do: GenStage.cast(feeder, :resume)
  def info(feeder), do: GenStage.call(feeder, :info, Opt.timeout(feeder))
  def queue(feeder), do: GenStage.call(feeder, :queue, Opt.timeout(feeder))

  defp start_links(opts) do
    {:ok, feeder} = Feeder.start_link(opts[:name])

    Opt.save_opts(opts[:name] || feeder, opts)

    opts
    |> Keyword.merge(name: feeder)
    |> start_consumers(interval: opts[:interval])

    {:ok, feeder}
  end

  defp start_consumers(opts, interval: 0) do
    opts
    |> Keyword.merge(producer_consumer: opts[:name])
    |> WorkerSupervisor.start_link()
  end

  defp start_consumers(opts, _) do
    {:ok, rate_limiter} = RateLimiter.start_link(opts)

    opts
    |> Keyword.merge(producer_consumer: rate_limiter)
    |> WorkerSupervisor.start_link()
  end
end


================================================
FILE: mix.exs
================================================
defmodule OPQ.Mixfile do
  use Mix.Project

  @source_url "https://github.com/fredwu/opq"
  @version "4.0.4"

  def project do
    [
      app: :opq,
      version: @version,
      elixir: "~> 1.13",
      elixirc_paths: elixirc_paths(Mix.env()),
      package: package(),
      name: "OPQ: One Pooled Queue",
      description: "A simple, in-memory queue with worker pooling and rate limiting in Elixir.",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      docs: docs(),
      test_coverage: [tool: ExCoveralls],
      preferred_cli_env: [coveralls: :test],
      aliases: [publish: ["hex.publish", &git_tag/1]]
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    [
      {:gen_stage, "~> 1.1"},
      {:recode, "~> 0.6", only: :dev},
      {:ex_doc, ">= 0.0.0", only: :dev},
      {:excoveralls, "~> 0.14", only: :test}
    ]
  end

  defp package do
    [
      maintainers: ["Fred Wu"],
      licenses: ["MIT"],
      links: %{"GitHub" => @source_url}
    ]
  end

  defp git_tag(_args) do
    System.cmd("git", ["tag", "v" <> Mix.Project.config()[:version]])
    System.cmd("git", ["push"])
    System.cmd("git", ["push", "--tags"])
  end

  defp docs do
    [
      extras: ["CHANGELOG.md": [title: "Changelog"], "README.md": [title: "Overview"]],
      main: "readme",
      source_url: @source_url,
      source_ref: "v#{@version}",
      formatters: ["html"]
    ]
  end
end


================================================
FILE: test/lib/opq/feeder_test.exs
================================================
defmodule OPQ.FeederTest do
  use OPQ.TestCase, async: true

  alias OPQ.Feeder

  doctest Feeder
end


================================================
FILE: test/lib/opq/options_test.exs
================================================
defmodule OPQ.OptionsTest do
  use OPQ.TestCase, async: true

  alias OPQ.Options

  doctest Options
end


================================================
FILE: test/lib/opq/worker_supervisor_test.exs
================================================
defmodule OPQ.WorkerSupervisorTest do
  use OPQ.TestCase, async: true

  alias OPQ.WorkerSupervisor

  doctest WorkerSupervisor
end


================================================
FILE: test/lib/opq/worker_test.exs
================================================
defmodule OPQ.WorkerTest do
  use OPQ.TestCase, async: true

  alias OPQ.Worker

  doctest Worker
end


================================================
FILE: test/lib/opq_test.exs
================================================
defmodule OPQTest do
  use OPQ.TestCase, async: true

  doctest OPQ

  @moduletag capture_log: true

  test "enqueue items - child_spec/1" do
    Supervisor.start_link([{OPQ, name: :opq}], strategy: :one_for_one)

    OPQ.enqueue(:opq, :a)
    OPQ.enqueue(:opq, :b)

    wait(fn ->
      assert_empty_queue(:opq)
    end)
  end

  test "enqueue items - start_link/1" do
    {:ok, opq} = OPQ.start_link()

    OPQ.enqueue(opq, :a)
    OPQ.enqueue(opq, :b)

    wait(fn ->
      assert_empty_queue(opq)
    end)
  end

  test "enqueue items - init/1" do
    {:ok, opq} = OPQ.init()

    OPQ.enqueue(opq, :a)
    OPQ.enqueue(opq, :b)

    wait(fn ->
      assert_empty_queue(opq)
    end)
  end

  test "enqueue functions" do
    Agent.start_link(fn -> [] end, name: Bucket)

    {:ok, opq} = OPQ.init()

    OPQ.enqueue(opq, fn -> Agent.update(Bucket, &[:a | &1]) end)
    OPQ.enqueue(opq, fn -> Agent.update(Bucket, &[:b | &1]) end)

    wait(fn ->
      assert_empty_queue(opq)

      assert Kernel.length(Agent.get(Bucket, & &1)) == 2
    end)
  end

  test "enqueue MFAs" do
    Agent.start_link(fn -> [] end, name: MfaBucket)

    {:ok, opq} = OPQ.init()

    OPQ.enqueue(opq, Agent, :update, [MfaBucket, &[:a | &1]])
    OPQ.enqueue(opq, Agent, :update, [MfaBucket, &[:b | &1]])

    wait(fn ->
      assert_empty_queue(opq)

      assert Kernel.length(Agent.get(MfaBucket, & &1)) == 2
    end)
  end

  test "enqueue to a named queue" do
    OPQ.init(name: :items)

    OPQ.enqueue(:items, :a)
    OPQ.enqueue(:items, :b)

    wait(fn ->
      assert_empty_queue(:items)
    end)
  end

  test "run out of demands from the workers" do
    {:ok, opq} = OPQ.init(workers: 2)

    OPQ.enqueue(opq, :a)
    OPQ.enqueue(opq, :b)

    wait(fn ->
      assert_empty_queue(opq)
    end)
  end

  test "single worker" do
    {:ok, opq} = OPQ.init(workers: 1)

    OPQ.enqueue(opq, :a)
    OPQ.enqueue(opq, :b)

    wait(fn ->
      assert_empty_queue(opq)
    end)
  end

  test "custom worker" do
    defmodule CustomWorker do
      def start_link(item) do
        Task.start_link(fn ->
          Agent.update(CustomWorkerBucket, &[item | &1])
        end)
      end
    end

    Agent.start_link(fn -> [] end, name: CustomWorkerBucket)

    {:ok, opq} = OPQ.init(worker: CustomWorker)

    OPQ.enqueue(opq, :a)
    OPQ.enqueue(opq, :b)

    wait(fn ->
      assert Kernel.length(Agent.get(CustomWorkerBucket, & &1)) == 2
    end)
  end

  test "rate limit" do
    Agent.start_link(fn -> [] end, name: RateLimitBucket)

    {:ok, opq} = OPQ.init(workers: 1, interval: 10)

    Task.async(fn ->
      OPQ.enqueue(opq, fn -> Agent.update(RateLimitBucket, &[:a | &1]) end)
      OPQ.enqueue(opq, fn -> Agent.update(RateLimitBucket, &[:b | &1]) end)
    end)

    Process.sleep(5)

    assert Kernel.length(Agent.get(RateLimitBucket, & &1)) == 1

    wait(fn ->
      assert Kernel.length(Agent.get(RateLimitBucket, & &1)) == 2
    end)
  end

  test "stop" do
    {:ok, opq} = OPQ.init(workers: 1)

    OPQ.enqueue(opq, :a)

    OPQ.stop(opq)

    refute Process.alive?(opq)

    agent = :"opq-#{Kernel.inspect(opq)}"

    assert catch_exit(Agent.get(agent, & &1))
  end

  test "pause & resume" do
    Agent.start_link(fn -> [] end, name: PauseBucket)

    {:ok, opq} = OPQ.init(workers: 1)

    OPQ.enqueue(opq, fn -> Agent.update(PauseBucket, &[:a | &1]) end)

    OPQ.pause(opq)

    OPQ.enqueue(opq, fn -> Agent.update(PauseBucket, &[:b | &1]) end)
    OPQ.enqueue(opq, fn -> Agent.update(PauseBucket, &[:c | &1]) end)

    wait(fn ->
      {status, _queue, _demand} = OPQ.info(opq)

      assert status == :paused
      assert Kernel.length(Agent.get(PauseBucket, & &1)) == 1
    end)

    OPQ.resume(opq)

    wait(fn ->
      {status, _queue, _demand} = OPQ.info(opq)

      assert status == :normal
      assert Kernel.length(Agent.get(PauseBucket, & &1)) == 3
    end)
  end

  test "graceful handle of exit signals" do
    Process.flag(:trap_exit, true)

    Agent.start_link(fn -> [] end, name: ExitBucket)

    {:ok, opq} = OPQ.init(workers: 1)

    OPQ.enqueue(opq, fn ->
      Process.sleep(10)
      Agent.update(ExitBucket, &[:a | &1])
    end)

    Process.sleep(1)

    Process.exit(opq, :SIGTERM)

    refute Process.alive?(opq)

    wait(fn ->
      assert Kernel.length(Agent.get(ExitBucket, & &1)) == 1
    end)
  end

  defp assert_empty_queue(queue_name) do
    assert queue_name
           |> OPQ.queue()
           |> Enum.empty?()
  end
end


================================================
FILE: test/support/test_case.ex
================================================
defmodule OPQ.TestCase do
  use ExUnit.CaseTemplate

  using do
    quote do
      import OPQ.TestHelpers
    end
  end
end


================================================
FILE: test/support/test_helpers.ex
================================================
# Credit: https://gist.github.com/cblavier/5e15791387a6e22b98d8
defmodule OPQ.TestHelpers do
  def wait(fun), do: wait(500, fun)
  def wait(0, fun), do: fun.()

  def wait(timeout, fun) do
    try do
      fun.()
    rescue
      _ ->
        :timer.sleep(10)
        wait(max(0, timeout - 10), fun)
    end
  end
end


================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
Download .txt
gitextract_hr9hujaw/

├── .formatter.exs
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .recode.exs
├── .tool-versions
├── CHANGELOG.md
├── README.md
├── config/
│   └── config.exs
├── lib/
│   ├── opq/
│   │   ├── feeder.ex
│   │   ├── options.ex
│   │   ├── options_handler.ex
│   │   ├── queue/
│   │   │   ├── enumerable.ex
│   │   │   └── inspect.ex
│   │   ├── queue.ex
│   │   ├── rate_limiter.ex
│   │   ├── worker.ex
│   │   └── worker_supervisor.ex
│   └── opq.ex
├── mix.exs
└── test/
    ├── lib/
    │   ├── opq/
    │   │   ├── feeder_test.exs
    │   │   ├── options_test.exs
    │   │   ├── worker_supervisor_test.exs
    │   │   └── worker_test.exs
    │   └── opq_test.exs
    ├── support/
    │   ├── test_case.ex
    │   └── test_helpers.ex
    └── test_helper.exs
Download .txt
SYMBOL INDEX (76 symbols across 16 files)

FILE: lib/opq.ex
  class OPQ (line 1) | defmodule OPQ
    method start_link (line 14) | def start_link(opts \\ []), do: init(opts)
    method init (line 16) | def init(opts \\ []) do
    method enqueue (line 22) | def enqueue(feeder, event) do
    method stop (line 33) | def stop(feeder) do
    method pause (line 39) | def pause(feeder), do: GenStage.cast(feeder, :pause)
    method resume (line 40) | def resume(feeder), do: GenStage.cast(feeder, :resume)
    method info (line 41) | def info(feeder), do: GenStage.call(feeder, :info, Opt.timeout(feeder))
    method queue (line 42) | def queue(feeder), do: GenStage.call(feeder, :queue, Opt.timeout(feeder))
    method start_links (line 44) | defp start_links(opts) do
    method start_consumers (line 56) | defp start_consumers(opts, interval: 0) do
    method start_consumers (line 62) | defp start_consumers(opts, _) do

FILE: lib/opq/feeder.ex
  class OPQ.Feeder (line 1) | defmodule OPQ.Feeder
    method start_link (line 8) | def start_link(nil), do: GenStage.start_link(__MODULE__, :ok)
    method start_link (line 9) | def start_link(name), do: GenStage.start_link(__MODULE__, :ok, name: n...
    method init (line 11) | def init(:ok) do
    method handle_cast (line 15) | def handle_cast(:stop, state) do
    method handle_cast (line 19) | def handle_cast(:pause, {_status, queue, demand}) do
    method handle_cast (line 23) | def handle_cast(:resume, {_status, queue, demand}) do
    method handle_cast (line 27) | def handle_cast({:enqueue, event}, {status, %OPQ.Queue{data: data}, pe...
    method handle_call (line 33) | def handle_call(:info, _from, state) do
    method handle_call (line 37) | def handle_call(:queue, _from, {_status, queue, _demand} = state) do
    method dispatch_or_pause (line 41) | defp dispatch_or_pause(:normal, queue, demand) do
    method dispatch_or_pause (line 45) | defp dispatch_or_pause(:paused, queue, demand) do
    method handle_demand (line 49) | def handle_demand(demand, {status, queue, pending_demand}) do
    method dispatch_events (line 53) | defp dispatch_events(:paused, queue, demand, events) do
    method dispatch_events (line 57) | defp dispatch_events(status, queue, 0, events) do
    method dispatch_events (line 61) | defp dispatch_events(status, %OPQ.Queue{data: data}, demand, events) do

FILE: lib/opq/options.ex
  class OPQ.Options (line 1) | defmodule OPQ.Options
    method assign_defaults (line 20) | def assign_defaults(opts) do
    method worker (line 32) | defp worker(), do: Application.get_env(:opq, :worker, @worker)
    method workers (line 33) | defp workers(), do: Application.get_env(:opq, :workers, @workers)
    method interval (line 34) | defp interval(), do: Application.get_env(:opq, :interval, @interval)
    method timeout (line 35) | defp timeout(), do: Application.get_env(:opq, :timeout, @timeout)

FILE: lib/opq/options_handler.ex
  class OPQ.OptionsHandler (line 1) | defmodule OPQ.OptionsHandler
    method save_opts (line 6) | def save_opts(feeder, opts) do
    method timeout (line 10) | def timeout(feeder), do: load_opts(feeder)[:timeout]
    method stop (line 12) | def stop(feeder), do: Agent.stop(name(feeder))
    method load_opts (line 14) | defp load_opts(feeder), do: Agent.get(name(feeder), & &1)
    method name (line 15) | defp name(feeder), do: :"opq-#{Kernel.inspect(feeder)}"

FILE: lib/opq/queue.ex
  class OPQ.Queue (line 1) | defmodule OPQ.Queue

FILE: lib/opq/rate_limiter.ex
  class OPQ.RateLimiter (line 1) | defmodule OPQ.RateLimiter
    method start_link (line 8) | def start_link(opts) do
    method init (line 12) | def init(opts) do
    method handle_subscribe (line 16) | def handle_subscribe(:producer, opts, from, _state) do
    method handle_subscribe (line 20) | def handle_subscribe(:consumer, _opts, _from, state) do
    method handle_events (line 24) | def handle_events(events, _from, {pending, interval}) do
    method handle_info (line 28) | def handle_info({:ask, from}, state) do
    method ask_and_schedule (line 32) | defp ask_and_schedule(from, {pending, interval}) do

FILE: lib/opq/worker.ex
  class OPQ.Worker (line 1) | defmodule OPQ.Worker
    method start_link (line 6) | def start_link(item) do
    method process_item (line 13) | defp process_item({mod, fun, args}), do: apply(mod, fun, args)
    method process_item (line 15) | defp process_item(item), do: item

FILE: lib/opq/worker_supervisor.ex
  class OPQ.WorkerSupervisor (line 1) | defmodule OPQ.WorkerSupervisor
    method start_link (line 8) | def start_link(opts) do
    method init (line 12) | def init(opts) do

FILE: mix.exs
  class OPQ.Mixfile (line 1) | defmodule OPQ.Mixfile
    method project (line 7) | def project do
    method application (line 25) | def application do
    method elixirc_paths (line 31) | defp elixirc_paths(:test), do: ["lib", "test/support"]
    method elixirc_paths (line 32) | defp elixirc_paths(_), do: ["lib"]
    method deps (line 34) | defp deps do
    method package (line 43) | defp package do
    method git_tag (line 51) | defp git_tag(_args) do
    method docs (line 57) | defp docs do

FILE: test/lib/opq/feeder_test.exs
  class OPQ.FeederTest (line 1) | defmodule OPQ.FeederTest

FILE: test/lib/opq/options_test.exs
  class OPQ.OptionsTest (line 1) | defmodule OPQ.OptionsTest

FILE: test/lib/opq/worker_supervisor_test.exs
  class OPQ.WorkerSupervisorTest (line 1) | defmodule OPQ.WorkerSupervisorTest

FILE: test/lib/opq/worker_test.exs
  class OPQ.WorkerTest (line 1) | defmodule OPQ.WorkerTest

FILE: test/lib/opq_test.exs
  class OPQTest (line 1) | defmodule OPQTest
    method assert_empty_queue (line 210) | defp assert_empty_queue(queue_name) do

FILE: test/support/test_case.ex
  class OPQ.TestCase (line 1) | defmodule OPQ.TestCase

FILE: test/support/test_helpers.ex
  class OPQ.TestHelpers (line 2) | defmodule OPQ.TestHelpers
    method wait (line 3) | def wait(fun), do: wait(500, fun)
    method wait (line 4) | def wait(0, fun), do: fun.()
    method wait (line 6) | def wait(timeout, fun) do
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (27K chars).
[
  {
    "path": ".formatter.exs",
    "chars": 134,
    "preview": "# Used by \"mix format\"\n[\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"],\n  plugins: [Recode.Forma"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 768,
    "preview": "name: CI\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: S"
  },
  {
    "path": ".gitignore",
    "chars": 507,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": ".recode.exs",
    "chars": 1420,
    "preview": "[\n  version: \"0.6.4\",\n  # Can also be set/reset with `--autocorrect`/`--no-autocorrect`.\n  autocorrect: true,\n  # With \""
  },
  {
    "path": ".tool-versions",
    "chars": 28,
    "preview": "erlang 26.1.1\nelixir 1.15.6\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1705,
    "preview": "# OPQ Changelog\n\n## master\n\n## v4.0.4 [2023-09-29]\n\n- [Improved] Updated all the dependencies\n- [Improved] Varies small "
  },
  {
    "path": "README.md",
    "chars": 4837,
    "preview": "# OPQ: One Pooled Queue\n\n[![Build Status](https://github.com/fredwu/opq/actions/workflows/ci.yml/badge.svg)](https://git"
  },
  {
    "path": "config/config.exs",
    "chars": 14,
    "preview": "import Config\n"
  },
  {
    "path": "lib/opq/feeder.ex",
    "chars": 1973,
    "preview": "defmodule OPQ.Feeder do\n  @moduledoc \"\"\"\n  A GenStage producer that feeds items in a buffered queue to the consumers.\n  "
  },
  {
    "path": "lib/opq/options.ex",
    "chars": 813,
    "preview": "defmodule OPQ.Options do\n  @moduledoc \"\"\"\n  Options for configuring OPQ.\n  \"\"\"\n\n  @worker OPQ.Worker\n  @workers 10\n  @in"
  },
  {
    "path": "lib/opq/options_handler.ex",
    "chars": 423,
    "preview": "defmodule OPQ.OptionsHandler do\n  @moduledoc \"\"\"\n  Saves and loads options to pass around.\n  \"\"\"\n\n  def save_opts(feeder"
  },
  {
    "path": "lib/opq/queue/enumerable.ex",
    "chars": 443,
    "preview": "defimpl Enumerable, for: OPQ.Queue do\n  @moduledoc \"\"\"\n  Implementation based on https://github.com/princemaple/elixir-q"
  },
  {
    "path": "lib/opq/queue/inspect.ex",
    "chars": 270,
    "preview": "defimpl Inspect, for: OPQ.Queue do\n  @moduledoc \"\"\"\n  Implementation based on https://github.com/princemaple/elixir-queu"
  },
  {
    "path": "lib/opq/queue.ex",
    "chars": 211,
    "preview": "defmodule OPQ.Queue do\n  @moduledoc \"\"\"\n  A `:queue` wrapper so that protocols like `Enumerable` can be implemented.\n  \""
  },
  {
    "path": "lib/opq/rate_limiter.ex",
    "chars": 883,
    "preview": "defmodule OPQ.RateLimiter do\n  @moduledoc \"\"\"\n  Provides rate limit.\n  \"\"\"\n\n  use GenStage\n\n  def start_link(opts) do\n  "
  },
  {
    "path": "lib/opq/worker.ex",
    "chars": 413,
    "preview": "defmodule OPQ.Worker do\n  @moduledoc \"\"\"\n  A default worker that simply executes an item if it's a function.\n  \"\"\"\n\n  de"
  },
  {
    "path": "lib/opq/worker_supervisor.ex",
    "chars": 648,
    "preview": "defmodule OPQ.WorkerSupervisor do\n  @moduledoc \"\"\"\n  A supervisor that subscribes to `Feeder` and spins up the worker po"
  },
  {
    "path": "lib/opq.ex",
    "chars": 1642,
    "preview": "defmodule OPQ do\n  @moduledoc \"\"\"\n  A simple, in-memory queue with worker pooling and rate limiting in Elixir.\n  \"\"\"\n\n  "
  },
  {
    "path": "mix.exs",
    "chars": 1553,
    "preview": "defmodule OPQ.Mixfile do\n  use Mix.Project\n\n  @source_url \"https://github.com/fredwu/opq\"\n  @version \"4.0.4\"\n\n  def proj"
  },
  {
    "path": "test/lib/opq/feeder_test.exs",
    "chars": 102,
    "preview": "defmodule OPQ.FeederTest do\n  use OPQ.TestCase, async: true\n\n  alias OPQ.Feeder\n\n  doctest Feeder\nend\n"
  },
  {
    "path": "test/lib/opq/options_test.exs",
    "chars": 105,
    "preview": "defmodule OPQ.OptionsTest do\n  use OPQ.TestCase, async: true\n\n  alias OPQ.Options\n\n  doctest Options\nend\n"
  },
  {
    "path": "test/lib/opq/worker_supervisor_test.exs",
    "chars": 132,
    "preview": "defmodule OPQ.WorkerSupervisorTest do\n  use OPQ.TestCase, async: true\n\n  alias OPQ.WorkerSupervisor\n\n  doctest WorkerSup"
  },
  {
    "path": "test/lib/opq/worker_test.exs",
    "chars": 102,
    "preview": "defmodule OPQ.WorkerTest do\n  use OPQ.TestCase, async: true\n\n  alias OPQ.Worker\n\n  doctest Worker\nend\n"
  },
  {
    "path": "test/lib/opq_test.exs",
    "chars": 4463,
    "preview": "defmodule OPQTest do\n  use OPQ.TestCase, async: true\n\n  doctest OPQ\n\n  @moduletag capture_log: true\n\n  test \"enqueue ite"
  },
  {
    "path": "test/support/test_case.ex",
    "chars": 124,
    "preview": "defmodule OPQ.TestCase do\n  use ExUnit.CaseTemplate\n\n  using do\n    quote do\n      import OPQ.TestHelpers\n    end\n  end\n"
  },
  {
    "path": "test/support/test_helpers.ex",
    "chars": 318,
    "preview": "# Credit: https://gist.github.com/cblavier/5e15791387a6e22b98d8\ndefmodule OPQ.TestHelpers do\n  def wait(fun), do: wait(5"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  }
]

About this extraction

This page contains the full source code of the fredwu/opq GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (23.5 KB), approximately 8.0k tokens, and a symbol index with 76 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!