|
1 | 1 | defmodule Electric.ExpiryManagerTest do |
2 | 2 | use ExUnit.Case, async: true |
3 | | - use Support.Mock |
4 | 3 | use Repatch.ExUnit |
5 | 4 |
|
6 | | - alias Electric.Replication.Changes |
7 | | - alias Electric.Replication.LogOffset |
8 | | - alias Electric.ShapeCache |
9 | 5 | alias Electric.ShapeCache.ExpiryManager |
10 | | - alias Electric.ShapeCache.Storage |
| 6 | + alias Electric.ShapeCache.ShapeCleaner |
| 7 | + alias Electric.ShapeCache.ShapeStatus |
11 | 8 | alias Electric.Shapes.Shape |
| 9 | + alias Support.RepatchExt |
12 | 10 |
|
13 | | - import Mox |
14 | 11 | import Support.ComponentSetup |
15 | 12 | import Support.TestUtils |
16 | 13 |
|
17 | | - @stub_inspector Support.StubInspector.new( |
18 | | - tables: [{1, {"public", "items"}}], |
19 | | - columns: [ |
20 | | - %{ |
21 | | - name: "id", |
22 | | - type: "int8", |
23 | | - type_id: {20, 1}, |
24 | | - pk_position: 0, |
25 | | - is_generated: false |
26 | | - }, |
27 | | - %{name: "value", type: "text", type_id: {25, 1}, is_generated: false} |
28 | | - ] |
29 | | - ) |
30 | | - @shape Shape.new!("items", inspector: @stub_inspector) |
31 | | - |
32 | | - # {xmin, xmax, xip_list} |
33 | | - @pg_snapshot_xmin_10 {10, 11, [10]} |
34 | | - |
35 | | - @moduletag :tmp_dir |
36 | | - |
37 | | - defmodule TempPubManager do |
38 | | - def add_shape(_handle, _, opts) do |
39 | | - send(opts[:test_pid], {:called, :prepare_tables_fn}) |
40 | | - end |
41 | | - end |
42 | | - |
43 | | - setup :verify_on_exit! |
44 | | - |
45 | | - setup do |
46 | | - %{inspector: @stub_inspector, pool: nil} |
47 | | - end |
48 | | - |
49 | 14 | setup [ |
50 | | - :with_persistent_kv, |
51 | 15 | :with_stack_id_from_test, |
52 | | - :with_async_deleter, |
53 | | - :with_pure_file_storage, |
54 | | - :with_shape_status, |
55 | | - :with_shape_cleaner, |
56 | | - :with_status_monitor, |
57 | | - :with_shape_monitor, |
58 | | - :with_log_chunking, |
59 | | - :with_registry, |
60 | | - :with_shape_log_collector, |
61 | | - :with_noop_publication_manager |
| 16 | + :with_status_monitor |
62 | 17 | ] |
63 | 18 |
|
64 | | - test "expires shapes if shape count has gone over max_shapes", ctx do |
65 | | - Support.TestUtils.patch_snapshotter(fn parent, shape_handle, _shape, %{storage: storage} -> |
66 | | - GenServer.cast(parent, {:pg_snapshot_known, shape_handle, @pg_snapshot_xmin_10}) |
67 | | - Storage.make_new_snapshot!([["test"]], storage) |
68 | | - GenServer.cast(parent, {:snapshot_started, shape_handle}) |
69 | | - end) |
70 | | - |
71 | | - %{shape_cache_opts: opts} = with_shape_cache(ctx) |
72 | | - |
73 | | - start_supervised!( |
74 | | - {ExpiryManager, max_shapes: 1, expiry_batch_size: 1, period: 10, stack_id: ctx.stack_id} |
75 | | - ) |
76 | | - |
77 | | - {shape_handle, _} = ShapeCache.get_or_create_shape_handle(@shape, opts) |
78 | | - assert :started = ShapeCache.await_snapshot_start(shape_handle, opts) |
| 19 | + @max_shapes 10 |
79 | 20 |
|
80 | | - consumer_ref = |
81 | | - Electric.Shapes.Consumer.whereis(ctx.stack_id, shape_handle) |
82 | | - |> Process.monitor() |
| 21 | + setup %{stack_id: stack_id} do |
| 22 | + ShapeStatus.initialise(stack_id) |
83 | 23 |
|
84 | | - storage = Storage.for_shape(shape_handle, ctx.storage) |
85 | | - writer = Storage.init_writer!(storage, @shape) |
86 | | - |
87 | | - Storage.append_to_log!( |
88 | | - changes_to_log_items([ |
89 | | - %Changes.NewRecord{ |
90 | | - relation: {"public", "items"}, |
91 | | - record: %{"id" => "1", "value" => "Alice"}, |
92 | | - log_offset: LogOffset.new(Electric.Postgres.Lsn.from_integer(1000), 0) |
93 | | - } |
94 | | - ]), |
95 | | - writer |
96 | | - ) |
| 24 | + expiry_manager = |
| 25 | + start_supervised!( |
| 26 | + {ExpiryManager, |
| 27 | + max_shapes: @max_shapes, expiry_batch_size: 1, period: 1, stack_id: stack_id} |
| 28 | + ) |
97 | 29 |
|
98 | | - assert Storage.snapshot_started?(storage) |
| 30 | + Repatch.patch(ShapeCleaner, :remove_shape, [mode: :shared], fn shape_handle, |
| 31 | + stack_id: stack_id -> |
| 32 | + ShapeStatus.remove_shape(stack_id, shape_handle) |
| 33 | + end) |
99 | 34 |
|
100 | | - assert Enum.count(Storage.get_log_stream(LogOffset.last_before_real_offsets(), storage)) == |
101 | | - 1 |
| 35 | + Repatch.allow(self(), expiry_manager) |
| 36 | + %{expiry_manager: expiry_manager} |
| 37 | + end |
102 | 38 |
|
103 | | - {new_shape_handle, _} = |
104 | | - ShapeCache.get_or_create_shape_handle(%{@shape | where: "1 == 1"}, opts) |
| 39 | + describe "when stack is active" do |
| 40 | + setup :set_status_to_active |
105 | 41 |
|
106 | | - assert :started = ShapeCache.await_snapshot_start(new_shape_handle, opts) |
| 42 | + test "expires shapes if shape count has gone over max_shapes", ctx do |
| 43 | + for i <- 1..(@max_shapes + 1) do |
| 44 | + ShapeStatus.add_shape(ctx.stack_id, create_shape(i)) |
| 45 | + end |
107 | 46 |
|
108 | | - assert_receive {:DOWN, ^consumer_ref, :process, _pid, {:shutdown, :cleanup}} |
| 47 | + assert RepatchExt.called_within_ms?(ShapeCleaner, :remove_shape, 2, 50, ctx.expiry_manager) |
| 48 | + end |
109 | 49 |
|
110 | | - assert :ok = await_for_storage_to_raise(storage) |
| 50 | + test "does not expires shapes if shape count has not gone over max_shapes", ctx do |
| 51 | + for i <- 1..@max_shapes do |
| 52 | + ShapeStatus.add_shape(ctx.stack_id, create_shape(i)) |
| 53 | + end |
111 | 54 |
|
112 | | - {shape_handle2, _} = ShapeCache.get_or_create_shape_handle(@shape, opts) |
113 | | - assert shape_handle != shape_handle2 |
114 | | - assert :started = ShapeCache.await_snapshot_start(shape_handle2, opts) |
| 55 | + refute RepatchExt.called_within_ms?(ShapeCleaner, :remove_shape, 2, 50, ctx.expiry_manager) |
| 56 | + end |
115 | 57 | end |
116 | 58 |
|
117 | | - defp await_for_storage_to_raise(storage, timeout \\ 5_000) |
| 59 | + describe "when stack is not active" do |
| 60 | + test "does not expires shapes even if shape count has gone over max_shapes", ctx do |
| 61 | + for i <- 1..(@max_shapes + 1) do |
| 62 | + ShapeStatus.add_shape(ctx.stack_id, create_shape(i)) |
| 63 | + end |
118 | 64 |
|
119 | | - defp await_for_storage_to_raise(_storage, timeout) when timeout <= 0 do |
120 | | - raise "Storage did not raise Storage.Error in time" |
| 65 | + refute RepatchExt.called_within_ms?(ShapeCleaner, :remove_shape, 2, 50, ctx.expiry_manager) |
| 66 | + end |
121 | 67 | end |
122 | 68 |
|
123 | | - defp await_for_storage_to_raise(storage, timeout) do |
124 | | - try do |
125 | | - start_time = System.monotonic_time() |
126 | | - Stream.run(Storage.get_log_stream(LogOffset.before_all(), storage)) |
127 | | - Process.sleep(50) |
128 | | - elapsed = System.monotonic_time() - start_time |
| 69 | + @inspector Support.StubInspector.new( |
| 70 | + tables: ["t1"], |
| 71 | + columns: [ |
| 72 | + %{name: "id", type: "int8", pk_position: 0} |
| 73 | + ] |
| 74 | + ) |
129 | 75 |
|
130 | | - await_for_storage_to_raise( |
131 | | - storage, |
132 | | - timeout - System.convert_time_unit(elapsed, :native, :millisecond) |
133 | | - ) |
134 | | - rescue |
135 | | - Storage.Error -> :ok |
136 | | - end |
| 76 | + defp create_shape(id) do |
| 77 | + Shape.new!("t1", where: "id = #{id}", inspector: @inspector) |
137 | 78 | end |
138 | 79 | end |
0 commit comments