redis-expiring-set 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in redis-expiring-set.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,92 @@
1
+ # Redis::ExpiringSet
2
+
3
+ ## Purpose
4
+
5
+ Redis::ExpiringSet provides a very specific but useful data-type to your Redis
6
+ environment - a set of objects where each has its own specified expiration time.
7
+
8
+ Imagine, for example, that you want to keep details on every email that your
9
+ system sent out in the last 24 hours. Dump the data into an ExpiringSet with a
10
+ 24-hour expiration time and let the set manage itself - you'll only ever see
11
+ data that is no more than 24 hours old.
12
+
13
+ ## Usage
14
+
15
+ The commands for Redis::ExpiringSet are designed to closely mirror those Redis
16
+ already has in place for working with sets. As of this release the commands are
17
+ not a complete copy of what's available for working with sets in Redis, but they
18
+ serve to be functional. Example usage:
19
+
20
+ require 'redis-expiring-set'
21
+
22
+ redis = Redis.new
23
+ expset = Redis::ExpiringSet.new(redis)
24
+
25
+ # add "something" to the set "some_set" that expires in 10 seconds
26
+ expset.xadd "some_set", "something", Time.now + 10
27
+ # This does the same thing; pass in a number instead of a Time object and it
28
+ # assumes you mean "expire x seconds from now"
29
+ expset.xadd "some_set", "something", 10
30
+
31
+ # Decide you want to remove the item before it expires? OK:
32
+ expset.xrem "some_set", "something"
33
+
34
+ # Need to know how big the set is?
35
+ expset.xcard "some_set"
36
+
37
+ # Want to actually look at your data?
38
+ expset.xmembers "some_set"
39
+
40
+ # By default, expired items are removed any time you go to read values
41
+ # (currently, when you call xcard and xmembers). If you want to clear them
42
+ # manually, though, you can use:
43
+ expset.xclearexpired "some_set"
44
+
45
+ ## Alternate Usage
46
+
47
+ "Ugh, you mean I have to instantiate some new object and use that to interact
48
+ with this new data type?"
49
+
50
+ Not if you don't want to. There's an optional file you can require which
51
+ monkeypatches these methods onto the Redis class so that you can use it without
52
+ ever having to touch an instance of Redis::ExpiringSet. Example:
53
+
54
+ require 'redis-expiring-set/monkeypatch'
55
+
56
+ At this point you can use `xadd`, `xrem`, `xcard`, `xmembers`, and
57
+ `xclearexpired` just like they were native redis methods. This just isn't the
58
+ default functionality because I don't want you to have to monkeypatch Redis for
59
+ this if you don't want to.
60
+
61
+ ## Performance
62
+
63
+ Redis::ExpiredSet uses nothing but native Redis functions to manipulate data,
64
+ and under the hood we use sorted sets. Expiring items in the cache is only done
65
+ when it's absolutely necessary, so there should be a minimal number of
66
+ unnecessary operations.
67
+
68
+ BUT, obviously, one of the chief advantages of using Redis is the performance,
69
+ so if you notice anything that might make this library any more performant, I'd
70
+ be glad to hear about it.
71
+
72
+ ## Contributions
73
+
74
+ If Redis::ExpiringSet interests you and you think you might want to contribute, hit me up
75
+ over Github. You can also just fork it and make some changes, but there's a
76
+ better chance that your work won't be duplicated or rendered obsolete if you
77
+ check in on the current development status first.
78
+
79
+ ## Development notes
80
+
81
+ You need Redis installed to do development on Redis::ExpiringSet, and currently the test
82
+ suites assume that you're using the default configurations for Redis. That
83
+ should probably change, but it probably won't until someone needs it to.
84
+
85
+ Gem requirements/etc. should be handled by Bundler.
86
+
87
+ ## License
88
+ Copyright (C) 2012 by Tommy Morgan
89
+
90
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
91
+
92
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/unit/test_*.rb']
7
+ t.verbose = true
8
+ t.warning = true
9
+ end
@@ -0,0 +1,46 @@
1
+ require "forwardable"
2
+
3
+ require "redis"
4
+ require "redis-expiring-set/version"
5
+
6
+
7
+ class Redis
8
+ module ExpiringSetMethods
9
+ def xadd(key, value, expires)
10
+ if expires.is_a? Numeric
11
+ expires = Time.now + expires
12
+ end
13
+
14
+ zadd(key, expires.to_f, value)
15
+ end
16
+
17
+ def xrem(key, value)
18
+ zrem(key, value)
19
+ end
20
+
21
+ def xmembers(key)
22
+ xclearexpired(key)
23
+ zrangebyscore(key, -Float::INFINITY, Float::INFINITY)
24
+ end
25
+
26
+ def xclearexpired(key)
27
+ zremrangebyscore key, -Float::INFINITY, Time.now.to_f.to_i
28
+ end
29
+
30
+ def xcard(key)
31
+ xclearexpired(key)
32
+ zcard(key)
33
+ end
34
+ end
35
+
36
+ class ExpiringSet
37
+ extend Forwardable
38
+ include ExpiringSetMethods
39
+
40
+ def_delegators :@redis, :zadd, :zcard, :zrem, :zrangebyscore, :zremrangebyscore
41
+
42
+ def initialize(redis)
43
+ @redis = redis
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ class Redis
2
+ include Redis::ExpiringSetMethods
3
+ end
@@ -0,0 +1,5 @@
1
+ class Redis
2
+ class ExpiringSet
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "redis-expiring-set/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "redis-expiring-set"
7
+ s.version = Redis::ExpiringSet::VERSION
8
+ s.authors = ["Tommy Morgan"]
9
+ s.email = ["tommy.morgan@gmail.com"]
10
+ s.homepage = "http://github.com/duwanis/redis-expiring-set"
11
+ s.summary = %q{Add an expiring set to redis.}
12
+ s.description = %q{Add some commands to Redis that make it easy to work with a set of objects that need to expire individually.}
13
+
14
+ s.rubyforge_project = "redis-expiring-set"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+ s.add_runtime_dependency "redis"
25
+ end
@@ -0,0 +1,229 @@
1
+ ## This is code stolen from the definition file for Minitest::Spec. We really
2
+ # like everything about Minitest::Spec except for the expectations part, so we
3
+ # are stealing it and using it here. Shamelessly.
4
+ #
5
+ # This code may go out of date in future versions of Ruby, so we should keep an
6
+ # eye on that. But that's better than reinventing the wheel.
7
+
8
+ module Kernel # :nodoc:
9
+ ##
10
+ # Describe a series of expectations for a given target +desc+.
11
+ #
12
+ # TODO: find good tutorial url.
13
+ #
14
+ # Defines a test class subclassing from either MiniTest::Spec or
15
+ # from the surrounding describe's class. The surrounding class may
16
+ # subclass MiniTest::Spec manually in order to easily share code:
17
+ #
18
+ # class MySpec < MiniTest::Spec
19
+ # # ... shared code ...
20
+ # end
21
+ #
22
+ # class TestStuff < MySpec
23
+ # it "does stuff" do
24
+ # # shared code available here
25
+ # end
26
+ # describe "inner stuff" do
27
+ # it "still does stuff" do
28
+ # # ...and here
29
+ # end
30
+ # end
31
+ # end
32
+
33
+ def describe desc, additional_desc = nil, &block # :doc:
34
+ stack = MiniTest::Spec.describe_stack
35
+ name = [stack.last, desc, additional_desc].compact.join("::")
36
+ sclas = stack.last || if Class === self && self < MiniTest::Spec then
37
+ self
38
+ else
39
+ MiniTest::Spec.spec_type desc
40
+ end
41
+
42
+ cls = sclas.create name, desc
43
+
44
+ stack.push cls
45
+ cls.class_eval(&block)
46
+ stack.pop
47
+ cls
48
+ end
49
+ private :describe
50
+ end
51
+
52
+ ##
53
+ # MiniTest::Spec -- The faster, better, less-magical spec framework!
54
+ #
55
+ # For a list of expectations, see MiniTest::Expectations.
56
+
57
+ class MiniTest::Spec < MiniTest::Unit::TestCase
58
+ ##
59
+ # Contains pairs of matchers and Spec classes to be used to
60
+ # calculate the superclass of a top-level describe. This allows for
61
+ # automatically customizable spec types.
62
+ #
63
+ # See: register_spec_type and spec_type
64
+
65
+ TYPES = [[//, MiniTest::Spec]]
66
+
67
+ ##
68
+ # Register a new type of spec that matches the spec's description.
69
+ # This method can take either a Regexp and a spec class or a spec
70
+ # class and a block that takes the description and returns true if
71
+ # it matches.
72
+ #
73
+ # Eg:
74
+ #
75
+ # register_spec_type(/Controller$/, MiniTest::Spec::Rails)
76
+ #
77
+ # or:
78
+ #
79
+ # register_spec_type(MiniTest::Spec::RailsModel) do |desc|
80
+ # desc.superclass == ActiveRecord::Base
81
+ # end
82
+
83
+ def self.register_spec_type(*args, &block)
84
+ if block then
85
+ matcher, klass = block, args.first
86
+ else
87
+ matcher, klass = *args
88
+ end
89
+ TYPES.unshift [matcher, klass]
90
+ end
91
+
92
+ ##
93
+ # Figure out the spec class to use based on a spec's description. Eg:
94
+ #
95
+ # spec_type("BlahController") # => MiniTest::Spec::Rails
96
+
97
+ def self.spec_type desc
98
+ TYPES.find { |matcher, klass|
99
+ if matcher.respond_to? :call then
100
+ matcher.call desc
101
+ else
102
+ matcher === desc.to_s
103
+ end
104
+ }.last
105
+ end
106
+
107
+ @@describe_stack = []
108
+ def self.describe_stack # :nodoc:
109
+ @@describe_stack
110
+ end
111
+
112
+ ##
113
+ # Returns the children of this spec.
114
+
115
+ def self.children
116
+ @children ||= []
117
+ end
118
+
119
+ def self.nuke_test_methods! # :nodoc:
120
+ self.public_instance_methods.grep(/^test_/).each do |name|
121
+ self.send :undef_method, name
122
+ end
123
+ end
124
+
125
+ ##
126
+ # Define a 'before' action. Inherits the way normal methods should.
127
+ #
128
+ # NOTE: +type+ is ignored and is only there to make porting easier.
129
+ #
130
+ # Equivalent to MiniTest::Unit::TestCase#setup.
131
+
132
+ def self.before type = :each, &block
133
+ raise "unsupported before type: #{type}" unless type == :each
134
+
135
+ add_setup_hook {|tc| tc.instance_eval(&block) }
136
+ end
137
+
138
+ ##
139
+ # Define an 'after' action. Inherits the way normal methods should.
140
+ #
141
+ # NOTE: +type+ is ignored and is only there to make porting easier.
142
+ #
143
+ # Equivalent to MiniTest::Unit::TestCase#teardown.
144
+
145
+ def self.after type = :each, &block
146
+ raise "unsupported after type: #{type}" unless type == :each
147
+
148
+ add_teardown_hook {|tc| tc.instance_eval(&block) }
149
+ end
150
+
151
+ ##
152
+ # Define an expectation with name +desc+. Name gets morphed to a
153
+ # proper test method name. For some freakish reason, people who
154
+ # write specs don't like class inheritence, so this goes way out of
155
+ # its way to make sure that expectations aren't inherited.
156
+ #
157
+ # This is also aliased to #specify and doesn't require a +desc+ arg.
158
+ #
159
+ # Hint: If you _do_ want inheritence, use minitest/unit. You can mix
160
+ # and match between assertions and expectations as much as you want.
161
+
162
+ def self.it desc = "anonymous", &block
163
+ block ||= proc { skip "(no tests defined)" }
164
+
165
+ @specs ||= 0
166
+ @specs += 1
167
+
168
+ name = "test_%04d_%s" % [ @specs, desc.gsub(/\W+/, '_').downcase ]
169
+
170
+ define_method name, &block
171
+
172
+ self.children.each do |mod|
173
+ mod.send :undef_method, name if mod.public_method_defined? name
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Essentially, define an accessor for +name+ with +block+.
179
+ #
180
+ # Why use let instead of def? I honestly don't know.
181
+
182
+ def self.let name, &block
183
+ define_method name do
184
+ @_memoized ||= {}
185
+ @_memoized.fetch(name) { |k| @_memoized[k] = instance_eval(&block) }
186
+ end
187
+ end
188
+
189
+ ##
190
+ # Another lazy man's accessor generator. Made even more lazy by
191
+ # setting the name for you to +subject+.
192
+
193
+ def self.subject &block
194
+ let :subject, &block
195
+ end
196
+
197
+ def self.create name, desc # :nodoc:
198
+ cls = Class.new(self) do
199
+ @name = name
200
+ @desc = desc
201
+
202
+ nuke_test_methods!
203
+ end
204
+
205
+ children << cls
206
+
207
+ cls
208
+ end
209
+
210
+ def self.to_s # :nodoc:
211
+ defined?(@name) ? @name : super
212
+ end
213
+
214
+ # :stopdoc:
215
+ def after_setup
216
+ run_setup_hooks
217
+ end
218
+
219
+ def before_teardown
220
+ run_teardown_hooks
221
+ end
222
+
223
+ class << self
224
+ attr_reader :desc
225
+ alias :specify :it
226
+ alias :name :to_s
227
+ end
228
+ # :startdoc:
229
+ end
@@ -0,0 +1,12 @@
1
+ require 'test/unit'
2
+
3
+ require 'partial_minispec'
4
+
5
+ # include the gem
6
+ require 'redis-expiring-set'
7
+
8
+ # Use database 15 for testing, so we don't risk overwriting any data that's
9
+ # actually useful
10
+ def clear_test_db
11
+ Redis.new(:db => 15).flushdb
12
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+ require 'redis-expiring-set/monkeypatch'
3
+
4
+ describe Redis::ExpiringSet do
5
+ describe "newly instantiated" do
6
+ before do
7
+ @redis = Redis.new(:db => 15)
8
+ end
9
+
10
+ after do
11
+ clear_test_db
12
+ end
13
+
14
+ it "allows things to be added to expiration sets using xadd" do
15
+ assert_equal true, @redis.xadd("some_set", "some_value", Time.now + 10)
16
+ end
17
+
18
+ it "allows expiration to set in terms of seconds as well" do
19
+ assert_equal true, @redis.xadd("some_set", "some_value", 60)
20
+ end
21
+
22
+ it "allows items to be removed from expiration sets using xrem" do
23
+ assert_equal true, @redis.xadd("some_set", "some_value", 60)
24
+ assert_equal true, @redis.xrem("some_set", "some_value")
25
+ end
26
+
27
+ it "allows for all items in an expiration set to be returned by xmembers" do
28
+ assert_equal true, @redis.xadd("some_set", "some_value", 300)
29
+ assert_equal ["some_value"], @redis.xmembers("some_set")
30
+ end
31
+
32
+ it "doesn't include any already expired items in xmembers" do
33
+ assert_equal true, @redis.xadd("some_set", "some_value", Time.now - 100)
34
+ assert_equal [], @redis.xmembers("some_set")
35
+ end
36
+
37
+ it "allows for a count of items in an expiration set using xcard" do
38
+ assert_equal 0, @redis.xcard("some_set")
39
+ assert_equal true, @redis.xadd("some_set", "some_value", 60)
40
+ assert_equal 1, @redis.xcard("some_set")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ describe Redis::ExpiringSet do
4
+ describe "newly instantiated" do
5
+ before do
6
+ @redis = Redis.new(:db => 15)
7
+ @expset = Redis::ExpiringSet.new(@redis)
8
+ end
9
+
10
+ after do
11
+ clear_test_db
12
+ end
13
+
14
+ it "allows things to be added to expiration sets using xadd" do
15
+ assert_equal true, @expset.xadd("some_set", "some_value", Time.now + 10)
16
+ end
17
+
18
+ it "allows expiration to set in terms of seconds as well" do
19
+ assert_equal true, @expset.xadd("some_set", "some_value", 60)
20
+ end
21
+
22
+ it "allows items to be removed from expiration sets using xrem" do
23
+ assert_equal true, @expset.xadd("some_set", "some_value", 60)
24
+ assert_equal true, @expset.xrem("some_set", "some_value")
25
+ end
26
+
27
+ it "allows for all items in an expiration set to be returned by xmembers" do
28
+ assert_equal true, @expset.xadd("some_set", "some_value", 300)
29
+ assert_equal ["some_value"], @expset.xmembers("some_set")
30
+ end
31
+
32
+ it "doesn't include any already expired items in xmembers" do
33
+ assert_equal true, @expset.xadd("some_set", "some_value", Time.now - 100)
34
+ assert_equal [], @expset.xmembers("some_set")
35
+ end
36
+
37
+ it "allows for a count of items in an expiration set using xcard" do
38
+ assert_equal 0, @expset.xcard("some_set")
39
+ assert_equal true, @expset.xadd("some_set", "some_value", 60)
40
+ assert_equal 1, @expset.xcard("some_set")
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-expiring-set
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tommy Morgan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &70159947898960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70159947898960
25
+ description: Add some commands to Redis that make it easy to work with a set of objects
26
+ that need to expire individually.
27
+ email:
28
+ - tommy.morgan@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - README.markdown
36
+ - Rakefile
37
+ - lib/redis-expiring-set.rb
38
+ - lib/redis-expiring-set/monkeypatch.rb
39
+ - lib/redis-expiring-set/version.rb
40
+ - redis-expiring-set.gemspec
41
+ - test/partial_minispec.rb
42
+ - test/test_helper.rb
43
+ - test/unit/test_monkeypatched.rb
44
+ - test/unit/test_namespaced.rb
45
+ homepage: http://github.com/duwanis/redis-expiring-set
46
+ licenses: []
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project: redis-expiring-set
65
+ rubygems_version: 1.8.15
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Add an expiring set to redis.
69
+ test_files:
70
+ - test/partial_minispec.rb
71
+ - test/test_helper.rb
72
+ - test/unit/test_monkeypatched.rb
73
+ - test/unit/test_namespaced.rb