rflow 0.0.5
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.
- 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
|