coney_island 0.0.3

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +32 -0
  5. data/bin/coney_island +10 -0
  6. data/lib/coney_island.rb +301 -0
  7. data/lib/coney_island/notifiers/airbrake_notifier.rb +9 -0
  8. data/lib/coney_island/notifiers/honeybadger_notifier.rb +9 -0
  9. data/lib/coney_island/version.rb +3 -0
  10. data/lib/tasks/coney_island_tasks.rake +4 -0
  11. data/test/coney_island_test.rb +7 -0
  12. data/test/dummy/README.rdoc +28 -0
  13. data/test/dummy/Rakefile +6 -0
  14. data/test/dummy/app/assets/javascripts/application.js +13 -0
  15. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  16. data/test/dummy/app/controllers/application_controller.rb +5 -0
  17. data/test/dummy/app/helpers/application_helper.rb +2 -0
  18. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  19. data/test/dummy/bin/bundle +3 -0
  20. data/test/dummy/bin/rails +4 -0
  21. data/test/dummy/bin/rake +4 -0
  22. data/test/dummy/config.ru +4 -0
  23. data/test/dummy/config/application.rb +23 -0
  24. data/test/dummy/config/boot.rb +5 -0
  25. data/test/dummy/config/database.yml +25 -0
  26. data/test/dummy/config/environment.rb +5 -0
  27. data/test/dummy/config/environments/development.rb +37 -0
  28. data/test/dummy/config/environments/production.rb +82 -0
  29. data/test/dummy/config/environments/test.rb +39 -0
  30. data/test/dummy/config/initializers/assets.rb +8 -0
  31. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  32. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  33. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  34. data/test/dummy/config/initializers/inflections.rb +16 -0
  35. data/test/dummy/config/initializers/mime_types.rb +4 -0
  36. data/test/dummy/config/initializers/session_store.rb +3 -0
  37. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  38. data/test/dummy/config/locales/en.yml +23 -0
  39. data/test/dummy/config/routes.rb +56 -0
  40. data/test/dummy/config/secrets.yml +22 -0
  41. data/test/dummy/public/404.html +67 -0
  42. data/test/dummy/public/422.html +67 -0
  43. data/test/dummy/public/500.html +66 -0
  44. data/test/dummy/public/favicon.ico +0 -0
  45. data/test/test_helper.rb +15 -0
  46. metadata +197 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 271f0c2f183fa09fb618281c98c3d96093d3281c
4
+ data.tar.gz: 23f311b3570b4417bc9075120a10f3ce649b3549
5
+ SHA512:
6
+ metadata.gz: 9375cb789ac04d3fc4d290e23438f61536985ab6df1c6274d2a391769e7ce471c5ee17691ad170ab2cd26c706be2963534afc2cd7824d0ff8ab0559ab4e93cf0
7
+ data.tar.gz: 3e69e4a3273ae65981e8ce1405c6b1173dd0dfb5372db22e73bad8f1f5fd71502326be5463528ba2b954fd4a2a39025a602137919549a3ebb3b757d5d2c149cb
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 YOURNAME
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.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = ConeyIsland
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ConeyIsland'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
data/bin/coney_island ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.push '.'
4
+ require 'config/environment.rb'
5
+
6
+ ## Copyright 2011 Eric Draut, all rights reserved
7
+
8
+ ConeyIsland.run_inline
9
+ ConeyIsland.initialize_background
10
+ ConeyIsland.start
@@ -0,0 +1,301 @@
1
+ module ConeyIsland
2
+ BG_TIMEOUT_SECONDS = 30
3
+
4
+ def self.run_inline
5
+ @run_inline = true
6
+ end
7
+
8
+ def self.notification_service=(service_name)
9
+ @notifier = "ConeyIsland::Notifiers::#{service_name}Notifier".constantize
10
+ end
11
+
12
+ def self.amqp_connection
13
+ @connection
14
+ end
15
+
16
+ def self.amqp_parameters=(params)
17
+ @amqp_parameters = params
18
+ @amqp_parameters ||= self.config['amqp_connection']
19
+ end
20
+
21
+ def self.amqp_parameters
22
+ @amqp_parameters
23
+ end
24
+
25
+ def self.handle_connection
26
+ @connection ||= AMQP.connect(self.amqp_parameters)
27
+ @channel ||= AMQP::Channel.new(@connection)
28
+ @exchange ||= @channel.topic('coney_island')
29
+ end
30
+
31
+ def self.exchange
32
+ @exchange
33
+ end
34
+
35
+ def self.channel
36
+ @channel
37
+ end
38
+
39
+ def self.config
40
+ if(File.exists?(File.join(Rails.root,"config","coney_island.yml")))
41
+ @config = Psych.load(File.read(File.join(Rails.root,"config","coney_island.yml")))
42
+ @config = @config[Rails.env]
43
+ end
44
+ end
45
+
46
+ #BEGIN web functionality
47
+
48
+ def self.submit(*args)
49
+ if RequestStore.store[:cache_jobs]
50
+ RequestStore.store[:jobs].push args
51
+ else
52
+ self.submit!(args)
53
+ end
54
+ end
55
+
56
+ def self.submit!(args)
57
+ if @run_inline
58
+ self.handle_publish(args)
59
+ else
60
+ EventMachine.next_tick do
61
+ self.handle_publish(args)
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.handle_publish(args)
67
+ self.handle_connection unless @run_inline
68
+ jobs = (args.first.is_a? Array) ? args : [args]
69
+ jobs.each do |args|
70
+ if (args.first.is_a? Class or args.first.is_a? Module) and (args[1].is_a? String or args[1].is_a? Symbol) and args.last.is_a? Hash and 3 == args.length
71
+ klass = args.shift
72
+ klass = klass.name unless @run_inline
73
+ method_name = args.shift
74
+ job_args = args.shift
75
+ job_args ||= {}
76
+ job_args['klass'] = klass
77
+ job_args['method_name'] = method_name
78
+ if @run_inline
79
+ job_args.stringify_keys!
80
+ method_args = job_args['args']
81
+ if job_args.has_key? 'instance_id'
82
+ instance_id = job_args.delete 'instance_id'
83
+ object = klass.find(instance_id)
84
+ else
85
+ object = klass
86
+ end
87
+ if method_args && (method_args.length > 0)
88
+ object.send method_name, *method_args
89
+ else
90
+ object.send method_name
91
+ end
92
+ else
93
+ work_queue = job_args.delete :work_queue
94
+ work_queue ||= 'default'
95
+ self.exchange.publish((job_args.to_json), routing_key: "carousels.#{work_queue}")
96
+ end
97
+ end
98
+ RequestStore.store[:completed_jobs] ||= 0
99
+ RequestStore.store[:completed_jobs] += 1
100
+ end
101
+ end
102
+
103
+ def self.cache_jobs
104
+ RequestStore.store[:cache_jobs] = true
105
+ RequestStore.store[:jobs] = []
106
+ end
107
+
108
+ def self.flush_jobs
109
+ jobs = RequestStore.store[:jobs].dup
110
+ self.submit!(jobs) if jobs.any?
111
+ RequestStore.store[:jobs] = []
112
+ end
113
+
114
+ def self.run_with_em(klass, method, *args)
115
+ EventMachine.run do
116
+ ConeyIsland.cache_jobs
117
+ ConeyIsland.submit(klass, method, *args)
118
+ ConeyIsland.flush_jobs
119
+ ConeyIsland.publisher_shutdown
120
+ end
121
+ end
122
+
123
+ def self.publisher_shutdown
124
+ EventMachine.add_periodic_timer(1) do
125
+ if RequestStore.store[:jobs] && (RequestStore.store[:jobs].length > RequestStore.store[:completed_jobs])
126
+ Rails.logger.info("Waiting for #{RequestStore.store[:jobs].length - RequestStore.store[:completed_jobs]} publishes to finish")
127
+ else
128
+ Rails.logger.info("Shutting down coney island publisher")
129
+ EventMachine.stop
130
+ end
131
+ end
132
+ end
133
+
134
+ # BEGIN background functionality
135
+
136
+ def self.initialize_background
137
+ ENV['NEW_RELIC_AGENT_ENABLED'] = 'false'
138
+ ENV['NEWRELIC_ENABLE'] = 'false'
139
+ @ticket = ARGV[0]
140
+
141
+ #TODO: set an env variable or constant that can be checked by pubnub to decide sync or not
142
+ @log_io = self.config['log'].constantize rescue nil
143
+ @log_io ||= self.config['log']
144
+ @log = Logger.new(@log_io)
145
+
146
+ @ticket ||= 'default'
147
+
148
+ @instance_config = self.config['carousels'][@ticket]
149
+
150
+ @prefetch_count = @instance_config['prefetch_count'] if @instance_config
151
+ @prefetch_count ||= 20
152
+
153
+ @worker_count = @instance_config['worker_count'] if @instance_config
154
+ @worker_count ||= 1
155
+ @child_count = @worker_count - 1
156
+ @child_pids = []
157
+
158
+ @full_instance_name = @ticket
159
+ @job_attempts = {}
160
+
161
+ @log.level = @config['log_level']
162
+ @log.info("config: #{self.config}")
163
+
164
+ end
165
+
166
+ def self.shutdown(signal)
167
+ shutdown_time = Time.now
168
+ @child_pids.each do |child_pid|
169
+ Process.kill(signal, child_pid)
170
+ end
171
+ @queue.unsubscribe
172
+ EventMachine.add_periodic_timer(1) do
173
+ if @job_attempts.any?
174
+ @log.info("Waiting for #{@job_attempts.length} requests to finish")
175
+ else
176
+ @log.info("Shutting down coney island #{@ticket}")
177
+ EventMachine.stop
178
+ end
179
+ end
180
+ end
181
+
182
+ def self.handle_job(metadata,args,job_id)
183
+ class_name = args['klass']
184
+ method_name = args['method_name']
185
+ klass = class_name.constantize
186
+ method_args = args['args']
187
+ timeout = args['timeout']
188
+ timeout ||= BG_TIMEOUT_SECONDS
189
+ begin
190
+ Timeout::timeout(timeout) do
191
+ if args.has_key? 'instance_id'
192
+ instance_id = args['instance_id']
193
+ object = klass.find(instance_id)
194
+ else
195
+ object = klass
196
+ end
197
+ if method_args and method_args.length > 0
198
+ object.send method_name, *method_args
199
+ else
200
+ object.send method_name
201
+ end
202
+ end
203
+ rescue Timeout::Error => e
204
+ if @job_attempts.has_key? job_id
205
+ if @job_attempts[job_id] >= 3
206
+ @log.error("Request #{job_id} timed out after #{timeout} seconds, bailing out after 3 attempts")
207
+ self.finalize_job(metadata,job_id)
208
+ self.poke_the_badger(e, {work_queue: @ticket, job_payload: args, reason: 'Bailed out after 3 attempts'})
209
+ else
210
+ @log.error("Request #{job_id} timed out after #{timeout} seconds on attempt number #{@job_attempts[job_id]}, retrying...")
211
+ @job_attempts[job_id] += 1
212
+ self.handle_job(metadata,args,job_id)
213
+ end
214
+ end
215
+ rescue Exception => e
216
+ self.poke_the_badger(e, {work_queue: @ticket, job_payload: args})
217
+ @log.error("Error executing #{class_name}##{method_name} #{job_id} for id #{args['instance_id']} with args #{args}:")
218
+ @log.error(e.message)
219
+ @log.error(e.backtrace.join("\n"))
220
+ self.finalize_job(metadata,job_id)
221
+ else
222
+ self.finalize_job(metadata,job_id)
223
+ end
224
+ end
225
+
226
+ def self.finalize_job(metadata,job_id)
227
+ metadata.ack
228
+ @log.info("finished job #{job_id}")
229
+ @job_attempts.delete job_id
230
+ end
231
+
232
+ def self.start
233
+ @child_count.times do
234
+ child_pid = Process.fork
235
+ unless child_pid
236
+ @log.info("started child for ticket #{@ticket} with pid #{Process.pid}")
237
+ break
238
+ end
239
+ @child_pids.push child_pid
240
+ end
241
+ defined?(ActiveRecord::Base) and
242
+ ActiveRecord::Base.establish_connection
243
+ EventMachine.run do
244
+
245
+ Signal.trap('INT') do
246
+ self.shutdown('INT')
247
+ end
248
+ Signal.trap('TERM') do
249
+ self.shutdown('TERM')
250
+ end
251
+
252
+ self.handle_connection
253
+ @log.info("Connecting to AMQP broker. Running #{AMQP::VERSION}")
254
+
255
+ #send a heartbeat every 15 seconds to avoid aggresive network configurations that close quiet connections
256
+ heartbeat_exchange = self.channel.fanout('coney_island_heartbeat')
257
+ EventMachine.add_periodic_timer(15) do
258
+ heartbeat_exchange.publish({:instance_name => @ticket})
259
+ end
260
+
261
+ self.channel.prefetch @prefetch_count
262
+ @queue = self.channel.queue(@full_instance_name, auto_delete: false, durable: true)
263
+ @queue.bind(self.exchange, routing_key: 'carousels.' + @ticket)
264
+ @queue.subscribe(:ack => true) do |metadata,payload|
265
+ begin
266
+ job_id = SecureRandom.uuid
267
+ @job_attempts[job_id] = 1
268
+ args = JSON.parse(payload)
269
+ @log.info ("Starting job #{job_id}: #{args}")
270
+ if args.has_key? 'delay'
271
+ EventMachine.add_timer(args['delay'].to_i) do
272
+ self.handle_job(metadata,args,job_id)
273
+ end
274
+ else
275
+ self.handle_job(metadata,args,job_id)
276
+ end
277
+ rescue Timeout::Error => e
278
+ self.poke_the_badger(e, {code_source: 'ConeyIsland', job_payload: args, reason: 'timeout in subscribe code before calling job method'})
279
+ rescue Exception => e
280
+ self.poke_the_badger(e, {code_source: 'ConeyIsland', job_payload: args})
281
+ @log.error("ConeyIsland code error, not application code:\n#{e.inspect}\nARGS: #{args}")
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ def self.poke_the_badger(message, context, attempts = 1)
288
+ begin
289
+ Timeout::timeout(3) do
290
+ @notifier.notify(message, context)
291
+ end
292
+ rescue
293
+ if attempts <= 3
294
+ attempts += 1
295
+ self.poke_the_badger(message, context, attempts)
296
+ end
297
+ end
298
+ end
299
+ end
300
+ require 'coney_island/notifiers/honeybadger_notifier'
301
+
@@ -0,0 +1,9 @@
1
+ module ConeyIsland
2
+ module Notifiers
3
+ class AirbrakeNotifier
4
+ def self.notify(message, extra_params)
5
+ Airbrake.notify(message, extra_params)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ConeyIsland
2
+ module Notifiers
3
+ class HoneybadgerNotifier
4
+ def self.notify(message, extra_params)
5
+ Honeybadger.notify(message, { context: extra_params })
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module ConeyIsland
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :coney_island do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class ConeyIslandTest < ActiveSupport::TestCase
4
+ test "truth" do
5
+ assert_kind_of Module, ConeyIsland
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.