@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 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(undefined, this.client);
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, undefined, this.client), this.tableMaintenanceMs);
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!::pgmb.subscription_id[]))
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!::pgmb.subscription_id[]))
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 type IMaintainEventsTableParams = void;
483
- /** 'MaintainEventsTable' return type */
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
- * SELECT pgmb.maintain_events_table()
495
+ * CALL pgmb.maintain_events_table(COALESCE(:ts, NOW()))
496
496
  * ```
497
497
  */
498
- export declare const maintainEventsTable: PreparedQuery<void, IMaintainEventsTableResult>;
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!::pgmb.subscription_id[]))" };
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!::pgmb.subscription_id[]))
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!::pgmb.subscription_id[]))\n\tRETURNING id\n)\nSELECT COUNT(*) AS \"deleted!\" FROM deleted" };
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!::pgmb.subscription_id[]))
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": "SELECT pgmb.maintain_events_table()" };
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
- * SELECT pgmb.maintain_events_table()
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.13",
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": "TZ=UTC NODE_ENV=test node --env-file ./.env.test --test tests/*.test.ts",
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 src/benchmark/run.ts",
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', '120 minutes'),
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', '*/30 * * * *');
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
- FOR p_info IN (
282
- SELECT inhrelid::regclass AS child
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
- CREATE OR REPLACE FUNCTION maintain_events_table(
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
- DECLARE
807
- pi INTERVAL := get_config_value('partition_interval');
808
- fic INTERVAL := get_config_value('future_intervals_to_create');
809
- rp INTERVAL := get_config_value('partition_retention_period');
810
- BEGIN
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
- PERFORM maintain_time_partitions_using_event_id(
828
- 'pgmb.subscription_events'::regclass,
829
- partition_interval := pi,
830
- future_interval := fic,
831
- retention_period := rp,
832
- -- turn off autovacuum on the events table, since we're not
833
- -- going to be updating/deleting rows from it.
834
- -- Also set fillfactor to 100 since we're only inserting.
835
- additional_sql := 'ALTER TABLE $1 SET(
836
- fillfactor = 100,
837
- autovacuum_enabled = false,
838
- toast.autovacuum_enabled = false
839
- );',
840
- current_ts := current_ts
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 VOLATILE PARALLEL UNSAFE
844
- SET search_path TO pgmb;
855
+ $$ LANGUAGE plpgsql;
845
856
 
846
- -- create the initial partitions
847
- SELECT maintain_events_table();
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$ SELECT pgmb.maintain_events_table(); $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!::pgmb.subscription_id[]));
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!::pgmb.subscription_id[]))
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
- SELECT pgmb.maintain_events_table();
163
+ CALL pgmb.maintain_events_table(COALESCE(:ts, NOW()));