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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0ab5c97e45f6171bf141d58d1cd8ce8c5524f5b2
4
- data.tar.gz: 0a1b8ef785008bec26a6907024617a4ef4a82a3e
3
+ metadata.gz: 169eb71de24fab5bde343459df49bf1395881fc4
4
+ data.tar.gz: fb06468f9fe5412dc655188823d5544be0ba9d48
5
5
  SHA512:
6
- metadata.gz: 31fad4af31402d2084e07c0b60baa14bce82a53efba6714056956f52da1360cfd5b92a6a0f6599789f687290ec743f867604346c4bc7a16c930aa52a69071585
7
- data.tar.gz: a7c2117c1abdee7840cfa66714e99ad15bd8a02456454d4f4d44ce9a00e019df5224e722c7e4f415116331bb3c3e2a08bb14b77d0204dd9c51a0941dcaaa305a
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
- module ActiveRecordInserter
11
- def insert_item(op:, entity_id:)
12
- create!(op: op, entity_id: entity_id)
13
- end
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;
@@ -1,3 +1,3 @@
1
1
  module Postqueue
2
- VERSION = "0.6.1"
2
+ VERSION = "0.7.0"
3
3
  end
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
- Postqueue.logger = Logger.new(File.open("log/test.log", "a"))
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.6.1
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: 2017-05-15 00:00:00.000000000 Z
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