qwirk 0.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.
Files changed (154) hide show
  1. data/History.md +7 -0
  2. data/LICENSE.txt +201 -0
  3. data/README.md +180 -0
  4. data/Rakefile +34 -0
  5. data/examples/README +1 -0
  6. data/examples/activemq.xml +84 -0
  7. data/examples/advanced_requestor/README.md +15 -0
  8. data/examples/advanced_requestor/base_request_worker.rb +18 -0
  9. data/examples/advanced_requestor/char_count_worker.rb +16 -0
  10. data/examples/advanced_requestor/config.ru +24 -0
  11. data/examples/advanced_requestor/exception_raiser_worker.rb +17 -0
  12. data/examples/advanced_requestor/length_worker.rb +14 -0
  13. data/examples/advanced_requestor/print_worker.rb +14 -0
  14. data/examples/advanced_requestor/publisher.rb +49 -0
  15. data/examples/advanced_requestor/qwirk.yml +16 -0
  16. data/examples/advanced_requestor/reverse_worker.rb +14 -0
  17. data/examples/advanced_requestor/triple_worker.rb +14 -0
  18. data/examples/batch/my_batch_worker.rb +30 -0
  19. data/examples/batch/my_line_worker.rb +8 -0
  20. data/examples/qwirk.yml +20 -0
  21. data/examples/requestor/README.md +13 -0
  22. data/examples/requestor/config.ru +13 -0
  23. data/examples/requestor/qwirk_persist.yml +5 -0
  24. data/examples/requestor/requestor.rb +68 -0
  25. data/examples/requestor/reverse_echo_worker.rb +15 -0
  26. data/examples/setup.rb +13 -0
  27. data/examples/shared/README.md +24 -0
  28. data/examples/shared/config.ru +13 -0
  29. data/examples/shared/publisher.rb +49 -0
  30. data/examples/shared/qwirk_persist.yml +5 -0
  31. data/examples/shared/shared_worker.rb +16 -0
  32. data/examples/simple/README +53 -0
  33. data/examples/simple/bar_worker.rb +10 -0
  34. data/examples/simple/baz_worker.rb +10 -0
  35. data/examples/simple/config.ru +14 -0
  36. data/examples/simple/publisher.rb +49 -0
  37. data/examples/simple/qwirk_persist.yml +4 -0
  38. data/examples/simple/tmp/kahadb/db-1.log +0 -0
  39. data/examples/simple/tmp/kahadb/db.data +0 -0
  40. data/examples/simple/tmp/kahadb/db.redo +0 -0
  41. data/examples/task/README +47 -0
  42. data/examples/task/config.ru +14 -0
  43. data/examples/task/foo_worker.rb +10 -0
  44. data/examples/task/messages.out +1000 -0
  45. data/examples/task/publisher.rb +25 -0
  46. data/examples/task/qwirk_persist.yml +5 -0
  47. data/examples/task/task.rb +36 -0
  48. data/lib/qwirk.rb +63 -0
  49. data/lib/qwirk/adapter.rb +45 -0
  50. data/lib/qwirk/base_worker.rb +96 -0
  51. data/lib/qwirk/batch.rb +4 -0
  52. data/lib/qwirk/batch/acquire_file_strategy.rb +47 -0
  53. data/lib/qwirk/batch/active_record.rb +3 -0
  54. data/lib/qwirk/batch/active_record/batch_job.rb +111 -0
  55. data/lib/qwirk/batch/active_record/failed_record.rb +5 -0
  56. data/lib/qwirk/batch/active_record/outstanding_record.rb +6 -0
  57. data/lib/qwirk/batch/file_status_strategy.rb +86 -0
  58. data/lib/qwirk/batch/file_worker.rb +228 -0
  59. data/lib/qwirk/batch/job_status.rb +29 -0
  60. data/lib/qwirk/batch/parse_file_strategy.rb +48 -0
  61. data/lib/qwirk/engine.rb +9 -0
  62. data/lib/qwirk/loggable.rb +23 -0
  63. data/lib/qwirk/manager.rb +140 -0
  64. data/lib/qwirk/marshal_strategy.rb +74 -0
  65. data/lib/qwirk/marshal_strategy/bson.rb +37 -0
  66. data/lib/qwirk/marshal_strategy/json.rb +37 -0
  67. data/lib/qwirk/marshal_strategy/none.rb +26 -0
  68. data/lib/qwirk/marshal_strategy/ruby.rb +26 -0
  69. data/lib/qwirk/marshal_strategy/string.rb +25 -0
  70. data/lib/qwirk/marshal_strategy/yaml.rb +25 -0
  71. data/lib/qwirk/publish_handle.rb +170 -0
  72. data/lib/qwirk/publisher.rb +67 -0
  73. data/lib/qwirk/queue_adapter.rb +3 -0
  74. data/lib/qwirk/queue_adapter/active_mq.rb +13 -0
  75. data/lib/qwirk/queue_adapter/active_mq/publisher.rb +12 -0
  76. data/lib/qwirk/queue_adapter/active_mq/worker_config.rb +16 -0
  77. data/lib/qwirk/queue_adapter/in_mem.rb +7 -0
  78. data/lib/qwirk/queue_adapter/in_mem/factory.rb +45 -0
  79. data/lib/qwirk/queue_adapter/in_mem/publisher.rb +98 -0
  80. data/lib/qwirk/queue_adapter/in_mem/queue.rb +88 -0
  81. data/lib/qwirk/queue_adapter/in_mem/reply_queue.rb +56 -0
  82. data/lib/qwirk/queue_adapter/in_mem/topic.rb +48 -0
  83. data/lib/qwirk/queue_adapter/in_mem/worker.rb +63 -0
  84. data/lib/qwirk/queue_adapter/in_mem/worker_config.rb +59 -0
  85. data/lib/qwirk/queue_adapter/jms.rb +50 -0
  86. data/lib/qwirk/queue_adapter/jms/connection.rb +42 -0
  87. data/lib/qwirk/queue_adapter/jms/consumer.rb +37 -0
  88. data/lib/qwirk/queue_adapter/jms/publisher.rb +126 -0
  89. data/lib/qwirk/queue_adapter/jms/worker.rb +89 -0
  90. data/lib/qwirk/queue_adapter/jms/worker_config.rb +38 -0
  91. data/lib/qwirk/remote_exception.rb +42 -0
  92. data/lib/qwirk/request_worker.rb +62 -0
  93. data/lib/qwirk/task.rb +177 -0
  94. data/lib/qwirk/task.rb.sav +194 -0
  95. data/lib/qwirk/version.rb +3 -0
  96. data/lib/qwirk/worker.rb +222 -0
  97. data/lib/qwirk/worker_config.rb +187 -0
  98. data/lib/rails/generators/qwirk/qwirk_generator.rb +82 -0
  99. data/lib/rails/generators/qwirk/templates/initializer.rb +6 -0
  100. data/lib/rails/generators/qwirk/templates/migration.rb +9 -0
  101. data/lib/rails/generators/qwirk/templates/schema.rb +28 -0
  102. data/lib/rails/railties/tasks.rake +8 -0
  103. data/lib/tasks/qwirk_tasks.rake +4 -0
  104. data/test/base_test.rb +248 -0
  105. data/test/database.yml +14 -0
  106. data/test/dummy/Rakefile +7 -0
  107. data/test/dummy/app/controllers/application_controller.rb +3 -0
  108. data/test/dummy/app/helpers/application_helper.rb +2 -0
  109. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  110. data/test/dummy/config.ru +4 -0
  111. data/test/dummy/config/application.rb +45 -0
  112. data/test/dummy/config/boot.rb +10 -0
  113. data/test/dummy/config/database.yml +22 -0
  114. data/test/dummy/config/environment.rb +5 -0
  115. data/test/dummy/config/environments/development.rb +26 -0
  116. data/test/dummy/config/environments/production.rb +49 -0
  117. data/test/dummy/config/environments/test.rb +35 -0
  118. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  119. data/test/dummy/config/initializers/inflections.rb +10 -0
  120. data/test/dummy/config/initializers/mime_types.rb +5 -0
  121. data/test/dummy/config/initializers/secret_token.rb +7 -0
  122. data/test/dummy/config/initializers/session_store.rb +8 -0
  123. data/test/dummy/config/locales/en.yml +5 -0
  124. data/test/dummy/config/routes.rb +58 -0
  125. data/test/dummy/log/development.log +0 -0
  126. data/test/dummy/log/production.log +0 -0
  127. data/test/dummy/log/server.log +0 -0
  128. data/test/dummy/log/test.log +0 -0
  129. data/test/dummy/public/404.html +26 -0
  130. data/test/dummy/public/422.html +26 -0
  131. data/test/dummy/public/500.html +26 -0
  132. data/test/dummy/public/favicon.ico +0 -0
  133. data/test/dummy/public/javascripts/application.js +2 -0
  134. data/test/dummy/public/javascripts/controls.js +965 -0
  135. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  136. data/test/dummy/public/javascripts/effects.js +1123 -0
  137. data/test/dummy/public/javascripts/prototype.js +6001 -0
  138. data/test/dummy/public/javascripts/rails.js +191 -0
  139. data/test/dummy/script/rails +6 -0
  140. data/test/integration/navigation_test.rb +7 -0
  141. data/test/jms.yml +9 -0
  142. data/test/jms_fail_test.rb +149 -0
  143. data/test/jms_requestor_block_test.rb +278 -0
  144. data/test/jms_requestor_test.rb +238 -0
  145. data/test/jms_test.rb +287 -0
  146. data/test/marshal_strategy_test.rb +62 -0
  147. data/test/support/integration_case.rb +5 -0
  148. data/test/test_helper.rb +7 -0
  149. data/test/test_helper.rbold +22 -0
  150. data/test/test_helper_active_record.rb +61 -0
  151. data/test/unit/qwirk/batch/acquire_file_strategy_test.rb +101 -0
  152. data/test/unit/qwirk/batch/active_record/batch_job_test.rb +35 -0
  153. data/test/unit/qwirk/batch/parse_file_strategy_test.rb +49 -0
  154. metadata +366 -0
@@ -0,0 +1,5 @@
1
+ module Qwirk::Batch::ActiveRecord
2
+ class FailedRecord < ActiveRecord::Base
3
+ set_table_name 'mt_failed_records'
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Qwirk::Batch::ActiveRecord
2
+ class OutstandingRecord < ActiveRecord::Base
3
+ set_table_name 'mt_outstanding_records'
4
+ end
5
+ end
6
+
@@ -0,0 +1,86 @@
1
+ require 'tmpdir'
2
+
3
+ module Qwirk
4
+ module Batch
5
+ class FileStatusStrategy
6
+
7
+ attr_reader :finished_count
8
+
9
+ if defined?(Rails) && defined?(Rails.root)
10
+ @@persist_dir = File.join(Rails.root, 'log', 'qwirk')
11
+ else
12
+ @@persist_dir = File.join(Dir.tmpdir, 'qwirk')
13
+ end
14
+
15
+ def self.persist_dir
16
+ @@persist_dir
17
+ end
18
+
19
+ def self.persist_dir=(dir)
20
+ @@persist_dir = dir
21
+ end
22
+
23
+ def initialize(options)
24
+ @worker_name = options[:worker_name]
25
+ @dir = options[:persist_dir] || @@persist_dir
26
+ end
27
+
28
+ # Resume any previous jobs that were stopped
29
+ def resume?
30
+ false
31
+ end
32
+
33
+ def start(file)
34
+ @file = file
35
+ @pending_hash = {}
36
+ @fail_array = []
37
+ @finished_count = 0
38
+ end
39
+
40
+ def stop
41
+ return unless @file
42
+ save_yaml = {
43
+ :file => @file,
44
+ :pending => @pending_hash,
45
+ :fail => @fail_array,
46
+ :finished_count => @finished_count
47
+ }
48
+ end
49
+
50
+ def finish
51
+ @file = nil
52
+ end
53
+
54
+ def start_record(message_id, file_pos)
55
+ @pending_hash[message_id] = file_pos
56
+ end
57
+
58
+ def finish_record(message_id)
59
+ @pending_hash.delete(message_id)
60
+ @finished_count += 1
61
+ end
62
+
63
+ def fail_record(message_id)
64
+ file_pos = @pending_hash.delete(message_id)
65
+ raise "Invalid message #{message_id}, not in pending_hash" unless file_pos
66
+ @fail_array << file_pos
67
+ end
68
+
69
+ def pending_count
70
+ @pending_hash.size
71
+ end
72
+
73
+ def failed_count
74
+ @fail_array.size
75
+ end
76
+
77
+ #######
78
+ private
79
+ #######
80
+
81
+ def save
82
+ persist_file =
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,228 @@
1
+ module Qwirk
2
+ module Batch
3
+
4
+ # Batch worker which reads records from files and queues them up for a separate worker (Qwirk::QueueAdapter::JMS::RequestWorker) to process.
5
+ # For instance, a worker of this type might look as follows:
6
+ # class MyBatchWorker
7
+ # include Qwirk::Batch::FileWorker
8
+ #
9
+ # file :glob => '/home/batch_files/input/**', :age => 1.minute, :max_outstanding_records => 100, :fail_threshold => 0.8, :save_period => 30.seconds
10
+ # marshal :string
11
+ # end
12
+ #
13
+ # The following options can be used for configuring the class
14
+ # file:
15
+ # :glob => <glob_path>
16
+ # The path where files will be processed from. Files will be renamed with a .processing extension while they are being processed
17
+ # and to a .completed extension when processing is completed.
18
+ # :age => <duration>
19
+ # How old a file must be before it will be processed. This is to prevent files that are in the middle of being uploaded from begin acquired.
20
+ # :poll_time => <duration>
21
+ # How often the glob is queried for new files.
22
+ # :max_outstanding_records => <integer>
23
+ # This is how many outstanding records can be queued at a time.
24
+ # :
25
+ module FileWorker
26
+ include Qwirk::BaseWorker
27
+
28
+ module ClassMethods
29
+ # Define the marshaling and time_to_live that will occur on the response
30
+ def file(options)
31
+ @file_options = options
32
+ end
33
+
34
+ def file_options
35
+ @file_options
36
+ end
37
+
38
+ def marshal(marshal_type)
39
+ @marshal_type = marshal_type
40
+ end
41
+
42
+ def marshal_type
43
+ @marshal_type
44
+ end
45
+
46
+ def queue(name, opts={})
47
+ @queue_name = name
48
+ @reply_queue_name = opts[:reply_queue]
49
+ end
50
+
51
+ def queue_name
52
+ @queue_name
53
+ end
54
+
55
+ def reply_queue_name
56
+ @reply_queue_name
57
+ end
58
+ end
59
+
60
+ def self.included(base)
61
+ Qwirk::BaseWorker.included(base)
62
+ base.extend(ClassMethods)
63
+ end
64
+
65
+ # Set the global default acquire_file_strategy for an organization
66
+ def self.default_acquire_file_strategy=(default_strategy)
67
+ @@default_acquire_file_strategy = default_strategy
68
+ end
69
+
70
+ def self.default_acquire_file_strategy
71
+ @@default_acquire_file_strategy
72
+ end
73
+
74
+ # Set the global default parse_file_strategy for an organization
75
+ def self.default_parse_file_strategy=(default_strategy)
76
+ @@default_parse_file_strategy = default_strategy
77
+ end
78
+
79
+ def self.default_parse_file_strategy
80
+ @@default_parse_file_strategy
81
+ end
82
+
83
+ # Set the global default process_file_strategy for an organization
84
+ def self.default_file_status_strategy=(default_strategy)
85
+ @@default_file_status_strategy = self.file_status_strategy_from_sym(default_strategy)
86
+ end
87
+
88
+ def self.default_file_status_strategy
89
+ self.file_status_strategy_to_sym(@@default_file_status_strategy)
90
+ end
91
+
92
+ def self.file_status_strategy_from_sym(strategy)
93
+ if strategy.kind_of?(Symbol)
94
+ if strategy == :active_record
95
+ require 'qwirk/batch/active_record'
96
+ Qwirk::Batch::ActiveRecord::BatchJob
97
+ elsif strategy == :mongoid
98
+ require 'qwirk/batch/mongoid'
99
+ Qwirk::Batch::Mongoid::BatchJob
100
+ else
101
+ raise "Invalid symbol for file_status_strategy=#{strategy}"
102
+ end
103
+ else
104
+ strategy
105
+ end
106
+ end
107
+
108
+ def self.file_status_strategy_to_sym(strategy)
109
+ if strategy == Qwirk::Batch::ActiveRecord::BatchJob
110
+ :active_record
111
+ elsif strategy == Qwirk::Batch::ActiveRecord::BatchJob
112
+ :mongoid
113
+ else
114
+ strategy
115
+ end
116
+ end
117
+
118
+ self.default_acquire_file_strategy = AcquireFileStrategy
119
+ self.default_parse_file_strategy = ParseFileStrategy
120
+ self.default_file_status_strategy = begin
121
+ if defined?(ActiveRecord)
122
+ :active_record
123
+ elsif defined?(Mongoid)
124
+ :mongoid
125
+ else
126
+ nil
127
+ end
128
+ end
129
+
130
+ def initialize(opts={})
131
+ super
132
+ @marshal_type = (self.class.marshal_type || :ruby).to_s
133
+ @marshaler = MarshalStrategy.find(@marshal_type)
134
+ @stopped = false
135
+ @queue_name = opts[:queue_name] || self.class.queue_name || (self.name.match(/(.*)File$/) && $1)
136
+ raise "queue_name not specified in #{self.class.name}" unless @queue_name
137
+ @reply_queue_name = opts[:reply_queue_name] || self.class.reply_queue_name || "#{@queue_name}Reply"
138
+
139
+ file_options = self.class.file_options
140
+ raise "file_options not set for #{self.class.name}" unless file_options
141
+ acquire_strategy_class = file_options.delete(:acquire_strategy) || FileWorker.default_acquire_file_strategy
142
+ parse_strategy_class = file_options.delete(:parse_strategy) || FileWorker.default_parse_file_strategy
143
+ status_strategy = file_options.delete(:status_strategy) || FileWorker.default_parse_file_strategy
144
+ raise 'No status_strategy defined' unless status_strategy
145
+ status_strategy_class = FileWorker.file_status_strategy_from_sym(status_strategy)
146
+ @acquire_file_strategy = acquire_strategy_class.new(file_options)
147
+ @parse_file_strategy = parse_strategy_class.new(file_options)
148
+ @file_status_strategy = status_strategy_class.new(file_options)
149
+ @max_outstanding_records = file_options[:max_outstanding_records] || 10
150
+ end
151
+
152
+ def start
153
+ #TODO: look for current job
154
+ while file = @acquire_file_strategy.acquire_file do
155
+ @parse_file_strategy.open(file)
156
+ @reply_thread = Thread.new do
157
+ java.lang.Thread.current_thread.name = "Qwirk worker (reply): #{worker}"
158
+ reply_event_loop
159
+ end
160
+ begin
161
+ @record_total = @parse_file_strategy.record_total
162
+ process_file
163
+ ensure
164
+ @parse_file_strategy.close
165
+ end
166
+ end
167
+ end
168
+
169
+
170
+ def stop
171
+ @stopped = true
172
+ @acquire_file_strategy.stop
173
+ end
174
+
175
+ def join
176
+ thread.join
177
+ end
178
+
179
+ def status
180
+ raise "Need to override status method in #{self.class.name}"
181
+ end
182
+
183
+ #########
184
+ protected
185
+ #########
186
+
187
+ def process_file
188
+ while record = @parse_file_strategy.next_record
189
+ obj = record_to_object(record)
190
+ end
191
+ end
192
+
193
+ # Allow extenders to manipulate the file record before sticking it on the queue
194
+ def record_to_object(record)
195
+ record
196
+ end
197
+
198
+ # Perform any additional operations on the given responses
199
+ def process_response(obj)
200
+ end
201
+
202
+ #######
203
+ private
204
+ #######
205
+
206
+ def reply_event_loop
207
+ @reply_session = Qwirk::QueueAdapter::JMS::Connection.create_session
208
+ @consumer = @reply_session.consumer(:queue_name => @queue_name)
209
+ @reply_session.start
210
+
211
+ while !@stopped && message = @consumer.receive
212
+ @message_mutex.synchronize do
213
+ obj = Qwirk::QueueAdapter::JMS.parse_response(message)
214
+ process_response(obj)
215
+ message.acknowledge
216
+ end
217
+ # TODO: @time_track now in WorkerConfig
218
+ #Qwirk.logger.info {"#{self}::on_message (#{('%.1f' % (@time_track.last_time*1000.0))}ms)"} if Qwirk::QueueAdapter::JMS::Connection.log_times?
219
+ end
220
+ @status = 'Exited'
221
+ Qwirk.logger.info "#{self}: Exiting"
222
+ rescue Exception => e
223
+ @status = "Exited with exception #{e.message}"
224
+ Qwirk.logger.error "#{self}: Exception, thread terminating: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,29 @@
1
+ module Qwirk
2
+ module Batch
3
+ module JobStatus
4
+
5
+ # Note: String max is set to 8 as defined in the schema.rb file
6
+
7
+ # Job has been acquired but is not yet running
8
+ INITED = 'Inited'
9
+
10
+ # Job is currently running
11
+ RUNNING = 'Running'
12
+
13
+ # Job has paused because the worker has been commanded to stop
14
+ PAUSED = 'Paused'
15
+
16
+ # A client has canceled the job
17
+ CANCELED = 'Canceled'
18
+
19
+ # The job has been aborted due to threshold constraints (too many record failures)
20
+ ABORTED = 'Aborted'
21
+
22
+ # The job has finished
23
+ FINISHED = 'Finished'
24
+
25
+ STATUSES = [INITED, RUNNING, PAUSED, CANCELED, ABORTED, FINISHED].freeze
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ module Qwirk
2
+ module Batch
3
+ # Default strategy for parsing a file
4
+ class ParseFileStrategy
5
+ def initialize(file_options)
6
+ end
7
+
8
+ # Open the file for processing
9
+ def open(file)
10
+ @file = file
11
+ @fin = File.open(@file, 'r')
12
+ @line_count = 0
13
+ end
14
+
15
+ # Goto a specific position in the file. This strategy uses line_counts. @fin.seek and @fin.tell would
16
+ # be faster but wouldn't allow the file to be edited if it was formatted incorrectly.
17
+ def file_position=(line_count)
18
+ if @line_count > line_count
19
+ @fin.seek(0)
20
+ @line_count = 0
21
+ end
22
+ next_record while @line_count < line_count
23
+ end
24
+
25
+ # Return the current position in the file
26
+ def file_position
27
+ @line_count
28
+ end
29
+
30
+ # Read the next record from the file
31
+ def next_record
32
+ @line_count += 1
33
+ @fin.gets
34
+ end
35
+
36
+ # Return an estimate of the total records in the file
37
+ def record_total
38
+ # Faster than reading file in ruby but won't count incomplete lines
39
+ %x{wc -l #{@file}}.split.first.to_i
40
+ end
41
+
42
+ # Close the file
43
+ def close
44
+ @fin.close
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails'
2
+
3
+ module Qwirk
4
+ class Engine < Rails::Engine
5
+ initializer "qwirk initialize" , :after =>"active_record.initialize_database" do
6
+ #config.before_configuration do
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Qwirk
2
+ module Loggable
3
+ def logger
4
+ @logger ||= (rails_logger || default_logger)
5
+ end
6
+
7
+ def rails_logger
8
+ (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
9
+ (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
10
+ end
11
+
12
+ def default_logger
13
+ require 'logger'
14
+ l = Logger.new($stdout)
15
+ l.level = Logger::DEBUG
16
+ l
17
+ end
18
+
19
+ def logger=(logger)
20
+ @logger = logger
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,140 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'socket'
4
+ require 'rumx'
5
+
6
+ module Qwirk
7
+ class Manager
8
+ include Rumx::Bean
9
+ attr_reader :env, :worker_configs, :name
10
+
11
+ bean_attr_accessor :poll_time, :float, 'How often the manager should poll the workers for their status for use by :idle_worker_timeout and :max_read_threshold'
12
+
13
+ @@default_options = {}
14
+
15
+ def self.default_options=(options)
16
+ @@default_options = options
17
+ end
18
+
19
+ # Constructs a manager. Accepts a hash of config options
20
+ # name - name which this bean will be added
21
+ # env - environment being executed under. For a rails project, this will be the value of Rails.env
22
+ # worker_file - the worker file is a hash with the environment or hostname as the primary key and a subhash with the worker names
23
+ # as the keys and the config options for the value. In this file, the env will be searched first and if that doesn't exist,
24
+ # the hostname will then be searched. Workers can be defined for development without having to specify the hostname. For
25
+ # production, a set of workers could be defined under production or specific workers for each host name.
26
+ # persist_file - WorkerConfig attributes that are modified externally (via Rumx interface) will be stored in this file. Without this
27
+ # option, external config changes that are made will be lost when the Manager is restarted.
28
+ def initialize(queue_adapter, options={})
29
+ options = @@default_options.merge(options)
30
+ #puts "creating qwirk manager with #{options.inspect}"
31
+ @stopped = false
32
+ @name = options[:name] || Qwirk::DEFAULT_NAME
33
+ @poll_time = 3.0
34
+ @worker_configs = []
35
+ @env = options[:env]
36
+ @worker_options = parse_worker_file(options[:worker_file])
37
+ @persist_file = options[:persist_file]
38
+ @persist_options = (@persist_file && File.exist?(@persist_file)) ? YAML.load_file(@persist_file) : {}
39
+
40
+ BaseWorker.worker_classes.each do |worker_class|
41
+ worker_config_class = worker_class.config_class
42
+ worker_class.each_config do |config_name, default_options|
43
+ # Least priority is config options defined in the Worker class, then the workers.yml file, highest priority is persist_file (ad-hoc changes made manually)
44
+ options = {}
45
+ options = options.merge(@worker_options[config_name]) if @worker_options[config_name]
46
+ options = options.merge(@persist_options[config_name]) if @persist_options[config_name]
47
+ worker_config = worker_config_class.new(queue_adapter, config_name, self, worker_class, default_options, options)
48
+ bean_add_child(config_name, worker_config)
49
+ @worker_configs << worker_config
50
+ end
51
+ end
52
+
53
+ start_timer_thread
54
+ stop_on_signal if options[:stop_on_signal]
55
+ end
56
+
57
+ def start_timer_thread
58
+ # TODO: Optionize hard-coded values
59
+ @timer_thread = Thread.new do
60
+ begin
61
+ while !@stopped
62
+ @worker_configs.each do |worker_config|
63
+ worker_config.periodic_call(@poll_time)
64
+ end
65
+ sleep @poll_time
66
+ end
67
+ rescue Exception => e
68
+ Qwirk.logger.error "Timer thread failed with exception: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
69
+ end
70
+ end
71
+ end
72
+
73
+ def stop
74
+ return if @stopped
75
+ @stopped = true
76
+ @timer_thread.wakeup
77
+ @worker_configs.each { |worker_config| worker_config.stop }
78
+ end
79
+
80
+ def stop_on_signal
81
+ ['HUP', 'INT', 'TERM'].each do |signal_name|
82
+ Signal.trap(signal_name) do
83
+ Qwirk.logger.info "Caught #{signal_name}"
84
+ stop
85
+ end
86
+ end
87
+ end
88
+
89
+ def save_persist_state
90
+ return unless @persist_file
91
+ new_persist_options = {}
92
+ BaseWorker.worker_classes.each do |worker_class|
93
+ worker_class.each_config do |config_name, options|
94
+ static_options = options.merge(@worker_options[config_name] || {})
95
+ worker_config = self[config_name]
96
+ hash = {}
97
+ # Only store off the config values that are specifically different from default values or values set in the workers.yml file
98
+ # Then updates to these values will be allowed w/o being hardcoded to an old default value.
99
+ worker_config.bean_get_attributes do |attribute_info|
100
+ if attribute_info.attribute[:config_item] && attribute_info.ancestry.size == 1
101
+ param_name = attribute_info.ancestry[0].to_sym
102
+ value = attribute_info.value
103
+ hash[param_name] = value if static_options[param_name] != value
104
+ end
105
+ end
106
+ new_persist_options[config_name] = hash unless hash.empty?
107
+ end
108
+ end
109
+ if new_persist_options != @persist_options
110
+ @persist_options = new_persist_options
111
+ File.open(@persist_file, 'w') do |out|
112
+ YAML.dump(@persist_options, out )
113
+ end
114
+ end
115
+ end
116
+
117
+ def [](name)
118
+ @worker_configs.each do |worker_config|
119
+ return worker_config if worker_config.name == name
120
+ end
121
+ return nil
122
+ end
123
+
124
+ #######
125
+ private
126
+ #######
127
+
128
+ def parse_worker_file(file)
129
+ if file && File.exist?(file)
130
+ hash = YAML.load(ERB.new(File.read(file), nil, '-').result(binding))
131
+ options = @env && hash[@env]
132
+ unless options
133
+ host = Socket.gethostname.sub(/\..*/, '')
134
+ options = hash[host]
135
+ end
136
+ end
137
+ return options || {}
138
+ end
139
+ end
140
+ end