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()