right_support 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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