boy_band 0.1.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.
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: []