├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── bench ├── queue_append.exs ├── queue_drop.exs ├── queue_split.exs ├── queue_split_vs_enum_take.exs └── vs_deque.exs ├── config └── config.exs ├── lib └── limited_queue │ └── limited_queue.ex ├── mix.exs ├── mix.lock └── test ├── limited_queue └── queue_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "lib/**/*.{ex,exs}", 4 | "test/**/*.{ex,exs}", 5 | "config/**/*.exs", 6 | "mix.exs" 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | check_formatting: 11 | name: Ensure Code is Formatted 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: erlef/setup-beam@v1.9.0 16 | with: 17 | elixir-version: '1.12' 18 | otp-version: '24' 19 | - run: mix format --check-formatted 20 | build: 21 | name: Build and test 22 | runs-on: ubuntu-20.04 23 | strategy: 24 | matrix: 25 | include: 26 | - elixir-version: '1.7' 27 | otp-version: '20' 28 | 29 | - elixir-version: '1.8' 30 | otp-version: '20' 31 | 32 | - elixir-version: '1.9' 33 | otp-version: '20' 34 | 35 | - elixir-version: '1.10' 36 | otp-version: '21' 37 | 38 | - elixir-version: '1.11' 39 | otp-version: '21' 40 | - elixir-version: '1.11' 41 | otp-version: '22' 42 | - elixir-version: '1.11' 43 | otp-version: '23' 44 | 45 | - elixir-version: '1.12' 46 | otp-version: '22' 47 | - elixir-version: '1.12' 48 | otp-version: '23' 49 | - elixir-version: '1.12' 50 | otp-version: '24' 51 | env: 52 | MIX_ENV: test 53 | steps: 54 | - name: Checkout Project 55 | uses: actions/checkout@v2 56 | 57 | - name: Set up Elixir 58 | uses: erlef/setup-beam@v1.9.0 59 | with: 60 | elixir-version: ${{ matrix.elixir-version }} 61 | otp-version: ${{ matrix.otp-version }} 62 | 63 | - name: Restore dependencies cache 64 | uses: actions/cache@v2 65 | with: 66 | path: deps 67 | key: ${{ runner.os }}-${{ matrix.elixir-version }}-${{ matrix.otp-version }}-mix-${{ hashFiles('**/mix.lock') }} 68 | restore-keys: ${{ runner.os }}-${{ matrix.elixir-version }}-${{ matrix.otp-version }}-mix- 69 | 70 | - name: Install dependencies 71 | run: mix deps.get 72 | 73 | - name: Compile code 74 | run: mix compile --warnings-as-errors 75 | 76 | - name: Run tests 77 | run: mix test 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /doc 6 | /bench/results 7 | /cover 8 | .idea/ 9 | *.iml 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Discord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # limited_queue 2 | 3 | [![CI](https://github.com/discord/limited_queue/actions/workflows/ci.yml/badge.svg)](https://github.com/discord/limited_queue/actions/workflows/ci.yml) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/limited_queue.svg?style=flat)](https://hex.pm/packages/limited_queue) 5 | [![Hex.pm License](http://img.shields.io/hexpm/l/limited_queue.svg?style=flat)](https://hex.pm/packages/limited_queue) 6 | [![HexDocs](https://img.shields.io/badge/HexDocs-Yes-blue)](https://hexdocs.pm/limited_queue) 7 | 8 | `limited_queue` is a simple Elixir queue, with a constant-time `size/1` and a maximum capacity. 9 | 10 | ## Usage 11 | 12 | Add it to `mix.exs` 13 | 14 | ```elixir 15 | defp deps do 16 | [{:limited_queue, "~> 0.1.0"}] 17 | end 18 | ``` 19 | 20 | Create a new queue with a capacity and drop strategy, then push and pop values from it. 21 | 22 | ```elixir 23 | queue = 24 | LimitedQueue.new(2, :drop_newest) 25 | |> LimitedQueue.push("a") 26 | |> LimitedQueue.push("b") 27 | |> LimitedQueue.push("c") 28 | 29 | {:ok, queue, "a"} = LimitedQueue.pop(queue) 30 | {:ok, queue, "b"} = LimitedQueue.pop(queue) 31 | {:error, :empty} = LimitedQueue.pop(queue) 32 | 0 = LimitedQueue.size(queue) 33 | 2 = LimitedQueue.capacity(queue) 34 | ``` 35 | 36 | You can also `append/2` multiple values to a queue at once, and get information about how many were dropped. 37 | 38 | ```elixir 39 | queue = LimitedQueue.new(2, :drop_newest) 40 | 41 | {queue, dropped} = LimitedQueue.append(queue, ["a", "b", "c"]) 42 | 1 = dropped 43 | 2 = LimitedQueue.size(queue) 44 | 45 | {:ok, queue, "a"} = LimitedQueue.pop(queue) 46 | {:ok, queue, "b"} = LimitedQueue.pop(queue) 47 | {:error, :empty} = LimitedQueue.pop(queue) 48 | ``` 49 | 50 | `split/2` allows getting multiple values from the queue at once. 51 | 52 | ```elixir 53 | queue = LimitedQueue.new(10, :drop_newest) 54 | 55 | {queue, 0} = LimitedQueue.append(queue, ["a", "b", "c"]) 56 | 57 | {queue, values} = LimitedQueue.split(queue, 2) 58 | ["a", "b"] = values 59 | 1 = LimitedQueue.size(queue) 60 | ``` 61 | 62 | ## Documentation 63 | 64 | This library contains internal documentation. 65 | Documentation is available on [HexDocs](https://hexdocs.pm/limited_queue), 66 | or you can generate the documentation from source: 67 | 68 | ```bash 69 | $ mix deps.get 70 | $ mix docs 71 | ``` 72 | 73 | ## Running the Tests 74 | 75 | Tests can be run by running `mix test` in the root directory of the library. 76 | 77 | ## Compared to `deque` 78 | 79 | `LimitedQueue` has similar performance to [deque](https://hex.pm/packages/deque) depending on the situation (see Benchmarks). 80 | It has a more special-purpose limited interface, it is a single-sided queue, and its internals are built on Erlang's `:queue`. 81 | 82 | ## Performance and Benchmarking 83 | 84 | Benchmarks can be run by running `mix run bench/.exs` in the root directory of the library. 85 | 86 | ## License 87 | 88 | `LimitedQueue` is released under [the MIT License](LICENSE). 89 | Check [LICENSE](LICENSE) file for more information. 90 | -------------------------------------------------------------------------------- /bench/queue_append.exs: -------------------------------------------------------------------------------- 1 | alias LimitedQueue, as: Queue 2 | 3 | defmodule BenchQueue do 4 | def make_list(size) do 5 | 1..size 6 | |> Enum.reduce([], fn i, list -> 7 | [i | list] 8 | end) 9 | end 10 | 11 | def append(%Queue{capacity: capacity, size: capacity} = state, events) do 12 | {state, length(events)} 13 | end 14 | 15 | def append(%Queue{} = state, events) do 16 | Enum.reduce(events, {state, 0}, fn event, {state, dropped} -> 17 | case push(state, event) do 18 | {:ok, state} -> 19 | {state, dropped} 20 | 21 | {:error, :full} -> 22 | {state, dropped + 1} 23 | end 24 | end) 25 | end 26 | 27 | def append_erlang(%Queue{capacity: capacity, size: capacity} = state, events) do 28 | {state, length(events)} 29 | end 30 | 31 | def append_erlang(%Queue{} = state, events) do 32 | events_count = length(events) 33 | max_append = state.capacity - state.size 34 | 35 | {events, events_count, dropped} = 36 | if events_count > max_append do 37 | dropped = events_count - max_append 38 | events = Enum.take(events, max_append) 39 | {events, max_append, dropped} 40 | else 41 | {events, events_count, 0} 42 | end 43 | 44 | events_queue = :queue.from_list(events) 45 | queue = :queue.join(events_queue, state.queue) 46 | state = %Queue{state | queue: queue, size: state.size + events_count} 47 | {state, dropped} 48 | end 49 | 50 | def append_erlang2(%Queue{capacity: capacity, size: capacity} = state, events) do 51 | {state, length(events)} 52 | end 53 | 54 | def append_erlang2(%Queue{} = state, events) do 55 | events_queue = :queue.from_list(events) 56 | events_count = :queue.len(events_queue) 57 | max_append = state.capacity - state.size 58 | 59 | {events_queue, events_count, dropped_count} = 60 | if events_count > max_append do 61 | dropped_count = events_count - max_append 62 | {events_queue, _dropped} = :queue.split(max_append, events_queue) 63 | {events_queue, max_append, dropped_count} 64 | else 65 | {events_queue, events_count, 0} 66 | end 67 | 68 | queue = :queue.join(events_queue, state.queue) 69 | state = %Queue{state | queue: queue, size: state.size + events_count} 70 | {state, dropped_count} 71 | end 72 | 73 | defp push(%Queue{capacity: capacity, size: capacity}, _element) do 74 | {:error, :full} 75 | end 76 | 77 | defp push(%Queue{} = state, element) do 78 | queue = :queue.in(element, state.queue) 79 | 80 | state = %Queue{ 81 | state 82 | | queue: queue, 83 | size: state.size + 1 84 | } 85 | 86 | {:ok, state} 87 | end 88 | end 89 | 90 | queue = Queue.new(100) 91 | list = BenchQueue.make_list(200) 92 | {append_queue, append_dropped} = BenchQueue.append(queue, list) 93 | {erlang_queue, erlang_dropped} = BenchQueue.append_erlang(queue, list) 94 | {erlang_queue2, erlang_dropped2} = BenchQueue.append_erlang2(queue, list) 95 | 96 | if Queue.to_list(append_queue) != Queue.to_list(erlang_queue) do 97 | IO.puts("original queue: #{inspect(Queue.to_list(queue), charlists: :as_lists)}") 98 | 99 | raise """ 100 | the resulting queues of different append implementations are not the same: 101 | 102 | #{inspect(append_queue, charlists: :as_lists)} != #{inspect(erlang_queue, charlists: :as_lists)} 103 | """ 104 | end 105 | 106 | if Queue.to_list(append_queue) != Queue.to_list(erlang_queue2) do 107 | IO.puts("original queue: #{inspect(Queue.to_list(queue), charlists: :as_lists)}") 108 | 109 | raise """ 110 | the resulting queues of different append implementations are not the same: 111 | 112 | #{inspect(append_queue, charlists: :as_lists)} != #{ 113 | inspect(erlang_queue2, charlists: :as_lists) 114 | } 115 | """ 116 | end 117 | 118 | if append_dropped != erlang_dropped do 119 | IO.puts("original queue: #{inspect(Queue.to_list(queue), charlists: :as_lists)}") 120 | 121 | raise """ 122 | the drop results of different append implementations are not the same: 123 | 124 | #{append_dropped} != #{erlang_dropped} 125 | """ 126 | end 127 | 128 | if append_dropped != erlang_dropped2 do 129 | IO.puts("original queue: #{inspect(Queue.to_list(queue), charlists: :as_lists)}") 130 | 131 | raise """ 132 | the drop results of different append implementations are not the same: 133 | 134 | #{append_dropped} != #{erlang_dropped2} 135 | """ 136 | end 137 | 138 | Benchee.run( 139 | %{ 140 | "Append with erlang queue from_list and Enum.take" => fn {queue, list} -> 141 | BenchQueue.append_erlang(queue, list) 142 | end, 143 | "Append using recursive elixir and push" => fn {queue, list} -> 144 | BenchQueue.append(queue, list) 145 | end, 146 | "Append with erlang queue from_list and :queue.split" => fn {queue, list} -> 147 | BenchQueue.append_erlang2(queue, list) 148 | end 149 | }, 150 | inputs: %{ 151 | "queue size 100, append 1" => {Queue.new(100), BenchQueue.make_list(1)}, 152 | "queue size 100, append 100" => {Queue.new(100), BenchQueue.make_list(100)}, 153 | "queue size 100, append 1_000" => {Queue.new(100), BenchQueue.make_list(1_000)}, 154 | "queue size 10_000, append 1_000" => {Queue.new(10_000), BenchQueue.make_list(1_000)}, 155 | "queue size 10_000, append 10_000" => {Queue.new(10_000), BenchQueue.make_list(10_000)}, 156 | "queue size 10_000, append 100_000" => {Queue.new(10_000), BenchQueue.make_list(100_000)}, 157 | "queue size 100_000, append 100_000" => {Queue.new(100_000), BenchQueue.make_list(100_000)} 158 | }, 159 | formatters: [ 160 | Benchee.Formatters.Console 161 | ], 162 | save: %{ 163 | path: "bench/results/queue/runs" 164 | }, 165 | time: 1 166 | ) 167 | -------------------------------------------------------------------------------- /bench/queue_drop.exs: -------------------------------------------------------------------------------- 1 | defmodule BenchQueue do 2 | def make_queue(size) do 3 | 1..size 4 | |> Enum.reduce(:queue.new(), fn i, queue -> 5 | :queue.in(i, queue) 6 | end) 7 | end 8 | 9 | def drop_split(queue, amount) do 10 | {_dropped_split, queue} = :queue.split(amount, queue) 11 | queue 12 | end 13 | 14 | def drop_reduce(queue, amount) do 15 | Enum.reduce(1..amount, queue, fn _, queue -> :queue.drop(queue) end) 16 | end 17 | end 18 | 19 | queue = BenchQueue.make_queue(10) 20 | drop_split_queue = BenchQueue.drop_split(queue, 5) 21 | drop_reduce_queue = BenchQueue.drop_reduce(queue, 5) 22 | 23 | if :queue.to_list(drop_split_queue) != :queue.to_list(drop_reduce_queue) do 24 | IO.puts("original queue: #{inspect(:queue.to_list(queue), charlists: :as_lists)}") 25 | 26 | raise """ 27 | the resulting queues of different drop implementations are not the same: 28 | 29 | #{inspect(drop_split_queue, charlists: :as_lists)} != #{ 30 | inspect(drop_reduce_queue, charlists: :as_lists) 31 | } 32 | """ 33 | end 34 | 35 | Benchee.run( 36 | %{ 37 | "Drop with erlang queue split" => fn {queue, amount} -> 38 | BenchQueue.drop_split(queue, amount) 39 | end, 40 | "Drop with recursive erlang queue drop" => fn {queue, amount} -> 41 | BenchQueue.drop_reduce(queue, amount) 42 | end 43 | }, 44 | inputs: %{ 45 | "queue size 10, drop 1" => {BenchQueue.make_queue(10), 1}, 46 | "queue size 10, drop 10" => {BenchQueue.make_queue(10), 10}, 47 | "queue size 1_000, drop 1" => {BenchQueue.make_queue(1000), 1}, 48 | "queue size 1_000, drop 500" => {BenchQueue.make_queue(1000), 500}, 49 | "queue size 1_000, drop 1_000" => {BenchQueue.make_queue(1000), 1000}, 50 | "queue size 100_000, drop 1" => {BenchQueue.make_queue(100_000), 1}, 51 | "queue size 100_000, drop 5_000" => {BenchQueue.make_queue(100_000), 5000}, 52 | "queue size 100_000, drop 50_000" => {BenchQueue.make_queue(100_000), 50000}, 53 | "queue size 100_000, drop 100_000" => {BenchQueue.make_queue(100_000), 100_000} 54 | }, 55 | formatters: [ 56 | Benchee.Formatters.Console 57 | ], 58 | save: %{ 59 | path: "bench/results/queue/runs/drop" 60 | }, 61 | time: 1 62 | ) 63 | -------------------------------------------------------------------------------- /bench/queue_split.exs: -------------------------------------------------------------------------------- 1 | defmodule BenchQueue do 2 | def make_queue(size) do 3 | 1..size 4 | |> Enum.reduce(:queue.new(), fn i, queue -> 5 | :queue.in(i, queue) 6 | end) 7 | end 8 | 9 | def split(queue, amount) do 10 | {queue, rev_list} = do_split_rev(queue, amount, []) 11 | {queue, Enum.reverse(rev_list)} 12 | end 13 | 14 | def split_erlang(queue, amount) do 15 | {split, queue} = :queue.split(amount, queue) 16 | {queue, :queue.to_list(split)} 17 | end 18 | 19 | defp pop(queue) do 20 | case :queue.out(queue) do 21 | {{:value, value}, queue} -> 22 | {:ok, queue, value} 23 | 24 | {:empty, _queue} -> 25 | {:error, :empty} 26 | end 27 | end 28 | 29 | defp do_split_rev(queue, 0, list) do 30 | {queue, list} 31 | end 32 | 33 | defp do_split_rev(queue, amount, list) do 34 | case pop(queue) do 35 | {:ok, queue, value} -> 36 | do_split_rev(queue, amount - 1, [value | list]) 37 | 38 | {:error, :empty} -> 39 | {queue, list} 40 | end 41 | end 42 | end 43 | 44 | queue = BenchQueue.make_queue(10) 45 | {split_queue, split_result} = BenchQueue.split(queue, 5) 46 | {split_erlang_queue, split_erlang_result} = BenchQueue.split_erlang(queue, 5) 47 | 48 | if :queue.to_list(split_queue) != :queue.to_list(split_erlang_queue) do 49 | IO.puts("original queue: #{inspect(:queue.to_list(queue), charlists: :as_lists)}") 50 | 51 | raise """ 52 | the resulting queues of different split implementations are not the same: 53 | 54 | #{inspect(split_queue, charlists: :as_lists)} != #{ 55 | inspect(split_erlang_queue, charlists: :as_lists) 56 | } 57 | """ 58 | end 59 | 60 | if split_result != split_erlang_result do 61 | IO.puts("original queue: #{inspect(:queue.to_list(queue), charlists: :as_lists)}") 62 | 63 | raise """ 64 | the results of different split implementations are not the same: 65 | 66 | #{inspect(split_result, charlists: :as_lists)} != #{ 67 | inspect(split_erlang_result, charlists: :as_lists) 68 | } 69 | """ 70 | end 71 | 72 | Benchee.run( 73 | %{ 74 | "Split with erlang queue split and to_list" => fn {queue, amount} -> 75 | BenchQueue.split_erlang(queue, amount) 76 | end, 77 | "split using recursive elixir and Enum.reverse" => fn {queue, amount} -> 78 | BenchQueue.split(queue, amount) 79 | end 80 | }, 81 | inputs: %{ 82 | "queue size 10, split 1" => {BenchQueue.make_queue(10), 1}, 83 | "queue size 10, split 10" => {BenchQueue.make_queue(10), 10}, 84 | "queue size 1_000, split 1" => {BenchQueue.make_queue(1000), 1}, 85 | "queue size 1_000, split 500" => {BenchQueue.make_queue(1000), 500}, 86 | "queue size 1_000, split 1_000" => {BenchQueue.make_queue(1000), 1000}, 87 | "queue size 100_000, split 1" => {BenchQueue.make_queue(100_000), 1}, 88 | "queue size 100_000, split 5_000" => {BenchQueue.make_queue(100_000), 5000}, 89 | "queue size 100_000, split 50_000" => {BenchQueue.make_queue(100_000), 50000}, 90 | "queue size 100_000, split 100_000" => {BenchQueue.make_queue(100_000), 100_000} 91 | }, 92 | formatters: [ 93 | Benchee.Formatters.Console 94 | ], 95 | save: %{ 96 | path: "bench/results/queue/runs" 97 | }, 98 | time: 1 99 | ) 100 | -------------------------------------------------------------------------------- /bench/queue_split_vs_enum_take.exs: -------------------------------------------------------------------------------- 1 | defmodule BenchQueue do 2 | def make_queue() do 3 | :queue.new() 4 | end 5 | 6 | def make_values(count) do 7 | Enum.reduce(1..count, [], fn i, acc -> [i | acc] end) 8 | end 9 | 10 | def split_then_join(queue, values, limit) do 11 | value_list = :queue.from_list(values) 12 | {_dropped_split, value_list} = :queue.split(limit, value_list) 13 | :queue.join(queue, value_list) 14 | end 15 | 16 | def take_then_join(queue, values, limit) do 17 | values = Enum.take(values, -limit) 18 | value_list = :queue.from_list(values) 19 | :queue.join(queue, value_list) 20 | end 21 | end 22 | 23 | queue = BenchQueue.make_queue() 24 | values = BenchQueue.make_values(100) 25 | split_queue = BenchQueue.split_then_join(queue, values, 50) 26 | take_queue = BenchQueue.take_then_join(queue, values, 50) 27 | 28 | if :queue.to_list(split_queue) != :queue.to_list(take_queue) do 29 | IO.puts("original queue: #{inspect(:queue.to_list(queue), charlists: :as_lists)}") 30 | 31 | raise """ 32 | the resulting queues of different implementations are not the same: 33 | 34 | #{inspect(split_queue, charlists: :as_lists)} != #{inspect(take_queue, charlists: :as_lists)} 35 | """ 36 | end 37 | 38 | Benchee.run( 39 | %{ 40 | "Limit with erlang queue split" => fn {queue, values, limit} -> 41 | BenchQueue.split_then_join(queue, values, limit) 42 | end, 43 | "Limit with elixir Enum.take" => fn {queue, values, limit} -> 44 | BenchQueue.take_then_join(queue, values, limit) 45 | end 46 | }, 47 | inputs: %{ 48 | "10 value, 1 limit" => {BenchQueue.make_queue(), BenchQueue.make_values(10), 1}, 49 | "10 value, 5 limit" => {BenchQueue.make_queue(), BenchQueue.make_values(10), 5}, 50 | "1_000 value, 1 limit" => {BenchQueue.make_queue(), BenchQueue.make_values(1_000), 1}, 51 | "1_000 value, 250 limit" => {BenchQueue.make_queue(), BenchQueue.make_values(1_000), 250}, 52 | "1_000 value, 1_000 limit" => {BenchQueue.make_queue(), BenchQueue.make_values(1_000), 1_000}, 53 | "100_000 value, 1 limit" => {BenchQueue.make_queue(), BenchQueue.make_values(100_000), 1}, 54 | "100_000 value, 1_000 limit" => 55 | {BenchQueue.make_queue(), BenchQueue.make_values(100_000), 1_000}, 56 | "100_000 value, 10_000 limit" => 57 | {BenchQueue.make_queue(), BenchQueue.make_values(100_000), 10_000}, 58 | "100_000 value, 100_000 limit" => 59 | {BenchQueue.make_queue(), BenchQueue.make_values(100_000), 100_000} 60 | }, 61 | formatters: [ 62 | Benchee.Formatters.Console 63 | ], 64 | save: %{ 65 | path: "bench/results/queue/runs/drop" 66 | }, 67 | time: 1 68 | ) 69 | -------------------------------------------------------------------------------- /bench/vs_deque.exs: -------------------------------------------------------------------------------- 1 | defmodule BenchQueueVsDeque do 2 | alias LimitedQueue 3 | alias Deque 4 | 5 | def make_queues(capacity) do 6 | queue_drop_old = LimitedQueue.new(capacity, :drop_oldest) 7 | queue_drop_new = LimitedQueue.new(capacity, :drop_newest) 8 | deque = Deque.new(capacity) 9 | {queue_drop_old, queue_drop_new, deque} 10 | end 11 | 12 | def make_full_queues(capacity) do 13 | {queue_drop_old, queue_drop_new, deque} = make_queues(capacity) 14 | 15 | values = make_values(capacity) 16 | {queue_drop_old, 0} = LimitedQueue.append(queue_drop_old, values) 17 | {queue_drop_new, 0} = LimitedQueue.append(queue_drop_new, values) 18 | deque = Enum.reduce(values, deque, &Deque.append(&2, &1)) 19 | 20 | {queue_drop_old, queue_drop_new, deque} 21 | end 22 | 23 | def make_values(amount) do 24 | List.duplicate(:test, amount) 25 | end 26 | end 27 | 28 | # mac can't measure nanoseconds, so make sure the tests run long enough to measure accurately 29 | # see this benchee issue: https://github.com/bencheeorg/benchee/issues/313 30 | run_multiplier = 1000 31 | 32 | push_tests = %{ 33 | "Push to Queue (drop oldest)" => fn {{queue_drop_old, _queue_drop_new, _deque}, values} -> 34 | for _ <- 1..run_multiplier do 35 | Enum.reduce(values, {queue_drop_old, 0}, fn value, {queue, dropped} -> 36 | dropped = 37 | if LimitedQueue.size(queue) == LimitedQueue.capacity(queue) do 38 | dropped + 1 39 | else 40 | dropped 41 | end 42 | 43 | queue = LimitedQueue.push(queue, value) 44 | {queue, dropped} 45 | end) 46 | end 47 | end, 48 | "Push to Deque" => fn {{_queue_drop_old, _queue_drop_new, deque}, values} -> 49 | for _ <- 1..run_multiplier do 50 | Enum.reduce(values, {deque, 0}, fn value, {deque, dropped} -> 51 | count = Enum.count(deque) 52 | deque = Deque.append(deque, value) 53 | 54 | if count == Enum.count(deque) do 55 | {deque, dropped + 1} 56 | else 57 | {deque, dropped} 58 | end 59 | end) 60 | end 61 | end, 62 | "Append to Queue (drop oldest)" => fn {{queue_drop_old, _queue_drop_new, _deque}, values} -> 63 | for _ <- 1..run_multiplier do 64 | LimitedQueue.append(queue_drop_old, values) 65 | end 66 | end 67 | } 68 | 69 | pop_tests = %{ 70 | "Pop from Queue (drop oldest)" => fn {{queue_drop_old, _queue_drop_new, _deque}, count} -> 71 | for _ <- 1..run_multiplier do 72 | Enum.reduce(1..count, {queue_drop_old, []}, fn _, {queue, values} -> 73 | {:ok, queue, value} = LimitedQueue.pop(queue) 74 | {queue, [value | values]} 75 | end) 76 | end 77 | end, 78 | "Pop from Deque" => fn {{_queue_drop_old, _queue_drop_new, deque}, count} -> 79 | for _ <- 1..run_multiplier do 80 | Enum.reduce(1..count, {deque, []}, fn _, {deque, values} -> 81 | {value, deque} = Deque.pop(deque) 82 | {deque, [value | values]} 83 | end) 84 | end 85 | end, 86 | "Split from Queue (drop oldest)" => fn {{queue_drop_old, _queue_drop_new, _deque}, count} -> 87 | for _ <- 1..run_multiplier do 88 | {_queue, _values} = LimitedQueue.split(queue_drop_old, count) 89 | end 90 | end 91 | } 92 | 93 | real_world_tests = %{ 94 | "Append and pop from Queue (drop oldest)" => fn {{queue_drop_old, _queue_drop_new, _deque}, 95 | values, push_batch_size, pop_batch_size} -> 96 | push_chunks = Enum.chunk_every(values, push_batch_size) 97 | pop_chunks = Enum.chunk_every(values, pop_batch_size) 98 | 99 | Enum.reduce(1..100, queue_drop_old, fn _run, queue -> 100 | queue = 101 | Enum.reduce(push_chunks, queue, fn chunk, queue -> 102 | {queue, 0} = LimitedQueue.append(queue, chunk) 103 | queue 104 | end) 105 | 106 | Enum.reduce(pop_chunks, queue, fn _chunk, queue -> 107 | {queue, _batch_values} = LimitedQueue.split(queue, pop_batch_size) 108 | queue 109 | end) 110 | end) 111 | end, 112 | "Append and pop from Deque" => fn {{_queue_drop_old, _queue_drop_new, deque}, values, 113 | push_batch_size, pop_batch_size} -> 114 | push_chunks = Enum.chunk_every(values, push_batch_size) 115 | pop_chunks = Enum.chunk_every(values, pop_batch_size) 116 | 117 | Enum.reduce(1..100, deque, fn _run, deque -> 118 | deque = 119 | Enum.reduce(push_chunks, deque, fn chunk, deque -> 120 | Enum.reduce(chunk, deque, &Deque.append(&2, &1)) 121 | end) 122 | 123 | Enum.reduce(pop_chunks, deque, fn _chunk, deque -> 124 | {deque, _batch_values} = 125 | Enum.reduce_while(1..pop_batch_size, {deque, []}, fn _, {deque, batch_values} -> 126 | {value, deque} = Deque.pop(deque) 127 | 128 | if value == nil do 129 | {:halt, {deque, batch_values}} 130 | else 131 | {:cont, {deque, [value | batch_values]}} 132 | end 133 | end) 134 | 135 | deque 136 | end) 137 | end) 138 | end 139 | } 140 | 141 | Benchee.run( 142 | push_tests, 143 | inputs: %{ 144 | "empty size 10, values 1" => 145 | {BenchQueueVsDeque.make_queues(10), BenchQueueVsDeque.make_values(1)}, 146 | "empty size 10, values 10" => 147 | {BenchQueueVsDeque.make_queues(10), BenchQueueVsDeque.make_values(10)}, 148 | "empty size 1_000, values 1" => 149 | {BenchQueueVsDeque.make_queues(1_000), BenchQueueVsDeque.make_values(1)}, 150 | "empty size 1_000, values 10" => 151 | {BenchQueueVsDeque.make_queues(1_000), BenchQueueVsDeque.make_values(10)}, 152 | "empty size 1_000_000, values 1" => 153 | {BenchQueueVsDeque.make_queues(1_000_000), BenchQueueVsDeque.make_values(1)}, 154 | "empty size 1_000_000, values 10" => 155 | {BenchQueueVsDeque.make_queues(1_000_000), BenchQueueVsDeque.make_values(10)}, 156 | "empty size 1_000_000, values 1_000" => 157 | {BenchQueueVsDeque.make_queues(1_000_000), BenchQueueVsDeque.make_values(1_000)} 158 | }, 159 | formatters: [ 160 | Benchee.Formatters.Console, 161 | {Benchee.Formatters.HTML, file: "bench/results/queue/html/empty-push.html"} 162 | ], 163 | save: %{ 164 | path: "bench/results/queue/runs/empty-push" 165 | }, 166 | time: 5 167 | ) 168 | 169 | Benchee.run( 170 | push_tests, 171 | inputs: %{ 172 | "full size 10, values 1" => 173 | {BenchQueueVsDeque.make_full_queues(10), BenchQueueVsDeque.make_values(1)}, 174 | "full size 10_000, values 1" => 175 | {BenchQueueVsDeque.make_full_queues(10_000), BenchQueueVsDeque.make_values(1)}, 176 | "full size 10_000, values 10" => 177 | {BenchQueueVsDeque.make_full_queues(10_000), BenchQueueVsDeque.make_values(10)}, 178 | "full size 10_000, values 1_000" => 179 | {BenchQueueVsDeque.make_full_queues(10_000), BenchQueueVsDeque.make_values(1_000)} 180 | }, 181 | formatters: [ 182 | Benchee.Formatters.Console, 183 | {Benchee.Formatters.HTML, file: "bench/results/queue/html/full-push.html"} 184 | ], 185 | save: %{ 186 | path: "bench/results/queue/runs/full-push" 187 | }, 188 | time: 5 189 | ) 190 | 191 | Benchee.run( 192 | pop_tests, 193 | inputs: %{ 194 | "full size 10, count 1" => {BenchQueueVsDeque.make_full_queues(10), 1}, 195 | "full size 10_000, count 1" => {BenchQueueVsDeque.make_full_queues(10_000), 1}, 196 | "full size 10_000, count 100" => {BenchQueueVsDeque.make_full_queues(10_000), 100}, 197 | "full size 100_000, count 1_000" => {BenchQueueVsDeque.make_full_queues(100_000), 1_000} 198 | }, 199 | formatters: [ 200 | Benchee.Formatters.Console, 201 | {Benchee.Formatters.HTML, file: "bench/results/queue/html/full-push.html"} 202 | ], 203 | save: %{ 204 | path: "bench/results/queue/runs/full-push" 205 | }, 206 | time: 5 207 | ) 208 | 209 | Benchee.run( 210 | real_world_tests, 211 | inputs: %{ 212 | "push batch 1, pop batch 10" => 213 | {BenchQueueVsDeque.make_queues(10_000), BenchQueueVsDeque.make_values(1_000), 1, 10}, 214 | "push batch 1, pop batch 100" => 215 | {BenchQueueVsDeque.make_queues(10_000), BenchQueueVsDeque.make_values(1_000), 1, 100}, 216 | "push batch 2, pop batch 100" => 217 | {BenchQueueVsDeque.make_queues(10_000), BenchQueueVsDeque.make_values(1_000), 2, 100}, 218 | "push batch 10, pop batch 100" => 219 | {BenchQueueVsDeque.make_queues(10_000), BenchQueueVsDeque.make_values(1_000), 10, 100} 220 | }, 221 | formatters: [ 222 | Benchee.Formatters.Console, 223 | {Benchee.Formatters.HTML, file: "bench/results/queue/html/real-world-push.html"} 224 | ], 225 | save: %{ 226 | path: "bench/results/queue/runs/real-world-push" 227 | }, 228 | time: 5 229 | ) 230 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /lib/limited_queue/limited_queue.ex: -------------------------------------------------------------------------------- 1 | defmodule LimitedQueue do 2 | @moduledoc """ 3 | An elixir wrapper for erlang's `:queue`, with a constant-time `size/1` and a maximum capacity. 4 | 5 | When items pushed on to the `LimitedQueue` put it over its maximum capacity, 6 | it will drop events according to its `drop_strategy/0`. 7 | """ 8 | 9 | @typedoc """ 10 | The opaque internal state of the `LimitedQueue`. 11 | """ 12 | @opaque t(value) :: %__MODULE__{ 13 | queue: :queue.queue(value), 14 | size: non_neg_integer(), 15 | capacity: pos_integer(), 16 | drop_strategy: drop_strategy() 17 | } 18 | 19 | @typedoc """ 20 | The `drop_strategy/0` determines how the queue handles dropping events when overloaded. 21 | 22 | `:drop_newest` (default) drops incoming events and is the most efficient 23 | because it will avoid touching the state when the queue is overloaded. 24 | 25 | `:drop_oldest` drops the oldest events from the queue, 26 | which may be better behavior where newer events are more relevant to process than older ones. 27 | """ 28 | @type drop_strategy :: :drop_newest | :drop_oldest 29 | 30 | @enforce_keys [:queue, :size, :capacity, :drop_strategy] 31 | defstruct [:queue, :size, :capacity, :drop_strategy] 32 | 33 | @doc """ 34 | Create a new `LimitedQueue` with the given maximum capacity. 35 | 36 | The `drop_strategy` determines how the queue handles dropping events when overloaded. 37 | See `drop_strategy/0` for more information. 38 | """ 39 | @spec new(capacity :: pos_integer()) :: t(value) when value: any() 40 | @spec new(capacity :: pos_integer(), drop_strategy()) :: t(value) when value: any() 41 | def new(capacity, drop_strategy \\ :drop_newest) 42 | when capacity > 0 and drop_strategy in [:drop_newest, :drop_oldest] do 43 | %__MODULE__{queue: :queue.new(), size: 0, capacity: capacity, drop_strategy: drop_strategy} 44 | end 45 | 46 | @doc """ 47 | Push a value to the back of the `LimitedQueue`. 48 | 49 | If the `LimitedQueue` is full, it will drop an event according to the `LimitedQueue`'s `drop_strategy/0`. 50 | """ 51 | @spec push(t(value), value) :: t(value) when value: any() 52 | def push( 53 | %__MODULE__{capacity: capacity, size: capacity, drop_strategy: :drop_newest} = state, 54 | _element 55 | ) do 56 | state 57 | end 58 | 59 | def push( 60 | %__MODULE__{capacity: capacity, size: capacity, drop_strategy: :drop_oldest} = state, 61 | element 62 | ) do 63 | queue = :queue.drop(state.queue) 64 | queue = :queue.in(element, queue) 65 | %__MODULE__{state | queue: queue} 66 | end 67 | 68 | def push(%__MODULE__{} = state, element) do 69 | queue = :queue.in(element, state.queue) 70 | 71 | %__MODULE__{ 72 | state 73 | | queue: queue, 74 | size: state.size + 1 75 | } 76 | end 77 | 78 | @doc """ 79 | Push multiple values to the back of the `LimitedQueue`. 80 | 81 | Returns the number of values that were dropped if the `LimitedQueue` reaches its capacity. 82 | """ 83 | @spec append(t(value), [value]) :: {t(value), dropped :: non_neg_integer()} when value: any() 84 | def append( 85 | %__MODULE__{capacity: capacity, size: capacity, drop_strategy: :drop_newest} = state, 86 | events 87 | ) do 88 | {state, length(events)} 89 | end 90 | 91 | def append(%__MODULE__{capacity: capacity, size: capacity} = state, [event]) do 92 | state = push(state, event) 93 | {state, 1} 94 | end 95 | 96 | def append(%__MODULE__{} = state, [event]) do 97 | state = push(state, event) 98 | {state, 0} 99 | end 100 | 101 | def append(%__MODULE__{} = state, events) do 102 | Enum.reduce(events, {state, 0}, fn value, {state, dropped} -> 103 | dropped = 104 | if state.size == state.capacity do 105 | dropped + 1 106 | else 107 | dropped 108 | end 109 | 110 | state = push(state, value) 111 | {state, dropped} 112 | end) 113 | end 114 | 115 | @doc """ 116 | Remove and return a value from the front of the `LimitedQueue`. 117 | If the `LimitedQueue` is empty, {:error, :empty} will be returned. 118 | """ 119 | @spec pop(t(value)) :: {:ok, t(value), value} | {:error, :empty} when value: any() 120 | def pop(%__MODULE__{} = state) do 121 | case :queue.out(state.queue) do 122 | {{:value, value}, queue} -> 123 | state = %__MODULE__{ 124 | state 125 | | queue: queue, 126 | size: state.size - 1 127 | } 128 | 129 | {:ok, state, value} 130 | 131 | {:empty, _queue} -> 132 | {:error, :empty} 133 | end 134 | end 135 | 136 | @doc """ 137 | Remove and return multiple values from the front of the `LimitedQueue`. 138 | If the `LimitedQueue` runs out of values, fewer values than the requested amount will be returned. 139 | """ 140 | @spec split(t(value), amount :: non_neg_integer()) :: {t(value), [value]} when value: any() 141 | def split(%__MODULE__{size: 0} = state, amount) when amount >= 0 do 142 | {state, []} 143 | end 144 | 145 | def split(%__MODULE__{size: size} = state, amount) when amount >= size do 146 | split = state.queue 147 | state = %__MODULE__{state | queue: :queue.new(), size: 0} 148 | {state, :queue.to_list(split)} 149 | end 150 | 151 | def split(%__MODULE__{} = state, amount) when amount > 0 do 152 | {split, queue} = :queue.split(amount, state.queue) 153 | state = %__MODULE__{state | queue: queue, size: state.size - amount} 154 | {state, :queue.to_list(split)} 155 | end 156 | 157 | def split(%__MODULE__{} = state, 0) do 158 | {state, []} 159 | end 160 | 161 | @doc """ 162 | Filters the queue, i.e. returns only those elements for which fun returns a truthy value. 163 | """ 164 | @spec filter(t(value), (value -> boolean)) :: t(value) when value: any() 165 | def filter(%__MODULE__{} = state, fun) do 166 | queue = :queue.filter(fun, state.queue) 167 | %__MODULE__{state | queue: queue, size: :queue.len(queue)} 168 | end 169 | 170 | @doc """ 171 | The current number of values stored in the `LimitedQueue`. 172 | """ 173 | @spec size(t(value)) :: non_neg_integer() when value: any() 174 | def size(%__MODULE__{} = state) do 175 | state.size 176 | end 177 | 178 | @doc """ 179 | The maximum capacity of the `LimitedQueue`. 180 | """ 181 | @spec capacity(t(value)) :: non_neg_integer() when value: any() 182 | def capacity(%__MODULE__{} = state) do 183 | state.capacity 184 | end 185 | 186 | @doc """ 187 | The contents of the `LimitedQueue` as a list. 188 | """ 189 | @spec to_list(t(value)) :: [value] when value: any() 190 | def to_list(%__MODULE__{} = state) do 191 | :queue.to_list(state.queue) 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LimitedQueue.Mixfile do 2 | use Mix.Project 3 | 4 | @github_url "https://github.com/discord/limited_queue" 5 | 6 | def project do 7 | [ 8 | app: :limited_queue, 9 | version: "0.1.1", 10 | elixir: "~> 1.7", 11 | elixirc_paths: elixirc_paths(Mix.env()), 12 | elixirc_options: [warnings_as_errors: ci?()], 13 | deps: deps(), 14 | docs: docs(), 15 | package: package(), 16 | test_coverage: [tool: ExCoveralls] 17 | ] 18 | end 19 | 20 | def application do 21 | [] 22 | end 23 | 24 | defp deps do 25 | [ 26 | {:benchee, "~> 1.0", only: [:dev], runtime: false}, 27 | {:benchee_html, "~> 1.0", only: [:dev], runtime: false}, 28 | {:deque, "~> 1.2", only: [:dev], runtime: false}, 29 | {:dialyxir, "~> 1.1.0", only: [:dev], runtime: false}, 30 | {:ex_doc, "~> 0.25.1", only: [:dev], runtime: false}, 31 | {:excoveralls, "~> 0.14.2", only: [:dev, :test], runtime: false} 32 | ] 33 | end 34 | 35 | defp ci?() do 36 | System.get_env("CI") == "true" 37 | end 38 | 39 | defp docs do 40 | source_ref = current_branch(ci?()) 41 | 42 | [ 43 | name: "limited_queue", 44 | extras: ["README.md", "LICENSE"], 45 | main: "readme", 46 | source_url_pattern: "#{@github_url}/blob/#{source_ref}/%{path}#L%{line}" 47 | ] 48 | end 49 | 50 | def package do 51 | [ 52 | name: :limited_queue, 53 | description: "Simple Elixir queue, with a constant-time `size/1` and a maximum capacity.", 54 | maintainers: [], 55 | licenses: ["MIT"], 56 | files: ["lib/*", "mix.exs", "README*", "LICENSE*"], 57 | links: %{ 58 | "GitHub" => @github_url 59 | } 60 | ] 61 | end 62 | 63 | @spec current_branch(is_continuous_integration :: boolean()) :: String.t() 64 | defp current_branch(true), do: "master" 65 | 66 | defp current_branch(false) do 67 | "git" 68 | |> System.cmd(["rev-parse", "--abbrev-ref", "HEAD"]) 69 | |> elem(0) 70 | end 71 | 72 | defp elixirc_paths(:test) do 73 | elixirc_paths(:dev) ++ ["test/support"] 74 | end 75 | 76 | defp elixirc_paths(_) do 77 | ["lib"] 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, 3 | "benchee_html": {:hex, :benchee_html, "1.0.0", "5b4d24effebd060f466fb460ec06576e7b34a00fc26b234fe4f12c4f05c95947", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "5280af9aac432ff5ca4216d03e8a93f32209510e925b60e7f27c33796f69e699"}, 4 | "benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"}, 5 | "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, 6 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 7 | "deque": {:hex, :deque, "1.2.0", "30404b86264be3eeb4e8331d88ef67d0fdc77e006b2fa7872be03923a47245b7", [:mix], [], "hexpm", "cbc965c2c04654fee7ed875bf5efb5c925e1c98b0351bf1cca10670a024fbd5a"}, 8 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, 9 | "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, 10 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 11 | "ex_doc": {:hex, :ex_doc, "0.25.5", "ac3c5425a80b4b7c4dfecdf51fa9c23a44877124dd8ca34ee45ff608b1c6deb9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "688cfa538cdc146bc4291607764a7f1fcfa4cce8009ecd62de03b27197528350"}, 12 | "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, 13 | "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, 14 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 15 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 16 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 17 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 19 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 20 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 21 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, 22 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 23 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 24 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/limited_queue/queue_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LimitedQueue.QueueTest do 2 | @moduledoc false 3 | use ExUnit.Case 4 | 5 | alias LimitedQueue, as: Queue 6 | 7 | describe "queue with capacity" do 8 | test "queue can be created" do 9 | capacity = 10 10 | queue = Queue.new(capacity) 11 | assert Queue.capacity(queue) == capacity 12 | assert Queue.size(queue) == 0 13 | end 14 | 15 | test "queue can be pushed to" do 16 | queue = Queue.new(10) 17 | queue = Queue.push(queue, "a") 18 | assert Queue.size(queue) == 1 19 | end 20 | 21 | test "queue can get full" do 22 | queue = Queue.new(2) 23 | queue = Queue.push(queue, "a") 24 | queue = Queue.push(queue, "a") 25 | queue = Queue.push(queue, "a") 26 | assert Queue.size(queue) == 2 27 | end 28 | 29 | test "queue can have values popped" do 30 | queue = Queue.new(2) 31 | queue = Queue.push(queue, "a") 32 | {:ok, queue, "a"} = Queue.pop(queue) 33 | assert Queue.size(queue) == 0 34 | end 35 | 36 | test "queue push is FIFO" do 37 | queue = Queue.new(2) 38 | queue = Queue.push(queue, "a") 39 | queue = Queue.push(queue, "b") 40 | {:ok, queue, "a"} = Queue.pop(queue) 41 | {:ok, queue, "b"} = Queue.pop(queue) 42 | assert Queue.size(queue) == 0 43 | end 44 | 45 | test "queue can be appended to" do 46 | queue = Queue.new(5) 47 | {queue, 0} = Queue.append(queue, ["a", "b", "c"]) 48 | assert Queue.size(queue) == 3 49 | end 50 | 51 | test "queue append is FIFO" do 52 | queue = Queue.new(4) 53 | {queue, 0} = Queue.append(queue, ["a", "b"]) 54 | {queue, 0} = Queue.append(queue, ["c", "d"]) 55 | {:ok, queue, "a"} = Queue.pop(queue) 56 | {:ok, queue, "b"} = Queue.pop(queue) 57 | {:ok, queue, "c"} = Queue.pop(queue) 58 | {:ok, queue, "d"} = Queue.pop(queue) 59 | assert Queue.size(queue) == 0 60 | end 61 | 62 | test "queue push respects the capacity" do 63 | queue = Queue.new(2) 64 | {queue, 0} = Queue.append(queue, ["a", "b"]) 65 | queue = Queue.push(queue, "a") 66 | assert Queue.size(queue) == 2 67 | end 68 | 69 | test "queue append respects the capacity" do 70 | queue = Queue.new(2) 71 | {queue, 1} = Queue.append(queue, ["a", "b", "c"]) 72 | assert Queue.size(queue) == 2 73 | end 74 | 75 | test "queue append drops the newest elements with push when it reaches capacity in :drop_newest mode" do 76 | queue = Queue.new(2, :drop_newest) 77 | queue = Queue.push(queue, "a") 78 | queue = Queue.push(queue, "b") 79 | assert Queue.size(queue) == 2 80 | queue = Queue.push(queue, "c") 81 | assert Queue.size(queue) == 2 82 | {:ok, queue, "a"} = Queue.pop(queue) 83 | {:ok, queue, "b"} = Queue.pop(queue) 84 | assert Queue.size(queue) == 0 85 | end 86 | 87 | test "queue append drops the newest elements with append when it reaches capacity in :drop_newest mode" do 88 | queue = Queue.new(2, :drop_newest) 89 | {queue, 1} = Queue.append(queue, ["a", "b", "c"]) 90 | {:ok, queue, "a"} = Queue.pop(queue) 91 | {:ok, queue, "b"} = Queue.pop(queue) 92 | assert Queue.size(queue) == 0 93 | end 94 | 95 | test "queue append drops the oldest elements with push when it reaches capacity in :drop_oldest mode" do 96 | queue = Queue.new(2, :drop_oldest) 97 | queue = Queue.push(queue, "a") 98 | assert Queue.size(queue) == 1 99 | queue = Queue.push(queue, "b") 100 | assert Queue.size(queue) == 2 101 | queue = Queue.push(queue, "c") 102 | assert Queue.size(queue) == 2 103 | {:ok, queue, "b"} = Queue.pop(queue) 104 | {:ok, queue, "c"} = Queue.pop(queue) 105 | assert Queue.size(queue) == 0 106 | end 107 | 108 | test "queue append keeps all elements with append when it is below capacity in :drop_oldest mode" do 109 | queue = Queue.new(2, :drop_oldest) 110 | {queue, 0} = Queue.append(queue, ["a"]) 111 | assert Queue.size(queue) == 1 112 | {:ok, queue, "a"} = Queue.pop(queue) 113 | assert Queue.size(queue) == 0 114 | end 115 | 116 | test "queue append keeps all elements with append when it is equal to the capacity in :drop_oldest mode" do 117 | queue = Queue.new(2, :drop_oldest) 118 | {queue, 0} = Queue.append(queue, ["a", "b"]) 119 | assert Queue.size(queue) == 2 120 | {:ok, queue, "a"} = Queue.pop(queue) 121 | {:ok, queue, "b"} = Queue.pop(queue) 122 | assert Queue.size(queue) == 0 123 | end 124 | 125 | test "queue append drops the oldest elements with append when it goes over capacity in :drop_oldest mode" do 126 | queue = Queue.new(3, :drop_oldest) 127 | {queue, 0} = Queue.append(queue, ["a", "b"]) 128 | assert Queue.size(queue) == 2 129 | {queue, 1} = Queue.append(queue, ["c", "d"]) 130 | assert Queue.size(queue) == 3 131 | {:ok, queue, "b"} = Queue.pop(queue) 132 | {:ok, queue, "c"} = Queue.pop(queue) 133 | {:ok, queue, "d"} = Queue.pop(queue) 134 | assert Queue.size(queue) == 0 135 | end 136 | 137 | test "queue append drops the oldest elements with append when more elements are added than its capacity in :drop_oldest mode" do 138 | queue = Queue.new(2, :drop_oldest) 139 | {queue, 1} = Queue.append(queue, ["a", "b", "c"]) 140 | assert Queue.size(queue) == 2 141 | {:ok, queue, "b"} = Queue.pop(queue) 142 | {:ok, queue, "c"} = Queue.pop(queue) 143 | assert Queue.size(queue) == 0 144 | end 145 | 146 | test "queue that has reached capacity can accept more elements again after some have been popped" do 147 | queue = Queue.new(2) 148 | {queue, 0} = Queue.append(queue, ["a", "b"]) 149 | queue = Queue.push(queue, "c") 150 | {:ok, queue, "a"} = Queue.pop(queue) 151 | {:ok, queue, "b"} = Queue.pop(queue) 152 | queue = Queue.push(queue, "c") 153 | assert Queue.size(queue) == 1 154 | end 155 | 156 | test "queue can be split" do 157 | queue = Queue.new(10) 158 | {queue, 0} = Queue.append(queue, ["a", "b", "c", "d"]) 159 | {queue, values} = Queue.split(queue, 2) 160 | assert Queue.size(queue) == 2 161 | assert is_list(values) 162 | assert values == ["a", "b"] 163 | 164 | {queue, values} = Queue.split(queue, 2) 165 | assert Queue.size(queue) == 0 166 | assert is_list(values) 167 | assert values == ["c", "d"] 168 | end 169 | 170 | test "popping an empty queue returns an error" do 171 | queue = Queue.new(10) 172 | {:error, :empty} = Queue.pop(queue) 173 | 174 | queue = Queue.push(queue, "a") 175 | {:ok, queue, "a"} = Queue.pop(queue) 176 | {:error, :empty} = Queue.pop(queue) 177 | 178 | {queue, 0} = Queue.append(queue, ["a", "b"]) 179 | {:ok, queue, "a"} = Queue.pop(queue) 180 | {:ok, queue, "b"} = Queue.pop(queue) 181 | {:error, :empty} = Queue.pop(queue) 182 | end 183 | 184 | test "queue can be viewed as a list" do 185 | queue = Queue.new(10) 186 | values = ["a", "b", "c"] 187 | {queue, 0} = Queue.append(queue, values) 188 | assert Queue.to_list(queue) == values 189 | end 190 | 191 | test "filter works" do 192 | item1 = %{a: 1, b: 1} 193 | item2 = %{a: 1, b: 2} 194 | item3 = %{a: 2, b: 3} 195 | 196 | queue = Queue.new(10) |> Queue.append([item1, item2, item3]) |> elem(0) 197 | 198 | queue = Queue.filter(queue, fn item -> item.a != 1 end) 199 | assert queue.size == 1 200 | assert Queue.to_list(queue) == [item3] 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------