rflow 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/Gemfile +5 -0
- data/NOTES +187 -0
- data/README +0 -0
- data/Rakefile +16 -0
- data/bin/rflow +215 -0
- data/example/basic_config.rb +49 -0
- data/example/basic_extensions.rb +142 -0
- data/example/http_config.rb +21 -0
- data/example/http_extensions.rb +262 -0
- data/lib/rflow.rb +440 -0
- data/lib/rflow/component.rb +192 -0
- data/lib/rflow/component/port.rb +150 -0
- data/lib/rflow/components.rb +10 -0
- data/lib/rflow/components/raw.rb +26 -0
- data/lib/rflow/components/raw/extensions.rb +18 -0
- data/lib/rflow/configuration.rb +290 -0
- data/lib/rflow/configuration/component.rb +27 -0
- data/lib/rflow/configuration/connection.rb +98 -0
- data/lib/rflow/configuration/migrations/20010101000001_create_settings.rb +14 -0
- data/lib/rflow/configuration/migrations/20010101000002_create_components.rb +19 -0
- data/lib/rflow/configuration/migrations/20010101000003_create_ports.rb +24 -0
- data/lib/rflow/configuration/migrations/20010101000004_create_connections.rb +27 -0
- data/lib/rflow/configuration/port.rb +30 -0
- data/lib/rflow/configuration/ruby_dsl.rb +183 -0
- data/lib/rflow/configuration/setting.rb +67 -0
- data/lib/rflow/configuration/uuid_keyed.rb +18 -0
- data/lib/rflow/connection.rb +59 -0
- data/lib/rflow/connections.rb +2 -0
- data/lib/rflow/connections/zmq_connection.rb +101 -0
- data/lib/rflow/message.rb +191 -0
- data/lib/rflow/port.rb +4 -0
- data/lib/rflow/util.rb +19 -0
- data/lib/rflow/version.rb +3 -0
- data/rflow.gemspec +42 -0
- data/schema/message.avsc +36 -0
- data/schema/raw.avsc +9 -0
- data/spec/fixtures/config_ints.rb +61 -0
- data/spec/fixtures/extensions_ints.rb +141 -0
- data/spec/rflow_configuration_spec.rb +73 -0
- data/spec/rflow_message_data_raw.rb +26 -0
- data/spec/rflow_message_data_spec.rb +60 -0
- data/spec/rflow_message_spec.rb +182 -0
- data/spec/rflow_spec.rb +100 -0
- data/spec/schema_spec.rb +28 -0
- data/spec/spec_helper.rb +37 -0
- data/temp.rb +295 -0
- metadata +270 -0
data/lib/rflow.rb
ADDED
@@ -0,0 +1,440 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
require 'eventmachine'
|
8
|
+
require 'log4r'
|
9
|
+
require 'sqlite3'
|
10
|
+
|
11
|
+
require 'rflow/configuration'
|
12
|
+
|
13
|
+
require 'rflow/component'
|
14
|
+
require 'rflow/message'
|
15
|
+
|
16
|
+
require 'rflow/components'
|
17
|
+
require 'rflow/connections'
|
18
|
+
|
19
|
+
|
20
|
+
class RFlow
|
21
|
+
include Log4r
|
22
|
+
|
23
|
+
class Error < StandardError; end
|
24
|
+
|
25
|
+
LOG_PATTERN_FORMAT = '%l [%d] %c (%p) - %M'
|
26
|
+
DATE_METHOD = 'xmlschema(6)'
|
27
|
+
LOG_PATTERN_FORMATTER = PatternFormatter.new :pattern => RFlow::LOG_PATTERN_FORMAT, :date_method => DATE_METHOD
|
28
|
+
|
29
|
+
# Might be slightly faster, but also not completely correct XML
|
30
|
+
#schema timestamps due to %z
|
31
|
+
#DATE_PATTERN_FORMAT = '%Y-%m-%dT%H:%M:%S.%9N %z'
|
32
|
+
#LOG_PATTERN_FORMATTER = PatternFormatter.new :pattern => RFlow::LOG_PATTERN_FORMAT, :date_pattern => DATE_PATTERN_FORMAT
|
33
|
+
|
34
|
+
class << self
|
35
|
+
attr_accessor :config_database_path
|
36
|
+
attr_accessor :logger
|
37
|
+
attr_accessor :configuration
|
38
|
+
attr_accessor :components
|
39
|
+
end
|
40
|
+
|
41
|
+
# def self.initialize_config_database(config_database_path, config_file_path=nil)
|
42
|
+
# # To handle relative paths in the config (all relative paths are
|
43
|
+
# # relative to the config database
|
44
|
+
# Dir.chdir File.dirname(config_database_path)
|
45
|
+
# Configuration.new(File.basename(config_database_path), config_file_path)
|
46
|
+
# end
|
47
|
+
|
48
|
+
def self.initialize_logger(log_file_path, log_level='INFO', include_stdout=nil)
|
49
|
+
rflow_logger = Logger.new((configuration['rflow.application_name'] rescue File.basename(log_file_path)))
|
50
|
+
rflow_logger.level = LNAMES.index log_level
|
51
|
+
# TODO: Remove this once all the logging puts in its own
|
52
|
+
# Class.Method names.
|
53
|
+
rflow_logger.trace = true
|
54
|
+
begin
|
55
|
+
rflow_logger.add FileOutputter.new('rflow.log_file', :filename => log_file_path, :formatter => LOG_PATTERN_FORMATTER)
|
56
|
+
rescue Exception => e
|
57
|
+
error_message = "Log file '#{File.expand_path log_file_path}' problem: #{e.message}\b#{e.backtrace.join("\n")}"
|
58
|
+
RFlow.logger.error error_message
|
59
|
+
raise ArgumentError, error_message
|
60
|
+
end
|
61
|
+
|
62
|
+
if include_stdout
|
63
|
+
rflow_logger.add StdoutOutputter.new('rflow_stdout', :formatter => RFlow::LOG_PATTERN_FORMATTER)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
RFlow.logger.info "Transitioning to running log file #{log_file_path} at level #{log_level}"
|
68
|
+
RFlow.logger = rflow_logger
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.reopen_log_file
|
72
|
+
# TODO: Make this less of a hack, although Log4r doesn't support
|
73
|
+
# it, so it might be permanent
|
74
|
+
log_file = Outputter['rflow.log_file'].instance_variable_get(:@out)
|
75
|
+
File.open(log_file.path, 'a') { |tmp_log_file| log_file.reopen(tmp_log_file) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.close_log_file
|
79
|
+
Outputter['rflow.log_file'].close
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.toggle_log_level
|
83
|
+
original_log_level = LNAMES[logger.level]
|
84
|
+
new_log_level = (original_log_level == 'DEBUG' ? configuration['rflow.log_level'] : 'DEBUG')
|
85
|
+
logger.warn "Changing log level from #{original_log_level} to #{new_log_level}"
|
86
|
+
logger.level = LNAMES.index new_log_level
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.trap_signals
|
90
|
+
# Gracefully shutdown on termination signals
|
91
|
+
['SIGTERM', 'SIGINT', 'SIGQUIT'].each do |signal|
|
92
|
+
Signal.trap signal do
|
93
|
+
logger.warn "Termination signal (#{signal}) received, shutting down"
|
94
|
+
shutdown
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Reload on HUP
|
99
|
+
['SIGHUP'].each do |signal|
|
100
|
+
Signal.trap signal do
|
101
|
+
logger.warn "Reload signal (#{signal}) received, reloading"
|
102
|
+
reload
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Ignore terminal signals
|
107
|
+
# TODO: Make sure this is valid for non-daemon (foreground) process
|
108
|
+
['SIGTSTP', 'SIGTTOU', 'SIGTTIN'].each do |signal|
|
109
|
+
Signal.trap signal do
|
110
|
+
logger.warn "Terminal signal (#{signal}) received, ignoring"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Reopen logs on USR1
|
115
|
+
['SIGUSR1'].each do |signal|
|
116
|
+
Signal.trap signal do
|
117
|
+
logger.warn "Reopen logs signal (#{signal}) received, reopening #{configuration['rflow.log_file_path']}"
|
118
|
+
reopen_log_file
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Toggle log level on USR2
|
123
|
+
['SIGUSR2'].each do |signal|
|
124
|
+
Signal.trap signal do
|
125
|
+
logger.warn "Toggle log level signal (#{signal}) received, toggling"
|
126
|
+
toggle_log_level
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# TODO: Manage SIGCHLD when spawning other processes
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
# returns a PID if a given path contains a non-stale PID file,
|
135
|
+
# nil otherwise.
|
136
|
+
def self.running_pid_file_path?(pid_file_path)
|
137
|
+
return nil unless File.exist? pid_file_path
|
138
|
+
running_pid? File.read(pid_file_path).to_i
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.running_pid?(pid)
|
142
|
+
return if pid <= 0
|
143
|
+
Process.kill(0, pid)
|
144
|
+
pid
|
145
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
|
149
|
+
# unlinks a PID file at given if it contains the current PID still
|
150
|
+
# potentially racy without locking the directory (which is
|
151
|
+
# non-portable and may interact badly with other programs), but the
|
152
|
+
# window for hitting the race condition is small
|
153
|
+
def self.remove_pid_file(pid_file_path)
|
154
|
+
(File.read(pid_file_path).to_i == $$ and File.unlink(pid_file_path)) rescue nil
|
155
|
+
logger.debug "Removed PID (#$$) file '#{File.expand_path pid_file_path}'"
|
156
|
+
end
|
157
|
+
|
158
|
+
# TODO: Handle multiple instances and existing PID file
|
159
|
+
def self.write_pid_file(pid_file_path)
|
160
|
+
pid = running_pid_file_path?(pid_file_path)
|
161
|
+
if pid && pid == $$
|
162
|
+
logger.warn "Already running (#{pid.to_s}), not writing PID to file '#{File.expand_path pid_file_path}'"
|
163
|
+
return pid_file_path
|
164
|
+
elsif pid
|
165
|
+
error_message = "Already running (#{pid.to_s}), possibly stale PID file '#{File.expand_path pid_file_path}'"
|
166
|
+
logger.error error_message
|
167
|
+
raise ArgumentError, error_message
|
168
|
+
elsif File.exist? pid_file_path
|
169
|
+
logger.warn "Found stale PID file '#{File.expand_path pid_file_path}', removing"
|
170
|
+
remove_pid_file pid_file_path
|
171
|
+
end
|
172
|
+
|
173
|
+
logger.debug "Writing PID (#$$) file '#{File.expand_path pid_file_path}'"
|
174
|
+
pid_fp = begin
|
175
|
+
tmp_pid_file_path = File.join(File.dirname(pid_file_path), ".#{File.basename(pid_file_path)}")
|
176
|
+
File.open(tmp_pid_file_path, File::RDWR|File::CREAT|File::EXCL, 0644)
|
177
|
+
rescue Errno::EEXIST
|
178
|
+
retry
|
179
|
+
end
|
180
|
+
pid_fp.syswrite("#$$\n")
|
181
|
+
File.rename(pid_fp.path, pid_file_path)
|
182
|
+
pid_fp.close
|
183
|
+
|
184
|
+
pid_file_path
|
185
|
+
end
|
186
|
+
|
187
|
+
# TODO: Refactor this to be cleaner
|
188
|
+
def self.daemonize!(application_name, pid_file_path)
|
189
|
+
logger.info "#{application_name} daemonizing"
|
190
|
+
|
191
|
+
# TODO: Drop privileges
|
192
|
+
|
193
|
+
# Daemonize, but don't chdir or close outputs
|
194
|
+
Process.daemon(true, true)
|
195
|
+
|
196
|
+
# Set the process name
|
197
|
+
$0 = application_name if application_name
|
198
|
+
|
199
|
+
# Write the PID file
|
200
|
+
write_pid_file pid_file_path
|
201
|
+
|
202
|
+
# Close standard IO
|
203
|
+
$stdout.sync = $stderr.sync = true
|
204
|
+
$stdin.binmode; $stdout.binmode; $stderr.binmode
|
205
|
+
begin; $stdin.reopen "/dev/null"; rescue ::Exception; end
|
206
|
+
begin; $stdout.reopen "/dev/null"; rescue ::Exception; end
|
207
|
+
begin; $stderr.reopen "/dev/null"; rescue ::Exception; end
|
208
|
+
|
209
|
+
$$
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
# Iterate through each component config in the configuration
|
214
|
+
# database and attempt to instantiate each one, storing the
|
215
|
+
# resulting instantiated components in the 'components' class
|
216
|
+
# instance attribute. This assumes that the specification of a
|
217
|
+
# component is a fully qualified Ruby class that has already been
|
218
|
+
# loaded. It will first attempt to find subclasses of
|
219
|
+
# RFlow::Component (in the available_components hash) and then
|
220
|
+
# attempt to constantize the specification into a different class. Future releases will
|
221
|
+
# support external (i.e. non-managed components), but the current
|
222
|
+
# stuff only supports Ruby classes
|
223
|
+
def self.instantiate_components!
|
224
|
+
logger.info "Instantiating components"
|
225
|
+
self.components = Hash.new
|
226
|
+
configuration.components.each do |component_config|
|
227
|
+
if component_config.managed?
|
228
|
+
logger.debug "Instantiating component '#{component_config.name}' as '#{component_config.specification}' (#{component_config.uuid})"
|
229
|
+
begin
|
230
|
+
logger.debug configuration.available_components.inspect
|
231
|
+
instantiated_component = if configuration.available_components.include? component_config.specification
|
232
|
+
logger.debug "Component found in configuration.available_components['#{component_config.specification}']"
|
233
|
+
configuration.available_components[component_config.specification].new(component_config.uuid, component_config.name)
|
234
|
+
else
|
235
|
+
logger.debug "Component not found in configuration.available_components, constantizing component '#{component_config.specification}'"
|
236
|
+
component_config.specification.constantize.new(component_config.uuid, component_config.name)
|
237
|
+
end
|
238
|
+
|
239
|
+
components[component_config.uuid] = instantiated_component
|
240
|
+
|
241
|
+
rescue NameError => e
|
242
|
+
error_message = "Could not instantiate component '#{component_config.name}' as '#{component_config.specification}' (#{component_config.uuid}): the class '#{component_config.specification}' was not found"
|
243
|
+
logger.error error_message
|
244
|
+
raise RuntimeError, error_message
|
245
|
+
rescue Exception => e
|
246
|
+
error_message = "Could not instantiate component '#{component_config.name}' as '#{component_config.specification}' (#{component_config.uuid}): #{e.class} #{e.message}"
|
247
|
+
logger.error error_message
|
248
|
+
raise RuntimeError, error_message
|
249
|
+
end
|
250
|
+
else
|
251
|
+
error_message = "Non-managed components not yet implemented for component '#{component_config.name}' as '#{component_config.specification}' (#{component_config.uuid})"
|
252
|
+
logger.error error_message
|
253
|
+
raise NotImplementedError, error_message
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
# Iterate through the instantiated components and send each
|
260
|
+
# component its soon-to-be connected port names and UUIDs
|
261
|
+
def self.configure_component_ports!
|
262
|
+
# Send the port configuration to each component
|
263
|
+
logger.info "Configuring component ports and assigning UUIDs to port names"
|
264
|
+
components.each do |component_instance_uuid, component|
|
265
|
+
RFlow.logger.debug "Configuring ports for component '#{component.name}' (#{component.instance_uuid})"
|
266
|
+
component_config = configuration.component(component.instance_uuid)
|
267
|
+
component_config.input_ports.each do |input_port_config|
|
268
|
+
RFlow.logger.debug "Configuring component '#{component.name}' (#{component.instance_uuid}) with input port '#{input_port_config.name}' (#{input_port_config.uuid})"
|
269
|
+
component.configure_input_port!(input_port_config.name, input_port_config.uuid)
|
270
|
+
end
|
271
|
+
component_config.output_ports.each do |output_port_config|
|
272
|
+
RFlow.logger.debug "Configuring component '#{component.name}' (#{component.instance_uuid}) with output port '#{output_port_config.name}' (#{output_port_config.uuid})"
|
273
|
+
component.configure_output_port!(output_port_config.name, output_port_config.uuid)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# Iterate through the instantiated components and send each
|
280
|
+
# component the information necessary to configure a connection on a
|
281
|
+
# specific port, specifically the port UUID, port key, type of connection, uuid
|
282
|
+
# of connection, and a configuration specific to the connection type
|
283
|
+
def self.configure_component_connections!
|
284
|
+
logger.info "Configuring component port connections"
|
285
|
+
components.each do |component_instance_uuid, component|
|
286
|
+
component_config = configuration.component(component.instance_uuid)
|
287
|
+
|
288
|
+
logger.debug "Configuring input connections for component '#{component.name}' (#{component.instance_uuid})"
|
289
|
+
component_config.input_ports.each do |input_port_config|
|
290
|
+
input_port_config.input_connections.each do |input_connection_config|
|
291
|
+
logger.debug "Configuring input port '#{input_port_config.name}' (#{input_port_config.uuid}) key '#{input_connection_config.input_port_key}' with #{input_connection_config.type.to_s} connection '#{input_connection_config.name}' (#{input_connection_config.uuid})"
|
292
|
+
component.configure_connection!(input_port_config.uuid, input_connection_config.input_port_key,
|
293
|
+
input_connection_config.type, input_connection_config.uuid, input_connection_config.name, input_connection_config.options)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
logger.debug "Configuring output connections for '#{component.name}' (#{component.instance_uuid})"
|
298
|
+
component_config.output_ports.each do |output_port_config|
|
299
|
+
output_port_config.output_connections.each do |output_connection_config|
|
300
|
+
logger.debug "Configuring output port '#{output_port_config.name}' (#{output_port_config.uuid}) key '#{output_connection_config.output_port_key}' with #{output_connection_config.type.to_s} connection '#{output_connection_config.name}' (#{output_connection_config.uuid})"
|
301
|
+
component.configure_connection!(output_port_config.uuid, output_connection_config.output_port_key,
|
302
|
+
output_connection_config.type, output_connection_config.uuid, output_connection_config.name, output_connection_config.options)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
# Send the component-specific configuration to the component
|
310
|
+
def self.configure_components!
|
311
|
+
logger.info "Configuring components with component-specific configurations"
|
312
|
+
components.each do |component_uuid, component|
|
313
|
+
component_config = configuration.component(component.instance_uuid)
|
314
|
+
logger.debug "Configuring component '#{component.name}' (#{component.instance_uuid})"
|
315
|
+
component.configure!(component_config.options)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Send a command to each component to tell them to connect their
|
320
|
+
# ports via their connections
|
321
|
+
def self.connect_components!
|
322
|
+
logger.info "Connecting components"
|
323
|
+
components.each do |component_uuid, component|
|
324
|
+
logger.debug "Connecting component '#{component.name}' (#{component.instance_uuid})"
|
325
|
+
component.connect!
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Start each component running
|
330
|
+
def self.run_components!
|
331
|
+
logger.info "Running components"
|
332
|
+
components.each do |component_uuid, component|
|
333
|
+
logger.debug "Running component '#{component.name}' (#{component.instance_uuid})"
|
334
|
+
component.run!
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def self.run(config_database_path, daemonize=nil)
|
339
|
+
self.configuration = Configuration.new(config_database_path)
|
340
|
+
|
341
|
+
# First change to the config database directory, which might hold
|
342
|
+
# relative paths for the other files/directories, such as the
|
343
|
+
# application_directory_path
|
344
|
+
Dir.chdir File.dirname(config_database_path)
|
345
|
+
|
346
|
+
# Bail unless you have some of the basic information. TODO:
|
347
|
+
# rethink this when things get more dynamic
|
348
|
+
unless configuration['rflow.application_directory_path']
|
349
|
+
error_message = "Empty configuration database! Use a view/controller (such as the RubyDSL) to create a configuration"
|
350
|
+
RFlow.logger.error "Empty configuration database! Use a view/controller (such as the RubyDSL) to create a configuration"
|
351
|
+
raise ArgumentError, error_message
|
352
|
+
end
|
353
|
+
|
354
|
+
Dir.chdir configuration['rflow.application_directory_path']
|
355
|
+
|
356
|
+
initialize_logger(configuration['rflow.log_file_path'], configuration['rflow.log_level'], !daemonize)
|
357
|
+
|
358
|
+
application_name = configuration['rflow.application_name']
|
359
|
+
logger.info "#{application_name} starting"
|
360
|
+
|
361
|
+
logger.info "#{application_name} configured, starting flow"
|
362
|
+
logger.debug "Available Components: #{RFlow::Configuration.available_components.inspect}"
|
363
|
+
logger.debug "Available Data Types: #{RFlow::Configuration.available_data_types.inspect}"
|
364
|
+
logger.debug "Available Data Extensions: #{RFlow::Configuration.available_data_extensions.inspect}"
|
365
|
+
|
366
|
+
# TODO: Start up a FlowManager component and connect it to the
|
367
|
+
# management interface on all the components.
|
368
|
+
|
369
|
+
instantiate_components!
|
370
|
+
configure_component_ports!
|
371
|
+
configure_component_connections!
|
372
|
+
configure_components!
|
373
|
+
|
374
|
+
# At this point, each component should have their entire
|
375
|
+
# configuration for the component-specific stuff and all the
|
376
|
+
# connections and be ready to be connected to the others and start
|
377
|
+
# running
|
378
|
+
|
379
|
+
|
380
|
+
# Daemonize
|
381
|
+
trap_signals
|
382
|
+
daemonize!(application_name, configuration['rflow.pid_file_path']) if daemonize
|
383
|
+
write_pid_file configuration['rflow.pid_file_path']
|
384
|
+
|
385
|
+
# Start the event loop and startup each component
|
386
|
+
EM.run do
|
387
|
+
connect_components!
|
388
|
+
|
389
|
+
components.each do |component_uuid, component|
|
390
|
+
RFlow.logger.debug component.to_s
|
391
|
+
end
|
392
|
+
|
393
|
+
run_components!
|
394
|
+
|
395
|
+
# Sit back and relax because everything is running
|
396
|
+
end
|
397
|
+
|
398
|
+
# Should never get here
|
399
|
+
shutdown
|
400
|
+
|
401
|
+
# TODO: Look into Parallel::ForkManager
|
402
|
+
rescue SystemExit => e
|
403
|
+
# Do nothing, just prevent a normal exit from causing an unsightly
|
404
|
+
# error in the logs
|
405
|
+
rescue Exception => e
|
406
|
+
logger.fatal "Exception caught: #{e.class} - #{e.message}\n#{e.backtrace.join "\n"}"
|
407
|
+
exit 1
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.shutdown
|
411
|
+
logger.info "#{configuration['rflow.application_name']} shutting down"
|
412
|
+
|
413
|
+
logger.debug "Shutting down components"
|
414
|
+
components.each do |component_instance_uuid, component|
|
415
|
+
logger.debug "Shutting down component '#{component.name}' (#{component.instance_uuid})"
|
416
|
+
component.shutdown!
|
417
|
+
end
|
418
|
+
|
419
|
+
# TODO: Ensure that all the components have shut down before
|
420
|
+
# cleaning up
|
421
|
+
|
422
|
+
logger.debug "Cleaning up components"
|
423
|
+
components.each do |component_instance_uuid, component|
|
424
|
+
logger.debug "Cleaning up component '#{component.name}' (#{component.instance_uuid})"
|
425
|
+
component.cleanup!
|
426
|
+
end
|
427
|
+
|
428
|
+
remove_pid_file configuration['rflow.pid_file_path']
|
429
|
+
logger.info "#{configuration['rflow.application_name']} exiting"
|
430
|
+
exit 0
|
431
|
+
end
|
432
|
+
|
433
|
+
def self.reload
|
434
|
+
# TODO: Actually do a real reload
|
435
|
+
logger.info "#{configuration['rflow.application_name']} reloading"
|
436
|
+
reload_log_file
|
437
|
+
logger.info "#{configuration['rflow.application_name']} reloaded"
|
438
|
+
end
|
439
|
+
|
440
|
+
end # class RFlow
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'rflow/message'
|
2
|
+
require 'rflow/component/port'
|
3
|
+
|
4
|
+
class RFlow
|
5
|
+
class Component
|
6
|
+
# Keep track of available component subclasses
|
7
|
+
def self.inherited(subclass)
|
8
|
+
RFlow::Configuration.add_available_component(subclass)
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# The Component class methods used in the creation of a component
|
13
|
+
class << self
|
14
|
+
def defined_input_ports
|
15
|
+
@defined_input_ports ||= Hash.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def defined_output_ports
|
19
|
+
@defined_output_ports ||= Hash.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# TODO: Update the class vs instance stuffs here to be correct
|
23
|
+
# Port defintions only have names
|
24
|
+
|
25
|
+
# TODO: consider class-based UUIDs to identify component types
|
26
|
+
|
27
|
+
# Define an input port with a given name
|
28
|
+
def input_port(port_name)
|
29
|
+
define_port(defined_input_ports, port_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Define an output port with a given name
|
33
|
+
def output_port(port_name)
|
34
|
+
define_port(defined_output_ports, port_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Helper method to keep things DRY for standard component
|
38
|
+
# definition methods input_port and output_port
|
39
|
+
def define_port(collection, port_name)
|
40
|
+
collection[port_name.to_s] = true
|
41
|
+
|
42
|
+
# Create the port accessor method based on the port name
|
43
|
+
define_method port_name.to_s.to_sym do
|
44
|
+
port = ports.by_name[port_name.to_s]
|
45
|
+
return port if port
|
46
|
+
|
47
|
+
# If the port was not connected, return a port-like object
|
48
|
+
# that can respond/log but doesn't send any data. Note,
|
49
|
+
# it won't be available in the 'by_uuid' collection, as it
|
50
|
+
# doesn't have a configured instance_uuid
|
51
|
+
RFlow.logger.debug "'#{self.name}##{port_name}' not connected, creating a disconnected port"
|
52
|
+
disconnected_port = DisconnectedPort.new(port_name, 0)
|
53
|
+
ports << disconnected_port
|
54
|
+
disconnected_port
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :instance_uuid
|
60
|
+
attr_reader :name
|
61
|
+
attr_reader :configuration
|
62
|
+
attr_reader :ports
|
63
|
+
|
64
|
+
def initialize(uuid, name=nil, configuration=nil)
|
65
|
+
@instance_uuid = uuid
|
66
|
+
@name = name
|
67
|
+
@ports = PortCollection.new
|
68
|
+
@configuration = configuration
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Returns a list of connected input ports. Each port will have
|
73
|
+
# one or more keys associated with a particular connection.
|
74
|
+
def input_ports
|
75
|
+
ports.by_type["RFlow::Component::InputPort"]
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Returns a list of connected output ports. Each port will have
|
80
|
+
# one or more keys associated with the particular connection.
|
81
|
+
def output_ports
|
82
|
+
ports.by_type["RFlow::Component::OutputPort"]
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Returns a list of disconnected output ports.
|
87
|
+
def disconnected_ports
|
88
|
+
ports.by_type["RFlow::Component::DisconnectedPort"]
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
# TODO: DRY up the following two methods by factoring out into a meta-method
|
93
|
+
|
94
|
+
def configure_input_port!(port_name, port_instance_uuid, port_options={})
|
95
|
+
unless self.class.defined_input_ports.include? port_name
|
96
|
+
raise ArgumentError, "Input port '#{port_name}' not defined on component '#{self.class}'"
|
97
|
+
end
|
98
|
+
ports << InputPort.new(port_name, port_instance_uuid, port_options)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def configure_output_port!(port_name, port_instance_uuid, port_options={})
|
103
|
+
unless self.class.defined_output_ports.include? port_name
|
104
|
+
raise ArgumentError, "Output port '#{port_name}' not defined on component '#{self.class}'"
|
105
|
+
end
|
106
|
+
ports << OutputPort.new(port_name, port_instance_uuid, port_options)
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
# Only supports Ruby types.
|
111
|
+
# TODO: figure out how to dynamically load the built-in
|
112
|
+
# connections, or require them at the top of the file and not rely
|
113
|
+
# on rflow.rb requiring 'rflow/connections'
|
114
|
+
def configure_connection!(port_instance_uuid, port_key, connection_type, connection_uuid, connection_name=nil, connection_options={})
|
115
|
+
case connection_type
|
116
|
+
when 'RFlow::Configuration::ZMQConnection'
|
117
|
+
connection = RFlow::Connections::ZMQConnection.new(connection_uuid, connection_name, connection_options)
|
118
|
+
else
|
119
|
+
raise ArgumentError, "Only ZMQConnections currently supported"
|
120
|
+
end
|
121
|
+
|
122
|
+
ports.by_uuid[port_instance_uuid.to_s].add_connection(port_key, connection)
|
123
|
+
connection
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# Tell the component to establish it's ports' connections, i.e. make
|
128
|
+
# the connection. Uses the underlying connection object. Also
|
129
|
+
# establishes the callbacks for each of the input ports
|
130
|
+
def connect!
|
131
|
+
input_ports.each do |input_port|
|
132
|
+
input_port.connect!
|
133
|
+
|
134
|
+
# Create the callbacks for recieving messages as a proc
|
135
|
+
input_port.keys.each do |input_port_key|
|
136
|
+
keyed_connections = input_port[input_port_key]
|
137
|
+
keyed_connections.each do |connection|
|
138
|
+
connection.recv_callback = Proc.new do |message|
|
139
|
+
process_message(input_port, input_port_key, connection, message)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
output_ports.each do |output_port|
|
146
|
+
output_port.connect!
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
def to_s
|
152
|
+
string = "Component '#{name}' (#{instance_uuid})\n"
|
153
|
+
ports.each do |port|
|
154
|
+
port.keys.each do |port_key|
|
155
|
+
port[port_key].each do |connection|
|
156
|
+
string << "\t#{port.class.to_s} '#{port.name}' (#{port.instance_uuid}) key '#{port_key}' connection '#{connection.name}' (#{connection.instance_uuid})\n"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
string
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
# Method that should be overridden by a subclass to provide for
|
165
|
+
# component-specific configuration. The subclass should use the
|
166
|
+
# self.configuration attribute (@configuration) to store its
|
167
|
+
# particular configuration. The incoming deserialized_configuration
|
168
|
+
# parameter is from the RFlow configuration database and is (most
|
169
|
+
# likely) a hash. Don't assume that the keys are symbols
|
170
|
+
def configure!(deserialized_configuration); end
|
171
|
+
|
172
|
+
# Main component running method. Subclasses should implement if
|
173
|
+
# they want to set up any EventMachine stuffs (servers, clients,
|
174
|
+
# etc)
|
175
|
+
def run!; end
|
176
|
+
|
177
|
+
# Method called when a message is received on an input port.
|
178
|
+
# Subclasses should implement if they want to receive messages
|
179
|
+
def process_message(input_port, input_port_key, connection, message); end
|
180
|
+
|
181
|
+
# Method called when RFlow is shutting down. Subclasses should
|
182
|
+
# implment to terminate any servers/clients (or let them finish)
|
183
|
+
# and stop sending new data through the flow
|
184
|
+
def shutdown!; end
|
185
|
+
|
186
|
+
# Method called after all components have been shutdown! and just
|
187
|
+
# before the global RFlow exit. Sublcasses should implement to
|
188
|
+
# cleanup any leftover state, e.g. flush file handles, etc
|
189
|
+
def cleanup!; end
|
190
|
+
|
191
|
+
end # class Component
|
192
|
+
end # class RFlow
|