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.
@@ -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