Class: LaunchDarkly::RedisFeatureStore

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

Overview

An implementation of the LaunchDarkly client’s feature store that uses a Redis instance. This object holds feature flags and related data received from the streaming API. 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` and `connection-pool` gems installed. Then, create an instance and store it in the `feature_store` property of your client configuration.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ RedisFeatureStore

Constructor for a RedisFeatureStore instance.

Parameters:

  • opts (Hash) (defaults to: {})

    the configuration options

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 (or related objects) to cache locally

  • :pool (Object)

    custom connection pool, used for testing only



38
39
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
# File 'lib/ldclient-rb/redis_store.rb', line 38

def initialize(opts = {})
  if !REDIS_ENABLED
    raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool")
  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

  expiration_seconds = opts[:expiration] || 15
  capacity = opts[:capacity] || 1000
  if expiration_seconds > 0
    @cache = ExpiringCache.new(capacity, expiration_seconds)
  else
    @cache = nil
  end

  @stopped = Concurrent::AtomicBoolean.new(false)
  @inited = MemoizedValue.new {
    query_inited
  }

  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.



86
87
88
# File 'lib/ldclient-rb/redis_store.rb', line 86

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.



79
80
81
# File 'lib/ldclient-rb/redis_store.rb', line 79

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

Instance Method Details

#all(kind) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/ldclient-rb/redis_store.rb', line 114

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

#delete(kind, key, version) ⇒ Object



133
134
135
# File 'lib/ldclient-rb/redis_store.rb', line 133

def delete(kind, key, version)
  update_with_versioning(kind, { key: key, version: version, deleted: true })
end

#get(kind, key) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ldclient-rb/redis_store.rb', line 90

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

#init(all_data) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/ldclient-rb/redis_store.rb', line 137

def init(all_data)
  @cache.clear if !@cache.nil?
  count = 0
  with_connection do |redis|
    all_data.each do |kind, items|
      begin
        redis.multi do |multi|
          multi.del(items_key(kind))
          count = count + items.count
          items.each { |key, item|
            redis.hset(items_key(kind), key, item.to_json)
          }
        end
        items.each { |key, item|
          put_cache(kind, key.to_sym, item)
        }
      rescue => e
        @logger.error { "RedisFeatureStore: could not initialize '#{kind[:namespace]}' in Redis, error: #{e}" }
      end    
    end
  end
  @inited.set(true)
  @logger.info { "RedisFeatureStore: initialized with #{count} items" }
end

#initialized?Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/ldclient-rb/redis_store.rb', line 166

def initialized?
  @inited.get
end

#stopObject



170
171
172
173
174
175
# File 'lib/ldclient-rb/redis_store.rb', line 170

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

#upsert(kind, item) ⇒ Object



162
163
164
# File 'lib/ldclient-rb/redis_store.rb', line 162

def upsert(kind, item)
  update_with_versioning(kind, item)
end