Skip to content

Commit

Permalink
Better type handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
JEG2 committed Jun 18, 2008
1 parent 8a01bb2 commit 176c706
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 16 deletions.
56 changes: 41 additions & 15 deletions lib/rrdb.rb
Expand Up @@ -3,7 +3,13 @@
class RRDB class RRDB
VERSION = "0.0.1" VERSION = "0.0.1"


class FieldNameConflictError < RuntimeError; end def self.const_missing(error_name)
if error_name.to_s =~ /Error\z/
const_set(error_name, Class.new(RuntimeError))
else
super
end
end


def self.run_command(command) def self.run_command(command)
output = `#{command} 2>&1` output = `#{command} 2>&1`
Expand Down Expand Up @@ -54,12 +60,27 @@ def path
File.join(self.class.config[:database_directory], "#{id}.rrd") File.join(self.class.config[:database_directory], "#{id}.rrd")
end end


def fields def fields(include_types = false)
rrdtool(:info).to_s.scan(/^ds\[([^\]]+)\]/).flatten.uniq schema = rrdtool(:info).to_s
fields = schema.scan(/^ds\[([^\]]+)\]/).flatten.uniq
if include_types
Hash[ *fields.map { |f|
[ f, "#{schema[/^ds\[#{f}\]\.type\s*=\s*"([^"]+)"/, 1]}:" +
"#{schema[/^ds\[#{f}\]\.minimal_heartbeat\s*=\s*(\d+)/, 1]}:" +
"#{schema[/^ds\[#{f}\]\.min\s*=\s*(\S+)/, 1].sub('NaN', 'U')}:" +
"#{schema[/^ds\[#{f}\]\.max\s*=\s*(\S+)/, 1].sub('NaN', 'U')}" ]
}.flatten ]
else
fields
end
rescue InfoError
include_types ? Hash.new : Array.new
end end


def step def step
(rrdtool(:info).to_s[/^step\s+=\s+(\d+)/, 1] || 300).to_i (rrdtool(:info).to_s[/^step\s+=\s+(\d+)/, 1] || 300).to_i
rescue InfoError
300
end end


def update(time, data) def update(time, data)
Expand Down Expand Up @@ -109,16 +130,10 @@ def create_database(time, field_names)
schema << " --start '#{(time - 10).to_i}'" schema << " --start '#{(time - 10).to_i}'"
end end
end end
field_names.each do |f| field_names.each { |f| schema << " 'DS:#{f}:#{field_type(f)}'" }
dst = if (setting = self.class.config[:data_sources]).is_a? String
setting
else
setting[f.to_sym] || setting[f]
end
schema << " 'DS:#{f}:#{dst}'"
end
(self.class.config[:reserve_fields].to_i - field_names.size).times do |i| (self.class.config[:reserve_fields].to_i - field_names.size).times do |i|
schema << " 'DS:_reserved#{i}:GAUGE:600:U:U'" name = "_reserved#{i}"
schema << " 'DS:#{name}:#{field_type(name)}'"
end end
Array(self.class.config[:round_robin_archives]).each do |a| Array(self.class.config[:round_robin_archives]).each do |a|
schema << " 'RRA:#{a}'" schema << " 'RRA:#{a}'"
Expand All @@ -133,19 +148,30 @@ def claim_new_fields(field_names)
reserved = old_fields.grep(/\A_reserved\d+\Z/). reserved = old_fields.grep(/\A_reserved\d+\Z/).
sort_by { |f| f[/\d+/].to_i } sort_by { |f| f[/\d+/].to_i }
if new_fields.size > reserved.size if new_fields.size > reserved.size

raise FieldsExhaustedError,
"There are not enough reserved fields to complete this update."
else else
claims = new_fields.zip(reserved). claims = new_fields.zip(reserved).
map { |n, o| " --data-source-rename '#{o}:#{n}'"}. map { |n, o| " -r '#{o}:#{n}'" +
" -d '#{n}:#{field_type(n)}'" }.
join.strip join.strip
rrdtool(:tune, claims) rrdtool(:tune, claims)
end end
end end
end end


def field_type(field_name)
if (setting = self.class.config[:data_sources]).is_a? String
setting
else
setting[field_name.to_sym] || setting[field_name] || "GAUGE:600:U:U"
end
end

def rrdtool(command, params = nil) def rrdtool(command, params = nil)
self.class.run_command( self.class.run_command(
"#{self.class.config[:rrdtool_path]} #{command} '#{path}' #{params}" "#{self.class.config[:rrdtool_path]} #{command} '#{path}' #{params}"
) ) or raise self.class.const_get("#{command.to_s.capitalize}Error"),
self.class.last_error
end end
end end
39 changes: 39 additions & 0 deletions test/tc_create_database.rb
Expand Up @@ -38,4 +38,43 @@ def test_step_can_be_customized_for_creation
@db.update(Time.now, :a => 1) @db.update(Time.now, :a => 1)
assert_equal(1_000, @db.step) assert_equal(1_000, @db.step)
end end

def test_step_defaults_to_three_hundred_for_a_non_existent_database
assert_equal(300, @db.step)
end

def test_illegal_schema_raise_create_error
RRDB.config(:round_robin_archives => nil)
assert_raise(RRDB::CreateError) { @db.update(Time.now, :a => 1) }
end

def test_field_type_can_be_set_globally_for_all_created_fields
RRDB.config(:reserve_fields => 0, :data_sources => "GAUGE:1200:U:U")
@db.update(Time.now, :a => 1, :b => 2, :c => 3)
@db.fields(true).each_value do |type|
assert_equal("GAUGE:1200:U:U", type)
end
end

def test_field_type_can_be_set_with_hash_lookup
RRDB.config( :reserve_fields => 0,
:data_sources => { :a => "GAUGE:100:U:U",
:b => "GAUGE:200:U:U",
:c => "GAUGE:300:U:U" } )
@db.update(Time.now, :a => 1, :b => 2, :c => 3)
types = @db.fields(true)
RRDB.config[:data_sources].each do |field, type|
assert_equal(type, types[field.to_s])
end
end

def test_field_type_can_be_set_with_lambda
RRDB.config( :reserve_fields => 0,
:data_sources => lambda { |f| f == :a ? "GAUGE:100:U:U" :
"GAUGE:200:U:U" } )
@db.update(Time.now, :a => 1, :b => 2, :c => 3)
@db.fields(true).each do |field, type|
assert_equal(RRDB.config[:data_sources][field.to_sym], type)
end
end
end end
7 changes: 7 additions & 0 deletions test/tc_fetch.rb
Expand Up @@ -63,4 +63,11 @@ def test_floats_are_supported
assert_instance_of(Float, float) assert_instance_of(Float, float)
assert_in_delta(2.9, float, 2 ** -20) assert_in_delta(2.9, float, 2 ** -20)
end end

def test_calling_fetch_on_a_non_existent_database_raises_fetch_error
assert_raise(RRDB::FetchError) do
@db.fetch( :AVERAGE, :start => Time.at(920804400),
:end => Time.at(920809200) )
end
end
end end
30 changes: 29 additions & 1 deletion test/tc_update.rb
Expand Up @@ -8,17 +8,38 @@
class TestUpdate < Test::Unit::TestCase class TestUpdate < Test::Unit::TestCase
include RRDManager include RRDManager


def test_fields_returns_an_empty_array_for_a_non_existent_database
assert_equal(Array.new, @db.fields)
end

def test_update_claims_new_fields_as_needed def test_update_claims_new_fields_as_needed
@db.update(Time.now, :a => 1) @db.update(Time.now, :a => 1)
%w[b c].each do |field| %w[b c].each do |field|
assert(!@db.fields.include?(field), "Unused field in database.") assert(!@db.fields.include?(field), "Unused field in database.")
end end
@db.update(Time.now, :b => 2, :c => 3) @db.update(Time.now + 10, :b => 2, :c => 3)
%w[b c].each do |field| %w[b c].each do |field|
assert(@db.fields.include?(field), "Field not claimed in database.") assert(@db.fields.include?(field), "Field not claimed in database.")
end end
end end


def test_fields_are_retyped_on_claim
RRDB.config(:reserve_fields => 2)
@db.update(Time.now, :a => 1)
assert_match(/\AGAUGE:/, @db.fields(true)["_reserved0"])
RRDB.config(:data_sources => {:b => "COUNTER:600:U:U"})
@db.update(Time.now + 10, :b => 2)
assert_match(/\ACOUNTER:/, @db.fields(true)["b"])
end

def test_running_out_of_fields_to_claim_raises_fields_exhausted_error
RRDB.config(:reserve_fields => 3)
test_update_claims_new_fields_as_needed
assert_raise(RRDB::FieldsExhaustedError) do
@db.update(Time.now + 20, :d => 0)
end
end

def test_unsupported_characters_are_removed_from_names def test_unsupported_characters_are_removed_from_names
@db.update(Time.now, "a'b=" => 4) @db.update(Time.now, "a'b=" => 4)
assert(@db.fields.include?("ab"), "Trimmed field name not found.") assert(@db.fields.include?("ab"), "Trimmed field name not found.")
Expand Down Expand Up @@ -49,4 +70,11 @@ def test_you_can_retrieve_the_field_name_used
assert_equal( ("a".."z").to_a.join[0..18], assert_equal( ("a".."z").to_a.join[0..18],
RRDB.field_name(("a".."z").to_a.join.gsub(/\b/, "'")) ) RRDB.field_name(("a".."z").to_a.join.gsub(/\b/, "'")) )
end end

def test_illegal_updates_raise_update_error
test_unsupported_characters_are_removed_from_names # create a db
assert_raise(RRDB::UpdateError) do
@db.update(Time.now - 10, Hash.new) # time in the past
end
end
end end

0 comments on commit 176c706

Please sign in to comment.