right_support 1.1.2 → 1.2.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/README.rdoc CHANGED
@@ -24,11 +24,22 @@ or where the log entries go.
24
24
  my_logger = MyAwesomeLogger.new
25
25
  use RightSupport::Rack::CustomLogger, Logger::INFO, my_logger
26
26
 
27
- == Input Validation
27
+ === String Manipulation
28
28
 
29
- The Validation module contains several format-checkers that can be used to validate
30
- your web app's models before saving, check for preconditions in your controllers, and
31
- so forth.
29
+ StringExtensions contains String#camelize, which is only defined if the
30
+ ActiveSupport gem cannot be loaded. It also has some RightScale-specific
31
+ methods:
32
+ * String#snake_case
33
+ * String#to_const
34
+ * String#to_const_path
35
+
36
+ Net::StringEncoder applies and removes URL-escape, base64 and other encodings.
37
+
38
+ === Input Validation
39
+
40
+ Validation contains several format-checkers that can be used to validate your
41
+ web app's models before saving, check for preconditions in your controllers,
42
+ and so forth.
32
43
 
33
44
  You can use it as a mixin by including the appropriate child module of
34
45
  RightSupport::Validation.
@@ -48,10 +59,10 @@ but does not pollute the dispatch table of your application classes.
48
59
  the_key = STDIN.read
49
60
  raise ArgumentError unless RightSupport::Validation.ssh_public_key?(the_key)
50
61
 
51
- == Request Balancing
62
+ === Client-Side Load Balancer
52
63
 
53
- The RequestBalancer class will randomly choose endpoints for a network request,
54
- which lets you perform easy client-side load balancing:
64
+ RequestBalancer randomly chooses endpoints for a network request, which lets
65
+ you perform easy client-side load balancing:
55
66
 
56
67
  include RightSupport::Net
57
68
 
@@ -61,19 +72,17 @@ which lets you perform easy client-side load balancing:
61
72
  end
62
73
 
63
74
  The balancer will keep trying requests until one of them succeeds without throwing
64
- any exceptions. (NB: a nil return value counts as success!!) If you specify that a
65
- certain class of exception is "fatal," then that exception will cause REST to re-
66
- raise immediately
75
+ any exceptions. (NB: a nil return value counts as success!!)
67
76
 
68
- == HTTPClient
77
+ === HTTP Client
69
78
 
70
79
  We provide a very thin wrapper around the rest-client gem that enables simple but
71
80
  robust rest requests with a timeout, headers, etc.
72
81
 
73
- The HTTPClient is interface-compatible with the RestClient
74
- module, but allows an optional timeout to be specified as an extra parameter.
82
+ HTTPClient is interface-compatible with the RestClient module, but allows an
83
+ optional timeout to be specified as an extra parameter.
75
84
 
76
- # Create a wrapper's object
85
+ # Create a wrapper object
77
86
  @client = RightSupport::Net::HTTPClient.new
78
87
 
79
88
  # Default timeout is 5 seconds
@@ -82,3 +91,18 @@ module, but allows an optional timeout to be specified as an extra parameter.
82
91
  # Make sure the HTTPClient request fails after 1 second so we can report an error
83
92
  # and move on!
84
93
  @client.get('http://localhost', {:headers => {'X-Hello'=>'hi!'}, :timeout => 1)}
94
+
95
+ === Statistics Gathering
96
+
97
+ Profile your compute-heavy and network activities using a Stats::Activity counter.
98
+
99
+ stats = RightSupport::Stats::Activity.new
100
+
101
+ puts 'enter 5 lines:'
102
+ 5.times do
103
+ stats.update(:read_input)
104
+ line = STDIN.readline
105
+ stats.finish
106
+ end
107
+
108
+ puts "Only %.1f lines/sec? You are a slow typist!" % [stats.avg_rate]
data/lib/right_support.rb CHANGED
@@ -1,8 +1,8 @@
1
- # Namespaces for RightSupport
2
1
  require 'right_support/ruby'
3
2
  require 'right_support/crypto'
4
3
  require 'right_support/db'
5
4
  require 'right_support/log'
6
5
  require 'right_support/net'
7
6
  require 'right_support/rack'
7
+ require 'right_support/stats'
8
8
  require 'right_support/validation'
@@ -1,10 +1,19 @@
1
- require 'yajl'
1
+ require 'digest/sha1'
2
2
 
3
3
  module RightSupport::Crypto
4
4
  class SignedHash
5
+
6
+ if require_succeeds?('yajl')
7
+ DefaultEncoding = ::Yajl
8
+ elsif require_succeeds?('json')
9
+ DefaultEncoding = ::JSON
10
+ else
11
+ DefaultEncoding = nil
12
+ end unless defined?(DefaultEncoding)
13
+
5
14
  DEFAULT_OPTIONS = {
6
15
  :digest => Digest::SHA1,
7
- :encoding => Yajl
16
+ :encoding => DefaultEncoding
8
17
  }
9
18
 
10
19
  def initialize(hash={}, options={})
@@ -29,6 +29,4 @@ module RightSupport
29
29
  end
30
30
  end
31
31
 
32
- Dir[File.expand_path('../db/*.rb', __FILE__)].each do |filename|
33
- require filename
34
- end
32
+ require 'right_support/db/cassandra_model'
@@ -1,8 +1,94 @@
1
+ #
2
+ # Copyright (c) 2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ begin
24
+ require 'cassandra/0.8'
25
+
26
+ class Cassandra
27
+ module Protocol
28
+ # Monkey patch _get_indexed_slices so that it accepts list of columns when doing indexed
29
+ # slice, otherwise not able to get specific columns using secondary index lookup
30
+ def _get_indexed_slices(column_family, index_clause, columns, count, start, finish, reversed, consistency)
31
+ column_parent = CassandraThrift::ColumnParent.new(:column_family => column_family)
32
+ if columns
33
+ predicate = CassandraThrift::SlicePredicate.new(:column_names => [columns].flatten)
34
+ else
35
+ predicate = CassandraThrift::SlicePredicate.new(:slice_range =>
36
+ CassandraThrift::SliceRange.new(
37
+ :reversed => reversed,
38
+ :count => count,
39
+ :start => start,
40
+ :finish => finish))
41
+ end
42
+ client.get_indexed_slices(column_parent, index_clause, predicate, consistency)
43
+ end
44
+ end
45
+
46
+ # Monkey patch get_indexed_slices so that it returns OrderedHash, otherwise cannot determine
47
+ # next start key when getting in chunks
48
+ def get_indexed_slices(column_family, index_clause, *columns_and_options)
49
+ return false if Cassandra.VERSION.to_f < 0.7
50
+
51
+ column_family, columns, _, options =
52
+ extract_and_validate_params(column_family, [], columns_and_options, READ_DEFAULTS.merge(:key_count => 100, :key_start => ""))
53
+
54
+ if index_clause.class != CassandraThrift::IndexClause
55
+ index_expressions = index_clause.collect do |expression|
56
+ create_index_expression(expression[:column_name], expression[:value], expression[:comparison])
57
+ end
58
+
59
+ index_clause = create_index_clause(index_expressions, options[:key_start], options[:key_count])
60
+ end
61
+
62
+ key_slices = _get_indexed_slices(column_family, index_clause, columns, options[:count], options[:start],
63
+ options[:finish], options[:reversed], options[:consistency])
64
+
65
+ key_slices.inject(OrderedHash.new){|h, key_slice| h[key_slice.key] = key_slice.columns; h}
66
+ end
67
+ end
68
+
69
+ rescue LoadError => e
70
+ # Make sure we're dealing with a legitimate missing-file LoadError
71
+ raise e unless e.message =~ /^no such file to load/
72
+ # Missing 'cassandra/0.8' indicates that the cassandra gem is not installed; we can ignore this
73
+ end
74
+
1
75
  module RightSupport::DB
76
+
77
+ # Base class for a column family in a keyspace
78
+ # Used to access data persisted in Cassandra
79
+ # Provides wrappers for Cassandra client methods
2
80
  class CassandraModel
81
+
82
+ # Default timeout for client connection to Cassandra server
3
83
  DEFAULT_TIMEOUT = 10
84
+
85
+ # Default maximum number of rows to retrieve in one chunk
86
+ DEFAULT_COUNT = 1000
87
+
88
+ # Wrappers for Cassandra client
4
89
  class << self
5
- @@logger=nil
90
+
91
+ @@logger = nil
6
92
  @@conn = nil
7
93
 
8
94
  attr_accessor :column_family
@@ -17,17 +103,22 @@ module RightSupport::DB
17
103
  end
18
104
 
19
105
  def logger=(l)
20
- @@logger=l
106
+ @@logger = l
21
107
  end
22
108
 
23
109
  def logger
24
110
  @@logger
25
111
  end
26
-
112
+
27
113
  def keyspace
28
114
  @keyspace + "_" + (ENV['RACK_ENV'] || 'development')
29
115
  end
30
116
 
117
+ # Client connected to Cassandra server
118
+ # Create connection if does not already exist
119
+ #
120
+ # === Return
121
+ # (Cassandra):: Client connected to server
31
122
  def conn
32
123
  return @@conn if @@conn
33
124
 
@@ -37,19 +128,49 @@ module RightSupport::DB
37
128
  @@conn
38
129
  end
39
130
 
40
- def all(k,opt={})
41
- list = real_get(k,opt)
131
+ # Get row(s) for specified key(s)
132
+ # Unless :count is specified, a maximum of 100 columns are retrieved
133
+ #
134
+ # === Parameters
135
+ # k(String|Array):: Individual primary key or list of keys on which to match
136
+ # opt(Hash):: Request options including :consistency and for column level
137
+ # control :count, :start, :finish, :reversed
138
+ #
139
+ # === Return
140
+ # (Object|nil):: Individual row, or nil if not found, or ordered hash of rows
141
+ def all(k, opt = {})
142
+ real_get(k, opt)
42
143
  end
43
144
 
44
- def get(key)
45
- if (hash = real_get(key)).empty?
145
+ # Get row for specified primary key and convert into object of given class
146
+ # Unless :count is specified, a maximum of 100 columns are retrieved
147
+ #
148
+ # === Parameters
149
+ # key(String):: Primary key on which to match
150
+ # opt(Hash):: Request options including :consistency and for column level
151
+ # control :count, :start, :finish, :reversed
152
+ #
153
+ # === Return
154
+ # (CassandraModel|nil):: Instantiated object of given class, or nil if not found
155
+ def get(key, opt = {})
156
+ if (attrs = real_get(key, opt)).empty?
46
157
  nil
47
158
  else
48
- new(key, hash)
159
+ new(key, attrs)
49
160
  end
50
161
  end
51
162
 
52
- def real_get(k,opt={})
163
+ # Get raw row(s) for specified primary key(s)
164
+ # Unless :count is specified, a maximum of 100 columns are retrieved
165
+ #
166
+ # === Parameters
167
+ # k(String|Array):: Individual primary key or list of keys on which to match
168
+ # opt(Hash):: Request options including :consistency and for column level
169
+ # control :count, :start, :finish, :reversed
170
+ #
171
+ # === Return
172
+ # (Cassandra::OrderedHash):: Individual row or OrderedHash of rows
173
+ def real_get(k, opt = {})
53
174
  if k.is_a?(Array)
54
175
  do_op(:multi_get, column_family, k, opt)
55
176
  else
@@ -57,19 +178,132 @@ module RightSupport::DB
57
178
  end
58
179
  end
59
180
 
60
- def insert(key, values,opt={})
61
- do_op(:insert, column_family, key, values,opt)
181
+ # Get all rows for specified secondary key
182
+ #
183
+ # === Parameters
184
+ # index(String):: Name of secondary index
185
+ # key(String):: Index value that each selected row is required to match
186
+ # columns(Array|nil):: Names of columns to be retrieved, defaults to all
187
+ # opt(Hash):: Request options with only :consistency used
188
+ #
189
+ # === Return
190
+ # (Array):: Rows retrieved with each member being an instantiated object of the
191
+ # given class as value, but object only contains values for the columns retrieved
192
+ def get_indexed(index, key, columns = nil, opt = {})
193
+ if rows = real_get_indexed(index, key, columns, opt)
194
+ rows.map do |key, columns|
195
+ attrs = columns.inject({}) { |a, c| a[c.column.name] = c.column.value; a }
196
+ new(key, attrs)
197
+ end
198
+ else
199
+ []
200
+ end
201
+ end
202
+
203
+ # Get all raw rows for specified secondary key
204
+ #
205
+ # === Parameters
206
+ # index(String):: Name of secondary index
207
+ # key(String):: Index value that each selected row is required to match
208
+ # columns(Array|nil):: Names of columns to be retrieved, defaults to all
209
+ # opt(Hash):: Request options with only :consistency used
210
+ #
211
+ # === Return
212
+ # (Hash):: Rows retrieved with primary key as key and value being an array
213
+ # of CassandraThrift::ColumnOrSuperColumn with attributes :name, :timestamp,
214
+ # and :value
215
+ def real_get_indexed(index, key, columns = nil, opt = {})
216
+ rows = {}
217
+ start = ""
218
+ count = DEFAULT_COUNT
219
+ expr = do_op(:create_idx_expr, index, key, "EQ")
220
+ opt = opt[:consistency] ? {:consistency => opt[:consistency]} : {}
221
+ while true
222
+ clause = do_op(:create_idx_clause, [expr], start, count)
223
+ chunk = do_op(:get_indexed_slices, column_family, clause, columns, opt)
224
+ rows.merge!(chunk)
225
+ if chunk.size == count
226
+ # Assume there are more chunks, use last key as start of next get
227
+ start = chunk.keys.last
228
+ else
229
+ # This must be the last chunk
230
+ break
231
+ end
232
+ end
233
+ rows
234
+ end
235
+
236
+ # Get specific columns in row with specified key
237
+ #
238
+ # === Parameters
239
+ # key(String):: Primary key on which to match
240
+ # columns(Array):: Names of columns to be retrieved
241
+ # opt(Hash):: Request options such as :consistency
242
+ #
243
+ # === Return
244
+ # (Array):: Values of selected columns in the order specified
245
+ def get_columns(key, columns, opt = {})
246
+ do_op(:get_columns, column_family, key, columns, sub_columns = nil, opt)
247
+ end
248
+
249
+ # Insert a row for a key
250
+ #
251
+ # === Parameters
252
+ # key(String):: Primary key for value
253
+ # values(Hash):: Values to be stored
254
+ # opt(Hash):: Request options such as :consistency
255
+ #
256
+ # === Return
257
+ # (Array):: Mutation map and consistency level
258
+ def insert(key, values, opt={})
259
+ do_op(:insert, column_family, key, values, opt)
62
260
  end
63
261
 
262
+ # Delete row or columns of row
263
+ #
264
+ # === Parameters
265
+ # args(Array):: Key, columns, options
266
+ #
267
+ # === Return
268
+ # (Array):: Mutation map and consistency level
64
269
  def remove(*args)
65
270
  do_op(:remove, column_family, *args)
66
271
  end
67
272
 
68
- def batch(*args,&block)
273
+ # Open a batch operation and yield self
274
+ # Inserts and deletes are queued until the block closes,
275
+ # and then sent atomically to the server
276
+ # Supports :consistency option, which overrides that set
277
+ # in individual commands
278
+ #
279
+ # === Parameters
280
+ # args(Array):: Batch options such as :consistency
281
+ #
282
+ # === Block
283
+ # Required block making Cassandra requests
284
+ #
285
+ # === Returns
286
+ # (Array):: Mutation map and consistency level
287
+ #
288
+ # === Raise
289
+ # Exception:: If block not specified
290
+ def batch(*args, &block)
69
291
  raise "Block required!" unless block_given?
70
- do_op(:batch,*args, &block)
292
+ do_op(:batch, *args, &block)
71
293
  end
72
-
294
+
295
+ # Execute Cassandra request
296
+ # Automatically reconnect and retry if IOError encountered
297
+ #
298
+ # === Parameters
299
+ # meth(Symbol):: Method to be executed
300
+ # args(Array):: Method arguments
301
+ #
302
+ # === Block
303
+ # Block if any to be executed by method
304
+ #
305
+ # === Return
306
+ # (Object):: Value returned by executed method
73
307
  def do_op(meth, *args, &block)
74
308
  conn.send(meth, *args, &block)
75
309
  rescue IOError
@@ -77,12 +311,21 @@ module RightSupport::DB
77
311
  retry
78
312
  end
79
313
 
314
+ # Reconnect to Cassandra server
315
+ #
316
+ # === Return
317
+ # true:: Always return true
80
318
  def reconnect
81
319
  config = @@config[ENV["RACK_ENV"]]
82
- @@conn = Cassandra.new(keyspace, config["server"],{:timeout => RightSupport::CassandraModel::DEFAULT_TIMEOUT})
320
+ @@conn = Cassandra.new(keyspace, config["server"], {:timeout => RightSupport::DB::CassandraModel::DEFAULT_TIMEOUT})
83
321
  @@conn.disable_node_auto_discovery!
322
+ true
84
323
  end
85
324
 
325
+ # Cassandra ring for given keyspace
326
+ #
327
+ # === Return
328
+ # (Array):: Members of ring
86
329
  def ring
87
330
  conn.ring
88
331
  end
@@ -90,25 +333,50 @@ module RightSupport::DB
90
333
 
91
334
  attr_accessor :key, :attributes
92
335
 
93
- def initialize(key, attrs={})
336
+ # Create column family object
337
+ #
338
+ # === Parameters
339
+ # key(String):: Primary key for object
340
+ # attrs(Hash):: Attributes for object which form Cassandra row
341
+ # with column name as key and column value as value
342
+ def initialize(key, attrs = {})
94
343
  self.key = key
95
344
  self.attributes = attrs
96
345
  end
97
346
 
347
+ # Store object in Cassandra
348
+ #
349
+ # === Return
350
+ # true:: Always return true
98
351
  def save
99
352
  self.class.insert(key, attributes)
100
353
  true
101
354
  end
102
355
 
356
+ # Load object from Cassandra without modifying this object
357
+ #
358
+ # === Return
359
+ # (CassandraModel):: Object as stored in Cassandra
103
360
  def reload
104
361
  self.class.get(key)
105
362
  end
106
363
 
364
+ # Reload object value from Cassandra and update this object
365
+ #
366
+ # === Return
367
+ # (CassandraModel):: This object after reload from Cassandra
107
368
  def reload!
108
369
  self.attributes = self.class.real_get(key)
109
370
  self
110
371
  end
111
372
 
373
+ # Column value
374
+ #
375
+ # === Parameters
376
+ # key(String|Integer):: Column name or key
377
+ #
378
+ # === Return
379
+ # (Object|nil):: Column value, or nil if not found
112
380
  def [](key)
113
381
  ret = attributes[key]
114
382
  return ret if ret
@@ -117,13 +385,26 @@ module RightSupport::DB
117
385
  end
118
386
  end
119
387
 
388
+ # Store new column value
389
+ #
390
+ # === Parameters
391
+ # key(String|Integer):: Column name or key
392
+ # value(Object):: Value to be stored
393
+ #
394
+ # === Return
395
+ # (Object):: Value stored
120
396
  def []=(key, value)
121
397
  attributes[key] = value
122
398
  end
123
399
 
400
+ # Delete object from Cassandra
401
+ #
402
+ # === Return
403
+ # true:: Always return true
124
404
  def destroy
125
405
  self.class.remove(key)
126
406
  end
127
407
 
128
- end
129
- end
408
+ end # CassandraModel
409
+
410
+ end # RightSupport::DB