Class: LaunchDarkly::RedisFeatureStore

Inherits:
Object
  • Object
show all
Defined in:
lib/ldclient-rb/redis_feature_store.rb

Overview

An implementation of the LaunchDarkly client’s feature store that uses a Redis instance. Feature data can also be further cached in memory to reduce overhead of calls to Redis.

To use this class, you must first have the redis, connection-pool, and moneta gems installed. Then, create an instance and store it in the feature_store property of your client configuration.

Constant Summary collapse

INIT_KEY =
:"$initialized"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ RedisFeatureStore

Constructor for a RedisFeatureStore instance.

Options Hash (opts):

  • :redis_url (String)

    URL of the Redis instance (shortcut for omitting redis_opts)

  • :redis_opts (Hash)

    options to pass to the Redis constructor (if you want to specify more than just redis_url)

  • :prefix (String)

    namespace prefix to add to all hash keys used by LaunchDarkly

  • :logger (Logger)

    a Logger instance; defaults to Config.default_logger

  • :max_connections (Integer)

    size of the Redis connection pool

  • :expiration (Integer)

    expiration time for the in-memory cache, in seconds; 0 for no local caching

  • :capacity (Integer)

    maximum number of feature flags to cache locally

  • :pool (Object)

    custom connection pool, used for testing only



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ldclient-rb/redis_feature_store.rb', line 40

def initialize(opts = {})
  if !REDIS_ENABLED
    raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool, moneta")
  end
  @redis_opts = opts[:redis_opts] || Hash.new
  if opts[:redis_url]
    @redis_opts[:url] = opts[:redis_url]
  end
  if !@redis_opts.include?(:url)
    @redis_opts[:url] = RedisFeatureStore.default_redis_url
  end
  max_connections = opts[:max_connections] || 16
  @pool = opts[:pool] || ConnectionPool.new(size: max_connections) do
    Redis.new(@redis_opts)
  end
  @prefix = opts[:prefix] || RedisFeatureStore.default_prefix
  @logger = opts[:logger] || Config.default_logger
  @features_key = @prefix + ':features'

  @expiration_seconds = opts[:expiration] || 15
  @capacity = opts[:capacity] || 1000
  # We're using Moneta only to provide expiration behavior for the in-memory cache.
  # Moneta can also be used as a wrapper for Redis, but it doesn't support the Redis
  # hash operations that we use.
  if @expiration_seconds > 0
    @cache = Moneta.new(:LRUHash, expires: true, threadsafe: true, max_count: @capacity)
  else
    @cache = Moneta.new(:Null)  # a stub that caches nothing
  end

  @stopped = Concurrent::AtomicBoolean.new(false)

  with_connection do |redis|
    @logger.info("RedisFeatureStore: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} \
and prefix: #{@prefix}")
  end
end

Class Method Details

.default_prefixObject

Default value for the prefix constructor parameter.



89
90
91
# File 'lib/ldclient-rb/redis_feature_store.rb', line 89

def self.default_prefix
  'launchdarkly'
end

.default_redis_urlObject

Default value for the redis_url constructor parameter; points to an instance of Redis running at localhost with its default port.



82
83
84
# File 'lib/ldclient-rb/redis_feature_store.rb', line 82

def self.default_redis_url
  'redis://localhost:6379/0'
end

Instance Method Details

#allObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ldclient-rb/redis_feature_store.rb', line 120

def all
  fs = {}
  with_connection do |redis|
    begin
      hashfs = redis.hgetall(@features_key)
    rescue => e
      @logger.error("RedisFeatureStore: could not retrieve all flags from Redis with error: #{e}; returning none")
      hashfs = {}
    end
    hashfs.each do |k, jsonFeature|
      f = JSON.parse(jsonFeature, symbolize_names: true)
      if !f[:deleted]
        fs[k.to_sym] = f
      end
    end
  end
  fs
end

#clear_local_cacheObject

exposed for testing



203
204
205
# File 'lib/ldclient-rb/redis_feature_store.rb', line 203

def clear_local_cache()
  @cache.clear
end

#delete(key, version) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/ldclient-rb/redis_feature_store.rb', line 139

def delete(key, version)
  with_connection do |redis|
    f = get_redis(redis, key)
    if f.nil?
      put_redis_and_cache(redis, key, { deleted: true, version: version })
    else
      if f[:version] < version
        f1 = f.clone
        f1[:deleted] = true
        f1[:version] = version
        put_redis_and_cache(redis, key, f1)
      else
        @logger.warn("RedisFeatureStore: attempted to delete flag: #{key} version: #{f[:version]} \
  with a version that is the same or older: #{version}")
      end
    end
  end
end

#get(key) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ldclient-rb/redis_feature_store.rb', line 93

def get(key)
  f = @cache[key.to_sym]
  if f.nil?
    @logger.debug("RedisFeatureStore: no cache hit for #{key}, requesting from Redis")
    f = with_connection do |redis|
      begin
        get_redis(redis,key.to_sym)
      rescue => e
        @logger.error("RedisFeatureStore: could not retrieve feature #{key} from Redis, with error: #{e}")
        nil
      end
    end
    if !f.nil?
      put_cache(key.to_sym, f)
    end
  end
  if f.nil?
    @logger.warn("RedisFeatureStore: feature #{key} not found")
    nil
  elsif f[:deleted]
    @logger.warn("RedisFeatureStore: feature #{key} was deleted, returning nil")
    nil
  else
    f
  end
end

#init(fs) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ldclient-rb/redis_feature_store.rb', line 158

def init(fs)
  @cache.clear
  with_connection do |redis|
    redis.multi do |multi|
      redis.del(@features_key)
      fs.each { |k, f| put_redis_and_cache(multi, k, f) }
    end
  end
  put_cache(INIT_KEY, true)
  @logger.info("RedisFeatureStore: initialized with #{fs.count} feature flags")
end

#initialized?Boolean



182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/ldclient-rb/redis_feature_store.rb', line 182

def initialized?
  if @cache[INIT_KEY].nil?
    if with_connection { |redis| redis.exists(@features_key) }
      put_cache(INIT_KEY, true)
      true
    else
      false
    end
  else
    put_cache(INIT_KEY, true)  # reset TTL
  end
end

#stopObject



195
196
197
198
199
200
# File 'lib/ldclient-rb/redis_feature_store.rb', line 195

def stop
  if @stopped.make_true
    @pool.shutdown { |redis| redis.close }
    @cache.clear
  end
end

#upsert(key, feature) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
# File 'lib/ldclient-rb/redis_feature_store.rb', line 170

def upsert(key, feature)
  with_connection do |redis|
    redis.watch(@features_key) do
      old = get_redis(redis, key)
      if old.nil? || (old[:version] < feature[:version])
        put_redis_and_cache(redis, key, feature)
      end
      redis.unwatch
    end
  end
end