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