instrumental_agent 0.8.3 → 0.9.0

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