pgq 0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +44 -0
- data/LICENSE +20 -0
- data/README.md +207 -0
- data/Rakefile +9 -0
- data/init.rb +1 -0
- data/lib/generators/pgq/add_generator.rb +40 -0
- data/lib/generators/pgq/add_templates/migration.rb +9 -0
- data/lib/generators/pgq/add_templates/pgq_class.rb +10 -0
- data/lib/generators/pgq/add_templates/spec.rb +8 -0
- data/lib/generators/pgq/config_generator.rb +18 -0
- data/lib/generators/pgq/config_templates/pgq.rb +12 -0
- data/lib/pgq.rb +9 -0
- data/lib/pgq/api.rb +88 -0
- data/lib/pgq/consumer.rb +23 -0
- data/lib/pgq/consumer_base.rb +175 -0
- data/lib/pgq/consumer_group.rb +27 -0
- data/lib/pgq/event.rb +43 -0
- data/lib/pgq/marshal64_coder.rb +13 -0
- data/lib/pgq/railtie.rb +9 -0
- data/lib/pgq/utils.rb +127 -0
- data/lib/pgq/version.rb +3 -0
- data/lib/pgq/worker.rb +83 -0
- data/lib/tasks/pgq.rake +64 -0
- data/pgq.gemspec +28 -0
- data/spec/consumer_base_spec.rb +149 -0
- data/spec/consumer_group_spec.rb +37 -0
- data/spec/consumer_spec.rb +53 -0
- data/spec/event_spec.rb +32 -0
- data/spec/marshal64_coder_spec.rb +22 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/worker_spec.rb +53 -0
- metadata +157 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pgq (0.1)
|
5
|
+
activerecord (>= 2.3.2)
|
6
|
+
activesupport (>= 2.3.2)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (3.2.3)
|
12
|
+
activesupport (= 3.2.3)
|
13
|
+
builder (~> 3.0.0)
|
14
|
+
activerecord (3.2.3)
|
15
|
+
activemodel (= 3.2.3)
|
16
|
+
activesupport (= 3.2.3)
|
17
|
+
arel (~> 3.0.2)
|
18
|
+
tzinfo (~> 0.3.29)
|
19
|
+
activesupport (3.2.3)
|
20
|
+
i18n (~> 0.6)
|
21
|
+
multi_json (~> 1.0)
|
22
|
+
arel (3.0.2)
|
23
|
+
builder (3.0.0)
|
24
|
+
diff-lcs (1.1.3)
|
25
|
+
i18n (0.6.0)
|
26
|
+
multi_json (1.3.4)
|
27
|
+
rake (0.9.2.2)
|
28
|
+
rspec (2.10.0)
|
29
|
+
rspec-core (~> 2.10.0)
|
30
|
+
rspec-expectations (~> 2.10.0)
|
31
|
+
rspec-mocks (~> 2.10.0)
|
32
|
+
rspec-core (2.10.0)
|
33
|
+
rspec-expectations (2.10.0)
|
34
|
+
diff-lcs (~> 1.1.3)
|
35
|
+
rspec-mocks (2.10.1)
|
36
|
+
tzinfo (0.3.33)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
pgq!
|
43
|
+
rake
|
44
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Makarchev K
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
Pgq Rails
|
2
|
+
=========
|
3
|
+
|
4
|
+
Queues system for AR/Rails based on PgQ Skytools for PostgreSQL, like Resque on Redis. Rails 3! only tested.
|
5
|
+
|
6
|
+
About PgQ
|
7
|
+
* http://wiki.postgresql.org/wiki/SkyTools#PgQ
|
8
|
+
|
9
|
+
|
10
|
+
Install
|
11
|
+
-------
|
12
|
+
|
13
|
+
Install skytools:
|
14
|
+
Ubuntu 11.10:
|
15
|
+
|
16
|
+
# apt-get install postgresql-server postgresql-client
|
17
|
+
# apt-get install skytools
|
18
|
+
|
19
|
+
Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'pgq'
|
23
|
+
```
|
24
|
+
|
25
|
+
Create ticker configs:
|
26
|
+
|
27
|
+
$ rails generate pgq:config
|
28
|
+
edit config: config/pgq_development.ini
|
29
|
+
|
30
|
+
|
31
|
+
Install pgq to database (if test database recreates all the time, should reinstall pgq each time):
|
32
|
+
|
33
|
+
$ rake pgq:install
|
34
|
+
or execute
|
35
|
+
$ pgqadm config/pgq_development.ini install
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
Run ticker daemon (ticker needs on production database, or development if we test the process of consuming):
|
40
|
+
Daemon run once, bind to the database. (If worker not consuming check that daemon started).
|
41
|
+
|
42
|
+
$ rake pgq:ticker:start (stop)
|
43
|
+
or execute
|
44
|
+
$ pgqadm config/pgq_development.ini ticker -d
|
45
|
+
|
46
|
+
|
47
|
+
Last, add to config/application.rb
|
48
|
+
|
49
|
+
config.autoload_paths += %W( #{config.root}/app/models/pgq )
|
50
|
+
|
51
|
+
|
52
|
+
Create Pgq consumer
|
53
|
+
-------------------
|
54
|
+
|
55
|
+
$ rails generate pgq:add my
|
56
|
+
|
57
|
+
|
58
|
+
This creates file app/models/pgq/pgq_my.rb and migration
|
59
|
+
|
60
|
+
$ rake db:migrate
|
61
|
+
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class PgqMy < Pgq::Consumer
|
65
|
+
|
66
|
+
def some_method1(a, b, c)
|
67
|
+
logger.info "async call some_method1 with #{[a, b, c].inspect}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def some_method2(x)
|
71
|
+
logger.info "async called some_method2 with #{x.inspect}"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
Insert event into queue like this:
|
78
|
+
|
79
|
+
PgqMy.some_method1(1, 2, 3)
|
80
|
+
|
81
|
+
or
|
82
|
+
|
83
|
+
PgqMy.add_event(:some_method2, some_x)
|
84
|
+
|
85
|
+
or
|
86
|
+
|
87
|
+
PgqMy.enqueue(:some_method1, 1, 2, 3)
|
88
|
+
|
89
|
+
|
90
|
+
Workers
|
91
|
+
-------
|
92
|
+
Start worker for queue:
|
93
|
+
|
94
|
+
$ rake pgq:worker QUEUES="my"
|
95
|
+
$ rake pgq:worker QUEUES="my,mailer,other"
|
96
|
+
$ rake pgq:worker QUEUES="all" RAILS_ENV=production
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
Also can consume manual, or write [bin_script](http://github.com/kostya/bin_script) like this:
|
101
|
+
```ruby
|
102
|
+
class PgqRunnerScript < BinScript
|
103
|
+
|
104
|
+
self.enable_locking = false
|
105
|
+
|
106
|
+
required :q, "queues separated by ','"
|
107
|
+
required :w, "watch file"
|
108
|
+
|
109
|
+
def do!
|
110
|
+
worker = Pgq::Worker.new(:logger => self.logger, :queues => params(:q),
|
111
|
+
:watch_file => params(:w) || "./tmp/stop_all.txt")
|
112
|
+
worker.run
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
and run:
|
119
|
+
|
120
|
+
$ ./bin/pgq_runner.rb -q my -l ./log/pgq_my.log
|
121
|
+
$ ./bin/pgq_runner.rb -q all -l ./log/pgq_all.log # this will consume all queues from config/queues_list.yml
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
### Admin interface
|
127
|
+
Admins interface is possible like Resque, but not realized yet.
|
128
|
+
For queues info, run in console:
|
129
|
+
|
130
|
+
> Pgq::Consumer.database.pgq_get_consumer_info
|
131
|
+
|
132
|
+
|
133
|
+
### Failed events
|
134
|
+
When any raise happens in consumer, its produce failed event, which can be retry, or delete.
|
135
|
+
|
136
|
+
Retry manual:
|
137
|
+
|
138
|
+
Pgq::Consumer.resend_failed_events(queue_name)
|
139
|
+
|
140
|
+
Delete manual:
|
141
|
+
|
142
|
+
Pgq::Consumer.clear_failed_events(queue_name)
|
143
|
+
|
144
|
+
|
145
|
+
### Divide events between workers, for one consumer class
|
146
|
+
create more queues: my_2, my_3
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class PgqMy < Pgq::Consumer
|
150
|
+
|
151
|
+
QUEUES = %w(my my_2 my_3).freeze
|
152
|
+
|
153
|
+
def self.next_queue_name
|
154
|
+
QUEUES.choice
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
```
|
159
|
+
And run 3 workers: for my, my_2, my_3
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
### Consume group of events extracted from pgq(batch):
|
165
|
+
Usually batch size is ~500 events.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class PgqMy < Pgq::ConsumerGroup
|
169
|
+
|
170
|
+
# {'type' => [events]}
|
171
|
+
def perform_group(events_hash)
|
172
|
+
raise "realize me"
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
|
179
|
+
|
180
|
+
### Options
|
181
|
+
create initializer with:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
class Pgq::Consumer
|
185
|
+
|
186
|
+
# specify database with pgq queues
|
187
|
+
def self.database
|
188
|
+
SomeDatabase
|
189
|
+
end
|
190
|
+
|
191
|
+
# specify coder
|
192
|
+
def self.coder
|
193
|
+
Pgq::Marshal64Coder
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
```
|
198
|
+
|
199
|
+
### Proxy method to consumer
|
200
|
+
Usefull in specs
|
201
|
+
|
202
|
+
``` ruby
|
203
|
+
PgqMy.proxy(:some_method1)
|
204
|
+
```
|
205
|
+
|
206
|
+
When code call PgqMy.some_method1(a,b,c) this would be convert into PgqMy.new.some_method1(a,b,c)
|
207
|
+
|
data/Rakefile
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/lib/pgq'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Pgq
|
2
|
+
class AddGenerator < Rails::Generators::Base
|
3
|
+
include Rails::Generators::Migration
|
4
|
+
source_root File.expand_path("../add_templates", __FILE__)
|
5
|
+
argument :queue_name, :type => :string
|
6
|
+
|
7
|
+
def self.next_migration_number(path)
|
8
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_to_queues_list
|
12
|
+
filename = Rails.root + 'config/queues_list.yml'
|
13
|
+
if File.exists?(filename)
|
14
|
+
x = YAML.load_file(filename)
|
15
|
+
x << name
|
16
|
+
File.open(filename, 'w'){|f| f.write(YAML.dump(x))}
|
17
|
+
else
|
18
|
+
x = [name]
|
19
|
+
File.open(filename, 'w'){|f| f.write(YAML.dump(x))}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_files
|
24
|
+
template "pgq_class.rb", "app/models/pgq/pgq_#{name}.rb"
|
25
|
+
template "spec.rb", "spec/models/pgq/pgq_#{name}_spec.rb"
|
26
|
+
migration_template "migration.rb", "db/migrate/create_#{name}_queue.rb"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def name
|
32
|
+
queue_name.underscore
|
33
|
+
end
|
34
|
+
|
35
|
+
def name_c
|
36
|
+
name.classify
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Pgq
|
2
|
+
class ConfigGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path("../config_templates", __FILE__)
|
4
|
+
|
5
|
+
def copy_configs
|
6
|
+
self.env_name = 'development'
|
7
|
+
template "pgq.rb", "config/pgq_development.ini"
|
8
|
+
self.env_name = 'test'
|
9
|
+
template "pgq.rb", "config/pgq_test.ini"
|
10
|
+
self.env_name = 'production'
|
11
|
+
template "pgq.rb", "config/pgq_production.ini"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
attr_accessor :env_name
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
[pgqadm]
|
2
|
+
job_name = pgqadm_<%= env_name %>
|
3
|
+
db = dbname=dbname user=user password=password host=127.0.0.1 port=5432
|
4
|
+
|
5
|
+
# how often to run maintenance [seconds]
|
6
|
+
maint_delay = 600
|
7
|
+
|
8
|
+
# how often to check for activity [seconds]
|
9
|
+
loop_delay = 0.1
|
10
|
+
|
11
|
+
logfile = log/%(job_name)s.log
|
12
|
+
pidfile = tmp/%(job_name)s.pid
|
data/lib/pgq.rb
ADDED
data/lib/pgq/api.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module Pgq::Api
|
2
|
+
# should mixin to class, which have connection
|
3
|
+
|
4
|
+
def pgq_create_queue(queue_name)
|
5
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.create_queue(?)", queue_name]).to_i
|
6
|
+
end
|
7
|
+
|
8
|
+
def pgq_drop_queue(queue_name)
|
9
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.drop_queue(?)", queue_name]).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def pgq_register_consumer(queue_name, consumer_id)
|
13
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.register_consumer(?, ?)", queue_name, consumer_id]).to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def pgq_unregister_consumer(queue_name, consumer_id)
|
17
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.unregister_consumer(?, ?)", queue_name, consumer_id]).to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def pgq_add_queue(queue_name, consumer_name)
|
21
|
+
pgq_create_queue(queue_name.to_s)
|
22
|
+
pgq_register_consumer(queue_name.to_s, consumer_name.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def pgq_remove_queue(queue_name, consumer_name)
|
26
|
+
pgq_unregister_consumer(queue_name.to_s, consumer_name.to_s)
|
27
|
+
pgq_drop_queue(queue_name.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def pgq_insert_event(queue_name, ev_type, ev_data, ev_extra1 = nil, ev_extra2 = nil, ev_extra3 = nil, ev_extra4 = nil)
|
31
|
+
result = connection.select_value(sanitize_sql_array ["SELECT pgq.insert_event(?, ?, ?, ?, ?, ?, ?)",
|
32
|
+
queue_name, ev_type, ev_data, ev_extra1, ev_extra2, ev_extra3, ev_extra4])
|
33
|
+
result ? result.to_i : nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def pgq_next_batch(queue_name, consumer_id)
|
37
|
+
result = connection.select_value(sanitize_sql_array ["SELECT pgq.next_batch(?, ?)", queue_name, consumer_id])
|
38
|
+
result ? result.to_i : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def pgq_get_batch_events(batch_id)
|
42
|
+
connection.select_all(sanitize_sql_array ["SELECT * FROM pgq.get_batch_events(?)", batch_id])
|
43
|
+
end
|
44
|
+
|
45
|
+
def pgq_event_failed(batch_id, event_id, reason)
|
46
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.event_failed(?, ?, ?)", batch_id, event_id, reason]).to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def pgq_event_retry(batch_id, event_id, retry_seconds)
|
50
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.event_retry(?, ?, ?)", batch_id, event_id, retry_seconds]).to_i
|
51
|
+
end
|
52
|
+
|
53
|
+
def pgq_finish_batch(batch_id)
|
54
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.finish_batch(?)", batch_id])
|
55
|
+
end
|
56
|
+
|
57
|
+
def pgq_get_queue_info(queue_name)
|
58
|
+
connection.select_value(sanitize_sql_array ["SELECT pgq.get_queue_info(?)", queue_name])
|
59
|
+
end
|
60
|
+
|
61
|
+
def pgq_failed_event_retry(queue_name, consumer, event_id)
|
62
|
+
connection.select_value(sanitize_sql_array ["SELECT * FROM pgq.failed_event_retry(?, ?, ?)", queue_name, consumer, event_id])
|
63
|
+
end
|
64
|
+
|
65
|
+
def pgq_failed_event_delete(queue_name, consumer, event_id)
|
66
|
+
connection.select_value(sanitize_sql_array ["SELECT * FROM pgq.failed_event_delete(?, ?, ?)", queue_name, consumer, event_id])
|
67
|
+
end
|
68
|
+
|
69
|
+
def pgq_failed_events_count(queue_name, consumer)
|
70
|
+
res = connection.select_value(sanitize_sql_array ["SELECT * FROM pgq.failed_event_count(?, ?)", queue_name, consumer])
|
71
|
+
res ? res.to_i : nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def pgq_failed_event_list queue_name, consumer, limit = nil, offset = nil, order = 'desc'
|
75
|
+
order = (order.to_s == 'desc') ? order : 'asc'
|
76
|
+
connection.select_all(sanitize_sql_array ["SELECT * FROM pgq.failed_event_list(?, ?, ?, ?) order by ev_id #{order}", queue_name, consumer, limit.to_i, offset.to_i])
|
77
|
+
end
|
78
|
+
|
79
|
+
# queue lag in seconds
|
80
|
+
def pgq_queue_lag(queue_name)
|
81
|
+
connection.select_value(sanitize_sql_array ["SELECT Max(EXTRACT(epoch FROM lag)) FROM pgq.get_consumer_info() where queue_name = ?", queue_name]).to_f
|
82
|
+
end
|
83
|
+
|
84
|
+
def pgq_get_consumer_info
|
85
|
+
connection.select_all("SELECT * FROM pgq.get_consumer_info()")
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|