traveladapter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/README.md +4 -0
- data/Rakefile +10 -0
- data/lib/traveladapter.rb +2 -0
- data/lib/traveladapter/adapter.rb +216 -0
- data/lib/traveladapter/version.rb +3 -0
- data/test/traveladapter_test.rb +98 -0
- data/traveladapter.gemspec +24 -0
- metadata +84 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -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,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
|