familia 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,24 @@
1
1
  FAMILIA, CHANGES
2
2
 
3
+
4
+ #### 0.7.1 (2011-04-09) ###############################
5
+
6
+ * FIXED: Explicitly convert boolean conf options to true or false. Redis client
7
+ treats "true" as false.
8
+ * FIXED: Clone options for each RedisObject instance to prevent cross contamination.
9
+ * CHANGE: Fix for Familia objects with custom suffixes
10
+ * CHANGE: Familia::String methods that modify the key now automatically update_expiration.
11
+ * CHANGE: Familia::Object.prefix replaces :: in class names with Familia.delim
12
+ * CHANGE: Use Familia::Object#object_proxy instead of Familia::Object.string :object
13
+ * ADDED: Familia.qnow, Familia::Object.qstamp, RedisObject#qstamp
14
+ * ADDED: RedisObject option: quantize
15
+ * ADDED: Familia::Object#savenx
16
+ * ADDED: Can now include a Familia object in Familia::Object.index (it uses its index)
17
+ * ADDED: Dependency multi_json
18
+ * ADDED: Hash#to_json, Hash.from_json, Array#to_json, Array.from_json
19
+ * ADDED: Familia::Object#update!
20
+
21
+
3
22
  #### 0.7.0 (2011-03-04) ###############################
4
23
 
5
24
  * CHANGE: Use RedisObject#multi_from_redis when possible (uses mget command)
@@ -1,8 +1,23 @@
1
- # Familia - 0.6 BETA
1
+ # Familia - 0.7 BETA
2
2
 
3
3
  **Organize and store ruby objects in Redis**
4
4
 
5
5
 
6
+ ## Basic Example
7
+
8
+ class Bone < Storable
9
+ include Familia
10
+ index [:token, :name]
11
+ field :token
12
+ field :name
13
+ list :owners
14
+ set :tags
15
+ zset :metrics
16
+ hash :props
17
+ string :value, :default => "GREAT!"
18
+ end
19
+
20
+
6
21
  ## More Information
7
22
 
8
23
  * [Codes](http://github.com/delano/familia)
data/Rakefile CHANGED
@@ -29,9 +29,7 @@ begin
29
29
  gem.add_dependency("uri-redis", ">= 0.4.2")
30
30
  gem.add_dependency("gibbler", ">= 0.8.6")
31
31
  gem.add_dependency("storable", ">= 0.8.6")
32
-
33
- #gem.add_development_dependency("rspec", ">= 1.2.9")
34
- #gem.add_development_dependency("mocha", ">= 0.9.8")
32
+ gem.add_dependency("multi_json", ">= 0.0.5")
35
33
  end
36
34
  Jeweler::GemcutterTasks.new
37
35
  rescue LoadError
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :MAJOR: 0
3
3
  :MINOR: 7
4
- :PATCH: 0
4
+ :PATCH: 1
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{familia}
8
- s.version = "0.7.0"
8
+ s.version = "0.7.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Delano Mandelbaum"]
12
- s.date = %q{2011-03-04}
12
+ s.date = %q{2011-04-11}
13
13
  s.description = %q{Organize and store ruby objects in Redis}
14
14
  s.email = %q{delano@solutious.com}
15
15
  s.extra_rdoc_files = [
@@ -55,17 +55,20 @@ Gem::Specification.new do |s|
55
55
  s.add_runtime_dependency(%q<uri-redis>, [">= 0.4.2"])
56
56
  s.add_runtime_dependency(%q<gibbler>, [">= 0.8.6"])
57
57
  s.add_runtime_dependency(%q<storable>, [">= 0.8.6"])
58
+ s.add_runtime_dependency(%q<multi_json>, [">= 0.0.5"])
58
59
  else
59
60
  s.add_dependency(%q<redis>, [">= 2.1.0"])
60
61
  s.add_dependency(%q<uri-redis>, [">= 0.4.2"])
61
62
  s.add_dependency(%q<gibbler>, [">= 0.8.6"])
62
63
  s.add_dependency(%q<storable>, [">= 0.8.6"])
64
+ s.add_dependency(%q<multi_json>, [">= 0.0.5"])
63
65
  end
64
66
  else
65
67
  s.add_dependency(%q<redis>, [">= 2.1.0"])
66
68
  s.add_dependency(%q<uri-redis>, [">= 0.4.2"])
67
69
  s.add_dependency(%q<gibbler>, [">= 0.8.6"])
68
70
  s.add_dependency(%q<storable>, [">= 0.8.6"])
71
+ s.add_dependency(%q<multi_json>, [">= 0.0.5"])
69
72
  end
70
73
  end
71
74
 
@@ -3,6 +3,7 @@ FAMILIA_LIB_HOME = File.expand_path File.dirname(__FILE__) unless defined?(FAMIL
3
3
  require 'uri/redis'
4
4
  require 'gibbler'
5
5
  require 'familia/core_ext'
6
+ require 'multi_json'
6
7
 
7
8
  module Familia
8
9
  module VERSION
@@ -85,7 +86,9 @@ module Familia
85
86
  uri &&= URI.parse uri if String === uri
86
87
  uri ||= Familia.uri
87
88
  conf = uri.conf
88
- conf[:thread_safe] = true unless conf.has_key?(:thread_safe)
89
+ conf[:thread_safe] = "true" unless conf.has_key?(:thread_safe)
90
+ conf[:thread_safe] = conf[:thread_safe].to_s == "true"
91
+ conf[:logging] = conf[:logging].to_s == "true"
89
92
  if conf.has_key?(:logging) && conf[:logging].to_s == "true"
90
93
  require 'logger'
91
94
  require 'log4r'
@@ -128,6 +131,16 @@ module Familia
128
131
  end
129
132
  @apiversion
130
133
  end
134
+ def now n=Time.now
135
+ n.utc.to_i
136
+ end
137
+ # A quantized timestamp
138
+ # e.g. 12:32 -> 12:30
139
+ #
140
+ def qnow quantum=10.minutes, now=Familia.now
141
+ rounded = now - (now % quantum)
142
+ Time.at(rounded).utc.to_i
143
+ end
131
144
  end
132
145
 
133
146
  class Problem < RuntimeError; end
@@ -148,11 +161,6 @@ module Familia
148
161
  obj.send :include, Gibbler::Complex
149
162
  obj.extend Familia::ClassMethods
150
163
  obj.class_zset :instances, :class => obj, :reference => true
151
- # :object is a special redis object because its reserved
152
- # for storing the marshaled instance data (e.g. to_json).
153
- # When it isn't defined explicitly we define it here b/c
154
- # it's assumed to exist in other places (see #save).
155
- obj.string :object, :class => obj unless obj.redis_object? :object
156
164
  Familia.classes << obj
157
165
  end
158
166
 
@@ -7,6 +7,27 @@ class Symbol
7
7
  end
8
8
  end
9
9
 
10
+ class Hash
11
+ unless method_defined?(:to_json)
12
+ def to_json
13
+ MultiJson.encode self
14
+ end
15
+ def self.from_json str
16
+ MultiJson.decode str
17
+ end
18
+ end
19
+ end
20
+ class Array
21
+ unless method_defined?(:to_json)
22
+ def to_json
23
+ MultiJson.encode self
24
+ end
25
+ def self.from_json str
26
+ MultiJson.decode str
27
+ end
28
+ end
29
+ end
30
+
10
31
  # Assumes Time::Units and Numeric mixins are available.
11
32
  class String
12
33
  def in_seconds
@@ -52,18 +52,12 @@ module Familia
52
52
  names
53
53
  end
54
54
  end
55
-
56
55
  def inherited(obj)
57
56
  obj.db = self.db
58
57
  obj.uri = self.uri
59
58
  obj.ttl = self.ttl
60
59
  obj.parent = self
61
60
  obj.class_zset :instances, :class => obj, :reference => true
62
- # :object is a special redis object because its reserved
63
- # for storing the marshaled instance data (e.g. to_json).
64
- # When it isn't defined explicitly we define it here b/c
65
- # it's assumed to exist in other places (see #save).
66
- obj.string :object, :class => obj unless obj.redis_object? :object
67
61
  Familia.classes << obj
68
62
  super(obj)
69
63
  end
@@ -97,13 +91,20 @@ module Familia
97
91
  redis_objects[name]
98
92
  end
99
93
 
94
+ def qstamp quantum=nil, pattern=nil, now=Familia.now
95
+ quantum ||= ttl || 10.minutes
96
+ pattern ||= '%H%M'
97
+ rounded = now - (now % quantum)
98
+ Time.at(rounded).utc.strftime(pattern)
99
+ end
100
+
100
101
  # Creates a class method called +name+ that
101
102
  # returns an instance of the RedisObject +klass+
102
103
  def install_class_redis_object name, klass, opts
103
104
  raise ArgumentError, "Name is blank" if name.to_s.empty?
104
105
  name = name.to_s.to_sym
105
- opts ||= {}
106
- opts[:parent] ||= self
106
+ opts = opts.nil? ? {} : opts.clone
107
+ opts[:parent] = self unless opts.has_key?(:parent)
107
108
  # TODO: investigate using metaclass.redis_objects
108
109
  class_redis_objects_order << name
109
110
  class_redis_objects[name] = OpenStruct.new
@@ -179,7 +180,7 @@ module Familia
179
180
  val
180
181
  end
181
182
  def prefix=(a) @prefix = a end
182
- def prefix(a=nil) @prefix = a if a; @prefix || self.name.downcase.to_sym end
183
+ def prefix(a=nil) @prefix = a if a; @prefix || self.name.downcase.gsub('::', Familia.delim).to_sym end
183
184
  # TODO: grab db, ttl, uri from parent
184
185
  #def parent=(a) @parent = a end
185
186
  #def parent(a=nil) @parent = a if a; @parent end
@@ -327,14 +328,22 @@ module Familia
327
328
  # See RedisObject.install_redis_object
328
329
  self.class.redis_objects.each_pair do |name, redis_object_definition|
329
330
  klass, opts = redis_object_definition.klass, redis_object_definition.opts
330
- opts ||= {}
331
- opts[:parent] ||= self
331
+ opts = opts.nil? ? {} : opts.clone
332
+ opts[:parent] = self unless opts.has_key?(:parent)
332
333
  redis_object = klass.new name, opts
333
334
  redis_object.freeze
334
335
  self.instance_variable_set "@#{name}", redis_object
335
336
  end
336
337
  end
337
338
 
339
+ def qstamp quantum=nil, pattern=nil, now=Familia.now
340
+ self.class.qstamp ttl, pattern, now
341
+ end
342
+
343
+ def from_redis
344
+ self.class.from_redis self.index
345
+ end
346
+
338
347
  def redis
339
348
  self.class.redis
340
349
  end
@@ -380,21 +389,43 @@ module Familia
380
389
  end
381
390
  self.class.rediskey self.index, suffix
382
391
  end
383
- def save
392
+ def object_proxy
393
+ @object_proxy ||= Familia::String.new self.rediskey, :ttl => ttl, :class => self.class
394
+ @object_proxy
395
+ end
396
+ def save meth=:set
384
397
  #Familia.trace :SAVE, Familia.redis(self.class.uri), redisuri, caller.first if Familia.debug?
385
398
  preprocess if respond_to?(:preprocess)
386
399
  self.update_time if self.respond_to?(:update_time)
387
- # TODO: Check here (run checkup)
388
- ret = self.object.set self # object is a name reserved by Familia
400
+ ret = object_proxy.send(meth, self) # object is a name reserved by Familia
389
401
  unless ret.nil?
390
402
  now = Time.now.utc.to_i
391
403
  self.class.instances.add now, self # use this set instead of Klass.keys
392
- self.object.update_expiration self.ttl # does nothing unless if not specified
404
+ object_proxy.update_expiration # does nothing unless if not specified
393
405
  end
394
- true
406
+ ret == "OK" || ret == true || ret == 1
407
+ end
408
+ def savenx
409
+ save :setnx
410
+ end
411
+ def update! hsh=nil
412
+ updated = false
413
+ hsh ||= {}
414
+ if hsh.empty?
415
+ raise Familia::Problem, "No #{self.class}#{to_hash} method" unless respond_to?(:to_hash)
416
+ ret = from_redis
417
+ hsh = ret.to_hash if ret
418
+ end
419
+ hsh.keys.each { |field|
420
+ v = hsh[field.to_s] || hsh[field.to_s.to_sym]
421
+ next if v.nil?
422
+ self.send(:"#{field}=", v)
423
+ updated = true
424
+ }
425
+ updated
395
426
  end
396
427
  def destroy!
397
- ret = self.object.delete
428
+ ret = object_proxy.delete
398
429
  if Familia.debug?
399
430
  Familia.trace :DELETED, Familia.redis(self.class.uri), "#{rediskey}: #{ret}", caller.first if Familia.debug?
400
431
  end
@@ -410,7 +441,9 @@ module Familia
410
441
  unless self.respond_to? meth
411
442
  raise NoIndex, "No such method: `#{meth}' for #{self.class}"
412
443
  end
413
- self.send(meth)
444
+ ret = self.send(meth)
445
+ ret = ret.index if ret.kind_of?(Familia)
446
+ ret
414
447
  }
415
448
  parts.join Familia.delim
416
449
  when Symbol, String
@@ -420,7 +453,9 @@ module Familia
420
453
  unless self.respond_to? self.class.index
421
454
  raise NoIndex, "No such method: `#{self.class.index}' for #{self.class}"
422
455
  end
423
- self.send( self.class.index)
456
+ ret = self.send(self.class.index)
457
+ ret = ret.index if ret.kind_of?(Familia)
458
+ ret
424
459
  end
425
460
  else
426
461
  raise Familia::NoIndex, self
@@ -81,8 +81,20 @@ module Familia
81
81
  # set the redis expire for this key whenever #save is called.
82
82
  # You can also call it explicitly via #update_expiration.
83
83
  #
84
+ # :quantize => append a quantized timestamp to the rediskey.
85
+ # Takes one of the following:
86
+ # Boolean: include the default stamp (now % 10 minutes)
87
+ # Integer: the number of seconds to quantize to (e.g. 1.hour)
88
+ # Array: All arguments for qstamp (quantum, pattern, Time.now)
89
+ #
84
90
  # :default => the default value (String-only)
85
91
  #
92
+ # :dump_method => the instance method to call to serialize the
93
+ # object before sending it to Redis (default: Familia.dump_method).
94
+ #
95
+ # :load_method => the class method to call to deserialize the
96
+ # object after it's read from Redis (default: Familia.load_method).
97
+ #
86
98
  # :db => the redis database to use (ignored if :redis is used).
87
99
  #
88
100
  # :redis => an instance of Redis.
@@ -146,7 +158,26 @@ module Familia
146
158
  # returns a redis key based on the parent
147
159
  # object so it will include the proper index.
148
160
  def rediskey
149
- parent? ? parent.rediskey(name, nil) : [name].flatten.compact.join(Familia.delim)
161
+ if parent?
162
+ # We need to check if the parent has a specific suffix
163
+ # for the case where we have specified one other than :object.
164
+ suffix = parent.kind_of?(Familia) && parent.class.suffix != :object ? parent.class.suffix : name
165
+ k = parent.rediskey(name, nil)
166
+ else
167
+ k = [name].flatten.compact.join(Familia.delim)
168
+ end
169
+ if @opts[:quantize]
170
+ args = case @opts[:quantize]
171
+ when Numeric
172
+ [@opts[:quantize]] # :quantize => 1.minute
173
+ when Array
174
+ @opts[:quantize] # :quantize => [1.day, '%m%D']
175
+ else
176
+ [] # :quantize => true
177
+ end
178
+ k = [k, qstamp(*args)].join(Familia.delim)
179
+ end
180
+ k
150
181
  end
151
182
 
152
183
  def class?
@@ -157,6 +188,13 @@ module Familia
157
188
  Class === parent || Module === parent || parent.kind_of?(Familia)
158
189
  end
159
190
 
191
+ def qstamp quantum=nil, pattern=nil, now=Familia.now
192
+ quantum ||= ttl || 10.minutes
193
+ pattern ||= '%H%M'
194
+ rounded = now - (now % quantum)
195
+ Time.at(rounded).utc.strftime(pattern)
196
+ end
197
+
160
198
  def update_expiration(ttl=nil)
161
199
  ttl ||= self.ttl
162
200
  return if ttl.to_i.zero? # nil will be zero
@@ -891,28 +929,44 @@ module Familia
891
929
  alias_method :replace, :value=
892
930
  alias_method :set, :value=
893
931
 
932
+ def setnx v
933
+ ret = redis.setnx rediskey, to_redis(v)
934
+ update_expiration
935
+ ret
936
+ end
937
+
894
938
  def increment
895
- redis.incr rediskey
939
+ ret = redis.incr rediskey
940
+ update_expiration
941
+ ret
896
942
  end
897
943
  alias_method :incr, :increment
898
944
 
899
945
  def incrementby int
900
- redis.incrby rediskey, int.to_i
946
+ ret = redis.incrby rediskey, int.to_i
947
+ update_expiration
948
+ ret
901
949
  end
902
950
  alias_method :incrby, :incrementby
903
951
 
904
952
  def decrement
905
- redis.decr rediskey
953
+ ret = redis.decr rediskey
954
+ update_expiration
955
+ ret
906
956
  end
907
957
  alias_method :decr, :decrement
908
958
 
909
959
  def decrementby int
910
- redis.decrby rediskey, int.to_i
960
+ ret = redis.decrby rediskey, int.to_i
961
+ update_expiration
962
+ ret
911
963
  end
912
964
  alias_method :decrby, :decrementby
913
965
 
914
966
  def append v
915
- redis.append rediskey, v
967
+ ret = redis.append rediskey, v
968
+ update_expiration
969
+ ret
916
970
  end
917
971
  alias_method :<<, :append
918
972
 
@@ -921,7 +975,9 @@ module Familia
921
975
  end
922
976
 
923
977
  def setbit offset, v
924
- redis.setbit rediskey, offset, v
978
+ ret = redis.setbit rediskey, offset, v
979
+ update_expiration
980
+ ret
925
981
  end
926
982
 
927
983
  def getrange spoint, epoint
@@ -929,11 +985,15 @@ module Familia
929
985
  end
930
986
 
931
987
  def setrange offset, v
932
- redis.setrange rediskey, offset, v
988
+ ret = redis.setrange rediskey, offset, v
989
+ update_expiration
990
+ ret
933
991
  end
934
992
 
935
993
  def getset v
936
- redis.getset rediskey, v
994
+ ret = redis.getset rediskey, v
995
+ update_expiration
996
+ ret
937
997
  end
938
998
 
939
999
  def nil?
@@ -32,3 +32,9 @@ class Customer < Storable
32
32
  class_string :message
33
33
  end
34
34
 
35
+ class Limiter < Storable
36
+ include Familia
37
+ index :name
38
+ field :name
39
+ string :counter, :quantize => true, :ttl => 1.hour, :quantize => [10.minutes, '%H:%M', 1302468980]
40
+ end
@@ -34,3 +34,20 @@ Bone.list? :owners
34
34
  definition = Bone.list :owners
35
35
  definition.klass
36
36
  #=> Familia::List
37
+
38
+ ## Familia.now
39
+ Familia.now Time.parse('2011-04-10 20:56:20 UTC').utc
40
+ #=> 1302468980
41
+
42
+ ## Familia.qnow
43
+ Familia.qnow 10.minutes, 1302468980
44
+ #=> 1302468600
45
+
46
+ ## Familia::Object.qstamp
47
+ Limiter.qstamp 10.minutes, '%H:%M', 1302468980
48
+ #=> '20:50'
49
+
50
+ ## Familia::Object#qstamp
51
+ limiter = Limiter.new :request
52
+ limiter.qstamp 10.minutes, '%H:%M', 1302468980
53
+ ##=> '20:50'
@@ -5,8 +5,8 @@ Familia.apiversion = 'v1'
5
5
 
6
6
 
7
7
  ## Redis Objects are unique per instance of a Familia class
8
- @a = Bone.new 'atoken'
9
- @b = Bone.new 'btoken'
8
+ @a = Bone.new 'atoken', :name1
9
+ @b = Bone.new 'atoken', :name2
10
10
  @a.owners.rediskey == @b.owners.rediskey
11
11
  #=> false
12
12
 
@@ -15,4 +15,31 @@ Familia.apiversion = 'v1'
15
15
  #=> true
16
16
 
17
17
 
18
+ ## Limiter#qstamp
19
+ @limiter = Limiter.new :requests
20
+ @limiter.counter.qstamp 10.minutes, '%H:%M', 1302468980
21
+ #=> '20:50'
18
22
 
23
+ ## Redis Objects can be stored to quantized keys
24
+ @limiter.counter.rediskey
25
+ #=> "v1:limiter:requests:counter:20:50"
26
+
27
+ ## Increment counter
28
+ @limiter.counter.clear
29
+ @limiter.counter.increment
30
+ #=> 1
31
+
32
+ ## Check ttl
33
+ @limiter.counter.ttl
34
+ #=> 3600
35
+
36
+ ## Check ttl for a different instance
37
+ ## (this exists to make sure options are cloned for each instance)
38
+ @limiter2 = Limiter.new :requests
39
+ @limiter2.counter.ttl
40
+ #=> 3600
41
+
42
+ ## Check realttl
43
+ sleep 1
44
+ @limiter.counter.realttl
45
+ #=> 3600-1
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: familia
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.7.0
5
+ version: 0.7.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Delano Mandelbaum
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-03-04 00:00:00 -05:00
13
+ date: 2011-04-11 00:00:00 -04:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -57,6 +57,17 @@ dependencies:
57
57
  version: 0.8.6
58
58
  type: :runtime
59
59
  version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: multi_json
62
+ prerelease: false
63
+ requirement: &id005 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.0.5
69
+ type: :runtime
70
+ version_requirements: *id005
60
71
  description: Organize and store ruby objects in Redis
61
72
  email: delano@solutious.com
62
73
  executables: []