sonar_connector 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +18 -0
  3. data/Rakefile +41 -0
  4. data/VERSION +1 -0
  5. data/bin/sonar-connector +69 -0
  6. data/config/config.example.json +82 -0
  7. data/lib/sonar_connector.rb +40 -0
  8. data/lib/sonar_connector/commands/command.rb +21 -0
  9. data/lib/sonar_connector/commands/commit_seppuku_command.rb +15 -0
  10. data/lib/sonar_connector/commands/increment_status_value_command.rb +14 -0
  11. data/lib/sonar_connector/commands/send_admin_email_command.rb +12 -0
  12. data/lib/sonar_connector/commands/update_disk_usage_command.rb +13 -0
  13. data/lib/sonar_connector/commands/update_status_command.rb +16 -0
  14. data/lib/sonar_connector/config.rb +166 -0
  15. data/lib/sonar_connector/connectors/base.rb +243 -0
  16. data/lib/sonar_connector/connectors/dummy_connector.rb +17 -0
  17. data/lib/sonar_connector/connectors/seppuku_connector.rb +26 -0
  18. data/lib/sonar_connector/consumer.rb +94 -0
  19. data/lib/sonar_connector/controller.rb +164 -0
  20. data/lib/sonar_connector/emailer.rb +16 -0
  21. data/lib/sonar_connector/rspec/spec_helper.rb +61 -0
  22. data/lib/sonar_connector/status.rb +43 -0
  23. data/lib/sonar_connector/utils.rb +39 -0
  24. data/script/console +10 -0
  25. data/spec/sonar_connector/commands/command_spec.rb +34 -0
  26. data/spec/sonar_connector/commands/commit_seppuku_command_spec.rb +25 -0
  27. data/spec/sonar_connector/commands/increment_status_value_command_spec.rb +25 -0
  28. data/spec/sonar_connector/commands/send_admin_email_command_spec.rb +14 -0
  29. data/spec/sonar_connector/commands/update_disk_usage_command_spec.rb +21 -0
  30. data/spec/sonar_connector/commands/update_status_command_spec.rb +24 -0
  31. data/spec/sonar_connector/config_spec.rb +93 -0
  32. data/spec/sonar_connector/connectors/base_spec.rb +207 -0
  33. data/spec/sonar_connector/connectors/dummy_connector_spec.rb +22 -0
  34. data/spec/sonar_connector/connectors/seppuku_connector_spec.rb +37 -0
  35. data/spec/sonar_connector/consumer_spec.rb +116 -0
  36. data/spec/sonar_connector/controller_spec.rb +46 -0
  37. data/spec/sonar_connector/emailer_spec.rb +36 -0
  38. data/spec/sonar_connector/status_spec.rb +78 -0
  39. data/spec/sonar_connector/utils_spec.rb +62 -0
  40. data/spec/spec.opts +2 -0
  41. data/spec/spec_helper.rb +6 -0
  42. metadata +235 -0
@@ -0,0 +1,243 @@
1
+ module Sonar
2
+ module Connector
3
+ class Base
4
+
5
+ # every connector has a unique name
6
+ attr_reader :name
7
+
8
+ # this connector
9
+ attr_reader :connector
10
+
11
+ # Connector-specific config hash
12
+ attr_reader :raw_config
13
+
14
+ # each connector instance has a working dir for its state and files
15
+ attr_reader :connector_dir
16
+
17
+ # logger instance
18
+ attr_reader :log
19
+
20
+ # state hash that is serialized and persisted to disk every cycle of the run loop
21
+ attr_reader :state
22
+
23
+ # repeat delay which is waited out on each cycle of the run loop
24
+ attr_reader :repeat_delay
25
+
26
+ # central command queue for sending messages back to the controller
27
+ attr_reader :queue
28
+
29
+ # run loop flag
30
+ attr_reader :run
31
+
32
+ # filestore for whole connector
33
+ attr_reader :connector_filestore
34
+
35
+ # filestore for an action run
36
+ attr_reader :filestore
37
+
38
+ # Array of associated connectors that provide source data via the file system
39
+ attr_reader :source_connectors
40
+
41
+ def initialize(connector_config, base_config)
42
+ @base_config = base_config
43
+ @raw_config = connector_config
44
+
45
+ @name = connector_config["name"]
46
+ @connector = self
47
+
48
+ # Create STDOUT logger and inherit the logger settings from the base controller config
49
+ @log_file = File.join base_config.log_dir, "connector_#{@name}.log"
50
+ @log = Sonar::Connector::Utils.stdout_logger base_config
51
+
52
+ # every connector instance must set the repeat delay
53
+ raise InvalidConfig.new("Connector '#{@name}': repeat_delay is missing or blank") if connector_config["repeat_delay"].blank?
54
+ @repeat_delay = connector_config["repeat_delay"].to_i
55
+ raise InvalidConfig.new("Connector '#{@name}': repeat_delay must be >= 1 second") if @repeat_delay < 1
56
+
57
+ @connector_dir = File.join(base_config.connectors_dir, @name)
58
+ FileUtils.mkdir_p(@connector_dir)
59
+ @state_file = File.join(@connector_dir, "state.yml")
60
+
61
+ # empty state hash which will get written to by parse, and then potentially over-written by load_state
62
+ @state = {}
63
+
64
+ @connector_filestore = Sonar::Connector::FileStore.new(@connector_dir,
65
+ "#{@name}_filestore",
66
+ [:working, :error, :complete, :actions],
67
+ :logger=>@log)
68
+
69
+ parse connector_config
70
+ load_state
71
+
72
+ @run = true
73
+ end
74
+
75
+ def prepare(queue)
76
+ @queue = queue
77
+ switch_to_log_file
78
+
79
+ cleanup_old_action_filestores # in case we were interrupted mid-action
80
+ cleanup # before we begin
81
+ end
82
+
83
+ # Logging defaults to use STDOUT. After initialization we need to switch the
84
+ # logger to use an output file.
85
+ def switch_to_log_file
86
+ @log = Sonar::Connector::Utils.disk_logger(log_file, base_config)
87
+ @connector_filestore.logger = @log if @connector_filestore
88
+ end
89
+
90
+ # Load the state hash from YAML file
91
+ def load_state
92
+ @state.merge! read_state
93
+ end
94
+
95
+ # Read state file
96
+ def read_state
97
+ s = {}
98
+ if File.exist?(state_file)
99
+ ds = YAML.load_file state_file
100
+ raise "State file did not contain a serialised hash." unless ds.is_a?(Hash)
101
+ s = ds # only return the parsed value if it is actually a hash
102
+ end
103
+ rescue Exception => e
104
+ log.error "Error loading #{state_file} so it was ignored. Original error: #{e.message}\n" + e.backtrace.join("\n")
105
+ ensure
106
+ return s
107
+ end
108
+
109
+ # Save the state hash to a YAML file
110
+ def save_state
111
+ make_dir
112
+ File.open(state_file, "w"){|f| f << state.to_yaml }
113
+ end
114
+
115
+ # Cleanup routine after connector shutdown
116
+ def cleanup
117
+ end
118
+
119
+ # the main run loop that every connector executes indefinitely
120
+ # until Thread.raise is called on this instance.
121
+ def start
122
+ begin
123
+ run_loop
124
+
125
+ @run = false
126
+ cleanup
127
+ true
128
+ rescue Exception=>e
129
+ log.error([e.class.to_s, e.message, *e.backtrace].join("\n"))
130
+ end
131
+ end
132
+
133
+ def run_loop
134
+ while run
135
+ begin
136
+ run_once
137
+ sleep_for repeat_delay
138
+ rescue ThreadTerminator
139
+ break
140
+ rescue Exception => e
141
+ log.error ["Connector '#{name} raised an unhandled exception:",
142
+ e.class.to_s,
143
+ e.message,
144
+ *e.backtrace].join("\n")
145
+ log.info "Connector blew up with an exception - waiting 5 seconds before retrying."
146
+ queue << Sonar::Connector::UpdateStatusCommand.new(connector, 'last_action', Sonar::Connector::ACTION_FAILED)
147
+ sleep_for 5
148
+ end
149
+ end
150
+ end
151
+
152
+ def run_once
153
+ log.info "beginning action"
154
+
155
+ with_action_filestore do
156
+ action
157
+
158
+ save_state
159
+ log.info "finished action,saved state"
160
+ log.info "working: #{filestore.count(:working)}, error: #{filestore.count(:error)}, complete: #{filestore.count(:complete)}"
161
+
162
+ queue << Sonar::Connector::UpdateStatusCommand.new(connector, 'last_action', Sonar::Connector::ACTION_OK)
163
+ queue << Sonar::Connector::UpdateDiskUsageCommand.new(connector)
164
+ queue << Sonar::Connector::UpdateStatusCommand.new(connector, 'working_count', filestore.count(:working))
165
+ queue << Sonar::Connector::UpdateStatusCommand.new(connector, 'error_count', filestore.count(:error))
166
+ queue << Sonar::Connector::UpdateStatusCommand.new(connector, 'complete_count', filestore.count(:complete))
167
+ end
168
+ end
169
+
170
+ # Connector subclasses can overload the parse method.
171
+ def parse(config)
172
+ log.warn "Method #parse called on connector base class. Connector #{name} should define #parse method."
173
+ end
174
+
175
+ def to_s
176
+ "#{self.class} '#{name}'"
177
+ end
178
+ alias :inspect :to_s
179
+
180
+ private
181
+
182
+ attr_reader :state_file, :base_config, :log_file
183
+ attr_writer :source_connectors
184
+
185
+ def sleep_for(seconds=0)
186
+ sleep seconds
187
+ end
188
+
189
+ def make_dir
190
+ FileUtils.mkdir_p(@connector_dir) unless File.directory?(@connector_dir)
191
+ end
192
+
193
+ def with_action_filestore
194
+ fs = create_action_filestore
195
+ begin
196
+ initialize_action_filestore(fs)
197
+ @filestore = fs
198
+ yield
199
+ ensure
200
+ @filestore = nil
201
+ finalize_action_filestore(fs)
202
+ end
203
+ end
204
+
205
+ def create_action_filestore
206
+ now = Time.new
207
+ fs_name = now.strftime("action_%Y%m%d_%H%M%S_") + UUIDTools::UUID.timestamp_create.to_s.gsub('-','_')
208
+ action_fs_root = connector_filestore.area_path(:actions)
209
+ fs=FileStore.new(action_fs_root, fs_name, [:working, :error, :complete], :logger=>@log)
210
+ log.info("action_filestore path: #{fs.filestore_path}")
211
+ fs
212
+ end
213
+
214
+ def initialize_action_filestore(fs)
215
+ # grab any unfinished work for this action
216
+ connector_filestore.flip(:working, fs, :working)
217
+ fs.scrub!(:working)
218
+ end
219
+
220
+ def finalize_action_filestore(fs)
221
+ [:complete, :error].each do |area|
222
+ fs.scrub!(area)
223
+ fs.flip(area, connector_filestore, area, false) # flip with uuid folders
224
+ end
225
+ fs.scrub!(:working)
226
+ fs.flip(:working, connector_filestore, :working) # flip without folders
227
+ fs.destroy!
228
+ end
229
+
230
+ def cleanup_old_action_filestores
231
+ actionfs_root = connector_filestore.area_path(:actions)
232
+
233
+ Dir.foreach(actionfs_root) do |fs_name|
234
+ fs_path = File.join(actionfs_root, fs_name)
235
+ if File.directory?(fs_path) && FileStore.valid_filestore_name?(fs_name)
236
+ fs = FileStore.new(actionfs_root, fs_name, [:working, :error, :complete], :logger=>@log)
237
+ finalize_action_filestore(fs)
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,17 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ ##
5
+ # Dummy connector type. Does nothing except report back to the controller.
6
+ class DummyConnector < Sonar::Connector::Base
7
+
8
+ def parse(config)
9
+ end
10
+
11
+ def action
12
+ log.debug "#{name} mumbled incoherently to itself."
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ # Suicide connector.Submits a command to cause the death of the entire connector framework.
5
+ # The connector will then get restarted by god or by the Windows service wrapper.
6
+ class SeppukuConnector < Sonar::Connector::Base
7
+
8
+ attr_accessor :run_count
9
+
10
+ def parse(config)
11
+ @run_count = 0
12
+ end
13
+
14
+ def action
15
+
16
+ if @run_count > 0
17
+ log.info "切腹! #{name} committing honourable suicide and terminating the connector service."
18
+ queue << Sonar::Connector::CommitSeppukuCommand.new
19
+ end
20
+
21
+ @run_count += 1
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,94 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ # Command execution context, whereby commands can be injected with consumer context
5
+ # variables for the logger and status objects.
6
+ class ExecutionContext
7
+ attr_reader :log
8
+ attr_reader :status
9
+ attr_reader :controller
10
+
11
+ def initialize(params)
12
+ @log = params[:log]
13
+ @status = params[:status]
14
+
15
+ # pass the controller all the way from the consumer initialisation into the command execution context,
16
+ # so that we can call privileged methods such as shutdown.
17
+ @controller = params[:controller]
18
+ end
19
+ end
20
+
21
+
22
+ ##
23
+ # Listens to thread message queue and processes messages.
24
+ class Consumer
25
+
26
+ attr_reader :base_config
27
+ attr_reader :queue
28
+ attr_reader :controller
29
+ attr_reader :status
30
+ attr_reader :log
31
+ attr_reader :run
32
+
33
+ def initialize(controller, base_config)
34
+ @controller = controller
35
+ @base_config = base_config
36
+
37
+ # Consumer holds the status object because changes
38
+ # to status should be centrally moderated.
39
+ @status = Sonar::Connector::Status.new(@base_config)
40
+
41
+ # Creat logger and inherit the logger settings from the base controller config
42
+ @log_file = File.join(base_config.log_dir, "consumer.log")
43
+ @log = Sonar::Connector::Utils.stdout_logger base_config
44
+
45
+ @run = true
46
+ end
47
+
48
+ # It's kinda evil to be passing in the controller here. The better option is to
49
+ # refactor the consumer to be part of the controller.
50
+ def prepare(queue)
51
+ @queue = queue
52
+ switch_to_log_file
53
+ end
54
+
55
+ def switch_to_log_file
56
+ FileUtils.mkdir_p(base_config.log_dir) unless File.directory?(base_config.log_dir)
57
+ @log = Sonar::Connector::Utils.disk_logger(@log_file, base_config)
58
+ end
59
+
60
+ def cleanup
61
+ log.info "Shut down consumer"
62
+ log.close
63
+ end
64
+
65
+ ##
66
+ # Main loop to watch the command queue and process commands.
67
+ def watch
68
+ while run
69
+ begin
70
+ run_once
71
+ rescue ThreadTerminator
72
+ break
73
+ end
74
+ end
75
+
76
+ @run = false
77
+ cleanup
78
+ true
79
+ end
80
+
81
+ def run_once
82
+ begin
83
+ command = queue.pop
84
+ command.execute ExecutionContext.new(:log=>log, :status=>status, :controller=>controller)
85
+ rescue ThreadTerminator => e
86
+ raise
87
+ rescue Exception => e
88
+ log.error ["Command #{command.class} raised an unhandled exception: ",
89
+ e.class.to_s, e.message, *e.backtrace].join('\n')
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,164 @@
1
+ module Sonar
2
+ module Connector
3
+
4
+ class ThreadTerminator < Exception; end
5
+
6
+ class Controller
7
+
8
+ DEFAULT_CONFIG_FILENAME = File.join("config", "config.json")
9
+
10
+ ##
11
+ # single command queue for threads to communicate with the controller
12
+ attr_reader :queue
13
+
14
+ ##
15
+ # array of instantiated connector instances
16
+ attr_reader :connectors
17
+
18
+ ##
19
+ # instance of Sonar::Connector::Consumer
20
+ attr_reader :consumer
21
+
22
+ ##
23
+ # instance of Sonar::Connector::Config
24
+ attr_reader :config
25
+
26
+ ##
27
+ # instance of Sonar::Connector::Status
28
+ attr_reader :status
29
+
30
+ ##
31
+ # controller logger
32
+ attr_reader :log
33
+
34
+ ##
35
+ # array of threads
36
+ attr_reader :threads
37
+
38
+ ##
39
+ # Parse the config file and create instances of each connector,
40
+ # parsing their config in turn.
41
+ def initialize(config_filename = DEFAULT_CONFIG_FILENAME)
42
+ @config = Sonar::Connector::Config.load config_filename
43
+ @log = Sonar::Connector::Utils.stdout_logger @config
44
+
45
+ @connectors = @config.connectors
46
+ @consumer = Sonar::Connector::Consumer.new(self, @config)
47
+
48
+ @threads = []
49
+
50
+ @queue = Queue.new
51
+
52
+ create_startup_dirs_and_files
53
+ rescue Sonar::Connector::InvalidConfig => e
54
+ $stderr << ([e.class.to_s, e.message, *e.backtrace].join("\n")) << "\n"
55
+ raise RuntimeError, "Invalid configuration in #{config_filename}: \n #{e.message}"
56
+ end
57
+
58
+ def switch_to_log_file
59
+ @log = Sonar::Connector::Utils.disk_logger(config.controller_log_file, config)
60
+ end
61
+
62
+ def start
63
+ prepare_connector
64
+ start_threads
65
+
66
+ # let the controlling thread go into an endless sleep
67
+ puts "Ctrl-C to stop."
68
+
69
+ # Standardize shutdown via ctrl-c and SIGTERM (from god)
70
+ trap "SIGINT", shutdown_lambda
71
+ trap "SIGTERM", shutdown_lambda
72
+
73
+ endless_sleep
74
+ end
75
+
76
+ # prepare the connector, start an IRB console, but don't start any threads
77
+ def start_console
78
+ prepare_connector
79
+ # make the Controller globally visible
80
+ Connector.const_set("CONTROLLER", self)
81
+
82
+ require 'irb'
83
+ IRB.start
84
+ end
85
+
86
+ def prepare_connector
87
+ switch_to_log_file
88
+ log_startup_params
89
+
90
+ connectors.each do |connector|
91
+ log.info "preparing connector '#{connector.name}'"
92
+ connector.prepare(queue)
93
+ end
94
+
95
+ log.info "preparing message queue consumer"
96
+ consumer.prepare(queue)
97
+ end
98
+
99
+ ##
100
+ # Main framework loop. Fire up one thread per connector,
101
+ # plus the message queue consumer. Then wait for quit signal.
102
+ def start_threads
103
+ # fire up the connector threads
104
+ connectors.each do |connector|
105
+ log.info "starting connector '#{connector.name}'"
106
+ threads << Thread.new { connector.start }
107
+ end
108
+
109
+ log.info "starting the message queue consumer"
110
+ threads << Thread.new{ consumer.watch }
111
+ end
112
+
113
+
114
+ def shutdown_lambda
115
+ lambda do
116
+ puts "\nGiving threads 10 seconds to shut down..."
117
+ threads.each{|t| t.raise(ThreadTerminator)}
118
+ begin
119
+ Timeout::timeout(10) {
120
+ threads.map(&:join)
121
+ }
122
+ rescue Timeout::Error
123
+ puts "...couldn't stop all threads cleanly."
124
+ log.info "Could not cleanly terminate all threads."
125
+ log.close
126
+ exit(1)
127
+ rescue ThreadTerminator
128
+ # ignore it, since it's come from one of the recently-nuked threads.
129
+ rescue Exception => e
130
+ log.debug ["Caught unhandled exception: ",
131
+ e.class.to_s,
132
+ e.message,
133
+ *e.backtrace].join("\n")
134
+ end
135
+
136
+ puts "...exited cleanly."
137
+ log.info "Terminated all threads cleanly."
138
+ log.close
139
+ exit(0)
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def log_startup_params
146
+ log.info "Startup: base directory is #{config.base_dir}"
147
+ log.info "Startup: logging directory is #{config.log_dir}"
148
+ log.info "Startup: log level is " + config.send(:raw_config)['log_level']
149
+ log.info "Startup: controller logging to #{config.controller_log_file}"
150
+ end
151
+
152
+ def create_startup_dirs_and_files
153
+ FileUtils.mkdir_p(config.base_dir) unless File.directory?(config.base_dir)
154
+ FileUtils.mkdir_p(config.log_dir) unless File.directory?(config.log_dir)
155
+ FileUtils.touch config.controller_log_file
156
+ end
157
+
158
+ def endless_sleep
159
+ sleep
160
+ end
161
+
162
+ end
163
+ end
164
+ end