boy_band 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +5 -0
  4. data/lib/boy_band.rb +328 -0
  5. metadata +89 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8021beb66e7073e64a3e28f48277f54439191c1f
4
+ data.tar.gz: 3849b716abeb87212d174b7415251a618a283408
5
+ SHA512:
6
+ metadata.gz: eec6e1c30dd08be55859f290dac58e4cbb82b59645b1f3f6eb2a809dcfa49d6305897a6ac45c5cb99794937b8714fc17507d240a0c7061db7491628b01c166c0
7
+ data.tar.gz: fb32e1a57b6c88093159be345da46a7d7769ac61968c592604917f2d9d75e25de36a8e414c1caa723a9c0503347d5e9a4c62d3e9b224bb21e5274eb96d53231b
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 CoughDrop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
9
+ Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ Async and background job tool. I don't like name collisions for gems, and kept trying
2
+ to think of something creative around 'N Sync, but this was the best I could come up with.
3
+
4
+ Basically a layer on top of Resque to handle my specific needs for workers and
5
+ progress-tracking for background jobs.
data/lib/boy_band.rb ADDED
@@ -0,0 +1,328 @@
1
+ require 'json'
2
+ require 'resque'
3
+ require 'rails'
4
+
5
+ module BoyBand
6
+ def self.job_instigator
7
+ if defined?(PaperTrail)
8
+ PaperTrail.whodunnit
9
+ else
10
+ 'unknown'
11
+ end
12
+ end
13
+
14
+ def self.set_job_instigator(name)
15
+ if defined?(PaperTrail)
16
+ PaperTrail.whodunnit = name
17
+ end
18
+ end
19
+
20
+ module WorkerMethods
21
+ def thread_id
22
+ "#{Process.pid}_#{Thread.current.object_id}"
23
+ end
24
+
25
+ def schedule_for(queue, klass, method_name, *args)
26
+ @queue = queue.to_s
27
+ job_hash = Digest::MD5.hexdigest(args.to_json)
28
+ note_job(job_hash)
29
+ size = Resque.size(queue)
30
+ if queue == :slow
31
+ Resque.enqueue(SlowWorker, klass.to_s, method_name, *args)
32
+ if size > 1000 && !Resque.redis.get("queue_warning_#{queue}")
33
+ Resque.redis.setex("queue_warning_#{queue}", 5.minutes.to_i, "true")
34
+ Rails.logger.error("job queue full: #{queue}, #{size} entries")
35
+ end
36
+ else
37
+ Resque.enqueue(Worker, klass.to_s, method_name, *args)
38
+ if size > 5000 && !Resque.redis.get("queue_warning_#{queue}")
39
+ Resque.redis.setex("queue_warning_#{queue}", 5.minutes.to_i, "true")
40
+ Rails.logger.error("job queue full: #{queue}, #{size} entries")
41
+ end
42
+ end
43
+ end
44
+
45
+ def note_job(hash)
46
+ if Resque.redis
47
+ timestamps = JSON.parse(Resque.redis.hget('hashed_jobs', hash) || "[]")
48
+ cutoff = 6.hours.ago.to_i
49
+ timestamps = timestamps.select{|ts| ts > cutoff }
50
+ timestamps.push(Time.now.to_i)
51
+ # Resque.redis.hset('hashed_jobs', hash, timestamps.to_json)
52
+ end
53
+ end
54
+
55
+ def clear_job(hash)
56
+ if Resque.redis
57
+ timestamps = JSON.parse(Resque.redis.hget('hashed_jobs', hash) || "[]")
58
+ timestamps.shift
59
+ # Resque.redis.hset('hashed_jobs', hash, timestamps.to_json)
60
+ end
61
+ end
62
+
63
+ def schedule(klass, method_name, *args)
64
+ schedule_for(:default, klass, method_name, *args)
65
+ end
66
+
67
+ def perform(*args)
68
+ perform_at(:normal, *args)
69
+ end
70
+
71
+ def ts
72
+ Time.now.to_i
73
+ end
74
+
75
+ def in_worker_process?
76
+ BoyBand.job_instigator.match(/^job/)
77
+ end
78
+
79
+ def perform_at(speed, *args)
80
+ args_copy = [] + args
81
+ klass_string = args_copy.shift
82
+ klass = Object.const_get(klass_string)
83
+ method_name = args_copy.shift
84
+ job_hash = Digest::MD5.hexdigest(args_copy.to_json)
85
+ hash = args_copy[0] if args_copy[0].is_a?(Hash)
86
+ hash ||= {'method' => method_name}
87
+ action = "#{klass_string} . #{hash['method']} (#{hash['id']})"
88
+ pre_whodunnit = BoyBand.job_instigator
89
+ BoyBand.set_job_instigator("job:#{action}")
90
+ Rails.logger.info("performing #{action}")
91
+ start = self.ts
92
+ klass.last_scheduled_stamp = hash['scheduled'] if klass.respond_to?('last_scheduled_stamp=')
93
+ klass.send(method_name, *args_copy)
94
+ diff = self.ts - start
95
+ Rails.logger.info("done performing #{action}, finished in #{diff}s")
96
+ # TODO: way to track what queue a job is coming from
97
+ if diff > 60 && speed == :normal
98
+ Rails.logger.error("long-running job, #{action}, #{diff}s")
99
+ elsif diff > 60*10 && speed == :slow
100
+ Rails.logger.error("long-running job, #{action} (expected slow), #{diff}s")
101
+ end
102
+ BoyBand.set_job_instigator(pre_whodunnit)
103
+ clear_job(job_hash)
104
+ rescue Resque::TermException
105
+ Resque.enqueue(self, *args)
106
+ end
107
+
108
+ def on_failure_retry(e, *args)
109
+ # TODO...
110
+ end
111
+
112
+ def scheduled_actions(queue='default')
113
+ queues = [queue]
114
+ if queue == '*'
115
+ queues = []
116
+ Resque.queues.each do |key|
117
+ queues << key
118
+ end
119
+ end
120
+
121
+ res = []
122
+ queues.each do |queue|
123
+ idx = Resque.size(queue)
124
+ idx.times do |i|
125
+ res << Resque.peek(queue, i)
126
+ end
127
+ end
128
+ res
129
+ end
130
+
131
+ def scheduled_for?(queue, klass, method_name, *args)
132
+ idx = Resque.size(queue)
133
+ queue_class = (queue == :slow ? 'SlowWorker' : 'Worker')
134
+ if false
135
+ job_hash = args.to_json
136
+ timestamps = JSON.parse(Resque.redis.hget('hashed_jobs', job_hash) || "[]")
137
+ cutoff = 6.hours.ago.to_i
138
+ return timestamps.select{|ts| ts > cutoff }.length > 0
139
+ else
140
+ start = 0
141
+ while start < idx
142
+ items = Resque.peek(queue, start, 1000)
143
+ start += items.length > 0 ? items.length : 1
144
+ items.each do |schedule|
145
+ if schedule && schedule['class'] == queue_class && schedule['args'][0] == klass.to_s && schedule['args'][1] == method_name.to_s
146
+ a1 = args
147
+ if a1.length == 1 && a1[0].is_a?(Hash)
148
+ a1 = [a1[0].dup]
149
+ a1[0].delete('scheduled')
150
+ end
151
+ a2 = schedule['args'][2..-1]
152
+ if a2.length == 1 && a2[0].is_a?(Hash)
153
+ a2 = [a2[0].dup]
154
+ a2[0].delete('scheduled')
155
+ end
156
+ if a1.to_json == a2.to_json
157
+ return true
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ return false
164
+ end
165
+
166
+ def scheduled?(klass, method_name, *args)
167
+ scheduled_for?('default', klass, method_name, *args)
168
+ end
169
+
170
+ def stop_stuck_workers
171
+ timeout = 8.hours.to_i
172
+ Resque.workers.each {|w| w.unregister_worker if w.processing['run_at'] && Time.now - w.processing['run_at'].to_time > timeout}
173
+ end
174
+
175
+ def prune_dead_workers
176
+ Resque.workers.each{|w| w.prune_dead_workers }
177
+ end
178
+
179
+ def kill_all_workers
180
+ Resque.workers.each{|w| w.unregister_worker }
181
+ end
182
+
183
+ def process_queues
184
+ schedules = []
185
+ Resque.queues.each do |key|
186
+ while Resque.size(key) > 0
187
+ schedules << {queue: key, action: Resque.pop(key)}
188
+ end
189
+ end
190
+ schedules.each do |schedule|
191
+ queue = schedule[:queue]
192
+ schedule = schedule[:action]
193
+ if queue == 'slow'
194
+ raise "unknown job: #{schedule.to_json}" if schedule['class'] != 'SlowWorker'
195
+ SlowWorker.perform(*(schedule['args']))
196
+ else
197
+ raise "unknown job: #{schedule.to_json}" if schedule['class'] != 'Worker'
198
+ Worker.perform(*(schedule['args']))
199
+ end
200
+ end
201
+ end
202
+
203
+ def queues_empty?
204
+ found = false
205
+ Resque.queues.each do |key|
206
+ return false if Resque.size(key) > 0
207
+ end
208
+ true
209
+ end
210
+
211
+ def flush_queues
212
+ if Resque.redis
213
+ Resque.queues.each do |key|
214
+ Resque.redis.ltrim("queue:#{key}", 1, 0)
215
+ end
216
+ end
217
+ Resque.redis.del('hashed_jobs')
218
+ end
219
+
220
+ def transfer_backlog(queue)
221
+ saves = []
222
+ dos = []
223
+ while Resque.size(queue) > 0 && (saves.length + dos.length) < 10000
224
+ job = Resque.pop(queue)
225
+ if job
226
+ if job['args'] && job['args'][2] && job['args'][2]['method'] == 'track_downstream_boards!'
227
+ saves.push(job)
228
+ else
229
+ dos.push(job)
230
+ end
231
+ end
232
+ end
233
+ dos.each{|job| Resque.enqueue(Worker, *job['args']) }; dos.length
234
+ hash = saves.group_by{|j| j['args'][2]['id'] }; hash.length
235
+ hash.each do |id, jobs|
236
+ list = []
237
+ jobs.each{|j| list += j['args'][2]['arguments'][0] }
238
+ args = jobs[0]['args'][2]
239
+ args['arguments'] = [list.uniq]
240
+ Resque.enqueue(SlowWorker, job['args'][0], job['args'][1], args)
241
+ end; hash.keys.length
242
+ Resque.size(queue)
243
+ end
244
+ end
245
+
246
+ module AsyncInstanceMethods
247
+ def schedule(method, *args)
248
+ return nil unless method
249
+ id = self.id
250
+ settings = {
251
+ 'id' => id,
252
+ 'method' => method,
253
+ 'scheduled' => self.class.scheduled_stamp,
254
+ 'arguments' => args
255
+ }
256
+ Worker.schedule(self.class, :perform_action, settings)
257
+ end
258
+
259
+ def schedule_once(method, *args)
260
+ return nil unless method && id
261
+ already_scheduled = Worker.scheduled?(self.class, :perform_action, {
262
+ 'id' => id,
263
+ 'method' => method,
264
+ 'scheduled' => self.class.scheduled_stamp,
265
+ 'arguments' => args
266
+ })
267
+ if !already_scheduled
268
+ schedule(method, *args)
269
+ else
270
+ false
271
+ end
272
+ end
273
+
274
+ def self.included(base)
275
+ base.define_singleton_method(:included) do |klass|
276
+ klass.cattr_accessor :last_scheduled_stamp
277
+ end
278
+ end
279
+ end
280
+
281
+ module AsyncClassMethods
282
+ def scheduled_stamp
283
+ Time.now.to_i
284
+ end
285
+
286
+ def schedule(method, *args)
287
+ return nil unless method
288
+ settings = {
289
+ 'method' => method,
290
+ 'scheduled' => self.scheduled_stamp,
291
+ 'arguments' => args
292
+ }
293
+ Worker.schedule(self, :perform_action, settings)
294
+ end
295
+
296
+ def schedule_once(method, *args)
297
+ return nil unless method
298
+ already_scheduled = Worker.scheduled?(self, :perform_action, {
299
+ 'method' => method,
300
+ 'scheduled' => self.scheduled_stamp,
301
+ 'arguments' => args
302
+ })
303
+ if !already_scheduled
304
+ schedule(method, *args)
305
+ else
306
+ false
307
+ end
308
+ end
309
+
310
+ def perform_action(settings)
311
+ obj = self
312
+ if settings['id']
313
+ obj = obj.find_by(:id => settings['id'].to_s)
314
+ obj.reload if obj
315
+ end
316
+ if !obj
317
+ # record not found so there's nothing to do on it
318
+ # TODO: probably log this somewhere so we don't lose it..
319
+ Rails.logger.warn "expected record not found: #{self.to_s}:#{settings['id']}"
320
+ elsif obj.respond_to?(settings['method'])
321
+ obj.send(settings['method'], *settings['arguments'])
322
+ else
323
+ id = settings['id'] ? "#{settings['id']}:" : ""
324
+ raise "method not found: #{self.to_s}:#{id}#{settings['method']}"
325
+ end
326
+ end
327
+ end
328
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: boy_band
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Brian Whitmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby-debug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Async/Background helper gem, used by multiple CoughDrop libraries
56
+ email: brian.whitmer@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files:
60
+ - LICENSE
61
+ files:
62
+ - LICENSE
63
+ - README.md
64
+ - lib/boy_band.rb
65
+ homepage: http://github.com/CoughDrop/boy_band
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.5.2
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: BoyBand
89
+ test_files: []