zermelo 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +10 -0
- data/.travis.yml +27 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +512 -0
- data/Rakefile +1 -0
- data/lib/zermelo/associations/association_data.rb +24 -0
- data/lib/zermelo/associations/belongs_to.rb +115 -0
- data/lib/zermelo/associations/class_methods.rb +244 -0
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +128 -0
- data/lib/zermelo/associations/has_many.rb +120 -0
- data/lib/zermelo/associations/has_one.rb +109 -0
- data/lib/zermelo/associations/has_sorted_set.rb +124 -0
- data/lib/zermelo/associations/index.rb +50 -0
- data/lib/zermelo/associations/index_data.rb +18 -0
- data/lib/zermelo/associations/unique_index.rb +44 -0
- data/lib/zermelo/backends/base.rb +115 -0
- data/lib/zermelo/backends/influxdb_backend.rb +178 -0
- data/lib/zermelo/backends/redis_backend.rb +281 -0
- data/lib/zermelo/filters/base.rb +235 -0
- data/lib/zermelo/filters/influxdb_filter.rb +162 -0
- data/lib/zermelo/filters/redis_filter.rb +558 -0
- data/lib/zermelo/filters/steps/base_step.rb +22 -0
- data/lib/zermelo/filters/steps/diff_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/diff_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/intersect_step.rb +17 -0
- data/lib/zermelo/filters/steps/limit_step.rb +17 -0
- data/lib/zermelo/filters/steps/offset_step.rb +17 -0
- data/lib/zermelo/filters/steps/sort_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_range_step.rb +17 -0
- data/lib/zermelo/filters/steps/union_step.rb +17 -0
- data/lib/zermelo/locks/no_lock.rb +16 -0
- data/lib/zermelo/locks/redis_lock.rb +221 -0
- data/lib/zermelo/records/base.rb +62 -0
- data/lib/zermelo/records/class_methods.rb +127 -0
- data/lib/zermelo/records/collection.rb +14 -0
- data/lib/zermelo/records/errors.rb +24 -0
- data/lib/zermelo/records/influxdb_record.rb +35 -0
- data/lib/zermelo/records/instance_methods.rb +224 -0
- data/lib/zermelo/records/key.rb +19 -0
- data/lib/zermelo/records/redis_record.rb +27 -0
- data/lib/zermelo/records/type_validator.rb +20 -0
- data/lib/zermelo/version.rb +3 -0
- data/lib/zermelo.rb +102 -0
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_many_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_one_spec.rb +6 -0
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +6 -0
- data/spec/lib/zermelo/associations/index_spec.rb +6 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +6 -0
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +170 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +258 -0
- data/spec/lib/zermelo/records/key_spec.rb +6 -0
- data/spec/lib/zermelo/records/redis_record_spec.rb +1426 -0
- data/spec/lib/zermelo/records/type_validator_spec.rb +6 -0
- data/spec/lib/zermelo/version_spec.rb +6 -0
- data/spec/lib/zermelo_spec.rb +6 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/profile_all_formatter.rb +44 -0
- data/spec/support/uncolored_doc_formatter.rb +74 -0
- data/zermelo.gemspec +30 -0
- metadata +174 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
module Zermelo
|
2
|
+
module Associations
|
3
|
+
class HasOne
|
4
|
+
|
5
|
+
def initialize(parent, name)
|
6
|
+
@parent = parent
|
7
|
+
|
8
|
+
@backend = parent.send(:backend)
|
9
|
+
|
10
|
+
# TODO would be better as a 'has_one' hash, a bit like belongs_to
|
11
|
+
@record_id_key = Zermelo::Records::Key.new(
|
12
|
+
:klass => parent.class.send(:class_key),
|
13
|
+
:id => parent.id,
|
14
|
+
:name => "#{name}_id",
|
15
|
+
:type => :string,
|
16
|
+
:object => :association
|
17
|
+
)
|
18
|
+
|
19
|
+
parent.class.send(:with_association_data, name.to_sym) do |data|
|
20
|
+
@associated_class = data.data_klass
|
21
|
+
@lock_klasses = [data.data_klass] + data.related_klasses
|
22
|
+
@inverse = data.inverse
|
23
|
+
@callbacks = data.callbacks
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def value
|
28
|
+
@parent.class.lock(*@associated_class) do
|
29
|
+
if id = @backend.get(@record_id_key)
|
30
|
+
@associated_class.send(:load, id)
|
31
|
+
else
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def value=(record)
|
38
|
+
if record.nil?
|
39
|
+
@parent.class.lock(*@lock_klasses) do
|
40
|
+
id = @backend.get(@record_id_key)
|
41
|
+
unless id.nil?
|
42
|
+
r = @associated_class.send(:load, id)
|
43
|
+
unless r.nil?
|
44
|
+
bc = @callbacks[:before_clear]
|
45
|
+
if bc.nil? || !@parent.respond_to?(bc) || !@parent.send(bc, r).is_a?(FalseClass)
|
46
|
+
delete_without_lock(r)
|
47
|
+
ac = @callbacks[:after_clear]
|
48
|
+
@parent.send(ac, r) if !ac.nil? && @parent.respond_to?(ac)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
raise 'Invalid record class' unless record.is_a?(@associated_class)
|
55
|
+
raise 'Record must have been saved' unless record.persisted?
|
56
|
+
@parent.class.lock(*@lock_klasses) do
|
57
|
+
bs = @callbacks[:before_set]
|
58
|
+
if bs.nil? || !@parent.respond_to?(bs) || !@parent.send(bs, r).is_a?(FalseClass)
|
59
|
+
unless @inverse.nil?
|
60
|
+
@associated_class.send(:load, record.id).send("#{@inverse}=", @parent)
|
61
|
+
end
|
62
|
+
|
63
|
+
new_txn = @backend.begin_transaction
|
64
|
+
@backend.set(@record_id_key, record.id)
|
65
|
+
@backend.commit_transaction if new_txn
|
66
|
+
as = @callbacks[:after_set]
|
67
|
+
@parent.send(as, record) if !as.nil? && @parent.respond_to?(as)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def delete_without_lock(record)
|
76
|
+
unless @inverse.nil?
|
77
|
+
record.send("#{@inverse}=", nil)
|
78
|
+
end
|
79
|
+
new_txn = @backend.begin_transaction
|
80
|
+
@backend.clear(@record_id_key)
|
81
|
+
@backend.commit_transaction if new_txn
|
82
|
+
end
|
83
|
+
|
84
|
+
# associated will be a belongs_to; on_remove already runs inside lock and transaction
|
85
|
+
def on_remove
|
86
|
+
unless @inverse.nil?
|
87
|
+
if record_id = @backend.get(@record_id_key)
|
88
|
+
@associated_class.send(:load, record_id).send("#{@inverse}=", nil)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
@backend.clear(@record_id_key)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.associated_ids_for(backend, class_key, name, *these_ids)
|
95
|
+
these_ids.each_with_object({}) do |this_id, memo|
|
96
|
+
key = Zermelo::Records::Key.new(
|
97
|
+
:klass => class_key,
|
98
|
+
:id => this_id,
|
99
|
+
:name => "#{name}_id",
|
100
|
+
:type => :string,
|
101
|
+
:object => :association
|
102
|
+
)
|
103
|
+
memo[this_id] = backend.get(key)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Zermelo
|
4
|
+
module Associations
|
5
|
+
class HasSortedSet
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :filter, :intersect, :union, :diff, :sort,
|
10
|
+
:intersect_range, :union_range, :diff_range,
|
11
|
+
:find_by_id, :find_by_ids, :find_by_id!, :find_by_ids!,
|
12
|
+
:all, :each, :collect, :map,
|
13
|
+
:select, :find_all, :reject, :destroy_all,
|
14
|
+
:ids, :count, :empty?, :exists?,
|
15
|
+
:first, :last,
|
16
|
+
:associated_ids_for
|
17
|
+
|
18
|
+
def initialize(parent, name)
|
19
|
+
@parent = parent
|
20
|
+
|
21
|
+
@backend = parent.send(:backend)
|
22
|
+
|
23
|
+
@record_ids_key = Zermelo::Records::Key.new(
|
24
|
+
:klass => parent.class.send(:class_key),
|
25
|
+
:id => parent.id,
|
26
|
+
:name => "#{name}_ids",
|
27
|
+
:type => :sorted_set,
|
28
|
+
:object => :association
|
29
|
+
)
|
30
|
+
|
31
|
+
parent.class.send(:with_association_data, name.to_sym) do |data|
|
32
|
+
@associated_class = data.data_klass
|
33
|
+
@lock_klasses = [data.data_klass] + data.related_klasses
|
34
|
+
@inverse = data.inverse
|
35
|
+
@sort_key = data.sort_key
|
36
|
+
@callbacks = data.callbacks
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def <<(record)
|
41
|
+
add(record)
|
42
|
+
self # for << 'a' << 'b'
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO collect all scores/ids and do a single zadd/single hmset
|
46
|
+
def add(*records)
|
47
|
+
raise 'No records to add' if records.empty?
|
48
|
+
raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
|
49
|
+
raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?}
|
50
|
+
@parent.class.lock(*@lock_klasses) do
|
51
|
+
ba = @callbacks[:before_add]
|
52
|
+
if ba.nil? || !@parent.respond_to?(ba) || !@parent.send(ba, *records).is_a?(FalseClass)
|
53
|
+
unless @inverse.nil?
|
54
|
+
records.each do |record|
|
55
|
+
@associated_class.send(:load, record.id).send("#{@inverse}=", @parent)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
new_txn = @backend.begin_transaction
|
60
|
+
@backend.add(@record_ids_key, (records.map {|r| [r.send(@sort_key.to_sym).to_f, r.id]}.flatten))
|
61
|
+
@backend.commit_transaction if new_txn
|
62
|
+
aa = @callbacks[:after_add]
|
63
|
+
@parent.send(aa, *records) if !aa.nil? && @parent.respond_to?(aa)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(*records)
|
69
|
+
raise 'No records to delete' if records.empty?
|
70
|
+
raise 'Invalid record class' if records.any? {|r| !r.is_a?(@associated_class)}
|
71
|
+
raise 'Record(s) must have been saved' unless records.all? {|r| r.persisted?}
|
72
|
+
@parent.class.lock(*@lock_klasses) do
|
73
|
+
br = @callbacks[:before_remove]
|
74
|
+
if br.nil? || !@parent.respond_to?(br) || !@parent.send(br, *records).is_a?(FalseClass)
|
75
|
+
unless @inverse.nil?
|
76
|
+
records.each do |record|
|
77
|
+
@associated_class.send(:load, record.id).send("#{@inverse}=", nil)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
new_txn = @backend.begin_transaction
|
82
|
+
@backend.delete(@record_ids_key, records.map(&:id))
|
83
|
+
@backend.commit_transaction if new_txn
|
84
|
+
ar = @callbacks[:after_remove]
|
85
|
+
@parent.send(ar, *records) if !ar.nil? && @parent.respond_to?(ar)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# associated will be a belongs_to; on remove already runs inside a lock and transaction
|
93
|
+
def on_remove
|
94
|
+
unless @inverse.nil?
|
95
|
+
self.ids.each do |record_id|
|
96
|
+
# clear the belongs_to inverse value with this @parent.id
|
97
|
+
@associated_class.send(:load, record_id).send("#{@inverse}=", nil)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
@backend.clear(@record_ids_key)
|
101
|
+
end
|
102
|
+
|
103
|
+
# creates a new filter class each time it's called, to store the
|
104
|
+
# state for this particular filter chain
|
105
|
+
def filter
|
106
|
+
@backend.filter(@record_ids_key, @associated_class)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.associated_ids_for(backend, class_key, name, *these_ids)
|
110
|
+
these_ids.each_with_object({}) do |this_id, memo|
|
111
|
+
key = Zermelo::Records::Key.new(
|
112
|
+
:klass => class_key,
|
113
|
+
:id => this_id,
|
114
|
+
:name => "#{name}_ids",
|
115
|
+
:type => :sorted_set,
|
116
|
+
:object => :association
|
117
|
+
)
|
118
|
+
memo[this_id] = backend.get(key)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# NB index instances are all internal to zermelo, not user-accessible
|
2
|
+
|
3
|
+
module Zermelo
|
4
|
+
module Associations
|
5
|
+
class Index
|
6
|
+
|
7
|
+
def initialize(parent_klass, name)
|
8
|
+
@parent_klass = parent_klass
|
9
|
+
@attribute_name = name
|
10
|
+
|
11
|
+
@backend = parent_klass.send(:backend)
|
12
|
+
@class_key = parent_klass.send(:class_key)
|
13
|
+
|
14
|
+
@indexers = {}
|
15
|
+
|
16
|
+
parent_klass.send(:with_index_data, name.to_sym) do |data|
|
17
|
+
@attribute_type = data.type
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete_id(id, value)
|
22
|
+
return unless indexer = key(value)
|
23
|
+
@backend.delete(indexer, id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_id(id, value)
|
27
|
+
return unless indexer = key(value)
|
28
|
+
@backend.add(indexer, id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def move_id(id, value_from, indexer_to, value_to)
|
32
|
+
return unless indexer = key(value_from)
|
33
|
+
@backend.move(indexer, id, indexer_to.key(value_to))
|
34
|
+
end
|
35
|
+
|
36
|
+
def key(value)
|
37
|
+
index_keys = @backend.index_keys(@attribute_type, value)
|
38
|
+
raise "Can't index '#{@value}' (#{@attribute_type}" if index_keys.nil?
|
39
|
+
|
40
|
+
@indexers[index_keys.join(":")] ||= Zermelo::Records::Key.new(
|
41
|
+
:klass => @class_key,
|
42
|
+
:name => "by_#{@attribute_name}:#{index_keys.join(':')}",
|
43
|
+
:type => :set,
|
44
|
+
:object => :index
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Zermelo
|
2
|
+
module Associations
|
3
|
+
class IndexData
|
4
|
+
attr_writer :data_klass_name
|
5
|
+
attr_accessor :name, :type, :index_klass
|
6
|
+
|
7
|
+
def initialize(opts = {})
|
8
|
+
[:name, :type, :index_klass].each do |a|
|
9
|
+
send("#{a}=".to_sym, opts[a])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def data_klass
|
14
|
+
@data_klass ||= @data_klass_name.constantize
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# NB index instances are all internal to zermelo, not user-accessible
|
2
|
+
|
3
|
+
module Zermelo
|
4
|
+
module Associations
|
5
|
+
class UniqueIndex
|
6
|
+
|
7
|
+
def initialize(parent_klass, name)
|
8
|
+
@parent_klass = parent_klass
|
9
|
+
@attribute_name = name
|
10
|
+
|
11
|
+
@backend = parent_klass.send(:backend)
|
12
|
+
@class_key = parent_klass.send(:class_key)
|
13
|
+
|
14
|
+
@indexers = {}
|
15
|
+
|
16
|
+
parent_klass.send(:with_index_data, name.to_sym) do |data|
|
17
|
+
@attribute_type = data.type
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete_id(id, value)
|
22
|
+
@backend.delete(key, @backend.index_keys(@attribute_type, value).join(':'))
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_id(id, value)
|
26
|
+
@backend.add(key, @backend.index_keys(@attribute_type, value).join(':') => id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def move_id(id, value_from, indexer_to, value_to)
|
30
|
+
@backend.move(key, {@backend.index_keys(@attribute_type, value_to).join(':') => id}, indexer_to.key)
|
31
|
+
end
|
32
|
+
|
33
|
+
def key
|
34
|
+
@indexer ||= Zermelo::Records::Key.new(
|
35
|
+
:klass => @class_key,
|
36
|
+
:name => "by_#{@attribute_name}",
|
37
|
+
:type => :hash,
|
38
|
+
:object => :index
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
require 'zermelo/locks/no_lock'
|
4
|
+
|
5
|
+
module Zermelo
|
6
|
+
|
7
|
+
module Backends
|
8
|
+
|
9
|
+
module Base
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
def escape_key_name(name)
|
14
|
+
name.gsub(/%/, '%%').gsub(/ /, '%20').gsub(/:/, '%3A')
|
15
|
+
end
|
16
|
+
|
17
|
+
def unescape_key_name(name)
|
18
|
+
name.gsub(/%3A/, ':').gsub(/%20/, ' ').gsub(/%%/, '%')
|
19
|
+
end
|
20
|
+
|
21
|
+
def index_keys(type, value)
|
22
|
+
return ["null", "null"] if value.nil?
|
23
|
+
|
24
|
+
case type
|
25
|
+
when :string
|
26
|
+
["string", escape_key_name(value)]
|
27
|
+
when :integer
|
28
|
+
["integer", escape_key_name(value.to_s)]
|
29
|
+
when :float
|
30
|
+
["float", escape_key_name(value.to_s)]
|
31
|
+
when :timestamp
|
32
|
+
case value
|
33
|
+
when Integer
|
34
|
+
["timestamp", escape_key_name(value.to_s)]
|
35
|
+
when Time, DateTime
|
36
|
+
["timestamp", escape_key_name(value.to_i.to_s)]
|
37
|
+
end
|
38
|
+
when :boolean
|
39
|
+
case value
|
40
|
+
when TrueClass
|
41
|
+
["boolean", "true"]
|
42
|
+
when FalseClass
|
43
|
+
["boolean", "false"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# for hashes, lists, sets
|
49
|
+
def add(key, value)
|
50
|
+
change(:add, key, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete(key, value)
|
54
|
+
change(:delete, key, value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def move(key, value, key_to)
|
58
|
+
change(:move, key, value, key_to)
|
59
|
+
end
|
60
|
+
|
61
|
+
def clear(key)
|
62
|
+
change(:clear, key)
|
63
|
+
end
|
64
|
+
|
65
|
+
# works for both simple and complex types (i.e. strings, numbers, booleans,
|
66
|
+
# hashes, lists, sets)
|
67
|
+
def set(key, value)
|
68
|
+
change(:set, key, value)
|
69
|
+
end
|
70
|
+
|
71
|
+
def purge(key)
|
72
|
+
change(:purge, key)
|
73
|
+
end
|
74
|
+
|
75
|
+
def get(attr_key)
|
76
|
+
get_multiple(attr_key)[attr_key.klass][attr_key.id][attr_key.name.to_s]
|
77
|
+
end
|
78
|
+
|
79
|
+
def lock(*klasses, &block)
|
80
|
+
ret = nil
|
81
|
+
# doesn't handle re-entrant case for influxdb, which has no locking yet
|
82
|
+
locking = Thread.current[:zermelo_locking]
|
83
|
+
if locking.nil?
|
84
|
+
lock_proc = proc do
|
85
|
+
begin
|
86
|
+
Thread.current[:zermelo_locking] = klasses
|
87
|
+
ret = block.call
|
88
|
+
ensure
|
89
|
+
Thread.current[:zermelo_locking] = nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
lock_klass = case self
|
94
|
+
when Zermelo::Backends::RedisBackend
|
95
|
+
Zermelo::Locks::RedisLock
|
96
|
+
else
|
97
|
+
Zermelo::Locks::NoLock
|
98
|
+
end
|
99
|
+
|
100
|
+
lock_klass.new.lock(*klasses, &lock_proc)
|
101
|
+
else
|
102
|
+
# accepts any subset of 'locking'
|
103
|
+
unless (klasses - locking).empty?
|
104
|
+
raise "Currently locking #{locking.map(&:name)}, cannot lock different set #{klasses.map(&:name)}"
|
105
|
+
end
|
106
|
+
ret = block.call
|
107
|
+
end
|
108
|
+
ret
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'zermelo/backends/base'
|
2
|
+
|
3
|
+
require 'zermelo/filters/influxdb_filter'
|
4
|
+
|
5
|
+
# NB influxdb doesn't support individually addressable deletes, so
|
6
|
+
# this backend only works to write new records
|
7
|
+
# (it could just write the new state of the record, and query id by newest limit 1,
|
8
|
+
# but for the moment, YAGNI)
|
9
|
+
|
10
|
+
module Zermelo
|
11
|
+
|
12
|
+
module Backends
|
13
|
+
|
14
|
+
class InfluxDBBackend
|
15
|
+
|
16
|
+
include Zermelo::Backends::Base
|
17
|
+
|
18
|
+
def filter(ids_key, record)
|
19
|
+
Zermelo::Filters::InfluxDBFilter.new(self, ids_key, record)
|
20
|
+
end
|
21
|
+
|
22
|
+
# TODO get filter calling this instead of using same logic
|
23
|
+
def exists?(key)
|
24
|
+
return if key.id.nil?
|
25
|
+
Zermelo.influxdb.query("SELECT id FROM /#{key.klass}\\/.*/ LIMIT 1").size > 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# nb: does lots of queries, should batch, but ensuring single operations are correct
|
29
|
+
# for now
|
30
|
+
def get_multiple(*attr_keys)
|
31
|
+
attr_keys.inject({}) do |memo, attr_key|
|
32
|
+
begin
|
33
|
+
records = Zermelo.influxdb.query("SELECT #{attr_key.name} FROM " +
|
34
|
+
"\"#{attr_key.klass}/#{attr_key.id}\" LIMIT 1")["#{attr_key.klass}/#{attr_key.id}"]
|
35
|
+
rescue InfluxDB::Error => ide
|
36
|
+
raise unless
|
37
|
+
/^Field #{attr_key.name} doesn't exist in series #{attr_key.klass}\/#{attr_key.id}$/ === ide.message
|
38
|
+
|
39
|
+
records = []
|
40
|
+
end
|
41
|
+
value = (records && !records.empty?) ? records.first[attr_key.name.to_s] : nil
|
42
|
+
|
43
|
+
memo[attr_key.klass] ||= {}
|
44
|
+
memo[attr_key.klass][attr_key.id] ||= {}
|
45
|
+
|
46
|
+
memo[attr_key.klass][attr_key.id][attr_key.name.to_s] = if value.nil?
|
47
|
+
nil
|
48
|
+
else
|
49
|
+
|
50
|
+
case attr_key.type
|
51
|
+
when :string
|
52
|
+
value.to_s
|
53
|
+
when :integer
|
54
|
+
value.to_i
|
55
|
+
when :float
|
56
|
+
value.to_f
|
57
|
+
when :timestamp
|
58
|
+
Time.at(value.to_f)
|
59
|
+
when :boolean
|
60
|
+
case value
|
61
|
+
when TrueClass
|
62
|
+
true
|
63
|
+
when FalseClass
|
64
|
+
false
|
65
|
+
when String
|
66
|
+
'true'.eql?(value.downcase)
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
when :list, :hash
|
71
|
+
value
|
72
|
+
when :set
|
73
|
+
Set.new(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
memo
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def begin_transaction
|
81
|
+
return false if @in_transaction
|
82
|
+
@in_transaction = true
|
83
|
+
@changes = []
|
84
|
+
end
|
85
|
+
|
86
|
+
def commit_transaction
|
87
|
+
return false unless @in_transaction
|
88
|
+
apply_changes(@changes)
|
89
|
+
@in_transaction = false
|
90
|
+
@changes = []
|
91
|
+
end
|
92
|
+
|
93
|
+
def abort_transaction
|
94
|
+
return false unless @in_transaction
|
95
|
+
@in_transaction = false
|
96
|
+
@changes = []
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def change(op, key, value = nil)
|
102
|
+
ch = [op, key, value]
|
103
|
+
if @in_transaction
|
104
|
+
@changes << ch
|
105
|
+
else
|
106
|
+
apply_changes([ch])
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# composite all new changes into records, and then into influxdb
|
111
|
+
# query statements
|
112
|
+
def apply_changes(changes)
|
113
|
+
records = {}
|
114
|
+
|
115
|
+
purges = []
|
116
|
+
|
117
|
+
changes.each do |ch|
|
118
|
+
op = ch[0]
|
119
|
+
key = ch[1]
|
120
|
+
value = ch[2]
|
121
|
+
|
122
|
+
next if key.id.nil?
|
123
|
+
|
124
|
+
records[key.klass] ||= {}
|
125
|
+
records[key.klass][key.id] ||= {}
|
126
|
+
|
127
|
+
records[key.klass][key.id][key.name] = case op
|
128
|
+
when :set
|
129
|
+
case key.type
|
130
|
+
when :string, :integer
|
131
|
+
value.to_s
|
132
|
+
when :timestamp
|
133
|
+
value.to_f
|
134
|
+
when :boolean
|
135
|
+
(!!value).to_s
|
136
|
+
when :list, :hash
|
137
|
+
value
|
138
|
+
when :set
|
139
|
+
value.to_a
|
140
|
+
end
|
141
|
+
when :add
|
142
|
+
case key.type
|
143
|
+
when :list, :hash
|
144
|
+
value
|
145
|
+
when :set
|
146
|
+
value.to_a
|
147
|
+
end
|
148
|
+
when :purge
|
149
|
+
purges << "\"#{key.klass}/#{key.id}\""
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
records.each_pair do |klass, klass_records|
|
155
|
+
klass_records.each_pair do |id, data|
|
156
|
+
begin
|
157
|
+
prior = Zermelo.influxdb.query("SELECT * FROM \"#{klass}/#{id}\" LIMIT 1")["#{klass}/#{id}"]
|
158
|
+
rescue InfluxDB::Error => ide
|
159
|
+
raise unless
|
160
|
+
(/^Couldn't look up columns for series: #{klass}\/#{id}$/ === ide.message) ||
|
161
|
+
(/^Couldn't look up columns$/ === ide.message) ||
|
162
|
+
(/^Couldn't find series: #{klass}\/#{id}$/ === ide.message)
|
163
|
+
|
164
|
+
prior = nil
|
165
|
+
end
|
166
|
+
record = prior.nil? ? {} : prior.first.delete_if {|k,v| ["time", "sequence_number"].include?(k) }
|
167
|
+
Zermelo.influxdb.write_point("#{klass}/#{id}", record.merge(data).merge('id' => id))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
purges.each {|purge| Zermelo.influxdb.query("DROP SERIES #{purge}") }
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|