traveladapter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in traveladapter.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ traveladapter
2
+ =============
3
+
4
+ Wrapper around key-value stores used for caching.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake"
3
+ require "rake/testtask"
4
+
5
+ task :default => [ :unittests ]
6
+
7
+ Rake::TestTask.new("unittests") do |t|
8
+ t.pattern = "test/*_test.rb"
9
+ t.verbose = true
10
+ end
@@ -0,0 +1,2 @@
1
+ require "traveladapter/version"
2
+ require "traveladapter/adapter"
@@ -0,0 +1,216 @@
1
+ require 'thread'
2
+
3
+ module TravelAdapter
4
+
5
+ # Versatile wrapper around key-value stores, providing abstraction
6
+ # and expiration facility.
7
+ class Adapter
8
+
9
+ # Create a new instance of TravelAdapter::Adapter object.
10
+ #
11
+ # If no object is passed, the Adapter will internally use a Hash.
12
+ #
13
+ # @param [Object] store Underlying key-value store (eg. Hash, Redis,
14
+ # LevelDB etc.)
15
+ def initialize(store=nil)
16
+ @semaphore = Mutex::new
17
+ if store.nil?
18
+ @store = Hash::new
19
+ else
20
+ @store = store
21
+ end
22
+ end
23
+
24
+ # Decrement a counter stored under a particular key. A counter can be
25
+ # any numeric object, as long as it responds sensibly to + and -
26
+ # messages.
27
+ #
28
+ # @param [String] key Key used to find a counter object.
29
+ # @return [Numeric] Counter value after decrementing.
30
+ def decrement(key)
31
+ change_by(key, -1)
32
+ end
33
+
34
+ # Delete a key-value pair stored with a particular key.
35
+ #
36
+ # @param [String] key Key used to find a counter object.
37
+ # @return [Boolean] true if the operation succeeds, false if not
38
+ def delete(key)
39
+ begin
40
+ @semaphore.synchronize do
41
+ atomic_delete(key)
42
+ atomic_delete(marshal_key(key))
43
+ atomic_delete(expiration_key(key))
44
+ end
45
+ return true
46
+ rescue Exception
47
+ return false
48
+ end
49
+ end
50
+
51
+ # Get the value stored with a particular key.
52
+ #
53
+ # @param [String] key Key used to find the value.
54
+ # @return [Object] object stored with a given key. Returns nil if the
55
+ # object is not found or the key has expired.
56
+ def get(key)
57
+ return nil unless atomic_has_key?(key)
58
+ if expired?(key)
59
+ delete(key)
60
+ return nil
61
+ elsif needs_marshaling?(key)
62
+ return Marshal::load(atomic_get(key))
63
+ else
64
+ return atomic_get(key)
65
+ end
66
+ end
67
+
68
+ # Check if anything is stored with a particular key.
69
+ #
70
+ # @param [String] key Key used to find a counter object.
71
+ # @return [Boolean] true if the key is not expired and associated with
72
+ # a value, otherwise false.
73
+ def has_key?(key)
74
+ !get(key).nil?
75
+ end
76
+
77
+ # Increment a counter stored under a particular key. A counter can be
78
+ # any numeric object, as long as it responds sensibly to + and -
79
+ # messages.
80
+ #
81
+ # @param [String] key Key used to find a counter object.
82
+ # @return [Numeric] Counter value after incrementing.
83
+ def increment(key)
84
+ change_by(key, 1)
85
+ end
86
+
87
+ # Get non-expired keys stored.
88
+ #
89
+ # @param [Regexp] pattern Regular expression used to filter out keys.
90
+ # @return [Array] Non-expired keys matching the given pattern.
91
+ def keys(pattern=/.*/)
92
+ re = /\A__(expire|marshal)__:/
93
+ return atomic_keys.select do |k|
94
+ k.match(re).nil? and not expired?(k) and !k.match(pattern).nil?
95
+ end
96
+ end
97
+
98
+ # Set the value stored with a particular key.
99
+ #
100
+ # @param [String] key Key used to for the object.
101
+ # @param [String] value Value for the key.
102
+ # @param [Integer] ttl Time (in seconds) to keep the value.
103
+ # @return [Object] object stored.
104
+ def set(key, value, ttl=(10 << 32))
105
+ @semaphore.synchronize do
106
+ if value.is_a?(String)
107
+ atomic_set(marshal_key(key), "0")
108
+ else
109
+ atomic_set(marshal_key(key), "1")
110
+ value = Marshal::dump(value)
111
+ end
112
+ atomic_set(key, value)
113
+ set_ttl(key, ttl)
114
+ end
115
+ return value
116
+ end
117
+
118
+ # Change how long the value is to be stored.
119
+ #
120
+ # @param [String] key Key used to for the object.
121
+ # @param [Integer] ttl Time from now (in seconds) to keep the value.
122
+ # @return [Time] Time when the key expires.
123
+ def set_ttl(key, ttl)
124
+ new_ttl = Time::now.to_i + ttl
125
+ atomic_set(expiration_key(key), new_ttl.to_s)
126
+ return Time::at(new_ttl)
127
+ end
128
+
129
+ # Get expiration time.
130
+ #
131
+ # @param [String] key Key used to for the object.
132
+ # @return [Integer] Number of seconds until object expiration
133
+ def get_ttl(key)
134
+ expires = atomic_get(expiration_key(key))
135
+ return expires.nil? ? nil : (expires.to_i - Time::now.to_i)
136
+ end
137
+
138
+ private
139
+
140
+ def atomic_has_key?(key)
141
+ !atomic_get(key).nil?
142
+ end
143
+
144
+ def atomic_delete(key)
145
+ if @store.respond_to?(:delete)
146
+ @store.delete(key)
147
+ elsif @store.respond_to?(:del)
148
+ @store.del(key)
149
+ else
150
+ @store.atomic_set(key, nil)
151
+ end
152
+ end
153
+
154
+ def atomic_get(key)
155
+ if @store.respond_to?(:fetch)
156
+ begin
157
+ return @store.fetch(key)
158
+ rescue KeyError
159
+ return nil
160
+ end
161
+ elsif @store.respond_to?(:get)
162
+ return @store.get(key)
163
+ else
164
+ return @store[key]
165
+ end
166
+ end
167
+
168
+ def atomic_keys
169
+ @store.keys
170
+ end
171
+
172
+ def atomic_set(key, value)
173
+ if @store.respond_to?(:set)
174
+ @store.set(key, value)
175
+ elsif @store.respond_to?(:put)
176
+ @store.put(key, value)
177
+ else
178
+ @store[key] = value
179
+ end
180
+ end
181
+
182
+ def change_by(key, value)
183
+ @semaphore.synchronize do
184
+ current_value = atomic_get(key)
185
+ unless expired?(key)
186
+ new_value = Marshal::load(current_value) + value
187
+ atomic_set(key, Marshal::dump(new_value))
188
+ return new_value
189
+ else
190
+ return false
191
+ end
192
+ end
193
+ end
194
+
195
+ def expiration_key(key)
196
+ sprintf('__expire__:%s', key)
197
+ end
198
+
199
+ def marshal_key(key)
200
+ sprintf('__marshal__:%s', key)
201
+ end
202
+
203
+ def expired?(key)
204
+ get_ttl(key) <= 0
205
+ end
206
+
207
+ def needs_marshaling?(key)
208
+ atomic_get(marshal_key(key)) == "1"
209
+ end
210
+
211
+ alias :[] :get
212
+ alias :[]= :set
213
+
214
+ end
215
+
216
+ end
@@ -0,0 +1,3 @@
1
+ module TravelAdapter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,98 @@
1
+ require "test/unit"
2
+ require "mocha"
3
+ require "pry"
4
+
5
+ require "traveladapter"
6
+
7
+ Person = Struct::new(:name, :age)
8
+
9
+ class TravelAdapterTest < Test::Unit::TestCase
10
+
11
+ def setup
12
+ @h_adapter = TravelAdapter::Adapter::new
13
+ end
14
+
15
+ def test_set_and_get_without_ttl
16
+ @h_adapter.set("chunky", "bacon")
17
+ assert_equal @h_adapter.get("chunky"), "bacon", "Wrong key has been set"
18
+ assert_equal @h_adapter.keys, ["chunky"], "Unexpected set of keys"
19
+ end
20
+
21
+ def test_aliased_set_and_get_without_ttl
22
+ @h_adapter["chunky"] = "bacon"
23
+ assert_equal @h_adapter["chunky"], "bacon", "Wrong key has been set"
24
+ assert_equal @h_adapter.keys, ["chunky"], "Unexpected set of keys"
25
+ end
26
+
27
+ def test_with_miss
28
+ assert @h_adapter["chunky"].nil?, "Did not expected to find anything"
29
+ end
30
+
31
+ def test_with_marshaling
32
+ @h_adapter["p"] = Person::new("John Smith", 29)
33
+ p = @h_adapter["p"]
34
+ assert p.is_a?(Person), "Expected a Person object"
35
+ assert p.name.is_a?(String), "Expected a String object"
36
+ assert p.age.is_a?(Fixnum), "Expected a Fixnum object"
37
+ assert_equal @h_adapter.keys, ["p"], "Unexpected set of keys"
38
+ end
39
+
40
+ def test_set_and_get_with_ttl
41
+ Time::expects(:now).times(3).returns(*%w(0 3 6))
42
+ @h_adapter.set("chunky", "bacon", 5)
43
+ assert_equal @h_adapter.get("chunky"), "bacon", "Unexpected value"
44
+ assert @h_adapter.get("chunky").nil?, "Expected the key to expire"
45
+ end
46
+
47
+ def test_keys_without_pattern_or_ttl
48
+ @h_adapter.set("hello", "world")
49
+ @h_adapter.set("chunky", "bacon")
50
+ assert @h_adapter.keys.include?("hello")
51
+ assert @h_adapter.keys.include?("chunky")
52
+ end
53
+
54
+ def test_keys_with_pattern
55
+ @h_adapter.set("hello", "world")
56
+ @h_adapter.set("chunky", "bacon")
57
+ assert @h_adapter.keys(/el/).include?("hello")
58
+ assert !@h_adapter.keys(/el/).include?("chunky")
59
+ end
60
+
61
+ def test_keys_with_ttl
62
+ Time::expects(:now).times(6).returns(*%w(0 0 4 4 6 6))
63
+ @h_adapter.set("hello", "world", 5)
64
+ @h_adapter.set("chunky", "bacon", 10)
65
+ keys_at_4 = @h_adapter.keys
66
+ assert keys_at_4.include?("hello")
67
+ assert keys_at_4.include?("chunky")
68
+ keys_at_6 = @h_adapter.keys
69
+ assert !keys_at_6.include?("hello")
70
+ assert keys_at_6.include?("chunky")
71
+ end
72
+
73
+ def test_get_ttl
74
+ Time::expects(:now).times(2).returns(*%w(0 2))
75
+ @h_adapter.set("chunky", "bacon", 10)
76
+ assert_equal @h_adapter.get_ttl("chunky"), 8, "Unexpected TTL"
77
+ end
78
+
79
+ def test_get_ttl_for_a_nonexistent_key
80
+ assert @h_adapter.get_ttl("chunky").nil?, "Unexpected TTL"
81
+ end
82
+
83
+ def test_delete
84
+ @h_adapter["chunky"] = "bacon"
85
+ assert @h_adapter.has_key?("chunky"), "Key expected but not found"
86
+ @h_adapter.delete("chunky")
87
+ assert !@h_adapter.has_key?("chunky"), "Key not expected but found"
88
+ end
89
+
90
+ def test_increment_and_decrement
91
+ @h_adapter["counter"] = 0
92
+ assert_equal @h_adapter.increment("counter"), 1, "Expected the counter to be 1"
93
+ assert_equal @h_adapter.get("counter"), 1, "Expected the counter to be 1"
94
+ assert_equal @h_adapter.decrement("counter"), 0, "Expected the counter to be 0"
95
+ assert_equal @h_adapter.get("counter"), 0, "Expected the counter to be 0"
96
+ end
97
+
98
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "traveladapter/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "traveladapter"
7
+ s.version = TravelAdapter::VERSION
8
+ s.authors = ["Marcin Wyszynski"]
9
+ s.email = ["marcin.pixie@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Wrapper around key-value stores used for caching.}
12
+ s.description = %q{Wrapper around key-value stores used for caching.}
13
+
14
+ s.rubyforge_project = "traveladapter"
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
+ s.add_dependency("rake")
22
+ s.add_development_dependency("mocha")
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: traveladapter
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Marcin Wyszynski
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-07-29 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :development
36
+ version_requirements: *id002
37
+ description: Wrapper around key-value stores used for caching.
38
+ email:
39
+ - marcin.pixie@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - .gitignore
48
+ - Gemfile
49
+ - README.md
50
+ - Rakefile
51
+ - lib/traveladapter.rb
52
+ - lib/traveladapter/adapter.rb
53
+ - lib/traveladapter/version.rb
54
+ - test/traveladapter_test.rb
55
+ - traveladapter.gemspec
56
+ homepage: ""
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options: []
61
+
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ requirements: []
77
+
78
+ rubyforge_project: traveladapter
79
+ rubygems_version: 1.8.15
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Wrapper around key-value stores used for caching.
83
+ test_files:
84
+ - test/traveladapter_test.rb