feat(streams): add stream entry log structure
This commit is contained in:
1 parent
56414bb279
commit
b399da2e2a
2 files changed
+314
No files matched your search
@@ -0,0 +1,309 @@
|
|||||||
|
defmodule Excache.Stream do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
alias Excache.Stream.Group
|
||||||
|
|
||||||
|
defstruct entries: :gb_trees.empty(),
|
||||||
|
lastId: {0, 0},
|
||||||
|
maxDeletedId: {0, 0},
|
||||||
|
length: 0,
|
||||||
|
entriesAdded: 0,
|
||||||
|
groups: %{}
|
||||||
|
|
||||||
|
def new, do: %__MODULE__{}
|
||||||
|
|
||||||
|
def length(%__MODULE__{length: length}), do: length
|
||||||
|
|
||||||
|
def formatId({ms, seq}), do: "#{ms}-#{seq}"
|
||||||
|
|
||||||
|
def parseId(raw, defaultSeq) do
|
||||||
|
case String.split(raw, "-", parts: 2) do
|
||||||
|
[ms] ->
|
||||||
|
withInteger(ms, fn value -> {:ok, {value, defaultSeq}} end)
|
||||||
|
|
||||||
|
[ms, "*"] ->
|
||||||
|
withInteger(ms, fn value -> {:ok, {:autoSeq, value}} end)
|
||||||
|
|
||||||
|
[ms, seq] ->
|
||||||
|
withInteger(ms, fn msValue ->
|
||||||
|
withInteger(seq, fn seqValue -> {:ok, {msValue, seqValue}} end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parseAddId("*", _now), do: {:ok, :auto}
|
||||||
|
|
||||||
|
def parseAddId(raw, _now) do
|
||||||
|
case String.split(raw, "-", parts: 2) do
|
||||||
|
[ms] ->
|
||||||
|
withInteger(ms, fn value -> {:ok, {:autoSeq, value}} end)
|
||||||
|
|
||||||
|
[ms, "*"] ->
|
||||||
|
withInteger(ms, fn value -> {:ok, {:autoSeq, value}} end)
|
||||||
|
|
||||||
|
[ms, seq] ->
|
||||||
|
withInteger(ms, fn msValue ->
|
||||||
|
withInteger(seq, fn seqValue -> {:ok, {:full, msValue, seqValue}} end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parseRangeStart("-"), do: {:ok, {:incl, {0, 0}}}
|
||||||
|
def parseRangeStart("+"), do: {:ok, {:incl, {maxMs(), maxSeq()}}}
|
||||||
|
def parseRangeStart("(" <> rest), do: rangeBound(rest, 0, :excl)
|
||||||
|
def parseRangeStart(raw), do: rangeBound(raw, 0, :incl)
|
||||||
|
|
||||||
|
def parseRangeEnd("-"), do: {:ok, {:incl, {0, 0}}}
|
||||||
|
def parseRangeEnd("+"), do: {:ok, {:incl, {maxMs(), maxSeq()}}}
|
||||||
|
def parseRangeEnd("(" <> rest), do: rangeBound(rest, maxSeq(), :excl)
|
||||||
|
def parseRangeEnd(raw), do: rangeBound(raw, maxSeq(), :incl)
|
||||||
|
|
||||||
|
def add(stream, idSpec, fields, now) do
|
||||||
|
case resolveId(stream, idSpec, now) do
|
||||||
|
{:ok, id} ->
|
||||||
|
entries = :gb_trees.insert(id, fields, stream.entries)
|
||||||
|
|
||||||
|
updated = %{
|
||||||
|
stream
|
||||||
|
| entries: entries,
|
||||||
|
lastId: id,
|
||||||
|
length: stream.length + 1,
|
||||||
|
entriesAdded: stream.entriesAdded + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, updated, id}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(stream, id) do
|
||||||
|
case :gb_trees.lookup(id, stream.entries) do
|
||||||
|
{:value, fields} -> {:ok, fields}
|
||||||
|
:none -> :miss
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def member?(stream, id), do: :gb_trees.is_defined(id, stream.entries)
|
||||||
|
|
||||||
|
def range(stream, {startKind, startId}, {endKind, endId}, count) do
|
||||||
|
lower = if startKind == :excl, do: successor(startId), else: startId
|
||||||
|
|
||||||
|
upperOk = fn id ->
|
||||||
|
case endKind do
|
||||||
|
:incl -> id <= endId
|
||||||
|
:excl -> id < endId
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if stream.length == 0 or lower > endId do
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
iterator = :gb_trees.iterator_from(lower, stream.entries)
|
||||||
|
collectForward(iterator, upperOk, count, [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reverseRange(stream, endBound, startBound, count) do
|
||||||
|
stream
|
||||||
|
|> range(startBound, endBound, :all)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> take(count)
|
||||||
|
end
|
||||||
|
|
||||||
|
def afterId(stream, id, count) do
|
||||||
|
range(stream, {:excl, id}, {:incl, {maxMs(), maxSeq()}}, count)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(stream, ids) do
|
||||||
|
Enum.reduce(ids, {stream, 0}, fn id, {acc, removed} ->
|
||||||
|
if :gb_trees.is_defined(id, acc.entries) do
|
||||||
|
entries = :gb_trees.delete(id, acc.entries)
|
||||||
|
maxDeleted = max(acc.maxDeletedId, id)
|
||||||
|
{%{acc | entries: entries, length: acc.length - 1, maxDeletedId: maxDeleted}, removed + 1}
|
||||||
|
else
|
||||||
|
{acc, removed}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def trimMaxlen(stream, maxlen) when maxlen < 0, do: {stream, 0}
|
||||||
|
|
||||||
|
def trimMaxlen(stream, maxlen) do
|
||||||
|
dropSmallest(stream, max(stream.length - maxlen, 0), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def trimMinId(stream, minId) do
|
||||||
|
dropBelow(stream, minId, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def setLastId(stream, id), do: %{stream | lastId: id}
|
||||||
|
|
||||||
|
def lastEntryId(%__MODULE__{lastId: lastId}), do: lastId
|
||||||
|
|
||||||
|
def info(stream) do
|
||||||
|
%{
|
||||||
|
length: stream.length,
|
||||||
|
lastGeneratedId: stream.lastId,
|
||||||
|
maxDeletedId: stream.maxDeletedId,
|
||||||
|
entriesAdded: stream.entriesAdded,
|
||||||
|
groups: map_size(stream.groups),
|
||||||
|
firstEntry: firstEntryPair(stream),
|
||||||
|
lastEntry: lastEntry(stream)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp firstEntryPair(stream) do
|
||||||
|
case :gb_trees.is_empty(stream.entries) do
|
||||||
|
true -> nil
|
||||||
|
false -> :gb_trees.smallest(stream.entries) |> pairFromKV()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def createGroup(stream, name, startId) do
|
||||||
|
if Map.has_key?(stream.groups, name) do
|
||||||
|
{:error, :busygroup}
|
||||||
|
else
|
||||||
|
group = %Group{lastDeliveredId: startId, entriesRead: 0}
|
||||||
|
{:ok, %{stream | groups: Map.put(stream.groups, name, group)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroyGroup(stream, name) do
|
||||||
|
if Map.has_key?(stream.groups, name) do
|
||||||
|
{%{stream | groups: Map.delete(stream.groups, name)}, 1}
|
||||||
|
else
|
||||||
|
{stream, 0}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetchGroup(stream, name), do: Map.fetch(stream.groups, name)
|
||||||
|
|
||||||
|
def putGroup(stream, name, group), do: %{stream | groups: Map.put(stream.groups, name, group)}
|
||||||
|
|
||||||
|
def firstId(stream) do
|
||||||
|
case :gb_trees.is_empty(stream.entries) do
|
||||||
|
true -> nil
|
||||||
|
false -> :gb_trees.smallest(stream.entries) |> elem(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def lastEntry(stream) do
|
||||||
|
case :gb_trees.is_empty(stream.entries) do
|
||||||
|
true -> nil
|
||||||
|
false -> :gb_trees.largest(stream.entries) |> pairFromKV()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def toList(stream) do
|
||||||
|
stream.entries |> :gb_trees.to_list() |> Enum.map(&pairFromKV/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def successor({ms, seq}) when seq >= 0, do: {ms, seq + 1}
|
||||||
|
|
||||||
|
defp pairFromKV({id, fields}), do: {id, fields}
|
||||||
|
|
||||||
|
defp resolveId(stream, :auto, now) do
|
||||||
|
{lastMs, lastSeq} = stream.lastId
|
||||||
|
ms = max(now, lastMs)
|
||||||
|
seq = if ms == lastMs, do: lastSeq + 1, else: 0
|
||||||
|
validate(stream, {ms, seq})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolveId(stream, {:autoSeq, ms}, _now) do
|
||||||
|
{lastMs, lastSeq} = stream.lastId
|
||||||
|
|
||||||
|
cond do
|
||||||
|
ms < lastMs -> {:error, :smaller}
|
||||||
|
ms == lastMs -> validate(stream, {ms, lastSeq + 1})
|
||||||
|
true -> validate(stream, {ms, 0})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp resolveId(stream, {:full, ms, seq}, _now) do
|
||||||
|
validate(stream, {ms, seq})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate(stream, {0, 0}), do: validateAgainst(stream, {0, 0}, :zero)
|
||||||
|
defp validate(stream, id), do: validateAgainst(stream, id, :normal)
|
||||||
|
|
||||||
|
defp validateAgainst(_stream, {0, 0}, :zero), do: {:error, :zero}
|
||||||
|
|
||||||
|
defp validateAgainst(stream, id, _kind) do
|
||||||
|
if id > stream.lastId, do: {:ok, id}, else: {:error, :smaller}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp collectForward(_iterator, _upperOk, 0, acc), do: Enum.reverse(acc)
|
||||||
|
|
||||||
|
defp collectForward(iterator, upperOk, count, acc) do
|
||||||
|
case :gb_trees.next(iterator) do
|
||||||
|
:none ->
|
||||||
|
Enum.reverse(acc)
|
||||||
|
|
||||||
|
{id, fields, nextIterator} ->
|
||||||
|
if upperOk.(id) do
|
||||||
|
remaining = if count == :all, do: :all, else: count - 1
|
||||||
|
collectForward(nextIterator, upperOk, remaining, [{id, fields} | acc])
|
||||||
|
else
|
||||||
|
Enum.reverse(acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp take(list, :all), do: list
|
||||||
|
defp take(list, count), do: Enum.take(list, count)
|
||||||
|
|
||||||
|
defp dropSmallest(stream, 0, removed), do: {stream, removed}
|
||||||
|
|
||||||
|
defp dropSmallest(stream, remaining, removed) do
|
||||||
|
{id, _fields, entries} = :gb_trees.take_smallest(stream.entries)
|
||||||
|
maxDeleted = max(stream.maxDeletedId, id)
|
||||||
|
updated = %{stream | entries: entries, length: stream.length - 1, maxDeletedId: maxDeleted}
|
||||||
|
dropSmallest(updated, remaining - 1, removed + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp dropBelow(stream, minId, removed) do
|
||||||
|
case :gb_trees.is_empty(stream.entries) do
|
||||||
|
true ->
|
||||||
|
{stream, removed}
|
||||||
|
|
||||||
|
false ->
|
||||||
|
{id, _fields} = :gb_trees.smallest(stream.entries)
|
||||||
|
|
||||||
|
if id < minId do
|
||||||
|
{_, _, entries} = :gb_trees.take_smallest(stream.entries)
|
||||||
|
maxDeleted = max(stream.maxDeletedId, id)
|
||||||
|
|
||||||
|
updated = %{
|
||||||
|
stream
|
||||||
|
| entries: entries,
|
||||||
|
length: stream.length - 1,
|
||||||
|
maxDeletedId: maxDeleted
|
||||||
|
}
|
||||||
|
|
||||||
|
dropBelow(updated, minId, removed + 1)
|
||||||
|
else
|
||||||
|
{stream, removed}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp rangeBound(raw, defaultSeq, kind) do
|
||||||
|
case parseId(raw, defaultSeq) do
|
||||||
|
{:ok, {:autoSeq, _ms}} -> :error
|
||||||
|
{:ok, id} -> {:ok, {kind, id}}
|
||||||
|
:error -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp withInteger(raw, continue) do
|
||||||
|
case Integer.parse(raw) do
|
||||||
|
{value, ""} when value >= 0 -> continue.(value)
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maxMs, do: 0xFFFFFFFFFFFFFFFF
|
||||||
|
defp maxSeq, do: 0xFFFFFFFFFFFFFFFF
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
defmodule Excache.Stream.Group do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
defstruct lastDeliveredId: {0, 0}, pending: %{}, consumers: %{}, entriesRead: 0
|
||||||
|
end
|
||||||
Reference in new issue
Block a user