familia 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,118 @@
1
+ # encoding: utf-8
2
+ class Symbol
3
+ unless method_defined?(:to_proc)
4
+ def to_proc
5
+ proc { |obj, *args| obj.send(self, *args) }
6
+ end
7
+ end
8
+ end
9
+
10
+ # Assumes Time::Units and Numeric mixins are available.
11
+ class String
12
+ def in_seconds
13
+ # "60m" => ["60", "m"]
14
+ q,u = self.scan(/([\d\.]+)([s,m,h])?/).flatten
15
+ q &&= q.to_f and u ||= 's'
16
+ q &&= q.in_seconds(u)
17
+ end
18
+ end
19
+
20
+ class Time
21
+ module Units
22
+ PER_MICROSECOND = 0.000001.freeze
23
+ PER_MILLISECOND = 0.001.freeze
24
+ PER_MINUTE = 60.0.freeze
25
+ PER_HOUR = 3600.0.freeze
26
+ PER_DAY = 86400.0.freeze
27
+
28
+ def microseconds() seconds * PER_MICROSECOND end
29
+ def milliseconds() seconds * PER_MILLISECOND end
30
+ def seconds() self end
31
+ def minutes() seconds * PER_MINUTE end
32
+ def hours() seconds * PER_HOUR end
33
+ def days() seconds * PER_DAY end
34
+ def weeks() seconds * PER_DAY * 7 end
35
+ def years() seconds * PER_DAY * 365 end
36
+
37
+ def in_years() seconds / PER_DAY / 365 end
38
+ def in_weeks() seconds / PER_DAY / 7 end
39
+ def in_days() seconds / PER_DAY end
40
+ def in_hours() seconds / PER_HOUR end
41
+ def in_minutes() seconds / PER_MINUTE end
42
+ def in_milliseconds() seconds / PER_MILLISECOND end
43
+ def in_microseconds() seconds / PER_MICROSECOND end
44
+
45
+ def in_time
46
+ Time.at(self).utc
47
+ end
48
+
49
+ def in_seconds(u=nil)
50
+ case u.to_s
51
+ when /\A(y)|(years?)\z/
52
+ years
53
+ when /\A(w)|(weeks?)\z/
54
+ weeks
55
+ when /\A(d)|(days?)\z/
56
+ days
57
+ when /\A(h)|(hours?)\z/
58
+ hours
59
+ when /\A(m)|(minutes?)\z/
60
+ minutes
61
+ when /\A(ms)|(milliseconds?)\z/
62
+ milliseconds
63
+ when /\A(us)|(microseconds?)|(μs)\z/
64
+ microseconds
65
+ else
66
+ self
67
+ end
68
+ end
69
+
70
+ ## JRuby doesn't like using instance_methods.select here.
71
+ ## It could be a bug or something quirky with Attic
72
+ ## (although it works in 1.8 and 1.9). The error:
73
+ ##
74
+ ## lib/attic.rb:32:in `select': yield called out of block (LocalJumpError)
75
+ ## lib/stella/mixins/numeric.rb:24
76
+ ##
77
+ ## Create singular methods, like hour and day.
78
+ # instance_methods.select.each do |plural|
79
+ # singular = plural.to_s.chop
80
+ # alias_method singular, plural
81
+ # end
82
+
83
+ alias_method :ms, :milliseconds
84
+ alias_method :'μs', :microseconds
85
+ alias_method :second, :seconds
86
+ alias_method :minute, :minutes
87
+ alias_method :hour, :hours
88
+ alias_method :day, :days
89
+ alias_method :week, :weeks
90
+ alias_method :year, :years
91
+
92
+ end
93
+ end
94
+
95
+ class Numeric
96
+ include Time::Units
97
+
98
+ def to_ms
99
+ (self*1000.to_f)
100
+ end
101
+
102
+ # TODO: Use 1024?
103
+ def to_bytes
104
+ args = case self.abs.to_i
105
+ when (1000)..(1000**2)
106
+ '%3.2f%s' % [(self / 1000.to_f).to_s, 'KB']
107
+ when (1000**2)..(1000**3)
108
+ '%3.2f%s' % [(self / (1000**2).to_f).to_s, 'MB']
109
+ when (1000**3)..(1000**4)
110
+ '%3.2f%s' % [(self / (1000**3).to_f).to_s, 'GB']
111
+ when (1000**4)..(1000**6)
112
+ '%3.2f%s' % [(self / (1000**4).to_f).to_s, 'TB']
113
+ else
114
+ [self.to_i, 'B'].join
115
+ end
116
+ end
117
+ end
118
+
@@ -0,0 +1,70 @@
1
+
2
+ module Familia
3
+ #
4
+ # class Example
5
+ # include Familia
6
+ # field :name
7
+ # include Familia::Stamps
8
+ # end
9
+ #
10
+ module Stamps
11
+ def self.included(obj)
12
+ obj.module_eval do
13
+ field :created => Integer
14
+ field :updated => Integer
15
+ def init_stamps
16
+ now = Time.now.utc.to_i
17
+ @created ||= now
18
+ @updated ||= now
19
+ end
20
+ def created
21
+ @created ||= Time.now.utc.to_i
22
+ end
23
+ def updated
24
+ @updated ||= Time.now.utc.to_i
25
+ end
26
+ def created_age
27
+ Time.now.utc.to_i-created
28
+ end
29
+ def updated_age
30
+ Time.now.utc.to_i-updated
31
+ end
32
+ def update_time
33
+ @updated = Time.now.utc.to_i
34
+ end
35
+ def update_time!
36
+ update_time
37
+ save if respond_to? :save
38
+ @updated
39
+ end
40
+ end
41
+ end
42
+ end
43
+ module Status
44
+ def self.included(obj)
45
+ obj.module_eval do
46
+ field :status
47
+ field :message
48
+ def failure?() status? 'failure' end
49
+ def success?() status? 'success' end
50
+ def pending?() status? 'pending' end
51
+ def expired?() status? 'expired' end
52
+ def disabled?() status? 'disabled' end
53
+ def failure!(msg=nil) status! 'failure', msg end
54
+ def success!(msg=nil) status! 'success', msg end
55
+ def pending!(msg=nil) status! 'pending', msg end
56
+ def expired!(msg=nil) status! 'expired', msg end
57
+ def disabled!(msg=nil) status! 'disabled', msg end
58
+ private
59
+ def status?(s)
60
+ status.to_s == s.to_s
61
+ end
62
+ def status!(s, msg=nil)
63
+ @updated = Time.now.utc.to_f
64
+ @status, @message = s, msg
65
+ save if respond_to? :save
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,419 @@
1
+ require 'ostruct'
2
+
3
+ module Familia::Object
4
+ require 'familia/redisobject'
5
+
6
+
7
+ # Auto-extended into a class that includes Familia
8
+ module ClassMethods
9
+
10
+ RedisObject.klasses.each_pair do |kind, klass|
11
+ # e.g.
12
+ #
13
+ # list(name, klass, opts)
14
+ # list?(name)
15
+ # lists
16
+ #
17
+ define_method :"#{kind}" do |*args, &blk|
18
+ name, opts = *args
19
+ install_redis_object name, klass, opts
20
+ redis_objects[name.to_s.to_sym]
21
+ end
22
+ define_method :"#{kind}?" do |name|
23
+ obj = redis_objects[name.to_s.to_sym]
24
+ !obj.nil? && klass == obj.klass
25
+ end
26
+ define_method :"#{kind}s" do
27
+ names = redis_objects.keys.select { |name| send(:"#{kind}?", name) }
28
+ names.collect! { |name| redis_objects[name] }
29
+ names
30
+ end
31
+ # e.g.
32
+ #
33
+ # class_list(name, klass, opts)
34
+ # class_list?(name)
35
+ # class_lists
36
+ #
37
+ define_method :"class_#{kind}" do |*args, &blk|
38
+ name, opts = *args
39
+ install_class_redis_object name, klass, opts
40
+ end
41
+ define_method :"class_#{kind}?" do |name|
42
+ obj = class_redis_objects[name.to_s.to_sym]
43
+ !obj.nil? && klass == obj.klass
44
+ end
45
+ define_method :"class_#{kind}s" do
46
+ names = class_redis_objects.keys.select { |name| send(:"#{kind}?", name) }
47
+ names.collect! { |name| class_redis_objects[name] }
48
+ names
49
+ end
50
+ end
51
+
52
+ def inherited(obj)
53
+ obj.db = self.db
54
+ Familia.classes << obj
55
+ super(obj)
56
+ end
57
+ def extended(obj)
58
+ obj.db = self.db
59
+ Familia.classes << obj
60
+ end
61
+
62
+ # Creates an instance method called +name+ that
63
+ # returns an instance of the RedisObject +klass+
64
+ def install_redis_object name, klass, opts
65
+ name = name.to_s.to_sym
66
+ opts ||= {}
67
+ redis_objects[name] = OpenStruct.new
68
+ redis_objects[name].name = name
69
+ redis_objects[name].klass = klass
70
+ redis_objects[name].opts = opts
71
+ self.send :attr_reader, name
72
+ define_method "#{name}=" do |v|
73
+ self.send(name).replace v
74
+ end
75
+ redis_objects[name]
76
+ end
77
+
78
+ # Creates a class method called +name+ that
79
+ # returns an instance of the RedisObject +klass+
80
+ def install_class_redis_object name, klass, opts
81
+ name = name.to_s.to_sym
82
+ opts ||= {}
83
+ opts[:suffix] ||= nil
84
+ # TODO: investigate metaclass.redis_objects
85
+ class_redis_objects[name] = OpenStruct.new
86
+ class_redis_objects[name].name = name
87
+ class_redis_objects[name].klass = klass
88
+ class_redis_objects[name].opts = opts
89
+ # An accessor method created in the metclass will
90
+ # access the instance variables for this class.
91
+ metaclass.send :attr_reader, name
92
+ metaclass.send :define_method, "#{name}=" do |v|
93
+ send(name).replace v
94
+ end
95
+ redis_object = klass.new name, self, opts
96
+ redis_object.freeze
97
+ self.instance_variable_set("@#{name}", redis_object)
98
+ class_redis_objects[name]
99
+ end
100
+
101
+ def from_redisdump dump
102
+ dump # todo
103
+ end
104
+ def db(db=nil)
105
+ @db = db if db;
106
+ @db
107
+ end
108
+ def db=(db) @db = db end
109
+ def host(host=nil) @host = host if host; @host end
110
+ def host=(host) @host = host end
111
+ def port(port=nil) @port = port if port; @port end
112
+ def port=(port) @port = port end
113
+ def uri=(uri)
114
+ uri = URI.parse uri if String === uri
115
+ @uri = uri
116
+ end
117
+ def uri(uri=nil)
118
+ self.uri = uri unless uri.to_s.empty?
119
+ return @uri if @uri
120
+ @uri = URI.parse Familia.uri.to_s
121
+ @uri.db = @db if @db
122
+ Familia.connect @uri #unless Familia.connected?(@uri)
123
+ @uri
124
+ end
125
+ def redis
126
+ Familia.redis(self.uri)
127
+ end
128
+ def flushdb
129
+ Familia.info "flushing #{uri}"
130
+ redis.flushdb
131
+ end
132
+ def keys(suffix=nil)
133
+ self.redis.keys(rediskey('*',suffix)) || []
134
+ end
135
+ def all(suffix=nil)
136
+ # objects that could not be parsed will be nil
137
+ keys(suffix).collect { |k| from_key(k) }.compact
138
+ end
139
+ def any?(filter='*')
140
+ size(filter) > 0
141
+ end
142
+ def size(filter='*')
143
+ self.redis.keys(rediskey(filter)).compact.size
144
+ end
145
+ def suffix(a=nil, &blk)
146
+ @suffix = a || blk if a || !blk.nil?
147
+ val = @suffix || Familia.default_suffix
148
+ val
149
+ end
150
+ def prefix=(a) @prefix = a end
151
+ def prefix(a=nil) @prefix = a if a; @prefix || self.name.downcase.to_sym end
152
+ def index(i=nil, &blk)
153
+ @index = i || blk if i || !blk.nil?
154
+ @index ||= Familia.index
155
+ @index
156
+ end
157
+ def ttl(sec=nil)
158
+ @ttl = sec.to_i unless sec.nil?
159
+ @ttl
160
+ end
161
+ def suffixes
162
+ redis_objects.keys.uniq
163
+ end
164
+ def class_redis_objects
165
+ @class_redis_objects ||= {}
166
+ @class_redis_objects
167
+ end
168
+ def class_redis_objects? name
169
+ class_redis_objects.has_key? name.to_s.to_sym
170
+ end
171
+ def redis_object? name
172
+ redis_objects.has_key? name.to_s.to_sym
173
+ end
174
+ def redis_objects
175
+ @redis_objects ||= {}
176
+ @redis_objects
177
+ end
178
+ def create(*args)
179
+ me = new(*args)
180
+ raise "#{self} exists: #{me.to_json}" if me.exists?
181
+ me.save
182
+ me
183
+ end
184
+ def load_or_create(id)
185
+ if exists?(id)
186
+ from_redis(id)
187
+ else
188
+ me = new id
189
+ me.save
190
+ me
191
+ end
192
+ end
193
+
194
+ def multiget(*ids)
195
+ ids = rawmultiget(*ids)
196
+ ids.compact.collect { |json| self.from_json(json) }.compact
197
+ end
198
+ def rawmultiget(*ids)
199
+ ids.collect! { |objid| rediskey(objid) }
200
+ return [] if ids.compact.empty?
201
+ Familia.trace :MULTIGET, self.redis, "#{ids.size}: #{ids}", caller
202
+ ids = self.redis.mget *ids
203
+ end
204
+
205
+ def from_key(akey)
206
+ raise ArgumentError, "Null key" if akey.nil? || akey.empty?
207
+ Familia.trace :LOAD, redis, "#{self.uri}/#{akey}", caller if Familia.debug?
208
+ return unless redis.exists akey
209
+ v = redis.get akey
210
+ begin
211
+ if v.to_s.empty?
212
+ Familia.info "No content @ #{akey}"
213
+ nil
214
+ else
215
+ self.send Familia.load_method, v
216
+ end
217
+ rescue => ex
218
+ Familia.info v
219
+ Familia.info "Non-fatal error parsing JSON for #{akey}: #{ex.message}"
220
+ Familia.info ex.backtrace
221
+ nil
222
+ end
223
+ end
224
+ def from_redis(objid)
225
+ objid &&= objid.to_s
226
+ return nil if objid.nil? || objid.empty?
227
+ this_key = rediskey(objid, self.suffix)
228
+ me = from_key(this_key)
229
+ me
230
+ end
231
+ def exists?(objid, suffix=nil)
232
+ objid &&= objid.to_s
233
+ return false if objid.nil? || objid.empty?
234
+ ret = Familia.redis(self.uri).exists rediskey(objid, suffix)
235
+ Familia.trace :EXISTS, Familia.redis(self.uri), "#{rediskey(objid)} #{ret}", caller.first
236
+ ret
237
+ end
238
+ def destroy!(idx, suffix=nil) # TODO: remove suffix arg
239
+ ret = Familia.redis(self.uri).del rediskey(runid, suffix)
240
+ Familia.trace :DELETED, Familia.redis(self.uri), "#{rediskey(runid)}: #{ret}", caller.first
241
+ ret
242
+ end
243
+ def find(suffix='*')
244
+ list = Familia.redis(self.uri).keys(rediskey('*', suffix)) || []
245
+ end
246
+ def rediskey(idx, suffix=nil)
247
+ raise RuntimeError, "No index for #{self}" if idx.to_s.empty?
248
+ idx &&= idx.to_s
249
+ Familia.rediskey(prefix, idx, suffix)
250
+ end
251
+ def expand(short_idx, suffix=self.suffix)
252
+ expand_key = Familia.rediskey(self.prefix, "#{short_idx}*", suffix)
253
+ Familia.trace :EXPAND, Familia.redis(self.uri), expand_key, caller.first
254
+ list = Familia.redis(self.uri).keys expand_key
255
+ case list.size
256
+ when 0
257
+ nil
258
+ when 1
259
+ matches = list.first.match(/\A#{Familia.rediskey(prefix)}\:(.+?)\:#{suffix}/) || []
260
+ matches[1]
261
+ else
262
+ raise Familia::NonUniqueKey, "Short key returned more than 1 match"
263
+ end
264
+ end
265
+ ## TODO: Investigate
266
+ ##def float
267
+ ## Proc.new do |v|
268
+ ## v.nil? ? 0 : v.to_f
269
+ ## end
270
+ ##end
271
+ end
272
+
273
+
274
+ module InstanceMethods
275
+
276
+ # A default initialize method. This will be replaced
277
+ # if a class defines its own initialize method after
278
+ # including Familia. In that case, the replacement
279
+ # must call initialize_redis_objects.
280
+ def initialize *args
281
+ super # call Storable#initialize or equivalent
282
+ initialize_redis_objects
283
+ end
284
+
285
+ # This needs to be called in the initialize method of
286
+ # any class that includes Familia.
287
+ def initialize_redis_objects
288
+ # :object is a special redis object because its reserved
289
+ # for storing the marshaled instance data (e.g. to_json).
290
+ # When it isn't defined explicitly we define it here b/c
291
+ # it's assumed to exist in other places (see #save).
292
+ unless self.class.redis_object? :object
293
+ self.class.string :object, :class => self.class
294
+ end
295
+
296
+ # Generate instances of each RedisObject. These need to be
297
+ # unique for each instance of this class so they can refer
298
+ # to the index of this specific instance.
299
+ # i.e.
300
+ # familia_object.rediskey == v1:bone:INDEXVALUE:object
301
+ # familia_object.redis_object.rediskey == v1:bone:INDEXVALUE:name
302
+ #
303
+ # See RedisObject.install_redis_object
304
+ self.class.redis_objects.each_pair do |name, redis_object_definition|
305
+ klass, opts = redis_object_definition.klass, redis_object_definition.opts
306
+ redis_object = klass.new name, self, opts
307
+ redis_object.freeze
308
+ self.instance_variable_set "@#{name}", redis_object
309
+ end
310
+ end
311
+
312
+ def redis
313
+ self.class.redis
314
+ end
315
+
316
+ def redisinfo
317
+ info = {
318
+ :db => self.class.db || 0,
319
+ :key => rediskey,
320
+ :type => redistype,
321
+ :ttl => realttl
322
+ }
323
+ end
324
+ def exists?
325
+ Familia.redis(self.class.uri).exists rediskey
326
+ end
327
+ def destroy!
328
+ ret = Familia.redis(self.class.uri).del rediskey
329
+ if Familia.debug?
330
+ Familia.trace :DELETED, Familia.redis(self.class.uri), "#{rediskey}: #{ret}", caller.first
331
+ end
332
+ ret
333
+ end
334
+
335
+ #def rediskeys
336
+ # self.class.redis_objects.each do |redis_object_definition|
337
+ #
338
+ # end
339
+ #end
340
+
341
+ def allkeys
342
+ # TODO: Use redis_objects instead
343
+ keynames = [rediskey]
344
+ self.class.suffixes.each do |sfx|
345
+ keynames << rediskey(sfx)
346
+ end
347
+ keynames
348
+ end
349
+ def rediskey(suffix=nil)
350
+ raise Familia::EmptyIndex, self.class if index.to_s.empty?
351
+ if suffix.nil?
352
+ suffix = self.class.suffix.kind_of?(Proc) ?
353
+ self.class.suffix.call(self) :
354
+ self.class.suffix
355
+ end
356
+ self.class.rediskey self.index, suffix
357
+ end
358
+ def save
359
+ Familia.trace :SAVE, Familia.redis(self.class.uri), redisuri, caller.first
360
+ preprocess if respond_to?(:preprocess)
361
+ self.update_time if self.respond_to?(:update_time)
362
+ ret = self.object.value = self
363
+ self.object.update_expiration self.ttl # does nothing unless if not specified
364
+ true
365
+ end
366
+ def index
367
+ if @index.nil?
368
+ self.class.index.kind_of?(Proc) ?
369
+ self.class.index.call(self) :
370
+ self.send(self.class.index)
371
+ else
372
+ @index
373
+ end
374
+ end
375
+ def index=(i)
376
+ @index = i
377
+ end
378
+ def expire(ttl=nil)
379
+ ttl ||= self.class.ttl
380
+ Familia.redis(self.class.uri).expire rediskey, ttl.to_i
381
+ end
382
+ def realttl
383
+ Familia.redis(self.class.uri).ttl rediskey
384
+ end
385
+ def ttl=(v)
386
+ @ttl = v.to_i
387
+ end
388
+ def ttl
389
+ @ttl || self.class.ttl
390
+ end
391
+ def raw(suffix=nil)
392
+ suffix ||= :object
393
+ Familia.redis(self.class.uri).get rediskey(suffix)
394
+ end
395
+ def redisuri(suffix=nil)
396
+ u = URI.parse self.class.uri.to_s
397
+ u.db ||= self.class.db.to_s
398
+ u.key = rediskey(suffix)
399
+ u
400
+ end
401
+ def redistype(suffix=nil)
402
+ Familia.redis(self.class.uri).type rediskey(suffix)
403
+ end
404
+ # Finds the shortest available unique key (lower limit of 6)
405
+ def shortid
406
+ len = 6
407
+ loop do
408
+ begin
409
+ self.class.expand(@id.shorten(len))
410
+ break
411
+ rescue Familia::NonUniqueKey
412
+ len += 1
413
+ end
414
+ end
415
+ @id.shorten(len)
416
+ end
417
+ end
418
+
419
+ end