Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Release 0.4.1

### Bug Fixes

- Fixed `select_replace` match spec in `put_newer/5` and `put_all_newer/3` when
values contain maps (including maps nested inside tuples or lists). Maps in
match spec bodies are now wrapped with `{:const, map}` via `ms_literal/1`,
which tells ETS to treat them as opaque literals.

## Release 0.4.0

### Enhancements
Expand Down
11 changes: 8 additions & 3 deletions lib/partitioned_buffer/partition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,9 @@ defmodule PartitionedBuffer.Partition do
#
# In match spec bodies, bare tuples are interpreted as operations/function
# calls, NOT as literal data. We wrap key and value with ms_literal/1 so
# tuples use the {{...}} constructor form that ETS understands. This handles
# tuples and lists (including nested combinations). Map keys/values with
# embedded tuples are a known limitation of ETS select_replace.
# tuples use the {{...}} constructor form and maps use {:const, map} that
# ETS understands. This handles tuples, maps, and lists (including nested
# combinations).
[
{
# Match: {entry, key, value, existing_version, updates} where key is literal
Expand All @@ -549,6 +549,7 @@ defmodule PartitionedBuffer.Partition do
# Wraps a term so it is safe to use as a literal in a match spec body.
# In match spec bodies, bare tuples are interpreted as operations — not
# data. The {{...}} form tells ETS to construct a tuple from its elements.
# Maps use {:const, map} to be treated as opaque literals.
defp ms_literal(value) when is_tuple(value) do
value
|> Tuple.to_list()
Expand All @@ -561,6 +562,10 @@ defmodule PartitionedBuffer.Partition do
Enum.map(value, &ms_literal/1)
end

defp ms_literal(value) when is_map(value) do
{:const, value}
end

defp ms_literal(value) do
value
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule PartitionedBuffer.MixProject do
use Mix.Project

@version "0.4.0"
@version "0.4.1"
@source_url "https://github.com/appcues/partitioned_buffer"

def project do
Expand Down
43 changes: 43 additions & 0 deletions test/partitioned_buffer/map_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,49 @@ defmodule PartitionedBuffer.MapTest do
assert_receive {:process_completed, [{^key, ^value1, 200, 1}]}, @default_timeout
end

test "ok: updates existing entry with nested map value", %{buffer: buff} do
key = :nested_map

value0 = %{
users: %{admin: %{name: "alice", roles: [:admin, :user]}},
meta: %{nested: %{deep: %{level: 3}}}
}

value1 = %{
users: %{admin: %{name: {:x, "bob"}, roles: [:user]}},
meta: %{nested: %{deep: %{level: 5, extra: true}}}
}

assert M.put_newer(buff, key, value0, 100) == :ok
assert M.put_newer(buff, key, value1, 200) == :ok

assert M.size(buff) == 1
assert M.get(buff, key) == value1

assert_receive {@processing_stop_event, %{duration: _, size: 1},
%{buffer: ^buff, partition: _}},
@default_timeout

assert_receive {:process_completed, [{^key, ^value1, 200, 1}]}, @default_timeout
end

test "ok: updates existing entry with tuple value containing maps", %{buffer: buff} do
value0 = {:ok, %{a: 1, b: %{c: [1, 2, %{d: 3}]}}}
value1 = {:ok, %{a: 2, b: %{c: [3, 4, %{d: 5}]}}}

assert M.put_newer(buff, :tuple_map, value0, 100) == :ok
assert M.put_newer(buff, :tuple_map, value1, 200) == :ok

assert M.size(buff) == 1
assert M.get(buff, :tuple_map) == value1

assert_receive {@processing_stop_event, %{duration: _, size: 1},
%{buffer: ^buff, partition: _}},
@default_timeout

assert_receive {:process_completed, [{:tuple_map, ^value1, 200, 1}]}, @default_timeout
end

test "error: put_newer raises ArgumentError for non-integer version", %{buffer: buff} do
assert_raise ArgumentError, ~r/invalid entry/, fn ->
M.put_newer(buff, :key1, "value1", "not_an_integer")
Expand Down
Loading