simple_record 1.5.4 → 1.5.6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/simple_record.rb +81 -66
- data/lib/simple_record/active_sdb.rb +2 -2
- data/lib/simple_record/results_array.rb +5 -1
- data/lib/simple_record/sharding.rb +273 -0
- data/test/my_sharded_model.rb +41 -0
- data/test/test_base.rb +10 -0
- data/test/test_shards.rb +127 -0
- metadata +8 -3
data/lib/simple_record.rb
CHANGED
@@ -41,24 +41,25 @@ require File.expand_path(File.dirname(__FILE__) + "/simple_record/rails2")
|
|
41
41
|
require File.expand_path(File.dirname(__FILE__) + "/simple_record/results_array")
|
42
42
|
require File.expand_path(File.dirname(__FILE__) + "/simple_record/stats")
|
43
43
|
require File.expand_path(File.dirname(__FILE__) + "/simple_record/translations")
|
44
|
+
require_relative 'simple_record/sharding'
|
44
45
|
|
45
46
|
|
46
47
|
module SimpleRecord
|
47
48
|
|
48
|
-
@@options
|
49
|
-
@@stats
|
50
|
-
@@logging
|
51
|
-
@@s3
|
49
|
+
@@options = {}
|
50
|
+
@@stats = SimpleRecord::Stats.new
|
51
|
+
@@logging = false
|
52
|
+
@@s3 = nil
|
52
53
|
@@auto_close_s3 = false
|
53
|
-
@@logger
|
54
|
-
@@logger.level
|
54
|
+
@@logger = Logger.new(STDOUT)
|
55
|
+
@@logger.level = Logger::INFO
|
55
56
|
|
56
57
|
class << self;
|
57
58
|
attr_accessor :aws_access_key, :aws_secret_key
|
58
59
|
|
59
60
|
# Deprecated
|
60
61
|
def enable_logging
|
61
|
-
@@logging
|
62
|
+
@@logging = true
|
62
63
|
@@logger.level = Logger::DEBUG
|
63
64
|
end
|
64
65
|
|
@@ -127,7 +128,7 @@ module SimpleRecord
|
|
127
128
|
if options[:connection_mode] == :per_thread
|
128
129
|
@@auto_close_s3 = true
|
129
130
|
# todo: should we init this only when needed?
|
130
|
-
@@s3
|
131
|
+
@@s3 = Aws::S3.new(SimpleRecord.aws_access_key, SimpleRecord.aws_secret_key, {:connection_mode=>:per_thread})
|
131
132
|
end
|
132
133
|
end
|
133
134
|
|
@@ -139,7 +140,7 @@ module SimpleRecord
|
|
139
140
|
@@s3.close_connection if @@auto_close_s3
|
140
141
|
end
|
141
142
|
|
142
|
-
|
143
|
+
# If you'd like to specify the s3 connection to use for LOBs, you can pass it in here.
|
143
144
|
# We recommend that this connection matches the type of connection you're using for SimpleDB,
|
144
145
|
# at least if you're using per_thread connection mode.
|
145
146
|
def s3=(s3)
|
@@ -172,6 +173,8 @@ module SimpleRecord
|
|
172
173
|
include SimpleRecord::Translations
|
173
174
|
# include SimpleRecord::Attributes
|
174
175
|
extend SimpleRecord::Attributes
|
176
|
+
extend SimpleRecord::Sharding::ClassMethods
|
177
|
+
include SimpleRecord::Sharding
|
175
178
|
include SimpleRecord::Callbacks
|
176
179
|
include SimpleRecord::Json
|
177
180
|
include SimpleRecord::Logging
|
@@ -197,13 +200,13 @@ module SimpleRecord
|
|
197
200
|
#we have to handle the virtuals.
|
198
201
|
Attributes.handle_virtuals(attrs)
|
199
202
|
|
200
|
-
@errors=SimpleRecord_errors.new
|
201
|
-
@dirty
|
203
|
+
@errors=SimpleRecord_errors.new if not (defined?(ActiveModel))
|
204
|
+
@dirty = {}
|
202
205
|
|
203
|
-
@attributes
|
206
|
+
@attributes = {} # sdb values
|
204
207
|
@attributes_rb = {} # ruby values
|
205
|
-
@lobs
|
206
|
-
@new_record
|
208
|
+
@lobs = {}
|
209
|
+
@new_record = true
|
207
210
|
|
208
211
|
end
|
209
212
|
|
@@ -232,7 +235,6 @@ module SimpleRecord
|
|
232
235
|
end
|
233
236
|
|
234
237
|
|
235
|
-
|
236
238
|
def defined_attributes_local
|
237
239
|
# todo: store this somewhere so it doesn't keep going through this
|
238
240
|
ret = self.class.defined_attributes
|
@@ -240,8 +242,6 @@ module SimpleRecord
|
|
240
242
|
end
|
241
243
|
|
242
244
|
|
243
|
-
|
244
|
-
|
245
245
|
class << self;
|
246
246
|
attr_accessor :domain_prefix
|
247
247
|
end
|
@@ -297,7 +297,7 @@ module SimpleRecord
|
|
297
297
|
|
298
298
|
def get_attribute_sdb(name)
|
299
299
|
name = name.to_sym
|
300
|
-
ret
|
300
|
+
ret = strip_array(@attributes[sdb_att_name(name)])
|
301
301
|
return ret
|
302
302
|
end
|
303
303
|
|
@@ -307,7 +307,7 @@ module SimpleRecord
|
|
307
307
|
end
|
308
308
|
|
309
309
|
def get_att_meta(name)
|
310
|
-
name_s
|
310
|
+
name_s = name.to_s
|
311
311
|
att_meta = defined_attributes_local[name.to_sym]
|
312
312
|
if att_meta.nil? && has_id_on_end(name_s)
|
313
313
|
att_meta = defined_attributes_local[name_s[0..-4].to_sym]
|
@@ -339,7 +339,7 @@ module SimpleRecord
|
|
339
339
|
|
340
340
|
def make_dirty(arg, value)
|
341
341
|
sdb_att_name = sdb_att_name(arg)
|
342
|
-
arg
|
342
|
+
arg = arg.to_s
|
343
343
|
|
344
344
|
# puts "Marking #{arg} dirty with #{value}" if SimpleRecord.logging?
|
345
345
|
if @dirty.include?(sdb_att_name)
|
@@ -422,7 +422,7 @@ module SimpleRecord
|
|
422
422
|
return true if @dirty.size == 0 # Nothing to save so skip it
|
423
423
|
end
|
424
424
|
is_create = self[:id].nil?
|
425
|
-
ok
|
425
|
+
ok = pre_save(options) # Validates and sets ID
|
426
426
|
if ok
|
427
427
|
begin
|
428
428
|
dirty = @dirty
|
@@ -432,13 +432,14 @@ module SimpleRecord
|
|
432
432
|
return true if @dirty.size == 0 # This should probably never happen because after pre_save, created/updated dates are changed
|
433
433
|
options[:dirty_atts] = @dirty
|
434
434
|
end
|
435
|
-
to_delete
|
435
|
+
to_delete = get_atts_to_delete
|
436
436
|
SimpleRecord.stats.saves += 1
|
437
|
-
|
438
|
-
|
437
|
+
|
438
|
+
if self.class.is_sharded?
|
439
|
+
options[:domain] = sharded_domain
|
440
|
+
end
|
441
|
+
|
439
442
|
if super(options)
|
440
|
-
# puts 'dirty super=' + @dirty.inspect
|
441
|
-
# puts 'SELF AFTER super=' + self.inspect
|
442
443
|
self.class.cache_results(self)
|
443
444
|
delete_niled(to_delete)
|
444
445
|
save_lobs(dirty)
|
@@ -564,7 +565,7 @@ module SimpleRecord
|
|
564
565
|
def pre_save(options)
|
565
566
|
|
566
567
|
is_create = self[:id].nil?
|
567
|
-
ok
|
568
|
+
ok = run_before_validation && (is_create ? run_before_validation_on_create : run_before_validation_on_update)
|
568
569
|
return false unless ok
|
569
570
|
|
570
571
|
validate()
|
@@ -594,6 +595,7 @@ module SimpleRecord
|
|
594
595
|
# Now translate all fields into SimpleDB friendly strings
|
595
596
|
# convert_all_atts_to_sdb()
|
596
597
|
end
|
598
|
+
prepare_for_update
|
597
599
|
ok
|
598
600
|
end
|
599
601
|
|
@@ -652,7 +654,7 @@ module SimpleRecord
|
|
652
654
|
def self.delete_all(*params)
|
653
655
|
# could make this quicker by just getting item_names and deleting attributes rather than creating objects
|
654
656
|
obs = self.find(params)
|
655
|
-
i
|
657
|
+
i = 0
|
656
658
|
obs.each do |a|
|
657
659
|
a.delete
|
658
660
|
i+=1
|
@@ -662,7 +664,7 @@ module SimpleRecord
|
|
662
664
|
|
663
665
|
def self.destroy_all(*params)
|
664
666
|
obs = self.find(params)
|
665
|
-
i
|
667
|
+
i = 0
|
666
668
|
obs.each do |a|
|
667
669
|
a.destroy
|
668
670
|
i+=1
|
@@ -672,7 +674,11 @@ module SimpleRecord
|
|
672
674
|
|
673
675
|
def delete()
|
674
676
|
# TODO: DELETE CLOBS, etc from s3
|
675
|
-
|
677
|
+
options = {}
|
678
|
+
if self.class.is_sharded?
|
679
|
+
options[:domain] = sharded_domain
|
680
|
+
end
|
681
|
+
super(options)
|
676
682
|
end
|
677
683
|
|
678
684
|
def destroy
|
@@ -685,8 +691,8 @@ module SimpleRecord
|
|
685
691
|
def get_attribute(name)
|
686
692
|
# puts "GET #{arg}"
|
687
693
|
# Check if this arg is already converted
|
688
|
-
name_s
|
689
|
-
name
|
694
|
+
name_s = name.to_s
|
695
|
+
name = name.to_sym
|
690
696
|
att_meta = get_att_meta(name)
|
691
697
|
# puts "att_meta for #{name}: " + att_meta.inspect
|
692
698
|
if att_meta && att_meta.type == :clob
|
@@ -702,7 +708,7 @@ module SimpleRecord
|
|
702
708
|
# get it from s3
|
703
709
|
unless new_record?
|
704
710
|
begin
|
705
|
-
ret
|
711
|
+
ret = s3_bucket.get(s3_lob_id(name))
|
706
712
|
# puts 'got from s3 ' + ret.inspect
|
707
713
|
SimpleRecord.stats.s3_gets += 1
|
708
714
|
rescue Aws::AwsError => ex
|
@@ -725,8 +731,8 @@ module SimpleRecord
|
|
725
731
|
ret = @attributes_rb[name_s] # instance_variable_get(instance_var)
|
726
732
|
return ret unless ret.nil?
|
727
733
|
return nil if ret.is_a? RemoteNil
|
728
|
-
ret
|
729
|
-
ret
|
734
|
+
ret = get_attribute_sdb(name)
|
735
|
+
ret = sdb_to_ruby(name, ret)
|
730
736
|
@attributes_rb[name_s] = ret
|
731
737
|
return ret
|
732
738
|
end
|
@@ -736,22 +742,22 @@ module SimpleRecord
|
|
736
742
|
def set(name, value, dirtify=true)
|
737
743
|
# puts "SET #{name}=#{value.inspect}" if SimpleRecord.logging?
|
738
744
|
# puts "self=" + self.inspect
|
739
|
-
attname
|
740
|
-
name
|
741
|
-
att_meta
|
745
|
+
attname = name.to_s # default attname
|
746
|
+
name = name.to_sym
|
747
|
+
att_meta = get_att_meta(name)
|
742
748
|
store_rb_val = false
|
743
749
|
if att_meta.nil?
|
744
750
|
# check if it ends with id and see if att_meta is there
|
745
751
|
ends_with = name.to_s[-3, 3]
|
746
752
|
if ends_with == "_id"
|
747
753
|
# puts 'ends with id'
|
748
|
-
n2
|
754
|
+
n2 = name.to_s[0, name.length-3]
|
749
755
|
# puts 'n2=' + n2
|
750
756
|
att_meta = defined_attributes_local[n2.to_sym]
|
751
757
|
# puts 'defined_attributes_local=' + defined_attributes_local.inspect
|
752
|
-
attname
|
758
|
+
attname = name.to_s
|
753
759
|
attvalue = value
|
754
|
-
name
|
760
|
+
name = n2.to_sym
|
755
761
|
end
|
756
762
|
return if att_meta.nil?
|
757
763
|
else
|
@@ -761,8 +767,8 @@ module SimpleRecord
|
|
761
767
|
att_name = name.to_s
|
762
768
|
attvalue = value
|
763
769
|
else
|
764
|
-
attname
|
765
|
-
attvalue
|
770
|
+
attname = name.to_s + '_id'
|
771
|
+
attvalue = value.nil? ? nil : value.id
|
766
772
|
store_rb_val = true
|
767
773
|
end
|
768
774
|
elsif att_meta.type == :clob
|
@@ -770,7 +776,7 @@ module SimpleRecord
|
|
770
776
|
@lobs[name] = value
|
771
777
|
return
|
772
778
|
else
|
773
|
-
attname
|
779
|
+
attname = name.to_s
|
774
780
|
attvalue = att_meta.init_value(value)
|
775
781
|
# attvalue = value
|
776
782
|
#puts 'converted ' + value.inspect + ' to ' + attvalue.inspect
|
@@ -779,7 +785,7 @@ module SimpleRecord
|
|
779
785
|
attvalue = strip_array(attvalue)
|
780
786
|
make_dirty(name, attvalue) if dirtify
|
781
787
|
# puts "ARG=#{attname.to_s} setting to #{attvalue}"
|
782
|
-
sdb_val
|
788
|
+
sdb_val = ruby_to_sdb(name, attvalue)
|
783
789
|
# puts "sdb_val=" + sdb_val.to_s
|
784
790
|
@attributes[attname] = sdb_val
|
785
791
|
# attvalue = wrap_if_required(name, attvalue, sdb_val)
|
@@ -834,7 +840,7 @@ module SimpleRecord
|
|
834
840
|
if $&
|
835
841
|
before=$`
|
836
842
|
middle=$&
|
837
|
-
after=$'
|
843
|
+
after =$'
|
838
844
|
|
839
845
|
before =~ /'$/ #is there already a quote immediately before the match?
|
840
846
|
unless $&
|
@@ -868,34 +874,43 @@ module SimpleRecord
|
|
868
874
|
# :consistent_read => true/false -- as per http://developer.amazonwebservices.com/connect/entry.jspa?externalID=3572
|
869
875
|
def self.find(*params)
|
870
876
|
#puts 'params=' + params.inspect
|
871
|
-
q_type = :all
|
872
|
-
select_attributes=[]
|
873
877
|
|
878
|
+
q_type = :all
|
879
|
+
select_attributes=[]
|
874
880
|
if params.size > 0
|
875
881
|
q_type = params[0]
|
876
882
|
end
|
883
|
+
options = {}
|
884
|
+
if params.size > 1
|
885
|
+
options = params[1]
|
886
|
+
end
|
887
|
+
|
888
|
+
if !options[:shard_find] && is_sharded?
|
889
|
+
# then break off and get results across all shards
|
890
|
+
return find_sharded(*params)
|
891
|
+
end
|
877
892
|
|
878
893
|
# Pad and Offset number attributes
|
879
|
-
options = {}
|
880
894
|
params_dup = params.dup
|
881
895
|
if params.size > 1
|
882
896
|
options = params[1]
|
883
897
|
#puts 'options=' + options.inspect
|
884
898
|
#puts 'after collect=' + options.inspect
|
885
899
|
convert_condition_params(options)
|
886
|
-
per_token
|
900
|
+
per_token = options[:per_token]
|
887
901
|
consistent_read = options[:consistent_read]
|
888
902
|
if per_token || consistent_read then
|
889
|
-
op_dup
|
890
|
-
op_dup[:limit]
|
903
|
+
op_dup = options.dup
|
904
|
+
op_dup[:limit] = per_token # simpledb uses Limit as a paging thing, not what is normal
|
891
905
|
op_dup[:consistent_read] = consistent_read
|
892
|
-
params_dup[1]
|
906
|
+
params_dup[1] = op_dup
|
893
907
|
end
|
894
908
|
end
|
895
909
|
# puts 'params2=' + params.inspect
|
896
910
|
|
897
|
-
|
911
|
+
ret = q_type == :all ? [] : nil
|
898
912
|
begin
|
913
|
+
|
899
914
|
results=find_with_metadata(*params_dup)
|
900
915
|
# puts "RESULT=" + results.inspect
|
901
916
|
write_usage(:select, domain, q_type, options, results)
|
@@ -951,14 +966,14 @@ module SimpleRecord
|
|
951
966
|
def self.paginate(options={})
|
952
967
|
# options = args.pop
|
953
968
|
# puts 'paginate options=' + options.inspect if SimpleRecord.logging?
|
954
|
-
page
|
955
|
-
per_page
|
969
|
+
page = options[:page] || 1
|
970
|
+
per_page = options[:per_page] || 50
|
956
971
|
# total = options[:total_entries].to_i
|
957
|
-
options[:page]
|
972
|
+
options[:page] = page.to_i # makes sure it's to_i
|
958
973
|
options[:per_page] = per_page.to_i
|
959
|
-
options[:limit]
|
974
|
+
options[:limit] = options[:page] * options[:per_page]
|
960
975
|
# puts 'paging options=' + options.inspect
|
961
|
-
fr
|
976
|
+
fr = find(:all, options)
|
962
977
|
return fr
|
963
978
|
|
964
979
|
end
|
@@ -982,15 +997,15 @@ module SimpleRecord
|
|
982
997
|
# todo: cache each result
|
983
998
|
results.each do |item|
|
984
999
|
class_name = item.class.name
|
985
|
-
id
|
986
|
-
cache_key
|
1000
|
+
id = item.id
|
1001
|
+
cache_key = self.cache_key(class_name, id)
|
987
1002
|
#puts 'caching result at ' + cache_key + ': ' + results.inspect
|
988
1003
|
cache_store.write(cache_key, item, :expires_in =>30)
|
989
1004
|
end
|
990
1005
|
else
|
991
1006
|
class_name = results.class.name
|
992
|
-
id
|
993
|
-
cache_key
|
1007
|
+
id = results.id
|
1008
|
+
cache_key = self.cache_key(class_name, id)
|
994
1009
|
#puts 'caching result at ' + cache_key + ': ' + results.inspect
|
995
1010
|
cache_store.write(cache_key, results, :expires_in =>30)
|
996
1011
|
end
|
@@ -1051,8 +1066,8 @@ module SimpleRecord
|
|
1051
1066
|
|
1052
1067
|
class Activerecordtosdb_subrecord_array
|
1053
1068
|
def initialize(subname, referencename, referencevalue)
|
1054
|
-
@subname=subname.classify
|
1055
|
-
@referencename=referencename.tableize.singularize + "_id"
|
1069
|
+
@subname =subname.classify
|
1070
|
+
@referencename =referencename.tableize.singularize + "_id"
|
1056
1071
|
@referencevalue=referencevalue
|
1057
1072
|
end
|
1058
1073
|
|
@@ -1104,7 +1119,7 @@ module SimpleRecord
|
|
1104
1119
|
|
1105
1120
|
def create(*params)
|
1106
1121
|
params[0][@referencename]=@referencevalue
|
1107
|
-
record
|
1122
|
+
record = eval(@subname).new(*params)
|
1108
1123
|
record.save
|
1109
1124
|
end
|
1110
1125
|
|
@@ -966,9 +966,9 @@ module SimpleRecord
|
|
966
966
|
# sandy.reload
|
967
967
|
# puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
|
968
968
|
#
|
969
|
-
def delete
|
969
|
+
def delete(options={})
|
970
970
|
raise_on_id_absence
|
971
|
-
connection.delete_attributes(domain, id)
|
971
|
+
connection.delete_attributes(options[:domain] || domain, id)
|
972
972
|
end
|
973
973
|
|
974
974
|
# Item ID
|
@@ -5,7 +5,7 @@ module SimpleRecord
|
|
5
5
|
class ResultsArray
|
6
6
|
include Enumerable
|
7
7
|
|
8
|
-
attr_reader :next_token, :clz, :params, :items, :
|
8
|
+
attr_reader :next_token, :clz, :params, :items, :index, :box_usage, :request_id
|
9
9
|
|
10
10
|
|
11
11
|
def initialize(clz=nil, params=[], results=nil, next_token=nil)
|
@@ -27,6 +27,7 @@ module SimpleRecord
|
|
27
27
|
load_to(@options[:per_page] * @options[:page])
|
28
28
|
@start_at = @options[:per_page] * (@options[:page] - 1)
|
29
29
|
end
|
30
|
+
@index = 0
|
30
31
|
# puts 'RESULTS_ARRAY=' + self.inspect
|
31
32
|
end
|
32
33
|
|
@@ -96,6 +97,7 @@ module SimpleRecord
|
|
96
97
|
params_for_count[1] = params_for_count[1].dup # for deep clone
|
97
98
|
params_for_count[1].delete(:limit)
|
98
99
|
params_for_count[1].delete(:per_token)
|
100
|
+
params_for_count[1][:called_by] = :results_array
|
99
101
|
|
100
102
|
# puts '@params2=' + @params.inspect
|
101
103
|
# puts 'params_for_count=' + params_for_count.inspect
|
@@ -127,6 +129,7 @@ module SimpleRecord
|
|
127
129
|
# puts "i=" + i.to_s
|
128
130
|
yield v
|
129
131
|
i += 1
|
132
|
+
@index += 1
|
130
133
|
if !limit.nil? && i >= limit
|
131
134
|
return
|
132
135
|
end
|
@@ -194,6 +197,7 @@ module SimpleRecord
|
|
194
197
|
def load_next_token_set
|
195
198
|
options = @params[1]
|
196
199
|
options[:next_token] = @next_token
|
200
|
+
options[:called_by] = :results_array
|
197
201
|
res = @clz.find(*@params)
|
198
202
|
@currentset_items = res.items # get the real items array from the ResultsArray
|
199
203
|
@currentset_items.each do |item|
|
@@ -0,0 +1,273 @@
|
|
1
|
+
module SimpleRecord
|
2
|
+
|
3
|
+
module Sharding
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
# base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def shard(options=nil)
|
12
|
+
@sharding_options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def sharding_options
|
16
|
+
@sharding_options
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_sharded?
|
20
|
+
@sharding_options
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_sharded(*params)
|
24
|
+
puts 'find_sharded ' + params.inspect
|
25
|
+
|
26
|
+
options = params.size > 1 ? params[1] : {}
|
27
|
+
|
28
|
+
domains = sharded_domains
|
29
|
+
puts "sharded_domains=" + domains.inspect
|
30
|
+
|
31
|
+
single = false
|
32
|
+
case params.first
|
33
|
+
when nil then
|
34
|
+
raise "Invalid parameters passed to find: nil."
|
35
|
+
when :all, :first, :count
|
36
|
+
# nada
|
37
|
+
else # single id
|
38
|
+
unless params.first.is_a?(Array)
|
39
|
+
single = true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
results = ShardedResults.new(params)
|
44
|
+
domains.each do |d|
|
45
|
+
p2 = params.dup
|
46
|
+
op2 = options.dup
|
47
|
+
op2[:from] = d
|
48
|
+
op2[:shard_find] = true
|
49
|
+
p2[1] = op2
|
50
|
+
rs = find(*p2)
|
51
|
+
if params.first == :first || single
|
52
|
+
return rs if rs
|
53
|
+
else
|
54
|
+
results.add_results rs
|
55
|
+
end
|
56
|
+
end
|
57
|
+
puts 'results=' + results.inspect
|
58
|
+
if params.first == :first || single
|
59
|
+
# Then we found nothing by this point so return nil
|
60
|
+
return nil
|
61
|
+
elsif params.first == :count
|
62
|
+
return results.sum_count
|
63
|
+
end
|
64
|
+
results
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
def shards
|
69
|
+
send(sharding_options[:shards])
|
70
|
+
end
|
71
|
+
|
72
|
+
def sharded_domains
|
73
|
+
sharded_domains = []
|
74
|
+
shard_names = shards
|
75
|
+
shard_names.each do |s|
|
76
|
+
sharded_domains << "#{domain}_#{s}"
|
77
|
+
end
|
78
|
+
sharded_domains
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def sharded_domain
|
84
|
+
# puts 'getting sharded_domain'
|
85
|
+
options = self.class.sharding_options
|
86
|
+
# val = self.send(options[:on])
|
87
|
+
# puts "val=" + val.inspect
|
88
|
+
# shards = options[:shards] # is user passed in static array of shards
|
89
|
+
# if options[:shards].is_a?(Symbol)
|
90
|
+
# shards = self.send(shards)
|
91
|
+
# end
|
92
|
+
sharded_domain = "#{domain}_#{self.send(options[:map])}"
|
93
|
+
puts "sharded_domain=" + sharded_domain.inspect
|
94
|
+
sharded_domain
|
95
|
+
end
|
96
|
+
|
97
|
+
class ShardedResults
|
98
|
+
include Enumerable
|
99
|
+
|
100
|
+
def initialize(params)
|
101
|
+
@params = params
|
102
|
+
@options = params.size > 1 ? params[1] : {}
|
103
|
+
@results_arrays = []
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_results(rs)
|
107
|
+
# puts 'adding results=' + rs.inspect
|
108
|
+
@results_arrays << rs
|
109
|
+
end
|
110
|
+
|
111
|
+
# only used for count queries
|
112
|
+
def sum_count
|
113
|
+
x = 0
|
114
|
+
@results_arrays.each do |rs|
|
115
|
+
x += rs if rs
|
116
|
+
end
|
117
|
+
x
|
118
|
+
end
|
119
|
+
|
120
|
+
def <<(val)
|
121
|
+
raise "Not supported."
|
122
|
+
end
|
123
|
+
|
124
|
+
def element_at(index)
|
125
|
+
@results_arrays.each do |rs|
|
126
|
+
if rs.size > index
|
127
|
+
return rs[index]
|
128
|
+
end
|
129
|
+
index -= rs.size
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def [](*i)
|
134
|
+
if i.size == 1
|
135
|
+
# puts '[] i=' + i.to_s
|
136
|
+
index = i[0]
|
137
|
+
return element_at(index)
|
138
|
+
else
|
139
|
+
offset = i[0]
|
140
|
+
rows = i[1]
|
141
|
+
ret = []
|
142
|
+
x = offset
|
143
|
+
while x < (offset+rows)
|
144
|
+
ret << element_at(x)
|
145
|
+
x+=1
|
146
|
+
end
|
147
|
+
ret
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def first
|
152
|
+
@results_arrays.first.first
|
153
|
+
end
|
154
|
+
|
155
|
+
def last
|
156
|
+
@results_arrays.last.last
|
157
|
+
end
|
158
|
+
|
159
|
+
def empty?
|
160
|
+
@results_arrays.each do |rs|
|
161
|
+
return false if !rs.empty?
|
162
|
+
end
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
def include?(obj)
|
167
|
+
@results_arrays.each do |rs|
|
168
|
+
x = rs.include?(obj)
|
169
|
+
return true if x
|
170
|
+
end
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
def size
|
175
|
+
return @size if @size
|
176
|
+
s = 0
|
177
|
+
@results_arrays.each do |rs|
|
178
|
+
# puts 'rs=' + rs.inspect
|
179
|
+
# puts 'rs.size=' + rs.size.inspect
|
180
|
+
s += rs.size
|
181
|
+
end
|
182
|
+
@size = s
|
183
|
+
s
|
184
|
+
end
|
185
|
+
|
186
|
+
def length
|
187
|
+
return size
|
188
|
+
end
|
189
|
+
|
190
|
+
def each(&blk)
|
191
|
+
i = 0
|
192
|
+
@results_arrays.each do |rs|
|
193
|
+
rs.each(&blk)
|
194
|
+
i+=1
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# for will_paginate support
|
199
|
+
def total_pages
|
200
|
+
# puts 'total_pages'
|
201
|
+
# puts @params[1][:per_page].to_s
|
202
|
+
return 1 if @params[1][:per_page].nil?
|
203
|
+
ret = (size / @params[1][:per_page].to_f).ceil
|
204
|
+
#puts 'ret=' + ret.to_s
|
205
|
+
ret
|
206
|
+
end
|
207
|
+
|
208
|
+
def current_page
|
209
|
+
return query_options[:page] || 1
|
210
|
+
end
|
211
|
+
|
212
|
+
def query_options
|
213
|
+
return @options
|
214
|
+
end
|
215
|
+
|
216
|
+
def total_entries
|
217
|
+
return size
|
218
|
+
end
|
219
|
+
|
220
|
+
# Helper method that is true when someone tries to fetch a page with a
|
221
|
+
# larger number than the last page. Can be used in combination with flashes
|
222
|
+
# and redirecting.
|
223
|
+
def out_of_bounds?
|
224
|
+
current_page > total_pages
|
225
|
+
end
|
226
|
+
|
227
|
+
# Current offset of the paginated collection. If we're on the first page,
|
228
|
+
# it is always 0. If we're on the 2nd page and there are 30 entries per page,
|
229
|
+
# the offset is 30. This property is useful if you want to render ordinals
|
230
|
+
# side by side with records in the view: simply start with offset + 1.
|
231
|
+
def offset
|
232
|
+
(current_page - 1) * per_page
|
233
|
+
end
|
234
|
+
|
235
|
+
# current_page - 1 or nil if there is no previous page
|
236
|
+
def previous_page
|
237
|
+
current_page > 1 ? (current_page - 1) : nil
|
238
|
+
end
|
239
|
+
|
240
|
+
# current_page + 1 or nil if there is no next page
|
241
|
+
def next_page
|
242
|
+
current_page < total_pages ? (current_page + 1) : nil
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
def delete(item)
|
247
|
+
raise "Not supported"
|
248
|
+
end
|
249
|
+
|
250
|
+
def delete_at(index)
|
251
|
+
raise "Not supported"
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
# Some hashing algorithms
|
257
|
+
module Hashing
|
258
|
+
def self.sdbm_hash(str, len=str.length)
|
259
|
+
# puts 'sdbm_hash ' + str.inspect
|
260
|
+
hash = 0
|
261
|
+
len.times { |i|
|
262
|
+
c = str[i]
|
263
|
+
# puts "c=" + c.class.name + "--" + c.inspect + " -- " + c.ord.inspect
|
264
|
+
c = c.ord
|
265
|
+
hash = c + (hash << 6) + (hash << 16) - hash
|
266
|
+
}
|
267
|
+
# puts "hash=" + hash.inspect
|
268
|
+
return hash
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../lib/simple_record")
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + "/../lib/simple_record")
|
4
|
+
|
5
|
+
class MyShardedModel < SimpleRecord::Base
|
6
|
+
|
7
|
+
shard :shards=>:my_shards, :map=>:my_mapping_function
|
8
|
+
|
9
|
+
has_strings :name
|
10
|
+
|
11
|
+
def self.my_shards
|
12
|
+
Array(0...4)
|
13
|
+
end
|
14
|
+
|
15
|
+
def my_mapping_function
|
16
|
+
shard_num = SimpleRecord::Sharding::Hashing.sdbm_hash(self.id) % 4
|
17
|
+
puts "shard_num=" + shard_num.inspect
|
18
|
+
shard_num
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
class MyShardedByFieldModel < SimpleRecord::Base
|
27
|
+
|
28
|
+
shard :shards=>:my_shards, :map=>:my_mapping_function
|
29
|
+
|
30
|
+
has_strings :name, :state
|
31
|
+
|
32
|
+
def self.my_shards
|
33
|
+
['AL', 'CA', 'FL', 'NY']
|
34
|
+
end
|
35
|
+
|
36
|
+
def my_mapping_function
|
37
|
+
state
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
end
|
data/test/test_base.rb
CHANGED
@@ -19,6 +19,16 @@ class TestBase < Test::Unit::TestCase
|
|
19
19
|
SimpleRecord.close_connection
|
20
20
|
end
|
21
21
|
|
22
|
+
def delete_all(clz)
|
23
|
+
puts 'delete_all ' + clz.name
|
24
|
+
obs = clz.find(:all)
|
25
|
+
obs.each do |o|
|
26
|
+
o.delete
|
27
|
+
end
|
28
|
+
# @@sdb.select("select * from #{domain}") do |o|
|
29
|
+
# o.delete
|
30
|
+
# end
|
31
|
+
end
|
22
32
|
|
23
33
|
def reset_connection
|
24
34
|
puts 'reset_connection'
|
data/test/test_shards.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.join(File.dirname(__FILE__), "/../lib/simple_record")
|
3
|
+
require File.join(File.dirname(__FILE__), "./test_helpers")
|
4
|
+
require File.join(File.dirname(__FILE__), "./test_base")
|
5
|
+
require "yaml"
|
6
|
+
require 'aws'
|
7
|
+
require_relative 'my_sharded_model'
|
8
|
+
|
9
|
+
# Tests for SimpleRecord
|
10
|
+
#
|
11
|
+
class TestShards < TestBase
|
12
|
+
|
13
|
+
def setup
|
14
|
+
super
|
15
|
+
delete_all MyShardedModel
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
super
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
# We'll want to shard based on ID's, user decides how many shards and some mapping function will
|
24
|
+
# be used to select the shard.
|
25
|
+
def test_id_sharding
|
26
|
+
|
27
|
+
mm = MyShardedModel.new(:name=>"single")
|
28
|
+
mm.save
|
29
|
+
sleep 1
|
30
|
+
puts 'finding by id'
|
31
|
+
mm2 = MyShardedModel.find(mm.id)
|
32
|
+
p mm2
|
33
|
+
assert_equal mm.id, mm2.id
|
34
|
+
puts 'deleting'
|
35
|
+
mm2.delete
|
36
|
+
sleep 1
|
37
|
+
mm3 = MyShardedModel.find(mm.id)
|
38
|
+
assert_nil mm3
|
39
|
+
|
40
|
+
puts "saving 20 now"
|
41
|
+
saved = []
|
42
|
+
20.times do |i|
|
43
|
+
mm = MyShardedModel.new(:name=>"name #{i}")
|
44
|
+
mm.save
|
45
|
+
saved << mm
|
46
|
+
end
|
47
|
+
|
48
|
+
# todo: assert that we're actually sharding
|
49
|
+
|
50
|
+
puts "finding them all"
|
51
|
+
found = []
|
52
|
+
rs = MyShardedModel.find(:all)
|
53
|
+
rs.each do |m|
|
54
|
+
p m
|
55
|
+
found << m
|
56
|
+
end
|
57
|
+
saved.each do |so|
|
58
|
+
assert(found.find { |m1| m1.id == so.id })
|
59
|
+
end
|
60
|
+
|
61
|
+
puts "deleting all of them"
|
62
|
+
found.each do |fo|
|
63
|
+
fo.delete
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "Now ensure that all are deleted"
|
67
|
+
rs = MyShardedModel.find(:all)
|
68
|
+
assert rs.size == 0
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_field_sharding
|
73
|
+
|
74
|
+
states = MyShardedByFieldModel.shards
|
75
|
+
puts "states=" + states.inspect
|
76
|
+
|
77
|
+
mm = MyShardedByFieldModel.new(:name=>"single", :state=>"CA")
|
78
|
+
mm.save
|
79
|
+
sleep 1
|
80
|
+
puts 'finding by id'
|
81
|
+
mm2 = MyShardedByFieldModel.find(mm.id)
|
82
|
+
p mm2
|
83
|
+
assert_equal mm.id, mm2.id
|
84
|
+
puts 'deleting'
|
85
|
+
mm2.delete
|
86
|
+
sleep 1
|
87
|
+
mm3 = MyShardedByFieldModel.find(mm.id)
|
88
|
+
assert_nil mm3
|
89
|
+
|
90
|
+
puts "saving 20 now"
|
91
|
+
saved = []
|
92
|
+
20.times do |i|
|
93
|
+
mm = MyShardedByFieldModel.new(:name=>"name #{i}", :state=>states[i % states.size])
|
94
|
+
mm.save
|
95
|
+
p mm
|
96
|
+
saved << mm
|
97
|
+
end
|
98
|
+
|
99
|
+
# todo: assert that we're actually sharding
|
100
|
+
|
101
|
+
puts "finding them all"
|
102
|
+
found = []
|
103
|
+
rs = MyShardedByFieldModel.find(:all)
|
104
|
+
rs.each do |m|
|
105
|
+
p m
|
106
|
+
found << m
|
107
|
+
end
|
108
|
+
saved.each do |so|
|
109
|
+
assert(found.find { |m1| m1.id == so.id })
|
110
|
+
end
|
111
|
+
|
112
|
+
puts "deleting all of them"
|
113
|
+
found.each do |fo|
|
114
|
+
fo.delete
|
115
|
+
end
|
116
|
+
sleep 1
|
117
|
+
|
118
|
+
puts "Now ensure that all are deleted"
|
119
|
+
rs = MyShardedByFieldModel.find(:all)
|
120
|
+
assert rs.size == 0
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_time_sharding
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 5
|
8
|
-
-
|
9
|
-
version: 1.5.
|
8
|
+
- 6
|
9
|
+
version: 1.5.6
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Travis Reeder
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2010-
|
19
|
+
date: 2010-12-01 00:00:00 -08:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -53,6 +53,7 @@ files:
|
|
53
53
|
- lib/simple_record/password.rb
|
54
54
|
- lib/simple_record/rails2.rb
|
55
55
|
- lib/simple_record/results_array.rb
|
56
|
+
- lib/simple_record/sharding.rb
|
56
57
|
- lib/simple_record/stats.rb
|
57
58
|
- lib/simple_record/translations.rb
|
58
59
|
- README.markdown
|
@@ -61,6 +62,7 @@ files:
|
|
61
62
|
- test/my_base_model.rb
|
62
63
|
- test/my_child_model.rb
|
63
64
|
- test/my_model.rb
|
65
|
+
- test/my_sharded_model.rb
|
64
66
|
- test/paging_array_test.rb
|
65
67
|
- test/simple_record_test.rb
|
66
68
|
- test/temp.rb
|
@@ -76,6 +78,7 @@ files:
|
|
76
78
|
- test/test_pagination.rb
|
77
79
|
- test/test_rails3.rb
|
78
80
|
- test/test_results_array.rb
|
81
|
+
- test/test_shards.rb
|
79
82
|
- test/test_usage.rb
|
80
83
|
has_rdoc: true
|
81
84
|
homepage: http://github.com/appoxy/simple_record/
|
@@ -115,6 +118,7 @@ test_files:
|
|
115
118
|
- test/my_base_model.rb
|
116
119
|
- test/my_child_model.rb
|
117
120
|
- test/my_model.rb
|
121
|
+
- test/my_sharded_model.rb
|
118
122
|
- test/paging_array_test.rb
|
119
123
|
- test/simple_record_test.rb
|
120
124
|
- test/temp.rb
|
@@ -130,4 +134,5 @@ test_files:
|
|
130
134
|
- test/test_pagination.rb
|
131
135
|
- test/test_rails3.rb
|
132
136
|
- test/test_results_array.rb
|
137
|
+
- test/test_shards.rb
|
133
138
|
- test/test_usage.rb
|