Current section
Files
Jump to
Current section
Files
lib/bacnet/stack/log_buffer.ex
defmodule BACnet.Stack.LogBuffer do
@moduledoc """
Simple log buffer implementation for event and trend logs.
The type of the item is not enforced.
The log buffer can maintain a maximum buffer size by dropping the oldest item
on overflow (ring buffer).
"""
alias BACnet.Protocol.EventLogRecord
alias BACnet.Protocol.LogMultipleRecord
alias BACnet.Protocol.LogRecord
@behaviour BACnet.Stack.LogBufferBehaviour
@typedoc """
Log buffer item type.
"""
@type item :: EventLogRecord.t() | LogRecord.t() | LogMultipleRecord.t()
@typedoc """
Implementation detail and thus private API. Changes to it do not count
towards Semantic Versioning.
"""
@opaque items :: :queue.queue(item())
@typedoc """
Representative type for the log buffer.
"""
@type t :: %__MODULE__{
items: items(),
max_size: pos_integer() | nil,
size: non_neg_integer()
}
@fields [:items, :max_size, :size]
@enforce_keys @fields
defstruct @fields
@doc """
Creates a new log buffer with an optional max size.
"""
@spec new(pos_integer() | nil) :: t()
def new(max_size \\ nil) when is_nil(max_size) or (is_integer(max_size) and max_size >= 1) do
%__MODULE__{
items: :queue.new(),
max_size: max_size,
size: 0
}
end
@doc """
Checks in an item.
If the log buffer has a max size, the max size will be maintained (meaning
the oldest item gets dropped on overflow).
"""
@spec checkin(t(), item()) :: t()
def checkin(%__MODULE__{} = buffer, item) do
middle_queue = :queue.in(item, buffer.items)
{new_queue, new_size} =
case %{buffer | size: buffer.size + 1} do
%{max_size: nil, size: size} -> {middle_queue, size}
%{max_size: max, size: size} when size > max -> {:queue.drop(middle_queue), buffer.size}
%{size: size} -> {middle_queue, size}
end
%__MODULE__{buffer | items: new_queue, size: new_size}
end
@doc """
Checks out the oldest item in the log buffer.
If the log buffer is empty, `nil` will be returned as item.
"""
@spec checkout(t()) :: {item :: item() | nil, new_buffer :: t()}
def checkout(%__MODULE__{} = buffer) do
{item, new_queue, new_size} =
case :queue.out(buffer.items) do
{{:value, item}, q2} -> {item, q2, buffer.size - 1}
{:empty, q1} -> {nil, q1, buffer.size}
end
{item, %__MODULE__{buffer | items: new_queue, size: new_size}}
end
@doc """
Drops the specified amount of oldest items from the log buffer.
"""
@spec drop(t(), pos_integer()) :: t()
def drop(buffer, amount \\ 1)
def drop(%__MODULE__{size: size} = buffer, amount) when is_integer(amount) and amount >= size do
truncate(buffer)
end
def drop(%__MODULE__{} = buffer, amount) when is_integer(amount) and amount >= 1 do
%__MODULE__{
buffer
| items:
Enum.reduce(1..min(amount, buffer.size)//1, buffer.items, fn _index, acc ->
:queue.drop(acc)
end),
size: max(0, buffer.size - amount)
}
end
@doc """
Creates a new log buffer from the given list, with an optional max size.
The head of the list will be the oldest entry in the log buffer.
If a max size is specified, the list will be truncated to the max size (from the end).
The queue length will be calculated in any case. This operation is O(n).
"""
@spec from_list(list(), pos_integer() | nil) :: t()
def from_list(list, max_size \\ nil)
when (is_list(list) and is_nil(max_size)) or (is_integer(max_size) and max_size >= 1) do
buffer =
list
|> then(fn list ->
if max_size do
list
|> Enum.reverse()
|> Enum.take(max_size)
|> Enum.reverse()
else
list
end
end)
|> :queue.from_list()
%__MODULE__{
items: buffer,
max_size: max_size,
size: :queue.len(buffer)
}
end
@doc """
Get the size of the log buffer.
"""
@spec get_size(t()) :: non_neg_integer()
def get_size(%__MODULE__{size: size} = _buffer) do
size
end
@doc """
Peeks into the log buffer and returns the oldest item without removing it.
"""
@spec peek(t()) :: {:ok, item()} | :error
def peek(%__MODULE__{} = buffer) do
case :queue.peek(buffer.items) do
{:value, item} -> {:ok, item}
:empty -> :error
end
end
@doc """
Peeks into the log buffer and returns the newest item without removing it.
"""
@spec peek_r(t()) :: {:ok, item()} | :error
def peek_r(%__MODULE__{} = buffer) do
case :queue.peek_r(buffer.items) do
{:value, item} -> {:ok, item}
:empty -> :error
end
end
@doc """
Reads the specified count, starting from the given position (offset), from the log buffer.
"""
@spec read_range(t(), non_neg_integer(), pos_integer()) :: [item()]
def read_range(%__MODULE__{} = buffer, offset \\ 0, count)
when is_integer(offset) and offset >= 0 and is_integer(count) and count >= 1 do
Enum.slice(:queue.to_list(buffer.items), offset, count)
end
@doc """
Creates a list from the log buffer.
"""
@spec to_list(t()) :: [item()]
def to_list(%__MODULE__{} = buffer) do
:queue.to_list(buffer.items)
end
@doc """
Truncates the log buffer to size zero.
"""
@spec truncate(t()) :: t()
def truncate(%__MODULE__{} = buffer) do
%__MODULE__{buffer | items: :queue.new(), size: 0}
end
end