@haathie/pgmb 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +2 -2
- package/lib/queries.d.ts +8 -8
- package/lib/queries.js +6 -6
- package/package.json +4 -3
- package/sql/pgmb-0.2.14-0.2.15.sql +165 -0
- package/sql/pgmb-0.2.8-0.2.14.sql +113 -0
- package/sql/pgmb.sql +52 -41
- package/sql/queries.sql +3 -3
package/lib/client.js
CHANGED
|
@@ -62,14 +62,14 @@ export class PgmbClient extends PGMBEventBatcher {
|
|
|
62
62
|
const isPgCronEnabled = pgCronRslt?.value === 'true';
|
|
63
63
|
if (!isPgCronEnabled) {
|
|
64
64
|
// maintain event table
|
|
65
|
-
await maintainEventsTable.run(
|
|
65
|
+
await maintainEventsTable.run({}, this.client);
|
|
66
66
|
this.logger.debug('maintained events table');
|
|
67
67
|
if (this.pollEventsIntervalMs) {
|
|
68
68
|
this.#pollTask = this.#startLoop(pollForEvents.run.bind(pollForEvents, undefined, this.client), this.pollEventsIntervalMs);
|
|
69
69
|
}
|
|
70
70
|
if (this.tableMaintenanceMs) {
|
|
71
71
|
this.#tableMaintainTask = this.#startLoop(maintainEventsTable.run
|
|
72
|
-
.bind(maintainEventsTable,
|
|
72
|
+
.bind(maintainEventsTable, {}, this.client), this.tableMaintenanceMs);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
await assertGroup.run({ id: this.groupId }, this.client);
|
package/lib/queries.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface IMarkSubscriptionsActiveQuery {
|
|
|
103
103
|
* UPDATE pgmb.subscriptions
|
|
104
104
|
* SET
|
|
105
105
|
* last_active_at = NOW()
|
|
106
|
-
* WHERE id IN (SELECT * FROM unnest(:ids!::
|
|
106
|
+
* WHERE id IN (SELECT * FROM unnest(:ids!::text[]))
|
|
107
107
|
* ```
|
|
108
108
|
*/
|
|
109
109
|
export declare const markSubscriptionsActive: PreparedQuery<IMarkSubscriptionsActiveParams, void>;
|
|
@@ -427,7 +427,7 @@ export interface IRemoveExpiredSubscriptionsQuery {
|
|
|
427
427
|
* WHERE group_id = :groupId!
|
|
428
428
|
* AND expiry_interval IS NOT NULL
|
|
429
429
|
* AND pgmb.add_interval_imm(last_active_at, expiry_interval) < NOW()
|
|
430
|
-
* AND id NOT IN (select * from unnest(:activeIds!::
|
|
430
|
+
* AND id NOT IN (select * from unnest(:activeIds!::text[]))
|
|
431
431
|
* RETURNING id
|
|
432
432
|
* )
|
|
433
433
|
* SELECT COUNT(*) AS "deleted!" FROM deleted
|
|
@@ -479,11 +479,11 @@ export interface IUpdateConfigValueQuery {
|
|
|
479
479
|
*/
|
|
480
480
|
export declare const updateConfigValue: PreparedQuery<IUpdateConfigValueParams, IUpdateConfigValueResult>;
|
|
481
481
|
/** 'MaintainEventsTable' parameters type */
|
|
482
|
-
export
|
|
483
|
-
|
|
484
|
-
export interface IMaintainEventsTableResult {
|
|
485
|
-
maintainEventsTable: undefined | null;
|
|
482
|
+
export interface IMaintainEventsTableParams {
|
|
483
|
+
ts?: DateOrString | null | void;
|
|
486
484
|
}
|
|
485
|
+
/** 'MaintainEventsTable' return type */
|
|
486
|
+
export type IMaintainEventsTableResult = void;
|
|
487
487
|
/** 'MaintainEventsTable' query type */
|
|
488
488
|
export interface IMaintainEventsTableQuery {
|
|
489
489
|
params: IMaintainEventsTableParams;
|
|
@@ -492,7 +492,7 @@ export interface IMaintainEventsTableQuery {
|
|
|
492
492
|
/**
|
|
493
493
|
* Query generated from SQL:
|
|
494
494
|
* ```
|
|
495
|
-
*
|
|
495
|
+
* CALL pgmb.maintain_events_table(COALESCE(:ts, NOW()))
|
|
496
496
|
* ```
|
|
497
497
|
*/
|
|
498
|
-
export declare const maintainEventsTable: PreparedQuery<
|
|
498
|
+
export declare const maintainEventsTable: PreparedQuery<IMaintainEventsTableParams, void>;
|
package/lib/queries.js
CHANGED
|
@@ -46,14 +46,14 @@ const deleteSubscriptionsIR = { "usedParamSet": { "ids": true }, "params": [{ "n
|
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
48
|
export const deleteSubscriptions = new PreparedQuery(deleteSubscriptionsIR);
|
|
49
|
-
const markSubscriptionsActiveIR = { "usedParamSet": { "ids": true }, "params": [{ "name": "ids", "required": true, "transform": { "type": "scalar" }, "locs": [{ "a": 88, "b": 92 }] }], "statement": "UPDATE pgmb.subscriptions\nSET\n\tlast_active_at = NOW()\nWHERE id IN (SELECT * FROM unnest(:ids!::
|
|
49
|
+
const markSubscriptionsActiveIR = { "usedParamSet": { "ids": true }, "params": [{ "name": "ids", "required": true, "transform": { "type": "scalar" }, "locs": [{ "a": 88, "b": 92 }] }], "statement": "UPDATE pgmb.subscriptions\nSET\n\tlast_active_at = NOW()\nWHERE id IN (SELECT * FROM unnest(:ids!::text[]))" };
|
|
50
50
|
/**
|
|
51
51
|
* Query generated from SQL:
|
|
52
52
|
* ```
|
|
53
53
|
* UPDATE pgmb.subscriptions
|
|
54
54
|
* SET
|
|
55
55
|
* last_active_at = NOW()
|
|
56
|
-
* WHERE id IN (SELECT * FROM unnest(:ids!::
|
|
56
|
+
* WHERE id IN (SELECT * FROM unnest(:ids!::text[]))
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
59
|
export const markSubscriptionsActive = new PreparedQuery(markSubscriptionsActiveIR);
|
|
@@ -206,7 +206,7 @@ const findEventsIR = { "usedParamSet": { "ids": true }, "params": [{ "name": "id
|
|
|
206
206
|
* ```
|
|
207
207
|
*/
|
|
208
208
|
export const findEvents = new PreparedQuery(findEventsIR);
|
|
209
|
-
const removeExpiredSubscriptionsIR = { "usedParamSet": { "groupId": true, "activeIds": true }, "params": [{ "name": "groupId", "required": true, "transform": { "type": "scalar" }, "locs": [{ "a": 68, "b": 76 }] }, { "name": "activeIds", "required": true, "transform": { "type": "scalar" }, "locs": [{ "a": 219, "b": 229 }] }], "statement": "WITH deleted AS (\n\tDELETE FROM pgmb.subscriptions\n\tWHERE group_id = :groupId!\n\t\tAND expiry_interval IS NOT NULL\n\t\tAND pgmb.add_interval_imm(last_active_at, expiry_interval) < NOW()\n\t\tAND id NOT IN (select * from unnest(:activeIds!::
|
|
209
|
+
const removeExpiredSubscriptionsIR = { "usedParamSet": { "groupId": true, "activeIds": true }, "params": [{ "name": "groupId", "required": true, "transform": { "type": "scalar" }, "locs": [{ "a": 68, "b": 76 }] }, { "name": "activeIds", "required": true, "transform": { "type": "scalar" }, "locs": [{ "a": 219, "b": 229 }] }], "statement": "WITH deleted AS (\n\tDELETE FROM pgmb.subscriptions\n\tWHERE group_id = :groupId!\n\t\tAND expiry_interval IS NOT NULL\n\t\tAND pgmb.add_interval_imm(last_active_at, expiry_interval) < NOW()\n\t\tAND id NOT IN (select * from unnest(:activeIds!::text[]))\n\tRETURNING id\n)\nSELECT COUNT(*) AS \"deleted!\" FROM deleted" };
|
|
210
210
|
/**
|
|
211
211
|
* Query generated from SQL:
|
|
212
212
|
* ```
|
|
@@ -215,7 +215,7 @@ const removeExpiredSubscriptionsIR = { "usedParamSet": { "groupId": true, "activ
|
|
|
215
215
|
* WHERE group_id = :groupId!
|
|
216
216
|
* AND expiry_interval IS NOT NULL
|
|
217
217
|
* AND pgmb.add_interval_imm(last_active_at, expiry_interval) < NOW()
|
|
218
|
-
* AND id NOT IN (select * from unnest(:activeIds!::
|
|
218
|
+
* AND id NOT IN (select * from unnest(:activeIds!::text[]))
|
|
219
219
|
* RETURNING id
|
|
220
220
|
* )
|
|
221
221
|
* SELECT COUNT(*) AS "deleted!" FROM deleted
|
|
@@ -241,11 +241,11 @@ const updateConfigValueIR = { "usedParamSet": { "value": true, "key": true }, "p
|
|
|
241
241
|
* ```
|
|
242
242
|
*/
|
|
243
243
|
export const updateConfigValue = new PreparedQuery(updateConfigValueIR);
|
|
244
|
-
const maintainEventsTableIR = { "usedParamSet": {}, "params": [], "statement": "
|
|
244
|
+
const maintainEventsTableIR = { "usedParamSet": { "ts": true }, "params": [{ "name": "ts", "required": false, "transform": { "type": "scalar" }, "locs": [{ "a": 41, "b": 43 }] }], "statement": "CALL pgmb.maintain_events_table(COALESCE(:ts, NOW()))" };
|
|
245
245
|
/**
|
|
246
246
|
* Query generated from SQL:
|
|
247
247
|
* ```
|
|
248
|
-
*
|
|
248
|
+
* CALL pgmb.maintain_events_table(COALESCE(:ts, NOW()))
|
|
249
249
|
* ```
|
|
250
250
|
*/
|
|
251
251
|
export const maintainEventsTable = new PreparedQuery(maintainEventsTableIR);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haathie/pgmb",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "PG message broker, with a type-safe typescript client with built-in webhook & SSE support.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|
"main": "lib/index.js",
|
|
11
11
|
"repository": "https://github.com/haathie/pgmb",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "node --env-file ./.test.env --test tests/*.test.ts",
|
|
14
14
|
"prepare": "npm run build",
|
|
15
15
|
"build": "tsc -p tsconfig.build.json",
|
|
16
16
|
"lint": "eslint ./ --ext .js,.ts,.jsx,.tsx",
|
|
17
17
|
"lint:fix": "eslint ./ --fix --ext .js,.ts,.jsx,.tsx",
|
|
18
|
-
"benchmark": "TZ=utc node --env-file ./.env.test
|
|
18
|
+
"benchmark": "TZ=utc node --env-file ./.env.test benchmark/run.ts",
|
|
19
19
|
"pg:typegen": "pgtyped --config ./pgtyped.config.json"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@adiwajshing/eslint-config": "git+https://github.com/adiwajshing/eslint-config",
|
|
23
|
+
"@electric-sql/pglite": "^0.4.1",
|
|
23
24
|
"@pgtyped/cli": "^2.4.3",
|
|
24
25
|
"@types/amqplib": "^0.10.0",
|
|
25
26
|
"@types/chance": "^1.1.6",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
SET search_path TO pgmb;
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION maintain_time_partitions_using_event_id(
|
|
4
|
+
table_id regclass,
|
|
5
|
+
partition_interval INTERVAL,
|
|
6
|
+
future_interval INTERVAL,
|
|
7
|
+
retention_period INTERVAL,
|
|
8
|
+
additional_sql TEXT DEFAULT NULL,
|
|
9
|
+
current_ts timestamptz DEFAULT NOW()
|
|
10
|
+
)
|
|
11
|
+
RETURNS void AS $$
|
|
12
|
+
DECLARE
|
|
13
|
+
ts_trunc timestamptz := date_bin(partition_interval, current_ts, '2000-1-1');
|
|
14
|
+
oldest_pt_to_keep text := pgmb
|
|
15
|
+
.get_time_partition_name(table_id, ts_trunc - retention_period);
|
|
16
|
+
lock_key CONSTANT BIGINT :=
|
|
17
|
+
hashtext('pgmb.maintain_tp.' || table_id::text);
|
|
18
|
+
ranges_to_create tstzrange[];
|
|
19
|
+
partitions_to_drop regclass[];
|
|
20
|
+
p_to_drop regclass;
|
|
21
|
+
cur_range tstzrange;
|
|
22
|
+
max_retries constant int = 50;
|
|
23
|
+
BEGIN
|
|
24
|
+
ASSERT partition_interval >= interval '1 minute',
|
|
25
|
+
'partition_interval must be at least 1 minute';
|
|
26
|
+
ASSERT future_interval >= partition_interval,
|
|
27
|
+
'future_interval must be at least as large as partition_interval';
|
|
28
|
+
|
|
29
|
+
IF NOT pg_try_advisory_xact_lock(lock_key) THEN
|
|
30
|
+
-- another process is already maintaining partitions for this table
|
|
31
|
+
RETURN;
|
|
32
|
+
END IF;
|
|
33
|
+
|
|
34
|
+
-- find all intervals we need to create partitions for
|
|
35
|
+
WITH existing_part_ranges AS (
|
|
36
|
+
SELECT
|
|
37
|
+
tstzrange(
|
|
38
|
+
extract_date_from_event_id(lower_bound),
|
|
39
|
+
extract_date_from_event_id(upper_bound),
|
|
40
|
+
'[]'
|
|
41
|
+
) as range
|
|
42
|
+
FROM pgmb.get_partitions_and_bounds(table_id)
|
|
43
|
+
),
|
|
44
|
+
future_tzs AS (
|
|
45
|
+
SELECT
|
|
46
|
+
tstzrange(dt, dt + partition_interval, '[]') AS range
|
|
47
|
+
FROM generate_series(
|
|
48
|
+
ts_trunc,
|
|
49
|
+
ts_trunc + future_interval,
|
|
50
|
+
partition_interval
|
|
51
|
+
) AS gs(dt)
|
|
52
|
+
),
|
|
53
|
+
diffs AS (
|
|
54
|
+
SELECT
|
|
55
|
+
CASE WHEN epr.range IS NOT NULL
|
|
56
|
+
THEN (ftz.range::tstzmultirange - epr.range::tstzmultirange)
|
|
57
|
+
ELSE ftz.range::tstzmultirange
|
|
58
|
+
END AS ranges
|
|
59
|
+
FROM future_tzs ftz
|
|
60
|
+
LEFT JOIN existing_part_ranges epr ON ftz.range && epr.range
|
|
61
|
+
)
|
|
62
|
+
select ARRAY_AGG(u.range) FROM diffs
|
|
63
|
+
CROSS JOIN LATERAL unnest(diffs.ranges) AS u(range)
|
|
64
|
+
INTO ranges_to_create;
|
|
65
|
+
|
|
66
|
+
ranges_to_create := COALESCE(ranges_to_create, ARRAY[]::tstzrange[]);
|
|
67
|
+
|
|
68
|
+
SELECT ARRAY_AGG(inhrelid::regclass) INTO partitions_to_drop
|
|
69
|
+
FROM pg_catalog.pg_inherits
|
|
70
|
+
WHERE inhparent = table_id
|
|
71
|
+
AND inhrelid::regclass::text < oldest_pt_to_keep;
|
|
72
|
+
partitions_to_drop := COALESCE(partitions_to_drop, ARRAY[]::regclass[]);
|
|
73
|
+
|
|
74
|
+
-- check if nothing to do
|
|
75
|
+
IF
|
|
76
|
+
array_length(partitions_to_drop, 1) = 0
|
|
77
|
+
AND array_length(ranges_to_create, 1) = 0
|
|
78
|
+
THEN
|
|
79
|
+
RETURN;
|
|
80
|
+
END IF;
|
|
81
|
+
|
|
82
|
+
-- go from now to future_interval
|
|
83
|
+
FOREACH cur_range IN ARRAY ranges_to_create LOOP
|
|
84
|
+
DECLARE
|
|
85
|
+
start_ev_id event_id := pgmb.create_event_id(lower(cur_range), 0);
|
|
86
|
+
end_ev_id event_id := pgmb.create_event_id(upper(cur_range), 0);
|
|
87
|
+
pt_name TEXT := pgmb.get_time_partition_name(table_id, lower(cur_range));
|
|
88
|
+
BEGIN
|
|
89
|
+
RAISE NOTICE 'creating partition "%". start: %, end: %',
|
|
90
|
+
pt_name, lower(cur_range), upper(cur_range);
|
|
91
|
+
|
|
92
|
+
EXECUTE FORMAT(
|
|
93
|
+
'CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
|
|
94
|
+
pt_name, table_id, start_ev_id, end_ev_id
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
IF additional_sql IS NOT NULL THEN
|
|
98
|
+
EXECUTE REPLACE(additional_sql, '$1', pt_name);
|
|
99
|
+
END IF;
|
|
100
|
+
END;
|
|
101
|
+
END LOOP;
|
|
102
|
+
|
|
103
|
+
-- Drop old partitions
|
|
104
|
+
FOREACH p_to_drop IN ARRAY partitions_to_drop LOOP
|
|
105
|
+
EXECUTE format('DROP TABLE %I', p_to_drop);
|
|
106
|
+
END LOOP;
|
|
107
|
+
END;
|
|
108
|
+
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SECURITY DEFINER;
|
|
109
|
+
|
|
110
|
+
CREATE FUNCTION maintain_append_only_table(
|
|
111
|
+
tbl regclass,
|
|
112
|
+
current_ts timestamptz DEFAULT NOW()
|
|
113
|
+
)
|
|
114
|
+
RETURNS VOID AS $$
|
|
115
|
+
SELECT maintain_time_partitions_using_event_id(
|
|
116
|
+
tbl,
|
|
117
|
+
partition_interval := get_config_value('partition_interval')::interval,
|
|
118
|
+
future_interval := get_config_value('future_intervals_to_create')::interval,
|
|
119
|
+
retention_period := get_config_value('partition_retention_period')::interval,
|
|
120
|
+
-- turn off autovacuum on the events table, since we're not
|
|
121
|
+
-- going to be updating/deleting rows from it.
|
|
122
|
+
-- Also set fillfactor to 100 since we're only inserting.
|
|
123
|
+
additional_sql := 'ALTER TABLE $1 SET(
|
|
124
|
+
fillfactor = 100,
|
|
125
|
+
autovacuum_enabled = false,
|
|
126
|
+
toast.autovacuum_enabled = false
|
|
127
|
+
);',
|
|
128
|
+
current_ts := current_ts
|
|
129
|
+
);
|
|
130
|
+
$$ LANGUAGE sql VOLATILE PARALLEL UNSAFE SECURITY DEFINER
|
|
131
|
+
SET search_path TO pgmb;
|
|
132
|
+
|
|
133
|
+
CREATE OR REPLACE PROCEDURE maintain_events_table(
|
|
134
|
+
current_ts timestamptz DEFAULT NOW()
|
|
135
|
+
) AS $$
|
|
136
|
+
DECLARE
|
|
137
|
+
pi INTERVAL := pgmb.get_config_value('partition_interval');
|
|
138
|
+
fic INTERVAL := pgmb.get_config_value('future_intervals_to_create');
|
|
139
|
+
rp INTERVAL := pgmb.get_config_value('partition_retention_period');
|
|
140
|
+
BEGIN
|
|
141
|
+
SET search_path TO pgmb;
|
|
142
|
+
|
|
143
|
+
PERFORM maintain_append_only_table('events'::regclass, current_ts);
|
|
144
|
+
COMMIT;
|
|
145
|
+
|
|
146
|
+
PERFORM maintain_append_only_table('subscription_events'::regclass, current_ts);
|
|
147
|
+
COMMIT;
|
|
148
|
+
END;
|
|
149
|
+
$$ LANGUAGE plpgsql;
|
|
150
|
+
|
|
151
|
+
DO $$
|
|
152
|
+
BEGIN
|
|
153
|
+
IF get_config_value('use_pg_cron') <> 'true' THEN
|
|
154
|
+
RETURN;
|
|
155
|
+
END IF;
|
|
156
|
+
|
|
157
|
+
SELECT cron.schedule(
|
|
158
|
+
'pgmb_maintain_table_partitions',
|
|
159
|
+
get_config_value('pg_cron_partition_maintenance_cron'),
|
|
160
|
+
$CMD$ CALL pgmb.maintain_events_table(); $CMD$
|
|
161
|
+
);
|
|
162
|
+
END
|
|
163
|
+
$$;
|
|
164
|
+
|
|
165
|
+
DROP FUNCTION IF EXISTS maintain_events_table(timestamptz);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
SET search_path TO pgmb;
|
|
2
|
+
|
|
3
|
+
-- Partition maintenance function for events table. Creates partitions for
|
|
4
|
+
-- the current and next interval. Deletes partitions that are older than the
|
|
5
|
+
-- configured time interval.
|
|
6
|
+
-- Exact partition size and oldest partition interval can be configured
|
|
7
|
+
-- using the "subscriptions_config" table.
|
|
8
|
+
CREATE OR REPLACE FUNCTION maintain_time_partitions_using_event_id(
|
|
9
|
+
table_id regclass,
|
|
10
|
+
partition_interval INTERVAL,
|
|
11
|
+
future_interval INTERVAL,
|
|
12
|
+
retention_period INTERVAL,
|
|
13
|
+
additional_sql TEXT DEFAULT NULL,
|
|
14
|
+
current_ts timestamptz DEFAULT NOW()
|
|
15
|
+
)
|
|
16
|
+
RETURNS void AS $$
|
|
17
|
+
DECLARE
|
|
18
|
+
ts_trunc timestamptz := date_bin(partition_interval, current_ts, '2000-1-1');
|
|
19
|
+
oldest_pt_to_keep text := pgmb
|
|
20
|
+
.get_time_partition_name(table_id, ts_trunc - retention_period);
|
|
21
|
+
p_info RECORD;
|
|
22
|
+
lock_key CONSTANT BIGINT :=
|
|
23
|
+
hashtext('pgmb.maintain_tp.' || table_id::text);
|
|
24
|
+
ranges_to_create tstzrange[];
|
|
25
|
+
cur_range tstzrange;
|
|
26
|
+
max_retries constant int = 50;
|
|
27
|
+
BEGIN
|
|
28
|
+
ASSERT partition_interval >= interval '1 minute',
|
|
29
|
+
'partition_interval must be at least 1 minute';
|
|
30
|
+
ASSERT future_interval >= partition_interval,
|
|
31
|
+
'future_interval must be at least as large as partition_interval';
|
|
32
|
+
|
|
33
|
+
IF NOT pg_try_advisory_xact_lock(lock_key) THEN
|
|
34
|
+
-- another process is already maintaining partitions for this table
|
|
35
|
+
RETURN;
|
|
36
|
+
END IF;
|
|
37
|
+
|
|
38
|
+
-- find all intervals we need to create partitions for
|
|
39
|
+
WITH existing_part_ranges AS (
|
|
40
|
+
SELECT
|
|
41
|
+
tstzrange(
|
|
42
|
+
extract_date_from_event_id(lower_bound),
|
|
43
|
+
extract_date_from_event_id(upper_bound),
|
|
44
|
+
'[]'
|
|
45
|
+
) as range
|
|
46
|
+
FROM pgmb.get_partitions_and_bounds(table_id)
|
|
47
|
+
),
|
|
48
|
+
future_tzs AS (
|
|
49
|
+
SELECT
|
|
50
|
+
tstzrange(dt, dt + partition_interval, '[]') AS range
|
|
51
|
+
FROM generate_series(
|
|
52
|
+
ts_trunc,
|
|
53
|
+
ts_trunc + future_interval,
|
|
54
|
+
partition_interval
|
|
55
|
+
) AS gs(dt)
|
|
56
|
+
),
|
|
57
|
+
diffs AS (
|
|
58
|
+
SELECT
|
|
59
|
+
CASE WHEN epr.range IS NOT NULL
|
|
60
|
+
THEN (ftz.range::tstzmultirange - epr.range::tstzmultirange)
|
|
61
|
+
ELSE ftz.range::tstzmultirange
|
|
62
|
+
END AS ranges
|
|
63
|
+
FROM future_tzs ftz
|
|
64
|
+
LEFT JOIN existing_part_ranges epr ON ftz.range && epr.range
|
|
65
|
+
)
|
|
66
|
+
select ARRAY_AGG(u.range) FROM diffs
|
|
67
|
+
CROSS JOIN LATERAL unnest(diffs.ranges) AS u(range)
|
|
68
|
+
INTO ranges_to_create;
|
|
69
|
+
|
|
70
|
+
ranges_to_create := COALESCE(ranges_to_create, ARRAY[]::tstzrange[]);
|
|
71
|
+
|
|
72
|
+
FOR i IN 1..max_retries LOOP
|
|
73
|
+
BEGIN
|
|
74
|
+
-- go from now to future_interval
|
|
75
|
+
FOREACH cur_range IN ARRAY ranges_to_create LOOP
|
|
76
|
+
DECLARE
|
|
77
|
+
start_ev_id event_id := pgmb.create_event_id(lower(cur_range), 0);
|
|
78
|
+
end_ev_id event_id := pgmb.create_event_id(upper(cur_range), 0);
|
|
79
|
+
pt_name TEXT := pgmb.get_time_partition_name(table_id, lower(cur_range));
|
|
80
|
+
BEGIN
|
|
81
|
+
RAISE NOTICE 'creating partition "%". start: %, end: %',
|
|
82
|
+
pt_name, lower(cur_range), upper(cur_range);
|
|
83
|
+
|
|
84
|
+
EXECUTE FORMAT(
|
|
85
|
+
'CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
|
|
86
|
+
pt_name, table_id, start_ev_id, end_ev_id
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
IF additional_sql IS NOT NULL THEN
|
|
90
|
+
EXECUTE REPLACE(additional_sql, '$1', pt_name);
|
|
91
|
+
END IF;
|
|
92
|
+
END;
|
|
93
|
+
END LOOP;
|
|
94
|
+
|
|
95
|
+
-- Drop old partitions
|
|
96
|
+
FOR p_info IN (
|
|
97
|
+
SELECT inhrelid::regclass AS child
|
|
98
|
+
FROM pg_catalog.pg_inherits
|
|
99
|
+
WHERE inhparent = table_id
|
|
100
|
+
AND inhrelid::regclass::text < oldest_pt_to_keep
|
|
101
|
+
) LOOP
|
|
102
|
+
EXECUTE format('DROP TABLE %I', p_info.child);
|
|
103
|
+
END LOOP;
|
|
104
|
+
EXIT;
|
|
105
|
+
EXCEPTION WHEN lock_not_available OR deadlock_detected THEN
|
|
106
|
+
IF i = max_retries THEN
|
|
107
|
+
RAISE;
|
|
108
|
+
END IF;
|
|
109
|
+
PERFORM pg_sleep(1);
|
|
110
|
+
END;
|
|
111
|
+
END LOOP;
|
|
112
|
+
END;
|
|
113
|
+
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SECURITY DEFINER;
|
package/sql/pgmb.sql
CHANGED
|
@@ -43,12 +43,12 @@ $$ LANGUAGE sql STRICT STABLE PARALLEL SAFE SET SEARCH_PATH TO pgmb;
|
|
|
43
43
|
INSERT INTO config(id, value) VALUES
|
|
44
44
|
('plugin_version', '0.2.0'),
|
|
45
45
|
('partition_retention_period', '60 minutes'),
|
|
46
|
-
('future_intervals_to_create', '
|
|
46
|
+
('future_intervals_to_create', '3 hours'),
|
|
47
47
|
('partition_interval', '30 minutes'),
|
|
48
48
|
('poll_chunk_size', '10000'),
|
|
49
49
|
('pg_cron_poll_for_events_cron', '1 second'),
|
|
50
50
|
-- every 30 minutes
|
|
51
|
-
('pg_cron_partition_maintenance_cron', '
|
|
51
|
+
('pg_cron_partition_maintenance_cron', '0 * * * *');
|
|
52
52
|
|
|
53
53
|
-- we'll create the events table next & its functions ---------------
|
|
54
54
|
|
|
@@ -206,11 +206,13 @@ DECLARE
|
|
|
206
206
|
ts_trunc timestamptz := date_bin(partition_interval, current_ts, '2000-1-1');
|
|
207
207
|
oldest_pt_to_keep text := pgmb
|
|
208
208
|
.get_time_partition_name(table_id, ts_trunc - retention_period);
|
|
209
|
-
p_info RECORD;
|
|
210
209
|
lock_key CONSTANT BIGINT :=
|
|
211
210
|
hashtext('pgmb.maintain_tp.' || table_id::text);
|
|
212
211
|
ranges_to_create tstzrange[];
|
|
212
|
+
partitions_to_drop regclass[];
|
|
213
|
+
p_to_drop regclass;
|
|
213
214
|
cur_range tstzrange;
|
|
215
|
+
max_retries constant int = 50;
|
|
214
216
|
BEGIN
|
|
215
217
|
ASSERT partition_interval >= interval '1 minute',
|
|
216
218
|
'partition_interval must be at least 1 minute';
|
|
@@ -256,6 +258,20 @@ BEGIN
|
|
|
256
258
|
|
|
257
259
|
ranges_to_create := COALESCE(ranges_to_create, ARRAY[]::tstzrange[]);
|
|
258
260
|
|
|
261
|
+
SELECT ARRAY_AGG(inhrelid::regclass) INTO partitions_to_drop
|
|
262
|
+
FROM pg_catalog.pg_inherits
|
|
263
|
+
WHERE inhparent = table_id
|
|
264
|
+
AND inhrelid::regclass::text < oldest_pt_to_keep;
|
|
265
|
+
partitions_to_drop := COALESCE(partitions_to_drop, ARRAY[]::regclass[]);
|
|
266
|
+
|
|
267
|
+
-- check if nothing to do
|
|
268
|
+
IF
|
|
269
|
+
array_length(partitions_to_drop, 1) = 0
|
|
270
|
+
AND array_length(ranges_to_create, 1) = 0
|
|
271
|
+
THEN
|
|
272
|
+
RETURN;
|
|
273
|
+
END IF;
|
|
274
|
+
|
|
259
275
|
-- go from now to future_interval
|
|
260
276
|
FOREACH cur_range IN ARRAY ranges_to_create LOOP
|
|
261
277
|
DECLARE
|
|
@@ -278,13 +294,8 @@ BEGIN
|
|
|
278
294
|
END LOOP;
|
|
279
295
|
|
|
280
296
|
-- Drop old partitions
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
FROM pg_catalog.pg_inherits
|
|
284
|
-
WHERE inhparent = table_id
|
|
285
|
-
AND inhrelid::regclass::text < oldest_pt_to_keep
|
|
286
|
-
) LOOP
|
|
287
|
-
EXECUTE format('DROP TABLE %I', p_info.child);
|
|
297
|
+
FOREACH p_to_drop IN ARRAY partitions_to_drop LOOP
|
|
298
|
+
EXECUTE format('DROP TABLE %I', p_to_drop);
|
|
288
299
|
END LOOP;
|
|
289
300
|
END;
|
|
290
301
|
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE SECURITY DEFINER;
|
|
@@ -799,20 +810,20 @@ END
|
|
|
799
810
|
$$ LANGUAGE plpgsql VOLATILE PARALLEL UNSAFE
|
|
800
811
|
SET search_path TO pgmb;
|
|
801
812
|
|
|
802
|
-
|
|
813
|
+
-- contains fn to maintain partitions for an append-only table, this
|
|
814
|
+
-- can be used for both "events" and "subscription_events" tables.
|
|
815
|
+
-- It trims old partitions that are outside the retention period, and creates new
|
|
816
|
+
-- ones. Also ensures partitions aren't autovacuumed.
|
|
817
|
+
CREATE FUNCTION maintain_append_only_table(
|
|
818
|
+
tbl regclass,
|
|
803
819
|
current_ts timestamptz DEFAULT NOW()
|
|
804
820
|
)
|
|
805
821
|
RETURNS VOID AS $$
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
PERFORM maintain_time_partitions_using_event_id(
|
|
812
|
-
'pgmb.events'::regclass,
|
|
813
|
-
partition_interval := pi,
|
|
814
|
-
future_interval := fic,
|
|
815
|
-
retention_period := rp,
|
|
822
|
+
SELECT maintain_time_partitions_using_event_id(
|
|
823
|
+
tbl,
|
|
824
|
+
partition_interval := get_config_value('partition_interval')::interval,
|
|
825
|
+
future_interval := get_config_value('future_intervals_to_create')::interval,
|
|
826
|
+
retention_period := get_config_value('partition_retention_period')::interval,
|
|
816
827
|
-- turn off autovacuum on the events table, since we're not
|
|
817
828
|
-- going to be updating/deleting rows from it.
|
|
818
829
|
-- Also set fillfactor to 100 since we're only inserting.
|
|
@@ -823,28 +834,28 @@ BEGIN
|
|
|
823
834
|
);',
|
|
824
835
|
current_ts := current_ts
|
|
825
836
|
);
|
|
837
|
+
$$ LANGUAGE sql VOLATILE PARALLEL UNSAFE SECURITY DEFINER
|
|
838
|
+
SET search_path TO pgmb;
|
|
826
839
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
);
|
|
840
|
+
CREATE OR REPLACE PROCEDURE maintain_events_table(
|
|
841
|
+
current_ts timestamptz DEFAULT NOW()
|
|
842
|
+
) AS $$
|
|
843
|
+
BEGIN
|
|
844
|
+
SET search_path TO pgmb;
|
|
845
|
+
-- we commit after each maintainance function to release locks on the
|
|
846
|
+
-- partitions as soon as possible. This avoids blocking "poll_for_events",
|
|
847
|
+
-- & "read_next_events" functions, which when executing all concurrently,
|
|
848
|
+
-- may cause deadlocks due to lock contention on the partitions.
|
|
849
|
+
PERFORM maintain_append_only_table('events'::regclass, current_ts);
|
|
850
|
+
COMMIT;
|
|
851
|
+
|
|
852
|
+
PERFORM maintain_append_only_table('subscription_events'::regclass, current_ts);
|
|
853
|
+
COMMIT;
|
|
842
854
|
END;
|
|
843
|
-
$$ LANGUAGE plpgsql
|
|
844
|
-
SET search_path TO pgmb;
|
|
855
|
+
$$ LANGUAGE plpgsql;
|
|
845
856
|
|
|
846
|
-
|
|
847
|
-
SELECT
|
|
857
|
+
SELECT maintain_append_only_table('events'::regclass);
|
|
858
|
+
SELECT maintain_append_only_table('subscription_events'::regclass);
|
|
848
859
|
|
|
849
860
|
-- setup pg_cron if it's available ----------------
|
|
850
861
|
|
|
@@ -871,7 +882,7 @@ BEGIN
|
|
|
871
882
|
PERFORM cron.schedule(
|
|
872
883
|
'pgmb_maintain_table_partitions',
|
|
873
884
|
get_config_value('pg_cron_partition_maintenance_cron'),
|
|
874
|
-
$CMD$
|
|
885
|
+
$CMD$ CALL pgmb.maintain_events_table(); $CMD$
|
|
875
886
|
);
|
|
876
887
|
|
|
877
888
|
RAISE LOG 'Scheduled pgmb partition maintenance job: %',
|
package/sql/queries.sql
CHANGED
|
@@ -38,7 +38,7 @@ WHERE id IN :ids!;
|
|
|
38
38
|
UPDATE pgmb.subscriptions
|
|
39
39
|
SET
|
|
40
40
|
last_active_at = NOW()
|
|
41
|
-
WHERE id IN (SELECT * FROM unnest(:ids!::
|
|
41
|
+
WHERE id IN (SELECT * FROM unnest(:ids!::text[]));
|
|
42
42
|
|
|
43
43
|
/* @name pollForEvents */
|
|
44
44
|
SELECT count AS "count!" FROM pgmb.poll_for_events() AS count;
|
|
@@ -145,7 +145,7 @@ WITH deleted AS (
|
|
|
145
145
|
WHERE group_id = :groupId!
|
|
146
146
|
AND expiry_interval IS NOT NULL
|
|
147
147
|
AND pgmb.add_interval_imm(last_active_at, expiry_interval) < NOW()
|
|
148
|
-
AND id NOT IN (select * from unnest(:activeIds!::
|
|
148
|
+
AND id NOT IN (select * from unnest(:activeIds!::text[]))
|
|
149
149
|
RETURNING id
|
|
150
150
|
)
|
|
151
151
|
SELECT COUNT(*) AS "deleted!" FROM deleted;
|
|
@@ -160,4 +160,4 @@ WHERE id = :key!::pgmb.config_type
|
|
|
160
160
|
RETURNING 1 AS "updated!";
|
|
161
161
|
|
|
162
162
|
/* @name maintainEventsTable */
|
|
163
|
-
|
|
163
|
+
CALL pgmb.maintain_events_table(COALESCE(:ts, NOW()));
|