Skip to content

Process replication slot changes - #4930

Open
DilwoarH wants to merge 31 commits into
mainfrom
process-replication-slot-changes
Open

DilwoarH wants to merge 31 commits into
mainfrom
process-replication-slot-changes

Conversation

@DilwoarH

@DilwoarH DilwoarH commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

** DO NOT MERGE UNTIL #4946 IS MERGED IN FIRST**

What

The application code for processing slot changes for notifications table

Why

This is needed to process WAL logs in the database

Test results

Ran a load test on dev-c - this is the result after 2400 emails.

SELECT
	(SELECT count(*) FROM pg_logical_slot_peek_changes('notify_dashboard_replication_slot', NULL, NULL)) AS still_processing,
    (SELECT SUM(notification_count) FROM ft_service_stats) AS ft_stats_count,
    COUNT(*) AS notif_count,
    (SELECT SUM(notification_count) FROM ft_service_stats where notification_status = 'created') AS ft_created_count,
    COUNT(CASE WHEN notification_status = 'created' THEN 1 END) AS notif_created_count,
    (SELECT SUM(notification_count) FROM ft_service_stats where notification_status = 'sending') AS ft_sending_count,
    COUNT(CASE WHEN notification_status = 'sending' THEN 1 END) AS notif_sending_count,
    (SELECT SUM(notification_count) FROM ft_service_stats where notification_status = 'delivered') AS ft_delivered_count,
    COUNT(CASE WHEN notification_status = 'delivered' THEN 1 END) AS notif_delivered_count,
    (SELECT SUM(notification_count) FROM ft_service_stats where notification_status = 'temporary-failure') AS ft_temp_fail_count,
    COUNT(CASE WHEN notification_status = 'temporary-failure' THEN 1 END) AS notif_temp_fail_count,
    (SELECT SUM(notification_count) FROM ft_service_stats where notification_status = 'permanent-failure') AS ft_perm_fail_count,
    COUNT(CASE WHEN notification_status = 'permanent-failure' THEN 1 END) AS notif_perm_fail_count
FROM
    public.notifications;
still_processing ft_stats_count notif_count ft_created_count notif_created_count ft_sending_count notif_sending_count ft_delivered_count notif_delivered_count ft_temp_fail_count notif_temp_fail_count ft_perm_fail_count notif_perm_fail_count
0 2400 2400 0 0 0 0 1206 1206 234 234 960 960

How it works

605101533-57f970e2-60d7-462a-9fbd-b24d553d6663

@DilwoarH
DilwoarH force-pushed the process-replication-slot-changes branch 6 times, most recently from 6bddcf6 to 2d6790a Compare August 11, 2026 09:53
@DilwoarH
DilwoarH force-pushed the process-replication-slot-changes branch 2 times, most recently from da47acd to 1f988ae Compare August 18, 2026 00:15
@DilwoarH DilwoarH changed the title [WIP] Process replication slot changes Process replication slot changes Aug 18, 2026
@DilwoarH

Copy link
Copy Markdown
Contributor Author

requires #4936 to be merged first

@DilwoarH
DilwoarH force-pushed the process-replication-slot-changes branch from 1f988ae to 1d032b9 Compare August 18, 2026 00:29
Comment thread app/dao/fact_service_stats_dao.py Outdated
Comment thread app/dao/notifications_wal_changes_dao.py
if key_type == "test":
return None

if require_status_from_primary_row and not notification_status:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need this condition check? Updated notifications always have a previous notification_status from the database, this condition seems redundant and a bit confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed, however, it wasn't working as expected. I have fixed it and added some comments to support it

@DilwoarH
DilwoarH force-pushed the process-replication-slot-changes branch 3 times, most recently from 09efa7c to 29d32f3 Compare September 8, 2026 12:09


@notify_celery.task(name="process-notifications-replication-slot-changes")
def process_replication_slot_changes():

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to do - rename this to match task name

.filter(*filters)
.update(
{
"notification_count": func.greatest(FactServiceStats.notification_count + change_count, 0),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we error if change_count is bigger than notification_count as this shouldn't happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: We may need to test what happens if the row doesn't exist at all

AND pid = pg_backend_pid()
AND granted
AND classid = CAST((CAST(:lock_id AS bigint) >> 32) AS integer)
AND objid = CAST((CAST(:lock_id AS bigint) & 4294967295) AS integer)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4294967295

^^ document what this means

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also the 32



def _extract_row_data(change: dict[str, Any]) -> RowData:
if "columnnames" in change and "columnvalues" in change:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in format v1 therefore we can remove now

Comment on lines +333 to +334
if "identity" in change and change.get("action") in {"D", "U", "I"}:
return _extract_name_value_rows(change["identity"])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed for insert

Comment on lines +340 to +345
oldkeys = change.get("oldkeys") or {}
if "keynames" in oldkeys and "keyvalues" in oldkeys:
return _zip_values(oldkeys["keynames"], oldkeys["keyvalues"])

if "keys" in oldkeys:
return _extract_name_value_rows(oldkeys["keys"])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed

Comment on lines +381 to +384
if change.get("lsn"):
last_lsn = change["lsn"]
elif change.get("nextlsn"):
last_lsn = change["nextlsn"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be able to refactor out

Comment on lines +386 to +388
if table_name != REPLICATION_SLOT_TABLE_NAME.split(".")[-1]:
ignored_changes += 1
continue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't ever happen - so can be removed

Comment on lines +447 to +450
# If the key_type is "test", we ignore this change and return None to indicate
# that it should not be processed further.
if key_type == "test":
return None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially could be refactored to happen at an earlier stage

Comment on lines +408 to +410
if old_dimensions:
counter[old_dimensions] -= 1
updated = True

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no old dimension - we should continue because we don't want to increase the same status again

This will be replaced with the actual task in the following commits
DilwoarH and others added 29 commits September 25, 2026 10:42
the class is not processing all replication slot changes - just notifications
Also include bst_date in the dimensions
…t stats updates and slot advancement in a single transaction
This is to avoid unnessary calls
10 seconds sometimes feels delayed for users
@DilwoarH
DilwoarH force-pushed the process-replication-slot-changes branch from 29d32f3 to a1c7a1f Compare September 25, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants