Skip to content

Commit 2ce4971

Browse files
committed
use our own lightweight expiring cache instead of moneta
1 parent 38d63fe commit 2ce4971

File tree

5 files changed

+164
-12
lines changed

5 files changed

+164
-12
lines changed

ldclient-rb.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ Gem::Specification.new do |spec|
2626
spec.add_development_dependency "codeclimate-test-reporter", "~> 0"
2727
spec.add_development_dependency "redis", "~> 3.3.5"
2828
spec.add_development_dependency "connection_pool", ">= 2.1.2"
29-
spec.add_development_dependency "moneta", "~> 1.0.0"
3029
if RUBY_VERSION >= "2.0.0"
3130
spec.add_development_dependency "rake", "~> 10.0"
3231
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
3332
else
3433
spec.add_development_dependency "rake", "12.1.0"
3534
# higher versions of rake fail to install in JRuby 1.7
3635
end
36+
spec.add_development_dependency "timecop", "~> 0.9.1"
3737

3838
spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
3939
if RUBY_VERSION >= "2.1.0"

lib/ldclient-rb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require "ldclient-rb/evaluation"
33
require "ldclient-rb/ldclient"
44
require "ldclient-rb/cache_store"
5+
require "ldclient-rb/expiring_cache"
56
require "ldclient-rb/memoized_value"
67
require "ldclient-rb/in_memory_store"
78
require "ldclient-rb/config"

lib/ldclient-rb/expiring_cache.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
module LaunchDarkly
3+
# A thread-safe cache with maximum number of entries and TTL.
4+
# Adapted from https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/ttl/cache.rb
5+
# under MIT license with the following changes:
6+
# * made thread-safe
7+
# * removed many unused methods
8+
# * reading a key does not reset its expiration time, only writing
9+
class ExpiringCache
10+
def initialize(max_size, ttl)
11+
@max_size = max_size
12+
@ttl = ttl
13+
@data_lru = {}
14+
@data_ttl = {}
15+
@lock = Mutex.new
16+
end
17+
18+
def [](key)
19+
@lock.synchronize do
20+
ttl_evict
21+
@data_lru[key]
22+
end
23+
end
24+
25+
def []=(key, val)
26+
@lock.synchronize do
27+
ttl_evict
28+
29+
@data_lru.delete(key)
30+
@data_ttl.delete(key)
31+
32+
@data_lru[key] = val
33+
@data_ttl[key] = Time.now.to_f
34+
35+
if @data_lru.size > @max_size
36+
key, _ = @data_lru.first # hashes have a FIFO ordering in Ruby
37+
38+
@data_ttl.delete(key)
39+
@data_lru.delete(key)
40+
end
41+
42+
val
43+
end
44+
end
45+
46+
def delete(key)
47+
@lock.synchronize do
48+
ttl_evict
49+
50+
@data_lru.delete(key)
51+
@data_ttl.delete(key)
52+
end
53+
end
54+
55+
def clear
56+
@lock.synchronize do
57+
@data_lru.clear
58+
@data_ttl.clear
59+
end
60+
end
61+
62+
private
63+
64+
def ttl_evict
65+
ttl_horizon = Time.now.to_f - @ttl
66+
key, time = @data_ttl.first
67+
68+
until time.nil? || time > ttl_horizon
69+
@data_ttl.delete(key)
70+
@data_lru.delete(key)
71+
72+
key, time = @data_ttl.first
73+
end
74+
end
75+
end
76+
end

lib/ldclient-rb/redis_store.rb

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ module LaunchDarkly
99
# streaming API. Feature data can also be further cached in memory to reduce overhead
1010
# of calls to Redis.
1111
#
12-
# To use this class, you must first have the `redis`, `connection-pool`, and `moneta`
13-
# gems installed. Then, create an instance and store it in the `feature_store`
14-
# property of your client configuration.
12+
# To use this class, you must first have the `redis` and `connection-pool` gems
13+
# installed. Then, create an instance and store it in the `feature_store` property
14+
# of your client configuration.
1515
#
1616
class RedisFeatureStore
1717
begin
1818
require "redis"
1919
require "connection_pool"
20-
require "moneta"
2120
REDIS_ENABLED = true
2221
rescue ScriptError, StandardError
2322
REDIS_ENABLED = false
@@ -38,7 +37,7 @@ class RedisFeatureStore
3837
#
3938
def initialize(opts = {})
4039
if !REDIS_ENABLED
41-
raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool, moneta")
40+
raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool")
4241
end
4342
@redis_opts = opts[:redis_opts] || Hash.new
4443
if opts[:redis_url]
@@ -60,9 +59,9 @@ def initialize(opts = {})
6059
# Moneta can also be used as a wrapper for Redis, but it doesn't support the Redis
6160
# hash operations that we use.
6261
if @expiration_seconds > 0
63-
@cache = Moneta.new(:LRUHash, expires: true, threadsafe: true, max_count: @capacity)
62+
@cache = ExpiringCache.new(@capacity, @expiration_seconds)
6463
else
65-
@cache = Moneta.new(:Null) # a stub that caches nothing
64+
@cache = nil
6665
end
6766

6867
@stopped = Concurrent::AtomicBoolean.new(false)
@@ -92,7 +91,7 @@ def self.default_prefix
9291
end
9392

9493
def get(kind, key)
95-
f = @cache[cache_key(kind, key)]
94+
f = @cache.nil? ? nil : @cache[cache_key(kind, key)]
9695
if f.nil?
9796
@logger.debug { "RedisFeatureStore: no cache hit for #{key} in '#{kind[:namespace]}', requesting from Redis" }
9897
f = with_connection do |redis|
@@ -139,7 +138,7 @@ def delete(kind, key, version)
139138
end
140139

141140
def init(all_data)
142-
@cache.clear
141+
@cache.clear if !@cache.nil?
143142
count = 0
144143
with_connection do |redis|
145144
all_data.each do |kind, items|
@@ -174,7 +173,7 @@ def initialized?
174173
def stop
175174
if @stopped.make_true
176175
@pool.shutdown { |redis| redis.close }
177-
@cache.clear
176+
@cache.clear if !@cache.nil?
178177
end
179178
end
180179

@@ -213,7 +212,7 @@ def get_redis(kind, redis, key)
213212
end
214213

215214
def put_cache(kind, key, value)
216-
@cache.store(cache_key(kind, key), value, expires: @expiration_seconds)
215+
@cache[cache_key(kind, key)] = value if !@cache.nil?
217216
end
218217

219218
def update_with_versioning(kind, new_item)

spec/expiring_cache_spec.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
require 'timecop'
2+
3+
describe LaunchDarkly::ExpiringCache do
4+
subject { LaunchDarkly::ExpiringCache }
5+
6+
before(:each) do
7+
Timecop.freeze(Time.now)
8+
end
9+
10+
after(:each) do
11+
Timecop.return
12+
end
13+
14+
it "evicts entries based on TTL" do
15+
c = subject.new(3, 300)
16+
c[:a] = 1
17+
c[:b] = 2
18+
19+
Timecop.freeze(Time.now + 330)
20+
21+
c[:c] = 3
22+
23+
expect(c[:a]).to be nil
24+
expect(c[:b]).to be nil
25+
expect(c[:c]).to eq 3
26+
end
27+
28+
it "evicts entries based on max size" do
29+
c = subject.new(2, 300)
30+
c[:a] = 1
31+
c[:b] = 2
32+
c[:c] = 3
33+
34+
expect(c[:a]).to be nil
35+
expect(c[:b]).to eq 2
36+
expect(c[:c]).to eq 3
37+
end
38+
39+
it "does not reset LRU on get" do
40+
c = subject.new(2, 300)
41+
c[:a] = 1
42+
c[:b] = 2
43+
c[:a]
44+
c[:c] = 3
45+
46+
expect(c[:a]).to be nil
47+
expect(c[:b]).to eq 2
48+
expect(c[:c]).to eq 3
49+
end
50+
51+
it "resets LRU on put" do
52+
c = subject.new(2, 300)
53+
c[:a] = 1
54+
c[:b] = 2
55+
c[:a] = 1
56+
c[:c] = 3
57+
58+
expect(c[:a]).to eq 1
59+
expect(c[:b]).to be nil
60+
expect(c[:c]).to eq 3
61+
end
62+
63+
it "resets TTL on put" do
64+
c = subject.new(3, 300)
65+
c[:a] = 1
66+
c[:b] = 2
67+
68+
Timecop.freeze(Time.now + 330)
69+
c[:a] = 1
70+
c[:c] = 3
71+
72+
expect(c[:a]).to eq 1
73+
expect(c[:b]).to be nil
74+
expect(c[:c]).to eq 3
75+
end
76+
end

0 commit comments

Comments
 (0)