instrumental_agent 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ### 0.9.0 [February 20th, 2012]
2
+ * Added manual synchronous flushing command
3
+ * Fixed bug with data dropping on short-lived forks
4
+
1
5
  ### 0.8.3 [February 9th, 2012]
2
6
  * Removing symbol to proc use for compatibility with older version of Ruby
3
7
 
data/Gemfile CHANGED
@@ -1,3 +1,8 @@
1
1
  source :rubygems
2
2
 
3
3
  gemspec
4
+
5
+ if RUBY_VERSION < "1.9"
6
+ # Built and installed via ext/mkrf_conf.rb
7
+ gem 'system_timer', '~> 1.2'
8
+ end
data/README.md CHANGED
@@ -111,5 +111,4 @@ after "instrumental:util:deploy_end", "instrumental:record_deploy_notice"
111
111
 
112
112
  ## Troubleshooting & Help
113
113
 
114
- We are here to help, please email us at
115
- [support@instrumentalapp.com](mailto:support@instrumentalapp.com).
114
+ We are here to help. Email us at [support@instrumentalapp.com](mailto:support@instrumentalapp.com), or visit the [Instrumental Support](https://fastestforward.campfirenow.com/6b934) Campfire room.
data/ext/mkrf_conf.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'rubygems/command.rb'
3
+ require 'rubygems/dependency_installer.rb'
4
+ begin
5
+ Gem::Command.build_args = ARGV
6
+ rescue NoMethodError
7
+ end
8
+ inst = Gem::DependencyInstaller.new
9
+ begin
10
+ if RUBY_VERSION < "1.9"
11
+ inst.install "system_timer", "~> 1.2"
12
+ end
13
+ rescue
14
+ puts "Couldn't install system_timer gem, required on Ruby < 1.9"
15
+ exit(1)
16
+ end
17
+
18
+ f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w") # create dummy rakefile to indicate success
19
+ f.write("task :default\n")
20
+ f.close
@@ -14,11 +14,14 @@ Gem::Specification.new do |s|
14
14
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
15
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
16
  s.require_paths = ["lib"]
17
+ s.extensions = 'ext/mkrf_conf.rb'
17
18
  s.add_development_dependency(%q<rake>, [">= 0"])
18
19
  s.add_development_dependency(%q<rspec>, ["~> 2.0"])
19
- s.add_development_dependency(%q<guard>, [">= 0"])
20
- s.add_development_dependency(%q<guard-rspec>, [">= 0"])
21
- s.add_development_dependency(%q<growl_notify>, [">= 0"])
22
- s.add_development_dependency(%q<rb-fsevent>, [">= 0"])
23
20
  s.add_development_dependency(%q<fuubar>, [">= 0"])
21
+ if RUBY_VERSION >= "1.8.7"
22
+ s.add_development_dependency(%q<guard>, [">= 0"])
23
+ s.add_development_dependency(%q<guard-rspec>, [">= 0"])
24
+ s.add_development_dependency(%q<growl>, [">= 0"])
25
+ s.add_development_dependency(%q<rb-fsevent>, [">= 0"])
26
+ end
24
27
  end
@@ -3,7 +3,12 @@ require 'instrumental/version'
3
3
  require 'logger'
4
4
  require 'thread'
5
5
  require 'socket'
6
- require 'timeout'
6
+ if RUBY_VERSION < "1.9"
7
+ require 'system_timer'
8
+ else
9
+ require 'timeout'
10
+ end
11
+
7
12
 
8
13
  # Sets up a connection to the collector.
9
14
  #
@@ -14,6 +19,8 @@ module Instrumental
14
19
  MAX_RECONNECT_DELAY = 15
15
20
  MAX_BUFFER = 5000
16
21
  REPLY_TIMEOUT = 10
22
+ CONNECT_TIMEOUT = 20
23
+ EXIT_FLUSH_TIMEOUT = 5
17
24
 
18
25
  attr_accessor :host, :port, :synchronous, :queue
19
26
  attr_reader :connection, :enabled
@@ -60,12 +67,14 @@ module Instrumental
60
67
  @enabled = options[:enabled]
61
68
  @test_mode = options[:test_mode]
62
69
  @synchronous = options[:synchronous]
70
+ @allow_reconnect = true
63
71
  @pid = Process.pid
64
72
 
65
73
 
66
74
  if @enabled
67
75
  @failures = 0
68
76
  @queue = Queue.new
77
+ @sync_mutex = Mutex.new
69
78
  start_connection_worker
70
79
  setup_cleanup_at_exit
71
80
  end
@@ -136,7 +145,7 @@ module Instrumental
136
145
 
137
146
  # Send a notice to the server (deploys, downtime, etc.)
138
147
  #
139
- # agent.notice('A notice')
148
+ # agent.notice('A notice')
140
149
  def notice(note, time = Time.now, duration = 0)
141
150
  if valid_note?(note)
142
151
  send_command("notice", time.to_i, duration.to_i, note)
@@ -149,6 +158,19 @@ module Instrumental
149
158
  nil
150
159
  end
151
160
 
161
+ # Synchronously flush all pending metrics out to the server
162
+ # By default will not try to reconnect to the server if a
163
+ # connection failure happens during the flush, though you
164
+ # may optionally override this behavior by passing true.
165
+ #
166
+ # agent.flush
167
+ def flush(allow_reconnect = false)
168
+ queue_message('flush', {
169
+ :synchronous => true,
170
+ :allow_reconnect => allow_reconnect
171
+ })
172
+ end
173
+
152
174
  def enabled?
153
175
  @enabled
154
176
  end
@@ -167,6 +189,11 @@ module Instrumental
167
189
 
168
190
  private
169
191
 
192
+ def with_timeout(time, &block)
193
+ tmr_klass = RUBY_VERSION < "1.9" ? SystemTimer : Timeout
194
+ tmr_klass.timeout(time) { yield }
195
+ end
196
+
170
197
  def valid_note?(note)
171
198
  note !~ /[\n\r]/
172
199
  end
@@ -207,13 +234,10 @@ module Instrumental
207
234
  start_connection_worker
208
235
  end
209
236
 
210
- cmd = "%s %s\n" % [cmd, args.collect { |v| v.to_s }.join(" ")]
237
+ cmd = "%s %s\n" % [cmd, args.collect { |a| a.to_s }.join(" ")]
211
238
  if @queue.size < MAX_BUFFER
212
239
  logger.debug "Queueing: #{cmd.chomp}"
213
- @main_thread = Thread.current if @synchronous
214
- @queue << cmd
215
- Thread.stop if @synchronous
216
- cmd
240
+ queue_message(cmd, { :synchronous => @synchronous })
217
241
  else
218
242
  logger.warn "Dropping command, queue full(#{@queue.size}): #{cmd.chomp}"
219
243
  nil
@@ -221,12 +245,32 @@ module Instrumental
221
245
  end
222
246
  end
223
247
 
248
+ def queue_message(message, options = {})
249
+ if @enabled
250
+ options ||= {}
251
+ if options[:allow_reconnect].nil?
252
+ options[:allow_reconnect] = @allow_reconnect
253
+ end
254
+ synchronous = options.delete(:synchronous)
255
+ if synchronous
256
+ options[:sync_resource] ||= ConditionVariable.new
257
+ @queue << [message, options]
258
+ @sync_mutex.synchronize {
259
+ options[:sync_resource].wait(@sync_mutex)
260
+ }
261
+ else
262
+ @queue << [message, options]
263
+ end
264
+ end
265
+ message
266
+ end
267
+
224
268
  def test_connection
225
269
  # FIXME: Test connection state hack
226
270
  begin
227
271
  @socket.read_nonblock(1) # TODO: put data back?
228
272
  rescue Errno::EAGAIN
229
- # nop
273
+ # noop
230
274
  end
231
275
  end
232
276
 
@@ -235,16 +279,14 @@ module Instrumental
235
279
  disconnect
236
280
  logger.info "Starting thread"
237
281
  @thread = Thread.new do
238
- loop do
239
- break if connection_worker
240
- end
282
+ run_worker_loop
241
283
  end
242
284
  end
243
285
  end
244
286
 
245
287
  def send_with_reply_timeout(message)
246
288
  @socket.puts message
247
- Timeout.timeout(REPLY_TIMEOUT) do
289
+ with_timeout(REPLY_TIMEOUT) do
248
290
  response = @socket.gets
249
291
  if response.to_s.chomp != "ok"
250
292
  raise "Bad Response #{response.inspect} to #{message.inspect}"
@@ -252,31 +294,44 @@ module Instrumental
252
294
  end
253
295
  end
254
296
 
255
- def connection_worker
297
+ def run_worker_loop
256
298
  command_and_args = nil
299
+ command_options = nil
257
300
  logger.info "connecting to collector"
258
- @socket = TCPSocket.new(host, port)
301
+ @socket = with_timeout(CONNECT_TIMEOUT) { TCPSocket.new(host, port) }
259
302
  logger.info "connected to collector at #{host}:#{port}"
260
303
  send_with_reply_timeout "hello version #{Instrumental::VERSION} test_mode #{@test_mode}"
261
304
  send_with_reply_timeout "authenticate #{@api_key}"
262
305
  @failures = 0
263
306
  loop do
264
- command_and_args = @queue.pop
307
+ command_and_args, command_options = @queue.pop
308
+ sync_resource = command_options && command_options[:sync_resource]
265
309
  test_connection
266
-
267
310
  case command_and_args
268
311
  when 'exit'
269
312
  logger.info "exiting, #{@queue.size} commands remain"
270
313
  return true
314
+ when 'flush'
315
+ release_resource = true
271
316
  else
272
317
  logger.debug "Sending: #{command_and_args.chomp}"
273
318
  @socket.puts command_and_args
274
- command_and_args = nil
275
319
  end
276
- @main_thread.run if @synchronous
320
+ command_and_args = nil
321
+ command_options = nil
322
+ if sync_resource
323
+ @sync_mutex.synchronize do
324
+ sync_resource.signal
325
+ end
326
+ end
277
327
  end
278
328
  rescue Exception => err
279
- logger.error err.to_s
329
+ logger.debug err.backtrace.join("\n")
330
+ if @allow_reconnect == false ||
331
+ (command_options && command_options[:allow_reconnect] == false)
332
+ logger.error "Not trying to reconnect"
333
+ return
334
+ end
280
335
  if command_and_args
281
336
  logger.debug "requeueing: #{command_and_args}"
282
337
  @queue << command_and_args
@@ -293,16 +348,20 @@ module Instrumental
293
348
 
294
349
  def setup_cleanup_at_exit
295
350
  at_exit do
296
- if !@queue.empty? && @thread.alive?
297
- if @failures > 0
298
- logger.info "exit received but disconnected, dropping #{@queue.size} commands"
299
- @thread.kill
351
+ logger.info "Cleaning up agent, queue empty: #{@queue.empty?}, thread running: #{@thread.alive?}"
352
+ @allow_reconnect = false
353
+ logger.info "exit received, currently #{@queue.size} commands to be sent"
354
+ queue_message('exit')
355
+ begin
356
+ with_timeout(EXIT_FLUSH_TIMEOUT) { @thread.join }
357
+ rescue Timeout::Error
358
+ if @queue.size > 0
359
+ logger.error "Timed out working agent thread on exit, dropping #{@queue.size} metrics"
300
360
  else
301
- logger.info "exit received, #{@queue.size} commands to be sent"
302
- @queue << 'exit'
303
- @thread.join
361
+ logger.error "Timed out Instrumental Agent, exiting"
304
362
  end
305
363
  end
364
+
306
365
  end
307
366
  end
308
367
 
@@ -1,3 +1,3 @@
1
1
  module Instrumental
2
- VERSION = '0.8.3'
2
+ VERSION = '0.9.0'
3
3
  end
data/spec/agent_spec.rb CHANGED
@@ -27,6 +27,19 @@ describe Instrumental::Agent, "disabled" do
27
27
  @server.connect_count.should == 0
28
28
  end
29
29
 
30
+ it "should no op on flush without reconnect" do
31
+ 1.upto(100) { @agent.gauge('disabled_test', 1) }
32
+ @agent.flush(false)
33
+ wait
34
+ @server.commands.should be_empty
35
+ end
36
+
37
+ it "should no op on flush with reconnect" do
38
+ 1.upto(100) { @agent.gauge('disabled_test', 1) }
39
+ @agent.flush(true)
40
+ wait
41
+ @server.commands.should be_empty
42
+ end
30
43
  end
31
44
 
32
45
  describe Instrumental::Agent, "enabled in test_mode" do
@@ -81,7 +94,7 @@ describe Instrumental::Agent, "enabled in test_mode" do
81
94
  throw :an_exception
82
95
  sleep 1
83
96
  end
84
- }.should raise_error
97
+ }.should raise_error
85
98
  wait
86
99
  @server.commands.last.should =~ /gauge time_value_test .* #{now.to_i}/
87
100
  time = @server.commands.last.scan(/gauge time_value_test (.*) #{now.to_i}/)[0][0].to_f
@@ -216,6 +229,7 @@ describe Instrumental::Agent, "enabled" do
216
229
  5.times do |i|
217
230
  @agent.increment('overflow_test', i + 1, 300)
218
231
  end
232
+ @agent.instance_variable_get(:@queue).size.should == 0
219
233
  wait # let the server receive the commands
220
234
  @server.commands.should include("increment overflow_test 1 300")
221
235
  @server.commands.should include("increment overflow_test 2 300")
@@ -309,6 +323,15 @@ describe Instrumental::Agent, "enabled" do
309
323
  wait
310
324
  @server.commands.join("\n").should_not include("notice Test note")
311
325
  end
326
+
327
+ it "should allow flushing pending values to the server" do
328
+ 1.upto(100) { @agent.gauge('a', rand(50)) }
329
+ @agent.instance_variable_get(:@queue).size.should >= 100
330
+ @agent.flush
331
+ @agent.instance_variable_get(:@queue).size.should == 0
332
+ wait
333
+ @server.commands.grep(/^gauge a /).size.should == 100
334
+ end
312
335
  end
313
336
 
314
337
  describe Instrumental::Agent, "connection problems" do
@@ -333,7 +356,7 @@ describe Instrumental::Agent, "connection problems" do
333
356
  wait
334
357
  @agent.increment('reconnect_test', 1, 1234)
335
358
  wait
336
- @agent.queue.pop(true).should == "increment reconnect_test 1 1234\n"
359
+ @agent.queue.pop(true).should include("increment reconnect_test 1 1234\n")
337
360
  end
338
361
 
339
362
  it "should buffer commands when server is not responsive" do
@@ -342,7 +365,7 @@ describe Instrumental::Agent, "connection problems" do
342
365
  wait
343
366
  @agent.increment('reconnect_test', 1, 1234)
344
367
  wait
345
- @agent.queue.pop(true).should == "increment reconnect_test 1 1234\n"
368
+ @agent.queue.pop(true).should include("increment reconnect_test 1 1234\n")
346
369
  end
347
370
 
348
371
  it "should buffer commands when authentication fails" do
@@ -351,7 +374,31 @@ describe Instrumental::Agent, "connection problems" do
351
374
  wait
352
375
  @agent.increment('reconnect_test', 1, 1234)
353
376
  wait
354
- @agent.queue.pop(true).should == "increment reconnect_test 1 1234\n"
377
+ @agent.queue.pop(true).should include("increment reconnect_test 1 1234\n")
378
+ end
379
+
380
+ it "should send commands in a short-lived process" do
381
+ @server = TestServer.new
382
+ @agent = Instrumental::Agent.new('test_token', :collector => @server.host_and_port, :synchronous => false)
383
+ if pid = fork { @agent.increment('foo', 1, 1234) }
384
+ Process.wait(pid)
385
+ @server.commands.last.should == "increment foo 1 1234"
386
+ end
387
+ end
388
+
389
+ it "should not wait longer than EXIT_FLUSH_TIMEOUT seconds to exit a process" do
390
+ @server = TestServer.new
391
+ @agent = Instrumental::Agent.new('test_token', :collector => @server.host_and_port, :synchronous => false)
392
+ TCPSocket.stub!(:new) { |*args| sleep(5) && StringIO.new }
393
+ with_constants('Instrumental::Agent::EXIT_FLUSH_TIMEOUT' => 3) do
394
+ if (pid = fork { @agent.increment('foo', 1) })
395
+ tm = Time.now.to_f
396
+ Process.wait(pid)
397
+ diff = Time.now.to_f - tm
398
+ diff.should >= 3
399
+ diff.should < 5
400
+ end
401
+ end
355
402
  end
356
403
  end
357
404
 
data/spec/spec_helper.rb CHANGED
@@ -15,8 +15,10 @@ end
15
15
 
16
16
 
17
17
  def parse_constant(constant)
18
- source, _, constant_name = constant.to_s.rpartition('::')
19
-
18
+ constant = constant.to_s
19
+ parts = constant.split("::")
20
+ constant_name = parts.pop
21
+ source = parts.join("::")
20
22
  [source.constantize, constant_name]
21
23
  end
22
24
 
data/spec/test_server.rb CHANGED
@@ -55,6 +55,7 @@ class TestServer
55
55
  rescue Exception => err
56
56
  # FIXME: doesn't seem to be detecting failures of listen
57
57
  puts "failed to get port"
58
+ puts err.message
58
59
  @port += 1
59
60
  retry
60
61
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: instrumental_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -12,11 +12,11 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2012-02-09 00:00:00.000000000 Z
15
+ date: 2012-02-20 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: rake
19
- requirement: &70106588068980 !ruby/object:Gem::Requirement
19
+ requirement: &70233623514200 !ruby/object:Gem::Requirement
20
20
  none: false
21
21
  requirements:
22
22
  - - ! '>='
@@ -24,10 +24,10 @@ dependencies:
24
24
  version: '0'
25
25
  type: :development
26
26
  prerelease: false
27
- version_requirements: *70106588068980
27
+ version_requirements: *70233623514200
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: rspec
30
- requirement: &70106588083600 !ruby/object:Gem::Requirement
30
+ requirement: &70233623513680 !ruby/object:Gem::Requirement
31
31
  none: false
32
32
  requirements:
33
33
  - - ~>
@@ -35,10 +35,10 @@ dependencies:
35
35
  version: '2.0'
36
36
  type: :development
37
37
  prerelease: false
38
- version_requirements: *70106588083600
38
+ version_requirements: *70233623513680
39
39
  - !ruby/object:Gem::Dependency
40
- name: guard
41
- requirement: &70106588082500 !ruby/object:Gem::Requirement
40
+ name: fuubar
41
+ requirement: &70233623513200 !ruby/object:Gem::Requirement
42
42
  none: false
43
43
  requirements:
44
44
  - - ! '>='
@@ -46,10 +46,10 @@ dependencies:
46
46
  version: '0'
47
47
  type: :development
48
48
  prerelease: false
49
- version_requirements: *70106588082500
49
+ version_requirements: *70233623513200
50
50
  - !ruby/object:Gem::Dependency
51
- name: guard-rspec
52
- requirement: &70106588080820 !ruby/object:Gem::Requirement
51
+ name: guard
52
+ requirement: &70233623512700 !ruby/object:Gem::Requirement
53
53
  none: false
54
54
  requirements:
55
55
  - - ! '>='
@@ -57,10 +57,10 @@ dependencies:
57
57
  version: '0'
58
58
  type: :development
59
59
  prerelease: false
60
- version_requirements: *70106588080820
60
+ version_requirements: *70233623512700
61
61
  - !ruby/object:Gem::Dependency
62
- name: growl_notify
63
- requirement: &70106588078620 !ruby/object:Gem::Requirement
62
+ name: guard-rspec
63
+ requirement: &70233623512220 !ruby/object:Gem::Requirement
64
64
  none: false
65
65
  requirements:
66
66
  - - ! '>='
@@ -68,10 +68,10 @@ dependencies:
68
68
  version: '0'
69
69
  type: :development
70
70
  prerelease: false
71
- version_requirements: *70106588078620
71
+ version_requirements: *70233623512220
72
72
  - !ruby/object:Gem::Dependency
73
- name: rb-fsevent
74
- requirement: &70106588077720 !ruby/object:Gem::Requirement
73
+ name: growl
74
+ requirement: &70233623511740 !ruby/object:Gem::Requirement
75
75
  none: false
76
76
  requirements:
77
77
  - - ! '>='
@@ -79,10 +79,10 @@ dependencies:
79
79
  version: '0'
80
80
  type: :development
81
81
  prerelease: false
82
- version_requirements: *70106588077720
82
+ version_requirements: *70233623511740
83
83
  - !ruby/object:Gem::Dependency
84
- name: fuubar
85
- requirement: &70106588077080 !ruby/object:Gem::Requirement
84
+ name: rb-fsevent
85
+ requirement: &70233623511260 !ruby/object:Gem::Requirement
86
86
  none: false
87
87
  requirements:
88
88
  - - ! '>='
@@ -90,12 +90,13 @@ dependencies:
90
90
  version: '0'
91
91
  type: :development
92
92
  prerelease: false
93
- version_requirements: *70106588077080
93
+ version_requirements: *70233623511260
94
94
  description: Track anything.
95
95
  email:
96
96
  - support@instrumentalapp.com
97
97
  executables: []
98
- extensions: []
98
+ extensions:
99
+ - ext/mkrf_conf.rb
99
100
  extra_rdoc_files: []
100
101
  files:
101
102
  - .gitignore
@@ -105,6 +106,7 @@ files:
105
106
  - Guardfile
106
107
  - README.md
107
108
  - Rakefile
109
+ - ext/mkrf_conf.rb
108
110
  - instrumental_agent.gemspec
109
111
  - lib/instrumental/agent.rb
110
112
  - lib/instrumental/capistrano.rb
@@ -128,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
130
  version: '0'
129
131
  segments:
130
132
  - 0
131
- hash: -4247345232071751034
133
+ hash: -278624242877907037
132
134
  required_rubygems_version: !ruby/object:Gem::Requirement
133
135
  none: false
134
136
  requirements:
@@ -137,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
139
  version: '0'
138
140
  segments:
139
141
  - 0
140
- hash: -4247345232071751034
142
+ hash: -278624242877907037
141
143
  requirements: []
142
144
  rubyforge_project:
143
145
  rubygems_version: 1.8.15