Skip to content

Commit ab5a3d9

Browse files
author
Jandro Oliva
committed
Merge branch 'main' of github.com:Bluetab/td-cache into feature/td-7401
2 parents 0c0a544 + 1bf24c9 commit ab5a3d9

9 files changed

Lines changed: 188 additions & 18 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# The directory Mix will write compiled artifacts to.
22
/_build/
3+
/.elixir_ls
34

45
# If you run "mix test --cover", coverage assets end up here.
56
/cover/

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
[7.11.0] 2025-09-24
4+
5+
### Added
6+
7+
- [TD-7301] Add batch retrieval for Concepts
8+
39
## [7.7.0] 2025-06-20
410

511
### Added

lib/td_cache/concept_cache.ex

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule TdCache.ConceptCache do
1212
alias TdCache.Redix
1313
alias TdCache.RuleCache
1414
alias TdCache.TaxonomyCache
15+
alias TdCache.Utils.MapHelpers
1516

1617
require Logger
1718

@@ -66,6 +67,10 @@ defmodule TdCache.ConceptCache do
6667
GenServer.call(__MODULE__, {:get, id, property, opts})
6768
end
6869

70+
def get_many(ids, opts \\ []) do
71+
GenServer.call(__MODULE__, {:get_many, ids, opts})
72+
end
73+
6974
@doc """
7075
Reads a i18n of a concept for a given id from cache
7176
"""
@@ -169,6 +174,16 @@ defmodule TdCache.ConceptCache do
169174
{:reply, {:ok, prop}, state}
170175
end
171176

177+
@impl true
178+
def handle_call({:get_many, ids, opts}, _from, state) do
179+
concepts =
180+
ids
181+
|> Enum.uniq()
182+
|> read_concepts_batch(opts)
183+
184+
{:reply, {:ok, concepts}, state}
185+
end
186+
172187
@impl true
173188
def handle_call({:get_i18n, id}, _from, state) do
174189
prop =
@@ -243,6 +258,23 @@ defmodule TdCache.ConceptCache do
243258
end
244259
end
245260

261+
defp read_concepts_batch([], _opts), do: []
262+
263+
defp read_concepts_batch(ids, _opts) do
264+
transform_fun = fn [key, value] -> {String.to_atom(key), value} end
265+
266+
ids
267+
|> Enum.map(fn id -> ["HGETALL", "business_concept:#{id}"] end)
268+
|> Redix.transaction_pipeline()
269+
|> MapHelpers.zip_results_with_ids(ids)
270+
|> Enum.map(fn {id, hash} ->
271+
hash
272+
|> Redix.hash_to_map(transform_fun)
273+
|> Map.put(:id, id)
274+
|> concept_entry_to_map()
275+
end)
276+
end
277+
246278
defp read_concept_i18n(id) do
247279
concept_key = "business_concept:#{id}"
248280
{:ok, concept} = Redix.read_map(concept_key)

lib/td_cache/structure_cache.ex

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule TdCache.StructureCache do
66
alias TdCache.LinkCache
77
alias TdCache.Redix
88
alias TdCache.SystemCache
9+
alias TdCache.Utils.MapHelpers
910

1011
## Client API
1112

@@ -24,6 +25,15 @@ defmodule TdCache.StructureCache do
2425
{:ok, structure}
2526
end
2627

28+
def get_many(ids, opts \\ []) do
29+
structures =
30+
ids
31+
|> Enum.uniq()
32+
|> read_structures_batch(opts)
33+
34+
{:ok, structures}
35+
end
36+
2737
@doc """
2838
Deletes cache entries relating to a given structure id.
2939
"""
@@ -88,6 +98,40 @@ defmodule TdCache.StructureCache do
8898
end
8999
end
90100

101+
defp read_structures_batch([], _opts), do: []
102+
103+
defp read_structures_batch(ids, _opts) do
104+
transform_fun = fn [key, value] -> {String.to_atom(key), value} end
105+
106+
ids
107+
|> Enum.map(fn id -> ["HGETALL", "data_structure:#{id}"] end)
108+
|> Redix.transaction_pipeline()
109+
|> MapHelpers.zip_results_with_ids(ids)
110+
|> Enum.map(fn {id, hash} ->
111+
hash
112+
|> Redix.hash_to_map(transform_fun)
113+
|> enrich_structure_map(id)
114+
end)
115+
end
116+
117+
defp enrich_structure_map(structure, id) do
118+
{:ok, path} = Redix.read_list("data_structure:#{id}:path")
119+
{:ok, system} = SystemCache.get(Map.get(structure, :system_id))
120+
121+
metadata =
122+
case Map.get(structure, :metadata) do
123+
nil -> %{}
124+
metadata -> Jason.decode!(metadata)
125+
end
126+
127+
structure
128+
|> Map.update(:domain_ids, [], &Redix.to_integer_list!/1)
129+
|> put_optional(:path, path)
130+
|> put_optional(:system, system)
131+
|> Map.put(:metadata, metadata)
132+
|> Map.put(:id, id)
133+
end
134+
91135
def put_optional(map, _key, nil), do: map
92136
def put_optional(map, key, value), do: Map.put(map, key, value)
93137

lib/td_cache/utils/map_helpers.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,12 @@ defmodule TdCache.Utils.MapHelpers do
1717
def parse_string(:string, value) when is_atom(value), do: Atom.to_string(value)
1818
# def parse_string(:datetime, value) when is_binary(value), do: DateTime.from_iso8601(value)
1919
def parse_string(_, value), do: value
20+
21+
def zip_results_with_ids({:ok, results}, ids) do
22+
ids
23+
|> Enum.zip(results)
24+
|> Enum.filter(fn {_id, result} ->
25+
not Enum.empty?(result)
26+
end)
27+
end
2028
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule TdCache.MixProject do
44
def project do
55
[
66
app: :td_cache,
7-
version: "7.7.0",
7+
version: "7.11.0",
88
elixir: "~> 1.18",
99
elixirc_paths: elixirc_paths(Mix.env()),
1010
start_permanent: Mix.env() == :prod,

test/support/factory.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ defmodule TdCache.Factory do
2727
}
2828
end
2929

30+
def system_factory do
31+
%{
32+
id: System.unique_integer([:positive]),
33+
external_id: sequence("external_id_name"),
34+
name: sequence("system_name")
35+
}
36+
end
37+
38+
def structure_factory do
39+
%{
40+
id: System.unique_integer([:positive]),
41+
name: sequence("structure_name"),
42+
external_id: sequence("ds_external_id"),
43+
group: sequence("data_structure_version_group"),
44+
type: "type",
45+
path: ["foo", "bar"],
46+
updated_at: DateTime.utc_now(),
47+
metadata: %{"alias" => "source_alias"},
48+
system_id: System.unique_integer([:positive]),
49+
domain_ids: [1, 2],
50+
deleted_at: DateTime.utc_now()
51+
}
52+
end
53+
3054
def ingest_factory do
3155
%{
3256
id: unique_id(),

test/td_cache/concept_cache_test.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,41 @@ defmodule TdCache.ConceptCacheTest do
9494
assert {:ok, ^content} = ConceptCache.get(concept.id, :content)
9595
end
9696

97+
test "reads many concepts", %{domain: %{id: domain_id}} do
98+
%{id: shared_domain_id} = build(:domain)
99+
100+
[%{id: id1}, %{id: id2}, %{id: id3}] =
101+
inserted_concepts =
102+
Enum.map(1..3, fn i ->
103+
concept =
104+
if rem(i, 2) == 0 do
105+
build(:concept, domain_id: domain_id, shared_to_ids: [shared_domain_id])
106+
else
107+
build(:concept, domain_id: domain_id)
108+
end
109+
110+
{:ok, _} = ConceptCache.put(concept)
111+
concept
112+
end)
113+
114+
ids = [id1, id2, id3, id1]
115+
116+
not_valid_id = Enum.max(ids) + 1
117+
118+
{:ok, cache_concepts} = ConceptCache.get_many(Enum.shuffle(ids ++ [not_valid_id]))
119+
120+
assert Enum.count(cache_concepts) == 3
121+
122+
assert Enum.all?(cache_concepts, fn %{id: concept_id, name: concept_name} ->
123+
Enum.find(inserted_concepts, fn %{id: inserted_id, name: inserted_name} ->
124+
inserted_id == concept_id and inserted_name == concept_name
125+
end)
126+
end)
127+
128+
Redix.command!(["DEL"] ++ Redix.command!(["KEYS", "business_concept:*"]))
129+
Redix.command!(["DEL", "domain:deleted_ids"])
130+
end
131+
97132
test "reads the content property of a concept with specific lang", %{
98133
concept: concept
99134
} do

test/td_cache/structure_cache_test.exs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule TdCache.StructureCacheTest do
22
use ExUnit.Case
33

44
import Assertions
5+
import TdCache.Factory
56

67
alias TdCache.LinkCache
78
alias TdCache.Redix
@@ -11,21 +12,8 @@ defmodule TdCache.StructureCacheTest do
1112
doctest TdCache.StructureCache
1213

1314
setup do
14-
system = %{id: System.unique_integer([:positive]), external_id: "foo", name: "bar"}
15-
16-
structure = %{
17-
id: System.unique_integer([:positive]),
18-
name: "name",
19-
external_id: "ext_id",
20-
group: "group",
21-
type: "type",
22-
path: ["foo", "bar"],
23-
updated_at: DateTime.utc_now(),
24-
metadata: %{"alias" => "source_alias"},
25-
system_id: system.id,
26-
domain_ids: [1, 2],
27-
deleted_at: DateTime.utc_now()
28-
}
15+
system = build(:system)
16+
structure = build(:structure, system_id: system.id)
2917

3018
{:ok, _} = SystemCache.put(system)
3119

@@ -68,6 +56,38 @@ defmodule TdCache.StructureCacheTest do
6856
assert s.system == system
6957
end
7058

59+
test "reads many structures", %{system: system} do
60+
%{id: domain_id1} = build(:domain)
61+
%{id: domain_id2} = build(:domain)
62+
63+
[%{id: id1}, %{id: id2}, %{id: id3}] =
64+
inserted_structures =
65+
Enum.map(1..3, fn _ ->
66+
structure =
67+
build(:structure, domain_ids: [domain_id1, domain_id2], system_id: system.id)
68+
69+
{:ok, _} = StructureCache.put(structure)
70+
structure
71+
end)
72+
73+
ids = [id1, id2, id3, id1]
74+
75+
not_valid_id = Enum.max(ids) + 1
76+
77+
{:ok, cache_structures} = StructureCache.get_many(Enum.shuffle(ids ++ [not_valid_id]))
78+
79+
assert Enum.count(cache_structures) == 3
80+
81+
assert Enum.all?(cache_structures, fn %{id: structure_id, name: structure_name} ->
82+
Enum.find(inserted_structures, fn %{id: inserted_id, name: inserted_name} ->
83+
inserted_id == structure_id and inserted_name == structure_name
84+
end)
85+
end)
86+
87+
Redix.command!(["DEL"] ++ Redix.command!(["KEYS", "data_structure:*"]))
88+
Redix.command!(["DEL", "domain:deleted_ids"])
89+
end
90+
7191
test "returns an empty map for metadata if not present", %{structure: structure} do
7292
structure = Map.delete(structure, :metadata)
7393
assert {:ok, _} = StructureCache.put(structure)
@@ -93,15 +113,15 @@ defmodule TdCache.StructureCacheTest do
93113
end
94114

95115
test "does not update a structure already cached in redis having same update_at value", %{
96-
structure: structure
116+
structure: %{external_id: external_id} = structure
97117
} do
98118
{:ok, _} = StructureCache.put(structure)
99119
{:ok, s} = StructureCache.get(structure.id)
100120
assert s
101121
updated_structure = Map.put(structure, :external_id, "new_ext_id")
102122
{:ok, _} = StructureCache.put(updated_structure)
103123
{:ok, s} = StructureCache.get(structure.id)
104-
assert s.external_id == "ext_id"
124+
assert s.external_id == external_id
105125
end
106126

107127
test "updates a structure already cached in redis when deleted_at has changed", %{

0 commit comments

Comments
 (0)