data_store 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ module DataStore
2
+
3
+ class Table
4
+
5
+ include Celluloid
6
+
7
+ attr_reader :identifier, :table_index, :original_value
8
+
9
+ # Initialize the table by passsing an identifier
10
+ def initialize(identifier, table_index = 0)
11
+ @identifier = identifier
12
+ @table_index = table_index
13
+ end
14
+
15
+ # Return a the corresponding parent class, i.e the settings from the data_stores table
16
+ def parent
17
+ @parent ||= DataStore::Base.find(identifier: identifier)
18
+ end
19
+
20
+ # Return a table object enriched with Sequel::Model behaviour
21
+ def model
22
+ @model ||= Class.new(Sequel::Model(dataset))
23
+ end
24
+
25
+ # Add a new datapoint to the table
26
+ # In case of a counter type, store the difference between current and last value
27
+ # And calculates average values on the fly according to compression schema
28
+ #
29
+ # Options (hash):
30
+ # * created: timestamp
31
+ # * type: gauge or counter
32
+ # * table_index: in which compressed table
33
+ def add(value, options = {})
34
+ created = options[:created] || Time.now.utc.to_f
35
+ type = options[:type] || parent.type
36
+ @table_index = options[:table_index] if options[:table_index]
37
+ push(value, type, created)
38
+ end
39
+
40
+ # Return the most recent datapoint added
41
+ def last
42
+ model.order(:created).last
43
+ end
44
+
45
+ # Return the total number of datapoints in the table
46
+ def count
47
+ dataset.count
48
+ end
49
+
50
+ # Return the corresponding dataset with the datapoitns
51
+ def dataset
52
+ database[table_name]
53
+ end
54
+
55
+ # Fetch the corresponding datapoints
56
+ #
57
+ # Options:
58
+ # * :from
59
+ # * :till
60
+ #
61
+ def fetch(options)
62
+ datapoints = []
63
+ query = parent.db[timeslot(options)].where{created >= options[:from]}.where{created <= options[:till]}.order(:created)
64
+ query.all.map{|record| datapoints <<[record[:value], record[:created]]}
65
+ datapoints
66
+ end
67
+
68
+ # Import original datapoints, mostly to recreate compression tables
69
+ def import(datapoints)
70
+ datapoints.each do |data|
71
+ add!(data[0], table_index: 0, created: data[1])
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def timeslot(options)
78
+ distance = options[:till] - options[:from]
79
+ index = 0
80
+ parent.time_borders.each_with_index do |value, idx|
81
+ index = idx
82
+ break if value >= distance
83
+ end
84
+ parent.table_names[index]
85
+ end
86
+
87
+ def push(value, type, created)
88
+ value = difference_with_previous(value) if type.to_s == 'counter'
89
+ datapoint = { value: value, created: created }
90
+ datapoint[:original_value] = original_value if original_value
91
+ dataset << datapoint
92
+ calculate_average_values
93
+ end
94
+
95
+ def calculate_average_values
96
+ calculator = AverageCalculator.new(self)
97
+ calculator.perform
98
+ end
99
+
100
+ def difference_with_previous(value)
101
+ @original_value = value
102
+ unless last.nil?
103
+ value = value - last[:original_value]
104
+ last.delete if last[:value] == last[:original_value]
105
+ end
106
+ value
107
+ end
108
+
109
+ def database
110
+ @database ||= DataStore::Base.db
111
+ end
112
+
113
+ def table_name
114
+ parent.table_names[table_index]
115
+ end
116
+
117
+ end
118
+
119
+ end
@@ -1,3 +1,3 @@
1
1
  module DataStore
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
data/lib/data_store.rb CHANGED
@@ -1,5 +1,79 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'sequel'
4
+ require 'yaml'
5
+ require 'logger'
6
+ require 'celluloid'
7
+
8
+ $: << File.expand_path('../', __FILE__)
9
+ $: << File.expand_path('../data_store/', __FILE__)
10
+
11
+ Sequel.extension :migration
12
+ Sequel::Model.plugin :timestamps, :force=>true, :update_on_create=>true
13
+
1
14
  require 'data_store/version'
15
+ require 'data_store/connector'
16
+ require 'data_store/configuration'
17
+ require 'data_store/definitions'
18
+ require 'data_store/table'
19
+ require 'data_store/average_calculator'
20
+
21
+ module Kernel
22
+ def suppress_warnings
23
+ original_verbosity = $VERBOSE
24
+ $VERBOSE = nil
25
+ result = yield
26
+ $VERBOSE = original_verbosity
27
+ return result
28
+ end
29
+ end
2
30
 
3
31
  module DataStore
4
- # Your code goes here...
32
+
33
+ # Base class will be redefined during configure
34
+ # In order to assign Sequel::Model behaviour to it
35
+ # with the correctly defined (or configured) database connector
36
+ class Base
37
+ end
38
+
39
+ class << self
40
+
41
+ # Configure DataStore
42
+ #
43
+ # Example
44
+ # DataStore.configure |config|
45
+ # config.prefix = 'data_store_'
46
+ # config.database = :postgres
47
+ # end
48
+ def configure
49
+ yield(configuration)
50
+ define_base_class
51
+ end
52
+
53
+ # The configuration object. See {Configuration}
54
+ def configuration
55
+ @configuration ||= Configuration.new
56
+ end
57
+
58
+ private
59
+
60
+ def define_base_class
61
+ connector = DataStore::Connector.new
62
+ set_logger(connector.database)
63
+ connector.create_table!
64
+ suppress_warnings { self.const_set(:Base, Class.new(Sequel::Model(connector.dataset)))}
65
+ load 'base.rb'
66
+ connector.database.disconnect
67
+ end
68
+
69
+ def set_logger(db)
70
+ if configuration.enable_logging
71
+ logger = Logger.new(configuration.log_file)
72
+ logger.level = configuration.log_level
73
+ db.logger = logger
74
+ end
75
+ end
76
+
77
+ end
78
+
5
79
  end
@@ -0,0 +1,196 @@
1
+ require File.expand_path '../test_helper', __FILE__
2
+
3
+ class AverageCalculatorTest < Test::Unit::TestCase
4
+
5
+ context 'AverageCalculator for a gauge type' do
6
+
7
+ setup do
8
+ DataStore::Base.db.tables.each do |table|
9
+ DataStore::Base.db.drop_table(table)
10
+ end
11
+
12
+ DataStore::Connector.new.reset!
13
+ @record = DataStore::Base.create(identifier: 1,
14
+ type: 'gauge',
15
+ name: 'Electra',
16
+ frequency: 10,
17
+ description: 'Actual usage of electra in the home',
18
+ compression_schema: [2,2,2])
19
+
20
+ @table = DataStore::Table.new(1)
21
+ @calculator = DataStore::AverageCalculator.new(@table)
22
+ end
23
+
24
+ should 'be valid' do
25
+ assert @calculator
26
+ end
27
+
28
+ should 'return the identifier' do
29
+ assert_equal 1, @calculator.identifier
30
+ end
31
+
32
+ should 'calculate the average value for the first' do
33
+ @table.model.insert(value: 10, created: 0)
34
+ @table.model.insert(value: 11, created: 10)
35
+
36
+ @calculator.perform
37
+ assert_equal 10.5, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
38
+ end
39
+
40
+ context 'Scenario: adding values according expected frequency' do
41
+ should 'calculate the average values' do
42
+ time_now_utc_returns(10)
43
+
44
+ @table.model.insert(value: 10, created: 0)
45
+ @table.model.insert(value: 11, created: 10)
46
+
47
+ @calculator.perform
48
+
49
+ assert_equal 10.5, DataStore::Base.db[:ds_1_2].order(:created).first[:value]
50
+
51
+ @table.model.insert(value: 12, created: 20)
52
+ @table.model.insert(value: 13, created: 30)
53
+
54
+ time_now_utc_returns(30)
55
+
56
+ @calculator.perform
57
+
58
+ assert_equal 12.5, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
59
+ assert_equal 11.5, DataStore::Base.db[:ds_1_4].order(:created).last[:value]
60
+
61
+ @table.model.insert(value: 14, created: 40)
62
+ @table.model.insert(value: 15, created: 50)
63
+
64
+ time_now_utc_returns(50)
65
+
66
+ @calculator.perform
67
+
68
+ assert_equal 14.5, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
69
+
70
+ @table.model.insert(value: 16, created: 60)
71
+ @table.model.insert(value: 17, created: 70)
72
+
73
+ time_now_utc_returns(70)
74
+
75
+ @calculator.perform
76
+
77
+ assert_equal 16.5, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
78
+ assert_equal 15.5, DataStore::Base.db[:ds_1_4].order(:created).last[:value]
79
+ assert_equal 13.5, DataStore::Base.db[:ds_1_8].order(:created).last[:value]
80
+
81
+ assert_equal [:data_stores, :ds_1, :ds_1_2, :ds_1_4, :ds_1_8], DataStore::Base.db.tables.sort
82
+ end
83
+ end
84
+
85
+ context 'Scenario: adding values with an unexpected failure' do
86
+ should 'calculate the average values' do
87
+ time_now_utc_returns(10)
88
+
89
+ @table.model.insert(value: 10, created: 0)
90
+ @table.model.insert(value: 11, created: 10)
91
+
92
+ @calculator.perform
93
+
94
+ assert_equal 10.5, DataStore::Base.db[:ds_1_2].order(:created).first[:value]
95
+
96
+ @table.model.insert(value: 12, created: 20)
97
+
98
+ #No value at timestamp 30!
99
+ @table.model.insert(value: 14, created: 40)
100
+
101
+ time_now_utc_returns(40)
102
+
103
+ @calculator.perform
104
+
105
+ assert_equal 13.0, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
106
+ assert_equal 11.75, DataStore::Base.db[:ds_1_4].order(:created).last[:value]
107
+
108
+ @table.model.insert(value: 15, created: 50)
109
+ @table.model.insert(value: 16, created: 60)
110
+
111
+ time_now_utc_returns(60)
112
+
113
+ @calculator.perform
114
+
115
+ assert_equal 15.5, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
116
+
117
+ @table.model.insert(value: 17, created: 70)
118
+ @table.model.insert(value: 18, created: 80)
119
+
120
+ time_now_utc_returns(80)
121
+
122
+ @calculator.perform
123
+
124
+ assert_equal 17.5, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
125
+ assert_equal 16.5, DataStore::Base.db[:ds_1_4].order(:created).last[:value]
126
+ assert_equal 14.125, DataStore::Base.db[:ds_1_8].order(:created).last[:value]
127
+ end
128
+ end
129
+ end
130
+
131
+ context 'AverageCalculator for a counter type' do
132
+
133
+ setup do
134
+ DataStore::Base.db.tables.each do |table|
135
+ DataStore::Base.db.drop_table(table)
136
+ end
137
+
138
+ DataStore::Connector.new.reset!
139
+ @record = DataStore::Base.create(identifier: 1,
140
+ type: 'counter',
141
+ name: 'Electra',
142
+ frequency: 10,
143
+ description: 'Actual usage of gas in the home',
144
+ compression_schema: [2])
145
+
146
+ @table = DataStore::Table.new(1)
147
+ @calculator = DataStore::AverageCalculator.new(@table)
148
+ end
149
+
150
+ should 'be valid' do
151
+ assert @calculator
152
+ end
153
+
154
+ should 'return the identifier' do
155
+ assert_equal 1, @calculator.identifier
156
+ end
157
+
158
+ should 'calculate the average value' do
159
+ @table.model.insert(value: 10, original_value: 1010, created: 10)
160
+ @table.model.insert(value: 10, original_value: 1020, created: 20)
161
+
162
+ @calculator.perform
163
+
164
+ assert_equal 10.0, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
165
+ end
166
+
167
+ should 'calculate the average values according to compression_schema' do
168
+ @table.model.insert(value: 10, original_value: 1010, created: 10)
169
+ @table.model.insert(value: 10, original_value: 1020, created: 20)
170
+
171
+ time_now_utc_returns(20)
172
+ @calculator.perform
173
+
174
+ @table.model.insert(value: 20, original_value: 1040, created: 30)
175
+ @table.model.insert(value: 30, original_value: 1070, created: 40)
176
+
177
+ time_now_utc_returns(40)
178
+ @calculator.perform
179
+
180
+ assert_equal 25.0, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
181
+
182
+ assert_equal [:data_stores, :ds_1, :ds_1_2], DataStore::Base.db.tables.sort
183
+ end
184
+
185
+ should 'calculate the average value by ignoring the original values' do
186
+ @table.model.insert(value: 20, original_value: 12345, created: 10)
187
+ @table.model.insert(value: 30, original_value: 67890, created: 20)
188
+
189
+ @calculator.perform
190
+
191
+ assert_equal 25.0, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
192
+ end
193
+
194
+ end
195
+
196
+ end
@@ -0,0 +1,48 @@
1
+ require File.expand_path '../test_helper', __FILE__
2
+
3
+ class ConfigurationTest < Test::Unit::TestCase
4
+
5
+ context 'Configuration' do
6
+
7
+ should "provide default values" do
8
+ assert_config_default :prefix, 'ds_'
9
+ assert_config_default :database, :postgres
10
+ assert_config_default :compression_schema, [6,5,3,4,4,3]
11
+ assert_config_default :frequency, 10
12
+ assert_config_default :frequency_tolerance, 0.05
13
+ assert_config_default :maximum_datapoints, 800
14
+ assert_config_default :data_type, :double
15
+ assert_config_default :database_config_file, File.expand_path('../../config/database.yml', __FILE__)
16
+ assert_config_default :enable_logging, true
17
+ assert_config_default :log_file, $stdout
18
+ assert_config_default :log_level, Logger::ERROR
19
+ end
20
+
21
+ should "allow values to be overwritten" do
22
+ assert_config_overridable :prefix
23
+ assert_config_overridable :database
24
+ assert_config_overridable :compression_schema
25
+ assert_config_overridable :frequency
26
+ assert_config_overridable :frequency_tolerance
27
+ assert_config_overridable :maximum_datapoints
28
+ assert_config_overridable :data_type
29
+ assert_config_overridable :database_config_file
30
+ assert_config_overridable :enable_logging
31
+ assert_config_overridable :log_file
32
+ assert_config_overridable :log_level
33
+ end
34
+
35
+ end
36
+
37
+ def assert_config_default(option, default_value, config = nil)
38
+ config ||= DataStore::Configuration.new
39
+ assert_equal default_value, config.send(option)
40
+ end
41
+
42
+ def assert_config_overridable(option, value = 'a value')
43
+ config = DataStore::Configuration.new
44
+ config.send(:"#{option}=", value)
45
+ assert_equal value, config.send(option)
46
+ end
47
+
48
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path '../test_helper', __FILE__
2
+
3
+ class ConnectorTest < Test::Unit::TestCase
4
+
5
+ context 'DataStore::Connector connection with database' do
6
+
7
+ setup do
8
+ @connector = DataStore::Connector.new
9
+ end
10
+
11
+ should 'trigger the migration to create the database table' do
12
+ migration = mock
13
+ DataStore.expects(:create_data_stores).returns(migration)
14
+ migration.expects(:apply)
15
+ @connector.create_table!
16
+ end
17
+
18
+ should 'reset by dropping and recreating the database table' do
19
+ migration = mock
20
+ @connector.expects(:drop_table!)
21
+ DataStore.expects(:create_data_stores).returns(migration)
22
+ migration.expects(:apply)
23
+ @connector.reset!
24
+ end
25
+
26
+ teardown do
27
+ @connector.database.disconnect
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,117 @@
1
+ require File.expand_path '../test_helper', __FILE__
2
+
3
+ class DataStoreTest < Test::Unit::TestCase
4
+
5
+ context 'DataStore configuration' do
6
+
7
+ should 'have a configuration object' do
8
+ assert_equal true, DataStore.configuration.is_a?(DataStore::Configuration)
9
+ end
10
+
11
+ should 'be able to define the configuration' do
12
+ assert_equal ENV['DB'] || :postgres, DataStore.configuration.database
13
+ end
14
+
15
+ end
16
+
17
+ context 'DataStore::Base general' do
18
+
19
+ setup do
20
+ DataStore::Connector.new.reset!
21
+ end
22
+
23
+ context 'with added behaviour through Sequel::Model' do
24
+
25
+ setup do
26
+ @record = DataStore::Base.create(identifier: 1,
27
+ type: 'gauge',
28
+ name: 'Electra',
29
+ description: 'Actual usage of electra in the home',
30
+ compression_schema: [5,4,3])
31
+ end
32
+
33
+ should 'be valid' do
34
+ assert @record
35
+ end
36
+
37
+ should 'have added a record to the database' do
38
+ assert_equal 1, DataStore::Base.count
39
+ end
40
+
41
+ should 'have created the necessary tables' do
42
+ assert_equal 0, DataStore::Base.db[:ds_1].count
43
+ assert_equal 0, DataStore::Base.db[:ds_1_5].count
44
+ assert_equal 0, DataStore::Base.db[:ds_1_20].count
45
+ assert_equal 0, DataStore::Base.db[:ds_1_60].count
46
+ end
47
+
48
+ should 'return all table_names' do
49
+ assert_equal [:ds_1, :ds_1_5, :ds_1_20, :ds_1_60], @record.table_names
50
+ end
51
+
52
+ should 'return its time_borders' do
53
+ assert_equal [8000, 40000, 160000, 480000], @record.time_borders
54
+ end
55
+
56
+ should 'return its attributes' do
57
+ record = DataStore::Base.order(:created_at).last
58
+ assert_equal 1, record.identifier
59
+ assert_equal 'gauge', record.type
60
+ assert_equal 'Electra', record.name
61
+ assert_equal 'Actual usage of electra in the home', record.description
62
+ assert_equal [5,4,3], @record.compression_schema
63
+ end
64
+
65
+ should 'return default values if not set' do
66
+ assert_equal 10, @record.frequency
67
+ assert_equal 'double', @record.data_type
68
+ assert_equal 800, @record.maximum_datapoints
69
+ end
70
+
71
+ should 'have timestamps' do
72
+ assert @record.created_at
73
+ assert @record.updated_at
74
+ end
75
+
76
+ should 'create a record with a uniq identifier' do
77
+ assert_raise 'Sequel::DatabaseError(<SQLite3::ConstraintException: column identifier is not unique>)' do
78
+ DataStore::Base.create(identifier: 1, type: 'gauge', name: 'Electra')
79
+ end
80
+ end
81
+
82
+ should 'be able to update a record' do
83
+ @record.name = 'Gas'
84
+ @record.save
85
+ assert_equal 'Gas', DataStore::Base.order(:created_at).last.name
86
+ end
87
+
88
+ end
89
+
90
+ should 'create with the correct data type for value' do
91
+ record = DataStore::Base.create(identifier: 2, type: 'gauge', name: 'Electra', data_type: 'integer')
92
+ assert_equal :integer,Sequel::Model(DataStore::Base.db[:ds_2]).db_schema[:value][:type]
93
+ record.destroy
94
+ end
95
+
96
+ context 'handling of database tables for the datapoints' do
97
+
98
+ should 'create the necessary datapoint tables on create' do
99
+ DataStore::Base.any_instance.expects(:drop_tables!)
100
+ DataStore::Base.any_instance.expects(:create_tables!)
101
+ DataStore::Base.create(identifier: 1, type: 'gauge', name: 'Electra')
102
+ end
103
+
104
+ should 'destroy the corresponding datapoint tables on destroy' do
105
+ record = DataStore::Base.create(identifier: 1, type: 'gauge', name: 'Electra')
106
+ record.destroy
107
+ assert_raise { DataStore::Base.db[:ds_1].count }
108
+ assert_raise { DataStore::Base.db[:ds_5].count }
109
+ assert_raise { DataStore::Base.db[:ds_20].count }
110
+ assert_raise { DataStore::Base.db[:ds_60].count }
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
117
+ end
@@ -0,0 +1,66 @@
1
+ require File.expand_path '../test_helper', __FILE__
2
+
3
+ class IntegrationTest < Test::Unit::TestCase
4
+
5
+ context 'Integration test by adding datapoints through table object' do
6
+
7
+ setup do
8
+ DataStore::Connector.new.reset!
9
+ @record = DataStore::Base.create(identifier: 1,
10
+ type: 'counter',
11
+ name: 'Electra',
12
+ frequency: 10,
13
+ description: 'Actual usage of gas in the home',
14
+ compression_schema: [2,2])
15
+
16
+ @table = DataStore::Table.new(1)
17
+ @calculator = DataStore::AverageCalculator.new(@table)
18
+ end
19
+
20
+ should 'also calculate the average value' do
21
+ time_now_utc_returns(0)
22
+ @table.add(1000)
23
+
24
+ time_now_utc_returns(10)
25
+ @table.add(1010)
26
+
27
+ time_now_utc_returns(20)
28
+ @table.add(1020)
29
+
30
+ assert_equal 10.0, DataStore::Base.db[:ds_1_2].order(:created).last[:value]
31
+ end
32
+
33
+ end
34
+
35
+ context 'Import datapoints (gauge type)' do
36
+ setup do
37
+ start_time = 1349042407.00000
38
+ values = [2380.0, 2370.0, 2380.0, 2380.0, 2390.0, 2390.0, 2390.0, 2380.0, 2380.0, 2380.0, 2380.0, 2370.0, 2370.0, 2370.0,
39
+ 2380.0, 2380.0, 2380.0, 2380.0, 230.0, 230.0, 230.0, 230.0, 230.0, 230.0]
40
+ @datapoints = []
41
+ values.each do |value|
42
+ @datapoints << [value, start_time]
43
+ start_time += rand(9.95..10.05)
44
+ end
45
+ DataStore::Connector.new.reset!
46
+ @record = DataStore::Base.create(identifier: 1,
47
+ type: 'gauge',
48
+ name: 'Electra',
49
+ description: 'Actual usage of electra in the home',
50
+ compression_schema: [2,3])
51
+ @table = DataStore::Table.new(1)
52
+ end
53
+
54
+ should 'store the data and calculate all averages' do
55
+ @table.import(@datapoints)
56
+ assert_equal 24, @table.model.db[:ds_1].count
57
+ assert_equal 12, @table.model.db[:ds_1_2].count
58
+ assert_equal 4, @table.model.db[:ds_1_6].count
59
+
60
+ assert_equal 1842, @table.model.db[:ds_1].avg(:value).round
61
+ assert_equal 1842, @table.model.db[:ds_1_2].avg(:value).round
62
+ assert_equal 1842, @table.model.db[:ds_1_6].avg(:value).round
63
+ end
64
+ end
65
+
66
+ end