@haathie/pgmb 0.2.0
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/LICENSE +21 -0
- package/lib/abortable-async-iterator.d.ts +14 -0
- package/lib/abortable-async-iterator.js +86 -0
- package/lib/batcher.d.ts +12 -0
- package/lib/batcher.js +71 -0
- package/lib/client.d.ts +73 -0
- package/lib/client.js +432 -0
- package/lib/consts.d.ts +1 -0
- package/lib/consts.js +4 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +19 -0
- package/lib/queries.d.ts +453 -0
- package/lib/queries.js +235 -0
- package/lib/query-types.d.ts +17 -0
- package/lib/query-types.js +2 -0
- package/lib/retry-handler.d.ts +11 -0
- package/lib/retry-handler.js +93 -0
- package/lib/sse.d.ts +4 -0
- package/lib/sse.js +137 -0
- package/lib/types.d.ts +202 -0
- package/lib/types.js +2 -0
- package/lib/utils.d.ts +15 -0
- package/lib/utils.js +52 -0
- package/lib/webhook-handler.d.ts +6 -0
- package/lib/webhook-handler.js +68 -0
- package/package.json +52 -0
- package/readme.md +493 -0
- package/sql/pgmb-0.1.12-0.2.0.sql +1018 -0
- package/sql/pgmb-0.1.12.sql +612 -0
- package/sql/pgmb-0.1.5-0.1.6.sql +256 -0
- package/sql/pgmb-0.1.6-0.1.12.sql +95 -0
- package/sql/pgmb.sql +1030 -0
- package/sql/queries.sql +154 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
SET search_path = pgmb, pg_catalog;
|
|
2
|
+
|
|
3
|
+
-- Find all existing queues via the "pgmb.queues" table,
|
|
4
|
+
-- and drop the default on the "id" column
|
|
5
|
+
DO $$
|
|
6
|
+
DECLARE
|
|
7
|
+
queue_schema VARCHAR(64);
|
|
8
|
+
BEGIN
|
|
9
|
+
-- get all queue schemas
|
|
10
|
+
-- drop the default on the "id" column for each queue
|
|
11
|
+
FOR queue_schema IN (SELECT schema_name FROM pgmb.queues) LOOP
|
|
12
|
+
EXECUTE 'ALTER TABLE '
|
|
13
|
+
|| quote_ident(queue_schema)
|
|
14
|
+
|| '.live_messages ALTER COLUMN id DROP DEFAULT';
|
|
15
|
+
END LOOP;
|
|
16
|
+
END $$;
|
|
17
|
+
|
|
18
|
+
DROP FUNCTION create_message_id(timestamp with time zone,bigint);
|
|
19
|
+
|
|
20
|
+
DROP FUNCTION create_random_bigint(additive double precision);
|
|
21
|
+
|
|
22
|
+
DROP FUNCTION extract_date_from_message_id(message_id VARCHAR(64));
|
|
23
|
+
|
|
24
|
+
DROP FUNCTION get_queue_metrics(queue_name VARCHAR(64));
|
|
25
|
+
|
|
26
|
+
DROP FUNCTION get_all_queue_metrics();
|
|
27
|
+
|
|
28
|
+
CREATE OR REPLACE FUNCTION create_queue_table(queue_name VARCHAR(64), schema_name VARCHAR(64), queue_type pgmb.queue_type) RETURNS VOID AS $$
|
|
29
|
+
BEGIN
|
|
30
|
+
-- create the live_messages table
|
|
31
|
+
EXECUTE 'CREATE TABLE ' || quote_ident(schema_name) || '.live_messages (
|
|
32
|
+
id VARCHAR(22) PRIMARY KEY,
|
|
33
|
+
message BYTEA NOT NULL,
|
|
34
|
+
headers JSONB NOT NULL DEFAULT ''{}''::JSONB,
|
|
35
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
36
|
+
)';
|
|
37
|
+
IF queue_type = 'unlogged' THEN
|
|
38
|
+
EXECUTE 'ALTER TABLE ' || quote_ident(schema_name)
|
|
39
|
+
|| '.live_messages SET UNLOGGED';
|
|
40
|
+
END IF;
|
|
41
|
+
END;
|
|
42
|
+
$$ LANGUAGE plpgsql;
|
|
43
|
+
|
|
44
|
+
CREATE OR REPLACE FUNCTION create_random_bigint() RETURNS BIGINT AS $$
|
|
45
|
+
BEGIN
|
|
46
|
+
-- the message ID allows for 7 hex-bytes of randomness,
|
|
47
|
+
-- i.e. 28 bits of randomness. Thus, the max we allow is 2^28/2
|
|
48
|
+
-- i.e. 0xffffff8, which allows for batch inserts to increment the
|
|
49
|
+
-- randomness for up to another 2^28/2 messages (more than enough)
|
|
50
|
+
RETURN (random() * 0xffffff8)::BIGINT;
|
|
51
|
+
END
|
|
52
|
+
$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE;
|
|
53
|
+
|
|
54
|
+
CREATE OR REPLACE FUNCTION create_message_id(dt timestamptz = clock_timestamp(), rand bigint = pgmb.create_random_bigint()) RETURNS VARCHAR(22) AS $$
|
|
55
|
+
BEGIN
|
|
56
|
+
-- create a unique message ID, 16 chars of hex-date
|
|
57
|
+
-- some additional bytes of randomness
|
|
58
|
+
-- ensure the string is always, at most 32 bytes
|
|
59
|
+
RETURN substr(
|
|
60
|
+
'pm'
|
|
61
|
+
|| substr(lpad(to_hex((extract(epoch from dt) * 1000000)::bigint), 13, '0'), 1, 13)
|
|
62
|
+
|| lpad(to_hex(rand), 7, '0'),
|
|
63
|
+
1,
|
|
64
|
+
22
|
|
65
|
+
);
|
|
66
|
+
END
|
|
67
|
+
$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE;
|
|
68
|
+
|
|
69
|
+
CREATE OR REPLACE FUNCTION get_max_message_id(dt timestamptz = clock_timestamp()) RETURNS VARCHAR(22) AS $$
|
|
70
|
+
BEGIN
|
|
71
|
+
RETURN pgmb.create_message_id(
|
|
72
|
+
dt,
|
|
73
|
+
rand := 999999999999 -- max randomness
|
|
74
|
+
);
|
|
75
|
+
END
|
|
76
|
+
$$ LANGUAGE plpgsql VOLATILE PARALLEL SAFE;
|
|
77
|
+
|
|
78
|
+
CREATE OR REPLACE FUNCTION extract_date_from_message_id(message_id VARCHAR(22)) RETURNS TIMESTAMPTZ AS $$
|
|
79
|
+
BEGIN
|
|
80
|
+
-- convert it to a timestamp
|
|
81
|
+
RETURN to_timestamp(('0x' || substr(message_id, 3, 13))::numeric / 1000000);
|
|
82
|
+
END
|
|
83
|
+
$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE;
|
|
84
|
+
|
|
85
|
+
CREATE OR REPLACE FUNCTION send(queue_name VARCHAR(64), messages pgmb.enqueue_msg[]) RETURNS SETOF VARCHAR(22) AS $$
|
|
86
|
+
DECLARE
|
|
87
|
+
-- we'll have a starting random number, and each successive message ID's
|
|
88
|
+
-- random component will be this number + the ordinality of the message.
|
|
89
|
+
start_rand constant BIGINT = pgmb.create_random_bigint();
|
|
90
|
+
BEGIN
|
|
91
|
+
-- create the ID for each message, and then send to the internal _send fn
|
|
92
|
+
RETURN QUERY
|
|
93
|
+
WITH msg_records AS (
|
|
94
|
+
SELECT (
|
|
95
|
+
pgmb.create_message_id(
|
|
96
|
+
COALESCE(m.consume_at, clock_timestamp()),
|
|
97
|
+
start_rand + m.ordinality
|
|
98
|
+
),
|
|
99
|
+
m.message,
|
|
100
|
+
m.headers
|
|
101
|
+
)::pgmb.msg_record AS record
|
|
102
|
+
FROM unnest(messages) WITH ORDINALITY AS m
|
|
103
|
+
)
|
|
104
|
+
SELECT pgmb._send(queue_name, ARRAY_AGG(m.record)::pgmb.msg_record[])
|
|
105
|
+
FROM msg_records m;
|
|
106
|
+
END
|
|
107
|
+
$$ LANGUAGE plpgsql;
|
|
108
|
+
|
|
109
|
+
CREATE OR REPLACE FUNCTION _send(queue_name VARCHAR(64), messages pgmb.msg_record[]) RETURNS SETOF VARCHAR(22) AS $$
|
|
110
|
+
DECLARE
|
|
111
|
+
-- check if the queue already exists
|
|
112
|
+
schema_name VARCHAR(64);
|
|
113
|
+
default_headers JSONB;
|
|
114
|
+
BEGIN
|
|
115
|
+
-- each queue would have its own channel to listen on, so a consumer can
|
|
116
|
+
-- listen to a specific queue. This'll be used to notify the consumer when
|
|
117
|
+
-- new messages are added to the queue.
|
|
118
|
+
PERFORM pg_notify(
|
|
119
|
+
'chn_' || queue_name,
|
|
120
|
+
('{"count":' || array_length(messages, 1)::varchar || '}')::varchar
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
-- get schema name and default headers
|
|
124
|
+
SELECT q.schema_name, q.default_headers FROM pgmb.queues q
|
|
125
|
+
WHERE q.name = queue_name INTO schema_name, default_headers;
|
|
126
|
+
-- Insert the message into the queue and return all message IDs. We use the
|
|
127
|
+
-- ordinality of the array to ensure that each message is inserted in the same
|
|
128
|
+
-- order as it was sent. This is important for the consumer to process the
|
|
129
|
+
-- messages in the same order as they were sent.
|
|
130
|
+
RETURN QUERY
|
|
131
|
+
EXECUTE 'INSERT INTO '
|
|
132
|
+
|| quote_ident(schema_name)
|
|
133
|
+
|| '.live_messages (id, message, headers)
|
|
134
|
+
SELECT
|
|
135
|
+
id,
|
|
136
|
+
message,
|
|
137
|
+
COALESCE($1, ''{}''::JSONB) || COALESCE(headers, ''{}''::JSONB)
|
|
138
|
+
FROM unnest($2)
|
|
139
|
+
RETURNING id' USING default_headers, messages;
|
|
140
|
+
END
|
|
141
|
+
$$ LANGUAGE plpgsql;
|
|
142
|
+
|
|
143
|
+
CREATE OR REPLACE FUNCTION read_from_queue(queue_name VARCHAR(64), limit_count INTEGER = 1) RETURNS SETOF pgmb.msg_record AS $$
|
|
144
|
+
DECLARE
|
|
145
|
+
schema_name VARCHAR(64);
|
|
146
|
+
BEGIN
|
|
147
|
+
-- get schema name
|
|
148
|
+
SELECT q.schema_name FROM pgmb.queues q
|
|
149
|
+
WHERE q.name = queue_name INTO schema_name;
|
|
150
|
+
-- read the messages from the queue
|
|
151
|
+
RETURN QUERY EXECUTE 'SELECT id, message, headers
|
|
152
|
+
FROM ' || quote_ident(schema_name) || '.live_messages
|
|
153
|
+
WHERE id <= pgmb.get_max_message_id()
|
|
154
|
+
ORDER BY id ASC
|
|
155
|
+
FOR UPDATE SKIP LOCKED
|
|
156
|
+
LIMIT $1'
|
|
157
|
+
USING limit_count;
|
|
158
|
+
END
|
|
159
|
+
$$ LANGUAGE plpgsql;
|
|
160
|
+
|
|
161
|
+
CREATE OR REPLACE FUNCTION publish(messages pgmb.publish_msg[]) RETURNS SETOF VARCHAR(22) AS $$
|
|
162
|
+
DECLARE
|
|
163
|
+
start_rand constant BIGINT = pgmb.create_random_bigint();
|
|
164
|
+
BEGIN
|
|
165
|
+
-- Create message IDs for each message, then we'll send them to the individual
|
|
166
|
+
-- queues. The ID will be the same for all queues, but the headers may vary
|
|
167
|
+
-- across queues.
|
|
168
|
+
RETURN QUERY
|
|
169
|
+
WITH msg_records AS (
|
|
170
|
+
SELECT
|
|
171
|
+
pgmb.create_message_id(
|
|
172
|
+
COALESCE(consume_at, clock_timestamp()),
|
|
173
|
+
start_rand + ordinality
|
|
174
|
+
) AS id,
|
|
175
|
+
message,
|
|
176
|
+
JSONB_SET(
|
|
177
|
+
COALESCE(headers, '{}'::JSONB),
|
|
178
|
+
'{exchange}',
|
|
179
|
+
TO_JSONB(exchange)
|
|
180
|
+
) as headers,
|
|
181
|
+
exchange,
|
|
182
|
+
ordinality
|
|
183
|
+
FROM unnest(messages) WITH ORDINALITY
|
|
184
|
+
),
|
|
185
|
+
sends AS (
|
|
186
|
+
SELECT
|
|
187
|
+
pgmb._send(
|
|
188
|
+
q.queue_name,
|
|
189
|
+
ARRAY_AGG((m.id, m.message, m.headers)::pgmb.msg_record)
|
|
190
|
+
) as id
|
|
191
|
+
FROM msg_records m,
|
|
192
|
+
LATERAL (
|
|
193
|
+
SELECT DISTINCT name, unnest(queues) AS queue_name
|
|
194
|
+
FROM pgmb.exchanges e
|
|
195
|
+
WHERE e.name = m.exchange
|
|
196
|
+
) q
|
|
197
|
+
GROUP BY q.queue_name
|
|
198
|
+
)
|
|
199
|
+
-- we'll select an aggregate of "sends", to ensure that each "send" call
|
|
200
|
+
-- is executed. If this is not done, PG may optimize the query
|
|
201
|
+
-- and not execute the "sends" CTE at all, resulting in no messages being sent.
|
|
202
|
+
-- So, this aggregate call ensures PG does not optimize it away.
|
|
203
|
+
SELECT
|
|
204
|
+
CASE WHEN count(*) FILTER (WHERE sends.id IS NOT NULL) > 0 THEN m.id END
|
|
205
|
+
FROM msg_records m
|
|
206
|
+
LEFT JOIN sends ON sends.id = m.id
|
|
207
|
+
GROUP BY m.id, m.ordinality
|
|
208
|
+
ORDER BY m.ordinality;
|
|
209
|
+
END
|
|
210
|
+
$$ LANGUAGE plpgsql;
|
|
211
|
+
|
|
212
|
+
CREATE OR REPLACE FUNCTION get_queue_metrics(queue_name VARCHAR(64), approximate BOOLEAN = FALSE) RETURNS SETOF pgmb.metrics_result AS $$
|
|
213
|
+
DECLARE
|
|
214
|
+
schema_name VARCHAR(64);
|
|
215
|
+
BEGIN
|
|
216
|
+
-- get schema name
|
|
217
|
+
SELECT q.schema_name FROM pgmb.queues q
|
|
218
|
+
WHERE q.name = queue_name INTO schema_name;
|
|
219
|
+
-- get the metrics of the queue
|
|
220
|
+
RETURN QUERY EXECUTE 'SELECT
|
|
221
|
+
''' || queue_name || '''::varchar(64) AS queue_name,
|
|
222
|
+
' ||
|
|
223
|
+
(CASE WHEN approximate THEN
|
|
224
|
+
'COALESCE(pgmb.get_approximate_count(' || quote_literal(schema_name || '.live_messages') || '), 0) AS total_length,'
|
|
225
|
+
|| '0 AS consumable_length,'
|
|
226
|
+
ELSE
|
|
227
|
+
'count(*)::int AS total_length,'
|
|
228
|
+
|| '(count(*) FILTER (WHERE id <= pgmb.get_max_message_id()))::int AS consumable_length,'
|
|
229
|
+
END) || '
|
|
230
|
+
(clock_timestamp() - pgmb.extract_date_from_message_id(max(id))) AS newest_msg_age_sec,
|
|
231
|
+
(clock_timestamp() - pgmb.extract_date_from_message_id(min(id))) AS oldest_msg_age_sec
|
|
232
|
+
FROM ' || quote_ident(schema_name) || '.live_messages';
|
|
233
|
+
END
|
|
234
|
+
$$ LANGUAGE plpgsql;
|
|
235
|
+
|
|
236
|
+
CREATE OR REPLACE FUNCTION get_all_queue_metrics(approximate BOOLEAN = FALSE) RETURNS SETOF pgmb.metrics_result AS $$
|
|
237
|
+
BEGIN
|
|
238
|
+
RETURN QUERY
|
|
239
|
+
SELECT m.*
|
|
240
|
+
FROM pgmb.queues q, pgmb.get_queue_metrics(q.name, approximate) m
|
|
241
|
+
ORDER BY q.name ASC;
|
|
242
|
+
END
|
|
243
|
+
$$ LANGUAGE plpgsql;
|
|
244
|
+
|
|
245
|
+
CREATE OR REPLACE FUNCTION get_approximate_count(table_name regclass) RETURNS INTEGER AS $$
|
|
246
|
+
SELECT (
|
|
247
|
+
CASE WHEN c.reltuples < 0 THEN NULL -- never vacuumed
|
|
248
|
+
WHEN c.relpages = 0 THEN float8 '0' -- empty table
|
|
249
|
+
ELSE c.reltuples / c.relpages END
|
|
250
|
+
* (pg_catalog.pg_relation_size(c.oid)
|
|
251
|
+
/ pg_catalog.current_setting('block_size')::int)
|
|
252
|
+
)::bigint
|
|
253
|
+
FROM pg_catalog.pg_class c
|
|
254
|
+
WHERE c.oid = table_name
|
|
255
|
+
LIMIT 1;
|
|
256
|
+
$$ LANGUAGE sql;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION pgmb.ack_msgs(
|
|
2
|
+
queue_name VARCHAR(64),
|
|
3
|
+
success BOOLEAN,
|
|
4
|
+
ids VARCHAR(22)[]
|
|
5
|
+
)
|
|
6
|
+
RETURNS VOID AS $$
|
|
7
|
+
DECLARE
|
|
8
|
+
schema_name VARCHAR(64);
|
|
9
|
+
ack_setting pgmb.queue_ack_setting;
|
|
10
|
+
query_str TEXT;
|
|
11
|
+
deleted_msg_count int;
|
|
12
|
+
BEGIN
|
|
13
|
+
-- get schema name and ack setting
|
|
14
|
+
SELECT q.schema_name, q.ack_setting
|
|
15
|
+
FROM pgmb.queues q
|
|
16
|
+
WHERE q.name = queue_name
|
|
17
|
+
INTO schema_name, ack_setting;
|
|
18
|
+
|
|
19
|
+
-- we'll construct a single CTE query that'll delete messages,
|
|
20
|
+
-- requeue them if needed, and archive them if ack_setting is 'archive'.
|
|
21
|
+
query_str := 'WITH deleted_msgs AS (
|
|
22
|
+
DELETE FROM ' || quote_ident(schema_name) || '.live_messages
|
|
23
|
+
WHERE id = ANY($1)
|
|
24
|
+
RETURNING id, message, headers
|
|
25
|
+
)';
|
|
26
|
+
|
|
27
|
+
-- re-insert messages that can be retried
|
|
28
|
+
IF NOT success THEN
|
|
29
|
+
query_str := query_str || ',
|
|
30
|
+
requeued AS (
|
|
31
|
+
INSERT INTO '
|
|
32
|
+
|| quote_ident(schema_name)
|
|
33
|
+
|| '.live_messages (id, message, headers)
|
|
34
|
+
SELECT
|
|
35
|
+
pgmb.create_message_id(
|
|
36
|
+
clock_timestamp() + (interval ''1 second'') * (t.headers->''retriesLeftS''->0)::int,
|
|
37
|
+
rn
|
|
38
|
+
),
|
|
39
|
+
t.message,
|
|
40
|
+
t.headers
|
|
41
|
+
-- set retriesLeftS to the next retry
|
|
42
|
+
|| jsonb_build_object(''retriesLeftS'', (t.headers->''retriesLeftS'') #- ''{0}'')
|
|
43
|
+
-- set the originalMessageId
|
|
44
|
+
-- to the original message ID if it exists
|
|
45
|
+
|| jsonb_build_object(
|
|
46
|
+
''originalMessageId'', COALESCE(t.headers->''originalMessageId'', to_jsonb(t.id))
|
|
47
|
+
)
|
|
48
|
+
-- set the tries
|
|
49
|
+
|| jsonb_build_object(
|
|
50
|
+
''tries'',
|
|
51
|
+
CASE
|
|
52
|
+
WHEN jsonb_typeof(t.headers->''tries'') = ''number'' THEN
|
|
53
|
+
to_jsonb((t.headers->>''tries'')::INTEGER + 1)
|
|
54
|
+
ELSE
|
|
55
|
+
to_jsonb(1)
|
|
56
|
+
END
|
|
57
|
+
)
|
|
58
|
+
FROM (select *, row_number() over () AS rn FROM deleted_msgs) t
|
|
59
|
+
WHERE jsonb_typeof(t.headers -> ''retriesLeftS'' -> 0) = ''number''
|
|
60
|
+
RETURNING id
|
|
61
|
+
),
|
|
62
|
+
requeued_notify AS (
|
|
63
|
+
SELECT pg_notify(
|
|
64
|
+
''chn_' || queue_name || ''',
|
|
65
|
+
''{"count":'' || (select count(*) from requeued)::varchar || ''}''
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
';
|
|
69
|
+
END IF;
|
|
70
|
+
|
|
71
|
+
IF ack_setting = 'archive' THEN
|
|
72
|
+
-- Delete the messages from live_messages and insert them into
|
|
73
|
+
-- consumed_messages in one operation,
|
|
74
|
+
-- if the queue's ack_setting is set to 'archive'
|
|
75
|
+
query_str := query_str || ',
|
|
76
|
+
archived_records AS (
|
|
77
|
+
INSERT INTO ' || quote_ident(schema_name) || '.consumed_messages
|
|
78
|
+
(id, message, headers, success)
|
|
79
|
+
SELECT t.id, t.message, t.headers, $2::boolean
|
|
80
|
+
FROM deleted_msgs t
|
|
81
|
+
)';
|
|
82
|
+
END IF;
|
|
83
|
+
|
|
84
|
+
query_str := query_str || '
|
|
85
|
+
SELECT COUNT(*) FROM deleted_msgs';
|
|
86
|
+
|
|
87
|
+
EXECUTE query_str USING ids, success INTO deleted_msg_count;
|
|
88
|
+
|
|
89
|
+
-- Raise exception if no rows were affected
|
|
90
|
+
IF deleted_msg_count != array_length(ids, 1) THEN
|
|
91
|
+
RAISE EXCEPTION 'Only removed % out of % expected message(s).',
|
|
92
|
+
deleted_msg_count, array_length(ids, 1);
|
|
93
|
+
END IF;
|
|
94
|
+
END
|
|
95
|
+
$$ LANGUAGE plpgsql;
|