rflow 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +5 -0
  5. data/NOTES +187 -0
  6. data/README +0 -0
  7. data/Rakefile +16 -0
  8. data/bin/rflow +215 -0
  9. data/example/basic_config.rb +49 -0
  10. data/example/basic_extensions.rb +142 -0
  11. data/example/http_config.rb +21 -0
  12. data/example/http_extensions.rb +262 -0
  13. data/lib/rflow.rb +440 -0
  14. data/lib/rflow/component.rb +192 -0
  15. data/lib/rflow/component/port.rb +150 -0
  16. data/lib/rflow/components.rb +10 -0
  17. data/lib/rflow/components/raw.rb +26 -0
  18. data/lib/rflow/components/raw/extensions.rb +18 -0
  19. data/lib/rflow/configuration.rb +290 -0
  20. data/lib/rflow/configuration/component.rb +27 -0
  21. data/lib/rflow/configuration/connection.rb +98 -0
  22. data/lib/rflow/configuration/migrations/20010101000001_create_settings.rb +14 -0
  23. data/lib/rflow/configuration/migrations/20010101000002_create_components.rb +19 -0
  24. data/lib/rflow/configuration/migrations/20010101000003_create_ports.rb +24 -0
  25. data/lib/rflow/configuration/migrations/20010101000004_create_connections.rb +27 -0
  26. data/lib/rflow/configuration/port.rb +30 -0
  27. data/lib/rflow/configuration/ruby_dsl.rb +183 -0
  28. data/lib/rflow/configuration/setting.rb +67 -0
  29. data/lib/rflow/configuration/uuid_keyed.rb +18 -0
  30. data/lib/rflow/connection.rb +59 -0
  31. data/lib/rflow/connections.rb +2 -0
  32. data/lib/rflow/connections/zmq_connection.rb +101 -0
  33. data/lib/rflow/message.rb +191 -0
  34. data/lib/rflow/port.rb +4 -0
  35. data/lib/rflow/util.rb +19 -0
  36. data/lib/rflow/version.rb +3 -0
  37. data/rflow.gemspec +42 -0
  38. data/schema/message.avsc +36 -0
  39. data/schema/raw.avsc +9 -0
  40. data/spec/fixtures/config_ints.rb +61 -0
  41. data/spec/fixtures/extensions_ints.rb +141 -0
  42. data/spec/rflow_configuration_spec.rb +73 -0
  43. data/spec/rflow_message_data_raw.rb +26 -0
  44. data/spec/rflow_message_data_spec.rb +60 -0
  45. data/spec/rflow_message_spec.rb +182 -0
  46. data/spec/rflow_spec.rb +100 -0
  47. data/spec/schema_spec.rb +28 -0
  48. data/spec/spec_helper.rb +37 -0
  49. data/temp.rb +295 -0
  50. 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