Skip to content

Commit 5491d2c

Browse files
committed
Periodically refresh publication independently of schema reconciler
So far SchemaReconciler has been responsible both for updating the publication when it detected schema changes as well as when there are no schema changes. This latter periodic update is useful to keep the publication up-to-date with the filters but it's not really the responsibility of SchemaReconciler.
1 parent d1228a0 commit 5491d2c

8 files changed

Lines changed: 73 additions & 37 deletions

File tree

‎integration-tests/tests/not-owner-of-the-table.lux‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"-v $(realpath ../scripts/low_privilege_db.sh):/docker-entrypoint-initdb.d/initdb-low_privilege_db.sh"]
1616

1717
## Start the sync service
18-
[invoke setup_electric_with_env "ELECTRIC_TWEAKS_SCHEMA_RECONCILER_PERIOD=2s"]
18+
[invoke setup_electric_with_env "ELECTRIC_TWEAKS_PUBLICATION_REFRESH_PERIOD=2s"]
1919

2020
## Verify that the replication pipeline has initialized successfully
2121
[shell electric]
@@ -62,15 +62,17 @@
6262
??{"message":"Unable to create initial snapshot: must be owner of table items"}
6363

6464
[shell electric]
65-
??[info] Configuring publication electric_publication_integration to drop [] tables, and add [] tables
6665
??[info] Altering identity of public.items to FULL
6766
?\[error\] Failed to configure publication: %Postgrex.Error\{message: nil, postgres: %\{code: :insufficient_privilege,.*message: "must be owner of table items"
6867

69-
## Wait for the schema reconciler to run and verify that the table is removed from the publication
68+
## Wait for the publication to be refreshed and verify that the table is removed from it
7069
[shell electric]
7170
??[info] Configuring publication electric_publication_integration to drop ["public.items"] tables, and add [] tables
72-
## Wait for one more run of the schema reconciler to make sure the previous publication update has completed
73-
??[info] Configuring publication electric_publication_integration to drop [] tables, and add [] tables
71+
72+
# Call refresh_publication() once more to ensure the previous update has completed. This is
73+
# going to be a noop call since there are no changes pending for the publication.
74+
!Electric.Replication.PublicationManager.refresh_publication(stack_id: "single_stack", forced?: true)
75+
??:ok
7476

7577
[shell psql]
7678
!SELECT * FROM pg_publication_tables;

‎packages/sync-service/config/runtime.exs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,12 @@ config :electric,
242242
env!("ELECTRIC_TCP_SEND_TIMEOUT", &Electric.Config.parse_human_readable_time!/1, nil),
243243
feature_flags: env!("ELECTRIC_FEATURE_FLAGS", &Electric.Config.parse_feature_flags/1, nil),
244244
manual_table_publishing?: env!("ELECTRIC_MANUAL_TABLE_PUBLISHING", :boolean, nil),
245+
publication_refresh_period:
246+
env!(
247+
"ELECTRIC_TWEAKS_PUBLICATION_REFRESH_PERIOD",
248+
&Electric.Config.parse_human_readable_time!/1,
249+
nil
250+
),
245251
schema_reconciler_period:
246252
env!(
247253
"ELECTRIC_TWEAKS_SCHEMA_RECONCILER_PERIOD",

‎packages/sync-service/lib/electric/application.ex‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ defmodule Electric.Application do
133133
tweaks: [
134134
publication_alter_debounce_ms: get_env(opts, :publication_alter_debounce_ms),
135135
registry_partitions: get_env(opts, :process_registry_partitions),
136+
publication_refresh_period: get_env(opts, :publication_refresh_period),
136137
schema_reconciler_period: get_env(opts, :schema_reconciler_period)
137138
],
138139
manual_table_publishing?: get_env(opts, :manual_table_publishing?)

‎packages/sync-service/lib/electric/config.ex‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ defmodule Electric.Config do
9191
## Misc
9292
process_registry_partitions: &Electric.Config.Defaults.process_registry_partitions/0,
9393
feature_flags: if(Mix.env() == :test, do: @known_feature_flags, else: []),
94+
publication_refresh_period: 60_000,
9495
schema_reconciler_period: 60_000
9596
]
9697

‎packages/sync-service/lib/electric/connection/manager/supervisor.ex‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ defmodule Electric.Connection.Manager.Supervisor do
6262
can_alter_publication?: Keyword.fetch!(opts, :can_alter_publication?),
6363
manual_table_publishing?: Keyword.fetch!(opts, :manual_table_publishing?),
6464
db_pool: Keyword.fetch!(db_pool_opts, :name),
65-
update_debounce_timeout: Keyword.get(tweaks, :publication_alter_debounce_ms, 0)}
65+
update_debounce_timeout: Keyword.get(tweaks, :publication_alter_debounce_ms, 0),
66+
refresh_period: Keyword.get(tweaks, :publication_refresh_period, 60_000)}
6667

6768
shape_log_collector_spec =
6869
{Electric.Replication.ShapeLogCollector,

‎packages/sync-service/lib/electric/postgres/configuration.ex‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,6 @@ defmodule Electric.Postgres.Configuration do
207207
end
208208

209209
defp alter_publication!(conn, publication_name, relation_filters) do
210-
publication = Utils.quote_name(publication_name)
211-
212210
prev_published_tables =
213211
get_publication_tables(conn, publication_name)
214212
|> Enum.map(fn {_oid, relation, _replident} -> Utils.relation_to_sql(relation) end)
@@ -220,13 +218,18 @@ defmodule Electric.Postgres.Configuration do
220218
|> Enum.map(&Utils.relation_to_sql/1)
221219
|> MapSet.new()
222220

223-
to_drop = MapSet.difference(prev_published_tables, new_published_tables)
224-
to_add = MapSet.difference(new_published_tables, prev_published_tables)
221+
to_drop = MapSet.difference(prev_published_tables, new_published_tables) |> Enum.to_list()
222+
to_add = MapSet.difference(new_published_tables, prev_published_tables) |> Enum.to_list()
223+
execute_alter_publication_statements!(conn, publication_name, to_drop, to_add)
224+
end
225+
226+
defp execute_alter_publication_statements!(_, _, [], []), do: :ok
225227

228+
defp execute_alter_publication_statements!(conn, publication_name, to_drop, to_add) do
226229
Logger.info(
227-
"Configuring publication #{publication_name} to drop #{inspect(Enum.to_list(to_drop))} tables, and add #{inspect(Enum.to_list(to_add))} tables",
228-
publication_alter_drop_tables: Enum.to_list(to_drop),
229-
publication_alter_add_tables: Enum.to_list(to_add)
230+
"Configuring publication #{publication_name} to drop #{inspect(to_drop)} tables, and add #{inspect(to_add)} tables",
231+
publication_alter_drop_tables: to_drop,
232+
publication_alter_add_tables: to_add
230233
)
231234

232235
alter_ops =
@@ -236,7 +239,11 @@ defmodule Electric.Postgres.Configuration do
236239
Postgrex.query!(conn, "SAVEPOINT before_publication", [])
237240

238241
# PG 14 and below do not support filters on tables of publications
239-
case Postgrex.query(conn, "ALTER PUBLICATION #{publication} #{op} TABLE #{table}", []) do
242+
case Postgrex.query(
243+
conn,
244+
"ALTER PUBLICATION #{Utils.quote_name(publication_name)} #{op} TABLE #{table}",
245+
[]
246+
) do
240247
{:ok, _} ->
241248
Postgrex.query!(conn, "RELEASE SAVEPOINT before_publication", [])
242249
:ok

‎packages/sync-service/lib/electric/replication/publication_manager.ex‎

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ defmodule Electric.Replication.PublicationManager do
3434
:can_alter_publication?,
3535
:manual_table_publishing?,
3636
:configure_tables_for_replication_fn,
37+
:publication_refresh_period,
3738
:shape_cache,
3839
next_update_forced?: false
3940
]
@@ -50,6 +51,7 @@ defmodule Electric.Replication.PublicationManager do
5051
publication_name: String.t(),
5152
db_pool: term(),
5253
configure_tables_for_replication_fn: fun(),
54+
publication_refresh_period: non_neg_integer(),
5355
shape_cache: {module(), term()},
5456
next_update_forced?: boolean()
5557
}
@@ -80,7 +82,8 @@ defmodule Electric.Replication.PublicationManager do
8082
required: false,
8183
default: &Configuration.configure_publication!/4
8284
],
83-
server: [type: :any, required: false]
85+
server: [type: :any, required: false],
86+
refresh_period: [type: :pos_integer, required: false, default: 60_000]
8487
)
8588

8689
@behaviour __MODULE__
@@ -170,10 +173,11 @@ defmodule Electric.Replication.PublicationManager do
170173
can_alter_publication?: opts.can_alter_publication?,
171174
manual_table_publishing?: opts.manual_table_publishing?,
172175
shape_cache: Map.get(opts, :shape_cache, {Electric.ShapeCache, [stack_id: opts.stack_id]}),
173-
configure_tables_for_replication_fn: opts.configure_tables_for_replication_fn
176+
configure_tables_for_replication_fn: opts.configure_tables_for_replication_fn,
177+
publication_refresh_period: opts.refresh_period
174178
}
175179

176-
{:ok, state}
180+
{:ok, state, state.publication_refresh_period}
177181
end
178182

179183
@impl true
@@ -190,7 +194,7 @@ defmodule Electric.Replication.PublicationManager do
190194
state = schedule_update_publication(state.update_debounce_timeout, state)
191195
{:noreply, state}
192196
else
193-
{:reply, :ok, state}
197+
{:reply, :ok, state, state.publication_refresh_period}
194198
end
195199
end
196200
end
@@ -208,7 +212,7 @@ defmodule Electric.Replication.PublicationManager do
208212
state = schedule_update_publication(state.update_debounce_timeout, state)
209213
{:noreply, state}
210214
else
211-
{:reply, :ok, state}
215+
{:reply, :ok, state, state.publication_refresh_period}
212216
end
213217
end
214218
end
@@ -219,19 +223,21 @@ defmodule Electric.Replication.PublicationManager do
219223
state = schedule_update_publication(state.update_debounce_timeout, forced?, state)
220224
{:noreply, state}
221225
else
222-
{:reply, :ok, state}
226+
{:reply, :ok, state, state.publication_refresh_period}
223227
end
224228
end
225229

226230
def handle_call({:recover_shape, shape_handle, shape}, _from, state) do
227-
if is_tracking_shape_handle?(shape_handle, state) do
228-
Logger.debug("Shape already tracked: #{inspect(shape_handle)}")
229-
{:reply, :ok, state}
230-
else
231-
state = track_shape_handle(shape_handle, state)
232-
state = update_relation_filters_for_shape(shape, :add, state)
233-
{:reply, :ok, state}
234-
end
231+
state =
232+
if is_tracking_shape_handle?(shape_handle, state) do
233+
Logger.debug("Shape already tracked: #{inspect(shape_handle)}")
234+
state
235+
else
236+
state = track_shape_handle(shape_handle, state)
237+
update_relation_filters_for_shape(shape, :add, state)
238+
end
239+
240+
{:reply, :ok, state, state.publication_refresh_period}
235241
end
236242

237243
defguardp is_fatal(err)
@@ -241,16 +247,27 @@ defmodule Electric.Replication.PublicationManager do
241247
@impl true
242248
def handle_info(:update_publication, state) do
243249
# Clear out the timer ref
250+
if state.scheduled_updated_ref, do: Process.cancel_timer(state.scheduled_updated_ref)
244251
state = %{state | scheduled_updated_ref: nil}
245252

246253
# Invoke the actual handler for the publication update
247-
if not state.can_alter_publication? or state.manual_table_publishing? do
248-
check_publication_relations(state)
249-
else
250-
update_publication_state(state)
254+
updated_state =
255+
if not state.can_alter_publication? or state.manual_table_publishing? do
256+
check_publication_relations(state)
257+
else
258+
update_publication_state(state)
259+
end
260+
261+
with {:ok, state} <- updated_state do
262+
# Schedule a forced refresh to happen periodically unless there's an explicit call to
263+
# update the publication that happens sooner.
264+
{:noreply, state, state.publication_refresh_period}
251265
end
252266
end
253267

268+
def handle_info(:timeout, state),
269+
do: handle_info(:update_publication, %{state | next_update_forced?: true})
270+
254271
defp check_publication_relations(
255272
%__MODULE__{
256273
committed_relation_filters: committed_filters,
@@ -260,7 +277,7 @@ defmodule Electric.Replication.PublicationManager do
260277
) do
261278
if not forced? and filters_are_equal?(current_filters, committed_filters) do
262279
Logger.debug("No changes to publication, skipping checkup")
263-
{:noreply, reply_to_waiters(:ok, state)}
280+
{:ok, reply_to_waiters(:ok, state)}
264281
else
265282
# We cannot modify the publication, so we only check whether it is in the right state for
266283
# the set of currently active relation filters.
@@ -294,7 +311,7 @@ defmodule Electric.Replication.PublicationManager do
294311
error = %Electric.DbConfigurationError{type: reason, message: message}
295312

296313
state = reply_to_waiters({:error, error}, state)
297-
{:noreply, %{state | next_update_forced?: false}}
314+
{:ok, %{state | next_update_forced?: false}}
298315
end
299316
end
300317
end
@@ -326,12 +343,12 @@ defmodule Electric.Replication.PublicationManager do
326343
{:error, err} when retries < @max_retries and not is_fatal(err) ->
327344
Logger.warning("Failed to configure publication, retrying: #{inspect(err)}")
328345
state = schedule_update_publication(@retry_timeout, %{state | retries: retries + 1})
329-
{:noreply, state}
346+
{:ok, state}
330347

331348
{:error, err} ->
332349
Logger.error("Failed to configure publication: #{inspect(err)}")
333350
state = reply_to_waiters({:error, err}, state)
334-
{:noreply, %{state | next_update_forced?: false}}
351+
{:ok, %{state | next_update_forced?: false}}
335352
end
336353
end
337354

@@ -352,7 +369,7 @@ defmodule Electric.Replication.PublicationManager do
352369
state = reply_to_waiters(:ok, state)
353370
committed_filters = MapSet.difference(state.prepared_relation_filters, invalidated_relations)
354371

355-
{:noreply,
372+
{:ok,
356373
%{
357374
state
358375
| committed_relation_filters: committed_filters,

‎packages/sync-service/lib/electric/stack_supervisor.ex‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ defmodule Electric.StackSupervisor do
108108
on_cleanup: [type: {:fun, 1}]
109109
]
110110
],
111+
publication_refresh_period: [type: :non_neg_integer, default: 60_000],
111112
schema_reconciler_period: [type: :non_neg_integer, default: 60_000]
112113
]
113114
],

0 commit comments

Comments
 (0)