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.
- data/History.md +7 -0
- data/LICENSE.txt +201 -0
- data/README.md +180 -0
- data/Rakefile +34 -0
- data/examples/README +1 -0
- data/examples/activemq.xml +84 -0
- data/examples/advanced_requestor/README.md +15 -0
- data/examples/advanced_requestor/base_request_worker.rb +18 -0
- data/examples/advanced_requestor/char_count_worker.rb +16 -0
- data/examples/advanced_requestor/config.ru +24 -0
- data/examples/advanced_requestor/exception_raiser_worker.rb +17 -0
- data/examples/advanced_requestor/length_worker.rb +14 -0
- data/examples/advanced_requestor/print_worker.rb +14 -0
- data/examples/advanced_requestor/publisher.rb +49 -0
- data/examples/advanced_requestor/qwirk.yml +16 -0
- data/examples/advanced_requestor/reverse_worker.rb +14 -0
- data/examples/advanced_requestor/triple_worker.rb +14 -0
- data/examples/batch/my_batch_worker.rb +30 -0
- data/examples/batch/my_line_worker.rb +8 -0
- data/examples/qwirk.yml +20 -0
- data/examples/requestor/README.md +13 -0
- data/examples/requestor/config.ru +13 -0
- data/examples/requestor/qwirk_persist.yml +5 -0
- data/examples/requestor/requestor.rb +68 -0
- data/examples/requestor/reverse_echo_worker.rb +15 -0
- data/examples/setup.rb +13 -0
- data/examples/shared/README.md +24 -0
- data/examples/shared/config.ru +13 -0
- data/examples/shared/publisher.rb +49 -0
- data/examples/shared/qwirk_persist.yml +5 -0
- data/examples/shared/shared_worker.rb +16 -0
- data/examples/simple/README +53 -0
- data/examples/simple/bar_worker.rb +10 -0
- data/examples/simple/baz_worker.rb +10 -0
- data/examples/simple/config.ru +14 -0
- data/examples/simple/publisher.rb +49 -0
- data/examples/simple/qwirk_persist.yml +4 -0
- data/examples/simple/tmp/kahadb/db-1.log +0 -0
- data/examples/simple/tmp/kahadb/db.data +0 -0
- data/examples/simple/tmp/kahadb/db.redo +0 -0
- data/examples/task/README +47 -0
- data/examples/task/config.ru +14 -0
- data/examples/task/foo_worker.rb +10 -0
- data/examples/task/messages.out +1000 -0
- data/examples/task/publisher.rb +25 -0
- data/examples/task/qwirk_persist.yml +5 -0
- data/examples/task/task.rb +36 -0
- data/lib/qwirk.rb +63 -0
- data/lib/qwirk/adapter.rb +45 -0
- data/lib/qwirk/base_worker.rb +96 -0
- data/lib/qwirk/batch.rb +4 -0
- data/lib/qwirk/batch/acquire_file_strategy.rb +47 -0
- data/lib/qwirk/batch/active_record.rb +3 -0
- data/lib/qwirk/batch/active_record/batch_job.rb +111 -0
- data/lib/qwirk/batch/active_record/failed_record.rb +5 -0
- data/lib/qwirk/batch/active_record/outstanding_record.rb +6 -0
- data/lib/qwirk/batch/file_status_strategy.rb +86 -0
- data/lib/qwirk/batch/file_worker.rb +228 -0
- data/lib/qwirk/batch/job_status.rb +29 -0
- data/lib/qwirk/batch/parse_file_strategy.rb +48 -0
- data/lib/qwirk/engine.rb +9 -0
- data/lib/qwirk/loggable.rb +23 -0
- data/lib/qwirk/manager.rb +140 -0
- data/lib/qwirk/marshal_strategy.rb +74 -0
- data/lib/qwirk/marshal_strategy/bson.rb +37 -0
- data/lib/qwirk/marshal_strategy/json.rb +37 -0
- data/lib/qwirk/marshal_strategy/none.rb +26 -0
- data/lib/qwirk/marshal_strategy/ruby.rb +26 -0
- data/lib/qwirk/marshal_strategy/string.rb +25 -0
- data/lib/qwirk/marshal_strategy/yaml.rb +25 -0
- data/lib/qwirk/publish_handle.rb +170 -0
- data/lib/qwirk/publisher.rb +67 -0
- data/lib/qwirk/queue_adapter.rb +3 -0
- data/lib/qwirk/queue_adapter/active_mq.rb +13 -0
- data/lib/qwirk/queue_adapter/active_mq/publisher.rb +12 -0
- data/lib/qwirk/queue_adapter/active_mq/worker_config.rb +16 -0
- data/lib/qwirk/queue_adapter/in_mem.rb +7 -0
- data/lib/qwirk/queue_adapter/in_mem/factory.rb +45 -0
- data/lib/qwirk/queue_adapter/in_mem/publisher.rb +98 -0
- data/lib/qwirk/queue_adapter/in_mem/queue.rb +88 -0
- data/lib/qwirk/queue_adapter/in_mem/reply_queue.rb +56 -0
- data/lib/qwirk/queue_adapter/in_mem/topic.rb +48 -0
- data/lib/qwirk/queue_adapter/in_mem/worker.rb +63 -0
- data/lib/qwirk/queue_adapter/in_mem/worker_config.rb +59 -0
- data/lib/qwirk/queue_adapter/jms.rb +50 -0
- data/lib/qwirk/queue_adapter/jms/connection.rb +42 -0
- data/lib/qwirk/queue_adapter/jms/consumer.rb +37 -0
- data/lib/qwirk/queue_adapter/jms/publisher.rb +126 -0
- data/lib/qwirk/queue_adapter/jms/worker.rb +89 -0
- data/lib/qwirk/queue_adapter/jms/worker_config.rb +38 -0
- data/lib/qwirk/remote_exception.rb +42 -0
- data/lib/qwirk/request_worker.rb +62 -0
- data/lib/qwirk/task.rb +177 -0
- data/lib/qwirk/task.rb.sav +194 -0
- data/lib/qwirk/version.rb +3 -0
- data/lib/qwirk/worker.rb +222 -0
- data/lib/qwirk/worker_config.rb +187 -0
- data/lib/rails/generators/qwirk/qwirk_generator.rb +82 -0
- data/lib/rails/generators/qwirk/templates/initializer.rb +6 -0
- data/lib/rails/generators/qwirk/templates/migration.rb +9 -0
- data/lib/rails/generators/qwirk/templates/schema.rb +28 -0
- data/lib/rails/railties/tasks.rake +8 -0
- data/lib/tasks/qwirk_tasks.rake +4 -0
- data/test/base_test.rb +248 -0
- data/test/database.yml +14 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +22 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +26 -0
- data/test/dummy/config/environments/production.rb +49 -0
- data/test/dummy/config/environments/test.rb +35 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/log/development.log +0 -0
- data/test/dummy/log/production.log +0 -0
- data/test/dummy/log/server.log +0 -0
- data/test/dummy/log/test.log +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/javascripts/application.js +2 -0
- data/test/dummy/public/javascripts/controls.js +965 -0
- data/test/dummy/public/javascripts/dragdrop.js +974 -0
- data/test/dummy/public/javascripts/effects.js +1123 -0
- data/test/dummy/public/javascripts/prototype.js +6001 -0
- data/test/dummy/public/javascripts/rails.js +191 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/navigation_test.rb +7 -0
- data/test/jms.yml +9 -0
- data/test/jms_fail_test.rb +149 -0
- data/test/jms_requestor_block_test.rb +278 -0
- data/test/jms_requestor_test.rb +238 -0
- data/test/jms_test.rb +287 -0
- data/test/marshal_strategy_test.rb +62 -0
- data/test/support/integration_case.rb +5 -0
- data/test/test_helper.rb +7 -0
- data/test/test_helper.rbold +22 -0
- data/test/test_helper_active_record.rb +61 -0
- data/test/unit/qwirk/batch/acquire_file_strategy_test.rb +101 -0
- data/test/unit/qwirk/batch/active_record/batch_job_test.rb +35 -0
- data/test/unit/qwirk/batch/parse_file_strategy_test.rb +49 -0
- metadata +366 -0
|
@@ -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
|
data/lib/qwirk/engine.rb
ADDED
|
@@ -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
|