pgq 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ require 'pgq/consumer_base'
2
+
3
+ # Cute class, for magick inserts and light consume
4
+
5
+ class Pgq::Consumer < Pgq::ConsumerBase
6
+
7
+ # == magick insert events
8
+
9
+ def self.method_missing(method_name, *args)
10
+ enqueue(method_name, *args)
11
+ end
12
+
13
+ def self.add_event(method_name, *args)
14
+ enqueue(method_name, *args)
15
+ end
16
+
17
+ # == magick consume
18
+
19
+ def perform(method_name, *args)
20
+ self.send(method_name, *args)
21
+ end
22
+
23
+ end
@@ -0,0 +1,175 @@
1
+ require 'pgq/utils'
2
+ require 'pgq/api'
3
+ require 'active_support/inflector' unless ''.respond_to?(:underscore)
4
+
5
+ class Pgq::ConsumerBase
6
+ extend Pgq::Utils
7
+
8
+ @queue_name = 'default'
9
+ @consumer_name = 'default'
10
+
11
+ attr_accessor :logger, :queue_name, :consumer_name
12
+
13
+ # == connection
14
+
15
+ def self.database
16
+ ActiveRecord::Base # can redefine
17
+ end
18
+
19
+ def database
20
+ self.class.database
21
+ end
22
+
23
+ def self.connection
24
+ database.connection
25
+ end
26
+
27
+ def connection
28
+ self.class.connection
29
+ end
30
+
31
+ # == queue name
32
+
33
+ def self.extract_queue_name
34
+ self.name.to_s.gsub(/^pgq/i, '').underscore.gsub('/', '-')
35
+ end
36
+
37
+ def self.set_queue_name(name)
38
+ self.instance_variable_set('@queue_name', name.to_s)
39
+ end
40
+
41
+ # magic set queue_name from class name
42
+ def self.inherited(subclass)
43
+ subclass.set_queue_name(subclass.extract_queue_name)
44
+ subclass.instance_variable_set('@consumer_name', self.consumer_name)
45
+ end
46
+
47
+ def self.consumer_name
48
+ @consumer_name
49
+ end
50
+
51
+ def self.queue_name
52
+ @queue_name
53
+ end
54
+
55
+ # this method used when insert event, possible to reuse
56
+ def self.next_queue_name
57
+ self.queue_name
58
+ end
59
+
60
+ # == coder
61
+
62
+ def self.coder
63
+ Pgq::Marshal64Coder
64
+ end
65
+
66
+ def coder
67
+ self.class.coder
68
+ end
69
+
70
+ # == insert event
71
+
72
+ def self.enqueue(method_name, *args)
73
+ self.database.pgq_insert_event( self.next_queue_name, method_name.to_s, coder.dump(args) )
74
+ end
75
+
76
+ # == consumer part
77
+
78
+ def initialize(logger = nil, custom_queue_name = nil, custom_consumer_name = nil)
79
+ self.queue_name = custom_queue_name || self.class.queue_name
80
+ self.consumer_name = custom_consumer_name || self.class.consumer_name
81
+ self.logger = logger
82
+ @batch_id = nil
83
+ end
84
+
85
+ def perform_batch
86
+ events = []
87
+ pgq_events = get_batch_events
88
+
89
+ return 0 if pgq_events.blank?
90
+
91
+ events = pgq_events.map{|ev| Pgq::Event.new(self, ev) }
92
+ size = events.size
93
+ log_info "=> batch(#{queue_name}): events #{size}"
94
+
95
+ perform_events(events)
96
+
97
+ rescue Exception => ex
98
+ all_events_failed(events, ex)
99
+
100
+ rescue => ex
101
+ all_events_failed(events, ex)
102
+
103
+ ensure
104
+ finish_batch(events.size)
105
+
106
+ return events.size
107
+ end
108
+
109
+ def perform_events(events)
110
+ events.each do |event|
111
+ perform_event(event)
112
+ end
113
+ end
114
+
115
+ def perform_event(event)
116
+ type = event.type
117
+ data = event.data
118
+
119
+ perform(type, *data)
120
+
121
+ rescue Exception => ex
122
+ message = event.exception_message(ex)
123
+ self.log_error(message)
124
+ event.failed!(message)
125
+
126
+ rescue => ex
127
+ message = event.exception_message(ex)
128
+ self.log_error(message)
129
+ event.failed!(message)
130
+ end
131
+
132
+ def perform(type, *data)
133
+ raise "realize me"
134
+ end
135
+
136
+ def get_batch_events
137
+ @batch_id = database.pgq_next_batch(queue_name, consumer_name)
138
+ return nil if !@batch_id
139
+ database.pgq_get_batch_events(@batch_id)
140
+ end
141
+
142
+ def finish_batch(count = nil)
143
+ return unless @batch_id
144
+ database.pgq_finish_batch(@batch_id)
145
+ @batch_id = nil
146
+ end
147
+
148
+ def event_failed(event_id, reason)
149
+ database.pgq_event_failed(@batch_id, event_id, reason)
150
+ end
151
+
152
+ def event_retry(event_id)
153
+ database.pgq_event_retry(@batch_id, event_id, 0)
154
+ end
155
+
156
+ def all_events_failed(events, ex)
157
+ message = Pgq::Event.exception_message(ex)
158
+ log_error(message)
159
+
160
+ events.each do |event|
161
+ event.failed!(message)
162
+ end
163
+ end
164
+
165
+ # == log methods
166
+
167
+ def log_info(mes)
168
+ @logger.info(mes) if @logger
169
+ end
170
+
171
+ def log_error(mes)
172
+ @logger.error(mes) if @logger
173
+ end
174
+
175
+ end
@@ -0,0 +1,27 @@
1
+ # for consuming full batch (usefull if need group events for some group processing)
2
+ # usually group is ~500 events
3
+
4
+ require 'pgq/consumer'
5
+
6
+ class Pgq::ConsumerGroup < Pgq::Consumer
7
+
8
+ # {'type' => [events]}
9
+ def perform_group(events_hash)
10
+ raise "realize me"
11
+ end
12
+
13
+ def perform_events(events)
14
+ events = sum_events(events)
15
+ # log_info "consume events (#{self.queue_name}): #{events.map{|k,v| [k, v.size]}.inspect}"
16
+ perform_group(events) if events.present?
17
+ end
18
+
19
+ def sum_events(events)
20
+ events.inject({}) do |result, event|
21
+ result[event.type] ||= []
22
+ result[event.type] << event
23
+ result
24
+ end
25
+ end
26
+
27
+ end
data/lib/pgq/event.rb ADDED
@@ -0,0 +1,43 @@
1
+ class Pgq::Event
2
+ attr_reader :type, :data, :id, :consumer
3
+
4
+ def initialize(consumer, event)
5
+ @id = event['ev_id']
6
+ @type = event['ev_type']
7
+ @data = consumer.coder.load(event['ev_data']) if event['ev_data']
8
+ @consumer = consumer
9
+ end
10
+
11
+ def failed!(ex = 'Something happens')
12
+ if ex.is_a?(String)
13
+ @consumer.event_failed @id, ex
14
+ else # exception
15
+ @consumer.event_failed @id, exception_message(ex)
16
+ end
17
+ end
18
+
19
+ def retry!
20
+ @consumer.event_retry(@id)
21
+ end
22
+
23
+ def self.exception_message(e)
24
+ <<-EXCEPTION
25
+ Exception happend
26
+ Type: #{e.class.inspect}
27
+ Error occurs: #{e.message}
28
+ Backtrace: #{e.backtrace.join("\n") rescue ''}
29
+ EXCEPTION
30
+ end
31
+
32
+ # Prepare string with exception details
33
+ def exception_message(e)
34
+ <<-EXCEPTION
35
+ Exception happend
36
+ Type: #{type.inspect} #{e.class.inspect}
37
+ Data: #{data.inspect}
38
+ Error occurs: #{e.message}
39
+ Backtrace: #{e.backtrace.join("\n") rescue ''}
40
+ EXCEPTION
41
+ end
42
+
43
+ end
@@ -0,0 +1,13 @@
1
+ require 'base64'
2
+
3
+ module Pgq::Marshal64Coder
4
+
5
+ def self.dump(s)
6
+ Base64::encode64(Marshal.dump(s))
7
+ end
8
+
9
+ def self.load(s)
10
+ Marshal.load(Base64::decode64(s))
11
+ end
12
+
13
+ end
@@ -0,0 +1,9 @@
1
+ if defined?(Rails) && defined?(::Rails::Engine)
2
+
3
+ class Pgq::Engine < ::Rails::Engine
4
+ rake_tasks do
5
+ # load File.dirname(__FILE__) + "/../tasks/pgq.rake"
6
+ end
7
+ end
8
+
9
+ end
data/lib/pgq/utils.rb ADDED
@@ -0,0 +1,127 @@
1
+ module Pgq::Utils
2
+
3
+ # == all queues for database
4
+ def queues_list
5
+ database.pgq_get_consumer_info.map{|x| x['queue_name']}
6
+ end
7
+
8
+ # == methods for migrations
9
+ def add_queue(queue_name, consumer_name = self.consumer_name)
10
+ database.pgq_add_queue(queue_name, consumer_name)
11
+ end
12
+
13
+ def remove_queue(queue_name, consumer_name = self.consumer_name)
14
+ database.pgq_remove_queue(queue_name, consumer_name)
15
+ end
16
+
17
+ # == inspect queue
18
+ # { type => events_count }
19
+ def inspect_queue(queue_name)
20
+ ticks = database.pgq_get_queue_info(queue_name)
21
+ table = connection.select_value("SELECT queue_data_pfx as table FROM pgq.queue where queue_name = #{database.sanitize(queue_name)}")
22
+
23
+ result = {}
24
+
25
+ if ticks['current_batch']
26
+ sql = connection.select_value("SELECT * from pgq.batch_event_sql(#{database.sanitize(ticks['current_batch'].to_i)})")
27
+ last_event = connection.select_value("SELECT MAX(ev_id) AS count FROM (#{sql}) AS x")
28
+
29
+ stats = connection.select_all <<-SQL
30
+ SELECT count(*) as count, ev_type
31
+ FROM #{table}
32
+ WHERE ev_id > #{database.sanitize(last_event.to_i)}
33
+ GROUP BY ev_type
34
+ SQL
35
+
36
+ stats.each do |x|
37
+ result["#{x['ev_type']}"] = x['count'].to_i
38
+ end
39
+
40
+ else
41
+ stats = connection.select_all <<-SQL
42
+ SELECT ev_type
43
+ FROM #{table}
44
+ GROUP BY ev_type
45
+ SQL
46
+
47
+ stats.each do |x|
48
+ result["#{x['ev_type']}"] = 0
49
+ end
50
+ end
51
+
52
+ result
53
+ end
54
+
55
+ def inspect_self_queue
56
+ self.inspect_queue(self.queue_name)
57
+ end
58
+
59
+ # show hash stats, for londiste type of storage events
60
+ # { type => events_count }
61
+ def inspect_londiste_queue(queue_name)
62
+ ticks = database.pgq_get_consumer_info
63
+ table = connection.select_value(connection.sanitize_sql_array ["SELECT queue_data_pfx as table FROM pgq.queue where queue_name = ?", queue_name])
64
+
65
+ result = {}
66
+
67
+ if ticks['current_batch']
68
+ sql = connection.select_value("SELECT * from pgq.batch_event_sql(#{database.sanitize(ticks['current_batch'].to_i)})")
69
+ last_event = connection.select_value("SELECT MAX(ev_id) AS count FROM (#{sql}) AS x")
70
+
71
+ stats = connection.select_all <<-SQL
72
+ SELECT count(*) as count, ev_type, ev_extra1
73
+ FROM #{table}
74
+ WHERE ev_id > #{database.sanitize(last_event.to_i)}
75
+ GROUP BY ev_type, ev_extra1
76
+ SQL
77
+
78
+ stats.each do |x|
79
+ result["#{x['ev_extra1']}:#{x['ev_type']}"] = x['count'].to_i
80
+ end
81
+
82
+ else
83
+ stats = connection.select_all <<-SQL
84
+ SELECT ev_type, ev_extra1
85
+ FROM #{table}
86
+ GROUP BY ev_type, ev_extra1 ORDER BY ev_extra1, ev_type
87
+ SQL
88
+
89
+ stats.each do |x|
90
+ result["#{x['ev_extra1']}:#{x['ev_type']}"] = 0
91
+ end
92
+ end
93
+
94
+ result
95
+ end
96
+
97
+
98
+ # == proxing method for tests
99
+ def proxy(method_name)
100
+ self.should_receive(method_name) do |*data|
101
+ x = self.coder.load(self.coder.dump(data))
102
+ self.new.send(:perform, method_name, *x)
103
+ end.any_number_of_times
104
+ end
105
+
106
+ # == resend failed events in queue
107
+ def resend_failed_events(queue_name, limit = 5_000)
108
+ events = database.pgq_failed_event_list(queue_name, self.consumer_name, limit, nil, 'asc') || []
109
+
110
+ events.each do |event|
111
+ database.pgq_failed_event_retry(queue_name, self.consumer_name, event['ev_id'])
112
+ end
113
+
114
+ events.length
115
+ end
116
+
117
+ def clear_failed_events(queue_name, limit = 5_000)
118
+ events = database.pgq_failed_event_list(queue_name, self.consumer_name, limit, nil, 'asc') || []
119
+
120
+ events.each do |event|
121
+ database.pgq_failed_event_delete(queue_name, self.consumer_name, event['ev_id'])
122
+ end
123
+
124
+ events.length
125
+ end
126
+
127
+ end
@@ -0,0 +1,3 @@
1
+ module Pgq
2
+ VERSION = "0.1"
3
+ end
data/lib/pgq/worker.rb ADDED
@@ -0,0 +1,83 @@
1
+ require 'logger'
2
+
3
+ class Pgq::Worker
4
+ attr_reader :logger, :queues, :consumers, :sleep_time, :watch_file
5
+
6
+ def self.predict_queue_class(queue)
7
+ klass = nil
8
+ unless klass
9
+ queue.to_s.match(/([a-z_]+)/i)
10
+ klass_s = $1.to_s
11
+ klass_s.chop! if klass_s.size > 0 && klass_s[-1].chr == '_'
12
+ klass_s = "pgq_" + klass_s unless klass_s.start_with?("pgq_")
13
+ klass = klass_s.camelize.constantize rescue nil
14
+ klass = nil unless klass.is_a?(Class)
15
+ end
16
+ klass
17
+ end
18
+
19
+ def self.connection(queue)
20
+ klass = predict_queue_class(queue)
21
+ if klass
22
+ klass.connection
23
+ else
24
+ raise "can't find klass for queue #{queue}"
25
+ end
26
+ end
27
+
28
+ def initialize(h)
29
+ @logger = h[:logger] || (defined?(Rails) && Rails.logger) || Logger.new(STDOUT)
30
+ @consumers = []
31
+
32
+ queues = h[:queues]
33
+ raise "Queue not selected" if queues.blank?
34
+
35
+ if queues == ['all'] || queues == 'all'
36
+ if defined?(Rails) && File.exists?(Rails.root + "config/queues_list.yml")
37
+ queues = YAML.load_file(Rails.root + "config/queues_list.yml")
38
+ else
39
+ raise "You shoud create config/queues_list.yml for all queues"
40
+ end
41
+ end
42
+
43
+ queues = queues.split(',') if queues.is_a?(String)
44
+
45
+ queues.each do |queue|
46
+ klass = Pgq::Worker.predict_queue_class(queue)
47
+ if klass
48
+ @consumers << klass.new(@logger, queue)
49
+ else
50
+ raise "Unknown queue: #{queue}"
51
+ end
52
+ end
53
+
54
+ @watch_file = h[:watch_file]
55
+ @sleep_time = h[:sleep_time] || 0.5
56
+ end
57
+
58
+ def process_batch
59
+ process_count = 0
60
+
61
+ @consumers.each do |consumer|
62
+ process_count += consumer.perform_batch
63
+
64
+ if @watch_file && File.exists?(@watch_file)
65
+ logger.info "Found file #{@watch_file}, exiting!"
66
+ File.unlink(@watch_file)
67
+ return processed_count
68
+ end
69
+ end
70
+
71
+ process_count
72
+ end
73
+
74
+ def run
75
+ logger.info "Worker start"
76
+
77
+ loop do
78
+ processed_count = process_batch
79
+ sleep(@sleep_time) if processed_count == 0
80
+ end
81
+ end
82
+
83
+ end