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
[](https://github.com/fredwu/opq/actions)
[](https://codebeat.co/projects/github-com-fredwu-opq-master)
[](https://coveralls.io/github/fredwu/opq?branch=master)
[](https://hex.pm/packages/opq)
[](https://hexdocs.pm/opq/)
[](https://hex.pm/packages/opq)
[](https://github.com/fredwu/opq/blob/master/LICENSE.md)
[](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()
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
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[](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.