pgq 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ TODO
2
+ *.gem
3
+ lib/serv
4
+ README2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
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
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rspec/core/rake_task'
7
+
8
+ task :default => :spec
9
+ RSpec::Core::RakeTask.new(:spec)
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,9 @@
1
+ class Create<%= name_c %>Queue < ActiveRecord::Migration
2
+ def self.up
3
+ Pgq::Consumer.add_queue :<%= name %>
4
+ end
5
+
6
+ def self.down
7
+ Pgq::Consumer.remove_queue :<%= name %>
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class Pgq<%= name_c %> < Pgq::Consumer
2
+
3
+ # to insert event: Pgq<%= name_c %>.my_event(1, 2, 3)
4
+
5
+ # async execute
6
+ def my_event(a, b, c)
7
+ logger.info "async call my_event with #{[a, b, c].inspect}"
8
+ end
9
+
10
+ end
@@ -0,0 +1,8 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Pgq<%= name_c %> do
4
+ before(:each) do
5
+ @consumer = Pgq<%= name_c %>.new
6
+ end
7
+
8
+ 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
@@ -0,0 +1,9 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ module Pgq
5
+ end
6
+
7
+ Dir[File.dirname(__FILE__) + "/pgq/*.rb"].each{|f| require f }
8
+
9
+ ActiveRecord::Base.extend(Pgq::Api) if defined?(ActiveRecord::Base)
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