cequel 0.4.2 → 0.5.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/lib/cequel.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'active_support/core_ext'
2
2
  require 'cassandra-cql'
3
+ require 'connection_pool'
3
4
 
4
5
  require 'cequel/batch'
5
6
  require 'cequel/errors'
@@ -60,13 +60,34 @@ module Cequel
60
60
  append(generate_upsert_options(options)).
61
61
  append(" SET " << data.keys.map { |k| "? = ?" }.join(', '), *data.to_a.flatten).
62
62
  append(*row_specifications_cql)
63
-
64
63
  @keyspace.write(*statement.args)
65
64
  rescue EmptySubquery
66
65
  # Noop -- no rows to update
67
66
  end
68
67
 
69
- #
68
+ def increment(data, options = {})
69
+ operations = data.map do |key, value|
70
+ operator = value < 0 ? '-' : '+'
71
+ "? = ? #{operator} ?"
72
+ end
73
+ statement = Statement.new.
74
+ append("UPDATE #{@column_family}").
75
+ append(generate_upsert_options(options)).
76
+ append(
77
+ " SET " << operations.join(', '),
78
+ *data.flat_map { |column, count| [column, column, count.abs] }
79
+ ).append(*row_specifications_cql)
80
+
81
+ @keyspace.write(*statement.args)
82
+ end
83
+ alias_method :incr, :increment
84
+
85
+ def decrement(data, options = {})
86
+ increment(Hash[data.map { |column, count| [column, -count] }], options)
87
+ end
88
+ alias_method :decr, :decrement
89
+
90
+ #
70
91
  # Delete data from the column family
71
92
  #
72
93
  # @param columns zero or more columns to delete. Deletes the entire row if none specified.
@@ -5,25 +5,81 @@ module Cequel
5
5
  #
6
6
  class Keyspace
7
7
 
8
- #
9
- # Set a logger for logging queries. Queries logged at INFO level
10
- #
11
- attr_writer :logger, :slowlog, :slowlog_threshold, :connection
12
-
13
8
  #
14
9
  # @api private
15
10
  # @see Cequel.connect
16
11
  #
17
- def initialize(configuration = {})
18
- @name = configuration[:keyspace]
12
+ def initialize(configuration={})
13
+ configure(configuration)
14
+ end
15
+
16
+ def connection=(connection)
17
+ @connection = connection
18
+ end
19
+
20
+ def configure(configuration = {})
21
+ @configuration = configuration
19
22
  @hosts = configuration[:host] || configuration[:hosts]
20
23
  @thrift_options = configuration[:thrift].try(:symbolize_keys) || {}
24
+ @keyspace = configuration[:keyspace]
25
+ # reset the connections
26
+ clear_active_connections!
27
+ end
28
+
29
+ def logger=(logger)
30
+ @logger = logger
31
+ end
32
+
33
+ def logger
34
+ @logger
35
+ end
36
+
37
+ def slowlog=(slowlog)
38
+ @slowlog = slowlog
39
+ end
40
+
41
+ def slowlog
42
+ @slowlog
43
+ end
44
+
45
+ def slowlog_threshold=(slowlog_threshold)
46
+ @slowlog_threshold = slowlog_threshold
47
+ end
48
+
49
+ def slowlog_threshold
50
+ @slowlog_threshold
51
+ end
52
+
53
+ def connection_pool
54
+ return @connection_pool if defined? @connection_pool
55
+ if @configuration[:pool]
56
+ options = {
57
+ :size => configuration[:pool],
58
+ :timeout => configuration[:pool_timeout]
59
+ }
60
+ @connection_pool = ConnectionPool.new(:size => 10, :timeout => 5) do
61
+ build_connection
62
+ end
63
+ else
64
+ @connection_pool = nil
65
+ end
21
66
  end
22
67
 
23
68
  def connection
24
- @connection ||= CassandraCQL::Database.new(
25
- @hosts, {:keyspace => @name}, @thrift_options
26
- )
69
+ @connection ||= build_connection
70
+ end
71
+
72
+ def clear_active_connections!
73
+ remove_instance_variable(:@connection) if defined? @connection
74
+ remove_instance_variable(:@connection_pool) if defined? @connection_pool
75
+ end
76
+
77
+ def with_connection(&block)
78
+ if connection_pool
79
+ connection_pool.with(&block)
80
+ else
81
+ yield connection
82
+ end
27
83
  end
28
84
 
29
85
  #
@@ -44,7 +100,9 @@ module Cequel
44
100
  #
45
101
  def execute(statement, *bind_vars)
46
102
  log('CQL', statement, *bind_vars) do
47
- connection.execute(statement, *bind_vars)
103
+ with_connection do |conn|
104
+ conn.execute(statement, *bind_vars)
105
+ end
48
106
  end
49
107
  end
50
108
 
@@ -55,8 +113,8 @@ module Cequel
55
113
  # @param (see #execute)
56
114
  #
57
115
  def write(statement, *bind_vars)
58
- if @batch
59
- @batch.execute(statement, *bind_vars)
116
+ if get_batch
117
+ get_batch.execute(statement, *bind_vars)
60
118
  else
61
119
  execute(statement, *bind_vars)
62
120
  end
@@ -76,17 +134,40 @@ module Cequel
76
134
  # end
77
135
  #
78
136
  def batch(options = {})
79
- old_batch, @batch = @batch, Batch.new(self, options)
137
+ old_batch = get_batch
138
+ new_batch = Batch.new(self, options)
139
+ set_batch(new_batch)
80
140
  yield
81
- @batch.apply
141
+ new_batch.apply
82
142
  ensure
83
- @batch = old_batch
143
+ set_batch(old_batch)
84
144
  end
85
145
 
86
146
  private
87
147
 
148
+ def build_connection
149
+ options = @keyspace ? {:keyspace => @keyspace } : {}
150
+ CassandraCQL::Database.new(
151
+ @hosts,
152
+ options,
153
+ @thrift_options
154
+ )
155
+ end
156
+
157
+ def get_batch
158
+ ::Thread.current[batch_key]
159
+ end
160
+
161
+ def set_batch(batch)
162
+ ::Thread.current[batch_key] = batch
163
+ end
164
+
165
+ def batch_key
166
+ :"cequel-batch-#{object_id}"
167
+ end
168
+
88
169
  def log(label, statement, *bind_vars)
89
- return yield unless @logger || @slowlog
170
+ return yield unless logger || slowlog
90
171
  response = nil
91
172
  time = Benchmark.ms { response = yield }
92
173
  generate_message = proc do
@@ -95,9 +176,11 @@ module Cequel
95
176
  CassandraCQL::Statement.sanitize(statement, bind_vars)
96
177
  )
97
178
  end
98
- @logger.debug(&generate_message) if @logger
99
- threshold = @slowlog_threshold || 2000
100
- @slowlog.warn(&generate_message) if @slowlog && time >= threshold
179
+ logger.debug(&generate_message) if self.logger
180
+ threshold = self.slowlog_threshold || 2000
181
+ if slowlog && time >= threshold
182
+ slowlog.warn(&generate_message)
183
+ end
101
184
  response
102
185
  end
103
186
 
data/lib/cequel/model.rb CHANGED
@@ -5,6 +5,7 @@ require 'cequel/model/associations'
5
5
  require 'cequel/model/callbacks'
6
6
  require 'cequel/model/class_internals'
7
7
  require 'cequel/model/column'
8
+ require 'cequel/model/counter'
8
9
  require 'cequel/model/dictionary'
9
10
  require 'cequel/model/dirty'
10
11
  require 'cequel/model/dynamic'
@@ -37,6 +38,8 @@ module Cequel
37
38
  #
38
39
  module Model
39
40
 
41
+ @lock = Monitor.new
42
+
40
43
  extend ActiveSupport::Concern
41
44
  extend ActiveModel::Observing::ClassMethods
42
45
 
@@ -63,10 +66,12 @@ module Cequel
63
66
  end
64
67
 
65
68
  def self.keyspace
66
- @keyspace ||= Cequel.connect(@configuration).tap do |keyspace|
67
- keyspace.logger = @logger if @logger
68
- keyspace.slowlog = @slowlog if @slowlog
69
- keyspace.slowlog_threshold = @slowlog_threshold if @slowlog_threshold
69
+ @lock.synchronize do
70
+ @keyspace ||= Cequel.connect(@configuration).tap do |keyspace|
71
+ keyspace.logger = @logger if @logger
72
+ keyspace.slowlog = @slowlog if @slowlog
73
+ keyspace.slowlog_threshold = @slowlog_threshold if @slowlog_threshold
74
+ end
70
75
  end
71
76
  end
72
77
 
@@ -7,7 +7,7 @@ module Cequel
7
7
  #
8
8
  class ClassInternals
9
9
 
10
- attr_accessor :key, :current_scope, :default_scope
10
+ attr_accessor :key, :default_scope
11
11
  attr_reader :columns, :associations, :index_preference
12
12
 
13
13
  def initialize(clazz)
@@ -0,0 +1,35 @@
1
+ require 'cequel/model/readable_dictionary'
2
+
3
+ module Cequel
4
+
5
+ module Model
6
+
7
+ class Counter < ReadableDictionary
8
+
9
+ def increment(columns_or_deltas, delta = 1)
10
+ scope.increment(construct_deltas(columns_or_deltas, delta))
11
+ end
12
+
13
+ def decrement(columns_or_deltas, delta = 1)
14
+ scope.decrement(construct_deltas(columns_or_deltas, delta))
15
+ end
16
+
17
+ private
18
+
19
+ def construct_deltas(columns_or_deltas, delta)
20
+ if Hash === columns_or_deltas
21
+ columns_or_deltas
22
+ else
23
+ {}.tap do |deltas|
24
+ Array.wrap(columns_or_deltas).each do |column|
25
+ deltas[column] = delta
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -1,111 +1,67 @@
1
+ require 'cequel/model/readable_dictionary'
2
+
1
3
  module Cequel
2
4
 
3
5
  module Model
4
6
 
5
- class Dictionary
7
+ class Dictionary < ReadableDictionary
6
8
 
7
9
  class <<self
8
10
 
9
- attr_writer :column_family, :default_batch_size
10
-
11
- def key_alias
12
- @key_alias ||= :KEY
13
- end
14
-
15
- def key_type
16
- @key_type ||= :text
17
- end
18
-
19
- def comparator
20
- @comparator ||= :text
21
- end
22
-
23
11
  def validation
24
12
  @validation ||= :text
25
13
  end
26
14
 
27
- def key(key_alias, type)
28
- @key_alias, @key_type = key_alias, type
29
-
30
- module_eval(<<-RUBY)
31
- def #{key_alias.downcase}
32
- @key
33
- end
34
- RUBY
35
- end
36
-
37
15
  def maps(options)
38
16
  @comparator, @validation = *options.first
39
17
  end
40
18
 
41
- def column_family
42
- return @column_family if @column_family
43
- self.column_family_name = name.underscore.to_sym
44
- @column_family
45
- end
46
-
47
- def column_family_name=(column_family_name)
48
- self.column_family = Cequel::Model.keyspace[column_family_name]
49
- end
50
-
51
- def default_batch_size
52
- @default_batch_size || 1000
53
- end
54
-
55
- def [](key)
56
- new(key)
57
- end
58
- private :new
59
19
  end
60
20
 
61
- include Enumerable
62
-
63
- def initialize(key)
64
- @key = key
65
- setup
66
- end
67
-
68
- def []=(column, value)
69
- if value.nil?
70
- @deleted_columns << column
71
- @changed_columns.delete(column)
72
- else
73
- @changed_columns << column
74
- @deleted_columns.delete(column)
21
+ def each_pair(options = {})
22
+ return super if !block_given? || @loaded
23
+ new_columns = @changed_columns.dup
24
+ super do |column, value|
25
+ if @changed_columns.include?(column)
26
+ new_columns.delete(column)
27
+ yield column, @row[column]
28
+ elsif !@deleted_columns.include?(column)
29
+ yield column, value
30
+ end
75
31
  end
76
- @row[column] = value
32
+ new_columns.each do |column|
33
+ yield column, @row[column]
34
+ end
35
+ self
77
36
  end
78
37
 
79
38
  def [](column)
80
39
  if @loaded || @changed_columns.include?(column)
81
40
  @row[column]
82
41
  elsif !@deleted_columns.include?(column)
83
- value = scope.select(column).first[column]
42
+ value = super
84
43
  deserialize_value(column, value) if value
85
44
  end
86
45
  end
87
46
 
88
- def keys
89
- @loaded ? @row.keys : each_pair.map { |key, value| key }
90
- end
91
-
92
- def values
93
- @loaded ? @row.values : each_pair.map { |key, value| value }
94
- end
95
-
96
47
  def slice(*columns)
97
- if @loaded
98
- @row.slice(*columns)
99
- else
100
- deserialize_row(load_raw_slice(columns)).tap do |slice|
48
+ super.tap do |slice|
49
+ unless @loaded
101
50
  slice.merge!(@row.slice(*columns))
102
51
  @deleted_columns.each { |column| slice.delete(column) }
103
52
  end
104
53
  end
105
54
  end
106
55
 
107
- def key?(column)
108
- @row.key?(column) || load_raw_slice([column])[column].present?
56
+ def []=(column, value)
57
+ if value.nil?
58
+ @deleted_columns << column
59
+ @changed_columns.delete(column)
60
+ else
61
+ @changed_columns << column
62
+ @deleted_columns.delete(column)
63
+ end
64
+ @row[column] = value
109
65
  end
110
66
 
111
67
  def destroy
@@ -125,68 +81,14 @@ module Cequel
125
81
  self
126
82
  end
127
83
 
128
- def each_pair(options = {}, &block)
129
- return Enumerator.new(self, :each_pair, options) unless block
130
- return @row.each_pair(&block) if @loaded
131
- new_columns = @changed_columns.dup
132
- batch_size = options[:batch_size] || self.class.default_batch_size
133
- each_slice(batch_size) do |batch_results|
134
- batch_results.each_pair do |key, value|
135
- if @changed_columns.include?(key)
136
- new_columns.delete(key)
137
- yield key, @row[key]
138
- elsif !@deleted_columns.include?(key)
139
- yield key, value
140
- end
141
- end
142
- end
143
- new_columns.each do |key|
144
- yield key, @row[key]
145
- end
146
- end
147
-
148
- def each(&block)
149
- each_pair(&block)
150
- end
151
-
152
- def each_slice(batch_size)
153
- batch_scope = scope.select(:first => batch_size)
154
- key_alias = self.class.key_alias
155
- last_key = nil
156
- begin
157
- batch_results = batch_scope.first
158
- batch_results.delete(key_alias)
159
- result_length = batch_results.length
160
- batch_results.delete(last_key) unless last_key.nil?
161
- yield deserialize_row(batch_results)
162
- last_key = batch_results.keys.last
163
- batch_scope = batch_scope.select(:from => last_key)
164
- end while result_length == batch_size
165
- end
166
-
167
- def load
168
- @row = {}
169
- each_pair { |column, value| @row[column] = value }
170
- @loaded = true
171
- self
172
- end
173
-
174
- def loaded?
175
- !!@loaded
176
- end
177
-
178
84
  private
179
85
 
180
- def setup
181
- @row = {}
86
+ def setup(init_row = nil)
87
+ super
182
88
  @changed_columns = Set[]
183
89
  @deleted_columns = Set[]
184
90
  end
185
91
 
186
- def scope
187
- self.class.column_family.where(self.class.key_alias => @key)
188
- end
189
-
190
92
  #
191
93
  # Subclasses may override this method to implement custom serialization
192
94
  # strategies
@@ -211,10 +113,6 @@ module Cequel
211
113
  end
212
114
  end
213
115
 
214
- def load_raw_slice(columns)
215
- row = scope.select(*columns).first.except(self.class.key_alias)
216
- end
217
-
218
116
  end
219
117
 
220
118
  end