pgq 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|