sonar_connector 0.8.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 (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