postqueue 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/lib/postqueue/item/inserter.rb +6 -46
- data/lib/postqueue/tracker/tracker.sql +198 -0
- data/lib/postqueue/version.rb +1 -1
- data/spec/spec_helper.rb +4 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 169eb71de24fab5bde343459df49bf1395881fc4
|
4
|
+
data.tar.gz: fb06468f9fe5412dc655188823d5544be0ba9d48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5589efb14a39dedb9b56c207eaffdbef1e8533cc9717991a0b8d2cdb12a0966b15cbf712b7720ff2df3c1b0e75b7f66f9402b8bfa9eeb3b62308cff3cd6ed6a6
|
7
|
+
data.tar.gz: 26cf306354c7a91c9c1bb172cca2aaf2639195aa25c0b9005d4244335f9a8e46c5a85848fa78afe869addb0e0d03423eba53c1bcebb0f3128b3d00abae3bd192
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "active_record"
|
2
|
+
require "simple/sql"
|
2
3
|
|
3
4
|
module Postqueue
|
4
5
|
#
|
@@ -7,52 +8,11 @@ module Postqueue
|
|
7
8
|
# This source file provides multiple implementations to insert Postqueue::Items.
|
8
9
|
# Which one will be used depends on the "extend XXXInserter" line below.
|
9
10
|
class Item < ActiveRecord::Base
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def self.insert_item(op:, entity_id:)
|
12
|
+
# In contrast to ActiveRecord, which clocks in around 600µs per item,
|
13
|
+
# Simple::SQL's insert only takes 100µs per item. Using prepared
|
14
|
+
# statements would further reduce the runtime to 50µs
|
15
|
+
::Simple::SQL.insert table_name, op: op, entity_id: entity_id
|
14
16
|
end
|
15
|
-
|
16
|
-
module RawInserter
|
17
|
-
def insert_sql
|
18
|
-
"INSERT INTO #{table_name}(op, entity_id) VALUES($1, $2)"
|
19
|
-
end
|
20
|
-
|
21
|
-
def insert_item(op:, entity_id:)
|
22
|
-
connection.raw_connection.exec_params(insert_sql, [op, entity_id])
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
module PreparedRawInserter
|
27
|
-
def insert_sql
|
28
|
-
"INSERT INTO #{table_name}(op, entity_id) VALUES($1, $2)"
|
29
|
-
end
|
30
|
-
|
31
|
-
def prepared_inserter_statement(raw_connection)
|
32
|
-
@prepared_inserter_statements ||= {}
|
33
|
-
|
34
|
-
# a prepared connection is PER DATABASE CONNECTION. It is not shared across
|
35
|
-
# connections, and it is not per thread, since a Thread might use different
|
36
|
-
# connections during its lifetime.
|
37
|
-
@prepared_inserter_statements[raw_connection.object_id] ||= create_prepared_inserter_statement(raw_connection)
|
38
|
-
end
|
39
|
-
|
40
|
-
# prepares the INSERT statement, and returns its name
|
41
|
-
def create_prepared_inserter_statement(raw_connection)
|
42
|
-
name = "postqueue-insert-#{table_name}-#{raw_connection.object_id}"
|
43
|
-
raw_connection.prepare(name, insert_sql)
|
44
|
-
name
|
45
|
-
end
|
46
|
-
|
47
|
-
def insert_item(op:, entity_id:)
|
48
|
-
raw_connection = connection.raw_connection
|
49
|
-
statement_name = prepared_inserter_statement(raw_connection)
|
50
|
-
raw_connection.exec_prepared(statement_name, [op, entity_id])
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
# extend ActiveRecordInserter # 600µs per item
|
55
|
-
extend RawInserter # 100µs per item
|
56
|
-
# extend PreparedRawInserter # 50µs per item
|
57
17
|
end
|
58
18
|
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
-- An tracker history is important on most tables. Provide an tracker trigger that logs to
|
2
|
+
-- a dedicated tracker table for the major relations.
|
3
|
+
--
|
4
|
+
-- This file should be generic and not depend on application roles or structures,
|
5
|
+
-- as it's being listed here:
|
6
|
+
--
|
7
|
+
-- https://wiki.postgresql.org/wiki/Audit_trigger_91plus
|
8
|
+
--
|
9
|
+
-- This trigger was originally based on
|
10
|
+
-- http://wiki.postgresql.org/wiki/Audit_trigger
|
11
|
+
-- but has been completely rewritten.
|
12
|
+
--
|
13
|
+
-- Should really be converted into a relocatable EXTENSION, with control and upgrade files.
|
14
|
+
|
15
|
+
CREATE EXTENSION IF NOT EXISTS hstore;
|
16
|
+
|
17
|
+
<% if true %>
|
18
|
+
DROP SCHEMA IF EXISTS tracker CASCADE;
|
19
|
+
<% end %>
|
20
|
+
CREATE SCHEMA tracker;
|
21
|
+
REVOKE ALL ON SCHEMA tracker FROM public;
|
22
|
+
|
23
|
+
COMMENT ON SCHEMA tracker IS 'Out-of-table tracing';
|
24
|
+
|
25
|
+
CREATE TYPE tracker.actions AS ENUM(
|
26
|
+
'INSERT', 'UPDATE', 'DELETE'
|
27
|
+
);
|
28
|
+
|
29
|
+
--
|
30
|
+
-- Audited data. Lots of information is available, it's just a matter of how much
|
31
|
+
-- you really want to record. See:
|
32
|
+
--
|
33
|
+
-- http://www.postgresql.org/docs/9.1/static/functions-info.html
|
34
|
+
--
|
35
|
+
-- Remember, every column you add takes up more tracker table space and slows tracker
|
36
|
+
-- inserts.
|
37
|
+
--
|
38
|
+
-- Every index you add has a big impact too, so avoid adding indexes to the
|
39
|
+
-- tracker table unless you REALLY need them. The hstore GIST indexes are
|
40
|
+
-- particularly expensive.
|
41
|
+
--
|
42
|
+
-- It is sometimes worth copying the tracker table, or a coarse subset of it that
|
43
|
+
-- you're interested in, into a temporary table where you CREATE any useful
|
44
|
+
-- indexes and do your analysis.
|
45
|
+
--
|
46
|
+
CREATE TABLE tracker.events (
|
47
|
+
id bigserial primary key, -- id IS 'Unique identifier for each tracked event';
|
48
|
+
row_data hstore, -- row_data IS 'Record value. For INSERT this is the new tuple. For DELETE and UPDATE it is the old tuple.';
|
49
|
+
changed_fields hstore, -- changed_fields IS 'New values of fields changed by UPDATE. Null except for UPDATE events.';
|
50
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL, -- created_at IS 'Wall clock time at which tracked event''s trigger call occurred';
|
51
|
+
checked_out_at TIMESTAMP WITH TIME ZONE -- checked_out_at IS 'Wall clock time at which event is checked out for processing';
|
52
|
+
);
|
53
|
+
|
54
|
+
|
55
|
+
REVOKE ALL ON tracker.events FROM public;
|
56
|
+
|
57
|
+
/*
|
58
|
+
|
59
|
+
[TODO] - add comments on columns
|
60
|
+
|
61
|
+
*/
|
62
|
+
|
63
|
+
COMMENT ON TABLE tracker.events IS 'History of tracked actions on tracked tables, from tracker.if_modified_func()';
|
64
|
+
-- COMMENT ON COLUMN tracker.events.session_user_name IS 'Login / session user whose statement caused the tracked event';
|
65
|
+
|
66
|
+
/*
|
67
|
+
|
68
|
+
[TODO] - create indices for common search patterns
|
69
|
+
|
70
|
+
*/
|
71
|
+
|
72
|
+
-- CREATE INDEX events_table_oid_idx ON tracker.events(table_oid);
|
73
|
+
-- CREATE INDEX events_action_idx ON tracker.events(action);
|
74
|
+
|
75
|
+
/* [TODO] - change checked_out_at to default to 'infinity' */
|
76
|
+
|
77
|
+
|
78
|
+
/*
|
79
|
+
|
80
|
+
Track changes to a table at row level.
|
81
|
+
|
82
|
+
Optional parameters to trigger in CREATE TRIGGER call:
|
83
|
+
|
84
|
+
param 0: text, name of primary key column;
|
85
|
+
param 1: text[], columns to ignore in updates. Default [].
|
86
|
+
|
87
|
+
Updates to ignored cols are omitted from changed_fields.
|
88
|
+
|
89
|
+
Updates with only ignored cols changed are not inserted
|
90
|
+
into the tracker log.
|
91
|
+
|
92
|
+
Note that the user name logged is the login role for the session.
|
93
|
+
The tracker trigger cannot obtain the active role because it is reset
|
94
|
+
by the SECURITY DEFINER invocation of the tracker trigger its self.
|
95
|
+
|
96
|
+
*/
|
97
|
+
|
98
|
+
CREATE OR REPLACE FUNCTION tracker.if_modified_func() RETURNS TRIGGER AS $body$
|
99
|
+
DECLARE
|
100
|
+
event fq_table_name;
|
101
|
+
h_old hstore;
|
102
|
+
h_new hstore;
|
103
|
+
entity_pkey_name text;
|
104
|
+
BEGIN
|
105
|
+
IF TG_WHEN <> 'AFTER' THEN
|
106
|
+
RAISE EXCEPTION 'tracker.if_modified_func() may only run as an AFTER trigger';
|
107
|
+
END IF;
|
108
|
+
|
109
|
+
-- get args
|
110
|
+
|
111
|
+
entity_pkey_name = TG_ARGV[0];
|
112
|
+
|
113
|
+
-- fill row
|
114
|
+
|
115
|
+
IF (TG_OP = 'UPDATE') THEN
|
116
|
+
event.new_fields = jsonb(NEW.*);
|
117
|
+
event.old_fields = jsonb(OLD.*);
|
118
|
+
event.entity_id = event.new_fields->entity_pkey_name
|
119
|
+
ELSIF (TG_OP = 'DELETE') THEN
|
120
|
+
event.old_fields = jsonb(OLD.*);
|
121
|
+
event.entity_id = event.old_fields->entity_pkey_name
|
122
|
+
ELSIF (TG_OP = 'INSERT') THEN
|
123
|
+
event.new_fields = jsonb(NEW.*);
|
124
|
+
event.entity_id = event.new_fields->entity_pkey_name
|
125
|
+
END IF;
|
126
|
+
|
127
|
+
INSERT INTO tracker.events VALUES (event.*);
|
128
|
+
RETURN NULL;
|
129
|
+
END;
|
130
|
+
$body$
|
131
|
+
LANGUAGE plpgsql
|
132
|
+
SECURITY DEFINER
|
133
|
+
SET search_path = pg_catalog, public;
|
134
|
+
|
135
|
+
/*
|
136
|
+
|
137
|
+
Add tracking support to a table.
|
138
|
+
|
139
|
+
Arguments:
|
140
|
+
target_table: Table name, schema qualified if not on search_path
|
141
|
+
primary_key_name: Name of primary key column
|
142
|
+
ignored_cols: Columns to exclude from update diffs, ignore updates that change only ignored cols.
|
143
|
+
|
144
|
+
*/
|
145
|
+
|
146
|
+
CREATE OR REPLACE FUNCTION tracker.track_table(target_table regclass, entity_pkey_name text, ignored_cols text[])
|
147
|
+
RETURNS void AS $body$
|
148
|
+
DECLARE
|
149
|
+
_q_txt text;
|
150
|
+
_ignored_cols_snip text = '';
|
151
|
+
BEGIN
|
152
|
+
IF array_length(ignored_cols,1) > 0 THEN
|
153
|
+
_ignored_cols_snip = ', ' || quote_literal(ignored_cols);
|
154
|
+
END IF;
|
155
|
+
|
156
|
+
EXECUTE 'DROP TRIGGER IF EXISTS track_trigger_row ON ' || quote_ident(target_table::TEXT);
|
157
|
+
|
158
|
+
_q_txt = 'CREATE TRIGGER track_trigger_row AFTER INSERT OR UPDATE OR DELETE ON ' ||
|
159
|
+
quote_ident(target_table::TEXT) ||
|
160
|
+
' FOR EACH ROW EXECUTE PROCEDURE tracker.if_modified_func(' ||
|
161
|
+
quote_literal(entity_pkey_name) ||
|
162
|
+
_ignored_cols_snip || ');';
|
163
|
+
EXECUTE _q_txt;
|
164
|
+
END;
|
165
|
+
$body$
|
166
|
+
language 'plpgsql';
|
167
|
+
|
168
|
+
|
169
|
+
/*
|
170
|
+
|
171
|
+
Add tracking support to the given table. No cols are ignored. (Shortcut)
|
172
|
+
|
173
|
+
*/
|
174
|
+
|
175
|
+
CREATE OR REPLACE FUNCTION tracker.track_table(target_table regclass) RETURNS void AS $body$
|
176
|
+
SELECT tracker.track_table($1, 'id', ARRAY[]::text[]);
|
177
|
+
$body$ LANGUAGE 'sql';
|
178
|
+
|
179
|
+
COMMENT ON FUNCTION tracker.track_table(regclass) IS $body$
|
180
|
+
$body$;
|
181
|
+
|
182
|
+
|
183
|
+
--
|
184
|
+
------------------------------------------------------------------------------------
|
185
|
+
--
|
186
|
+
|
187
|
+
-- SELECT tracker.track_table('public.posts');
|
188
|
+
|
189
|
+
--SELECT tracker.track_table('public.users', 'id', array['created_at','updated_at']);
|
190
|
+
-- -- SELECT tracker.track_table('public.users', ARRAY[]::text[]);
|
191
|
+
-- -- SELECT tracker.track_table('public.users');
|
192
|
+
--
|
193
|
+
-- INSERT INTO users (email, created_at, updated_at) VALUES('me@mo' || (1000 * random())::integer , NOW(), NOW());
|
194
|
+
-- UPDATE users SET username='mimi' WHERE email LIKE '%mo%';
|
195
|
+
-- UPDATE users SET username='momo' WHERE email LIKE '%mo%';
|
196
|
+
-- DELETE FROM users WHERE email LIKE '%mo%';
|
197
|
+
--
|
198
|
+
-- SELECT table_schema || '.' || table_name || '.' || entity_pkey, action, row_data, changed_fields, created_at FROM tracker.events;
|
data/lib/postqueue/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -11,7 +11,10 @@ end
|
|
11
11
|
require "postqueue"
|
12
12
|
require "./spec/support/configure_active_record"
|
13
13
|
|
14
|
-
|
14
|
+
logger = Logger.new(File.open("log/test.log", "a"))
|
15
|
+
logger.level = Logger::INFO
|
16
|
+
|
17
|
+
Simple::SQL.logger = Postqueue.logger = logger
|
15
18
|
|
16
19
|
RSpec.configure do |config|
|
17
20
|
config.run_all_when_everything_filtered = true
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: postqueue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- radiospiel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: simple-sql
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.2.6
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.2.6
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
154
|
name: table_print
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -177,6 +191,7 @@ files:
|
|
177
191
|
- lib/postqueue/queue/runner.rb
|
178
192
|
- lib/postqueue/queue/select_and_lock.rb
|
179
193
|
- lib/postqueue/queue/timing.rb
|
194
|
+
- lib/postqueue/tracker/tracker.sql
|
180
195
|
- lib/postqueue/version.rb
|
181
196
|
- lib/tracker.rb
|
182
197
|
- lib/tracker/advisory_lock.rb
|