Class: Coverband::Adapters::HashRedisStore

Inherits:
Base
  • Object
show all
Defined in:
lib/coverband/adapters/hash_redis_store.rb

Defined Under Namespace

Classes: GetCoverageNullCacheStore, GetCoverageRedisCacheStore

Constant Summary collapse

FILE_KEY =
"file"
FILE_LENGTH_KEY =
"file_length"
META_DATA_KEYS =
[DATA_KEY, FIRST_UPDATED_KEY, LAST_UPDATED_KEY, FILE_HASH].freeze
REDIS_STORAGE_FORMAT_VERSION =

This key isn’t related to the coverband version, but to the internal format used to store data to redis. It is changed only when breaking changes to our redis format are required.

"coverband_hash_4_0"
JSON_PAYLOAD_EXPIRATION =
5 * 60

Constants inherited from Base

Base::ABSTRACT_KEY, Base::DATA_KEY, Base::FILE_HASH, Base::FIRST_UPDATED_KEY, Base::LAST_UPDATED_KEY

Instance Attribute Summary collapse

Attributes inherited from Base

#type

Instance Method Summary collapse

Methods inherited from Base

#covered_files, #get_coverage_report, #migrate!, #save_coverage

Constructor Details

#initialize(redis, opts = {}) ⇒ HashRedisStore

Returns a new instance of HashRedisStore.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/coverband/adapters/hash_redis_store.rb', line 98

def initialize(redis, opts = {})
  super()
  @redis_namespace = opts[:redis_namespace]
  @save_report_batch_size = opts[:save_report_batch_size] || 100
  @format_version = REDIS_STORAGE_FORMAT_VERSION
  @redis = redis
  raise "HashRedisStore requires redis >= 2.6.0" unless supported?

  @ttl = opts[:ttl]
  @relative_file_converter = opts[:relative_file_converter] || Utils::RelativeFileConverter

  @get_coverage_cache = if opts[:get_coverage_cache]
    key_prefix = [REDIS_STORAGE_FORMAT_VERSION, @redis_namespace].compact.join(".")
    GetCoverageRedisCacheStore.new(redis, key_prefix)
  else
    GetCoverageNullCacheStore
  end
end

Instance Attribute Details

#get_coverage_cacheObject (readonly)

Returns the value of attribute get_coverage_cache.



96
97
98
# File 'lib/coverband/adapters/hash_redis_store.rb', line 96

def get_coverage_cache
  @get_coverage_cache
end

#redis_namespaceObject (readonly)

Returns the value of attribute redis_namespace.



96
97
98
# File 'lib/coverband/adapters/hash_redis_store.rb', line 96

def redis_namespace
  @redis_namespace
end

Instance Method Details

#cached_file_countObject



268
269
270
# File 'lib/coverband/adapters/hash_redis_store.rb', line 268

def cached_file_count
  @cached_file_count ||= file_count(Coverband::RUNTIME_TYPE)
end

#clear!Object



124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/coverband/adapters/hash_redis_store.rb', line 124

def clear!
  old_type = type
  Coverband::TYPES.each do |type|
    self.type = type
    file_keys = files_set
    @redis.del(*file_keys) if file_keys.any?
    @redis.del(files_key)
    @redis.del(files_key(type))
    @get_coverage_cache.clear!(type)
  end
  self.type = old_type
end

#clear_file!(file) ⇒ Object



137
138
139
140
141
142
143
144
145
# File 'lib/coverband/adapters/hash_redis_store.rb', line 137

def clear_file!(file)
  file_hash = file_hash(file)
  relative_path_file = @relative_file_converter.convert(file)
  Coverband::TYPES.each do |type|
    @redis.del(key(relative_path_file, type, file_hash: file_hash))
    @get_coverage_cache.clear!(type)
  end
  @redis.srem(files_key, relative_path_file)
end

#coverage(local_type = nil, opts = {}) ⇒ Object

NOTE: This method should be used for full coverage or filename coverage look ups When paging code should use coverage_for_types and pull eager and runtime together as matched pairs



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/coverband/adapters/hash_redis_store.rb', line 177

def coverage(local_type = nil, opts = {})
  page_size = opts[:page_size] || 250
  cached_results = @get_coverage_cache.fetch(local_type || type) do |sleep_time|
    files_set = if opts[:page]
      raise "call coverage_for_types with paging"
    elsif opts[:filename]
      type_key_prefix = key_prefix(local_type)
      # NOTE: a better way to extract filename from key would be better
      files_set(local_type).select do |cache_key|
        cache_key.sub(type_key_prefix, "").match(short_name(opts[:filename]))
      end || {}
    else
      files_set(local_type)
    end
    # below uses batches with a sleep in between to avoid overloading redis
    files_set.each_slice(page_size).flat_map do |key_batch|
      sleep sleep_time
      @redis.pipelined do |pipeline|
        key_batch.each do |key|
          pipeline.hgetall(key)
        end
      end
    end
  end

  cached_results.each_with_object({}) do |data_from_redis, hash|
    add_coverage_for_file(data_from_redis, hash)
  end
end

#coverage_for_types(_types, opts = {}) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/coverband/adapters/hash_redis_store.rb', line 218

def coverage_for_types(_types, opts = {})
  page_size = opts[:page_size] || 250
  hash_data = {}

  runtime_file_set = files_set(Coverband::RUNTIME_TYPE)
  @cached_file_count = runtime_file_set.length
  runtime_file_set = runtime_file_set.each_slice(page_size).to_a[opts[:page] - 1] || []

  hash_data[Coverband::RUNTIME_TYPE] = runtime_file_set.each_slice(page_size).flat_map do |key_batch|
    @redis.pipelined do |pipeline|
      key_batch.each do |key|
        pipeline.hgetall(key)
      end
    end
  end

  # NOTE: This is kind of hacky, we find all the matching eager loading data
  # for current page of runtime data.
  eager_key_pre = key_prefix(Coverband::EAGER_TYPE)
  runtime_key_pre = key_prefix(Coverband::RUNTIME_TYPE)
  matched_file_set = runtime_file_set.map do |runtime_key|
    runtime_key.sub(runtime_key_pre, eager_key_pre)
  end

  hash_data[Coverband::EAGER_TYPE] = matched_file_set.each_slice(page_size).flat_map do |key_batch|
    @redis.pipelined do |pipeline|
      key_batch.each do |key|
        pipeline.hgetall(key)
      end
    end
  end

  hash_data[Coverband::RUNTIME_TYPE] = hash_data[Coverband::RUNTIME_TYPE].each_with_object({}) do |data_from_redis, hash|
    add_coverage_for_file(data_from_redis, hash)
  end
  hash_data[Coverband::EAGER_TYPE] = hash_data[Coverband::EAGER_TYPE].each_with_object({}) do |data_from_redis, hash|
    add_coverage_for_file(data_from_redis, hash)
  end
  hash_data
end

#file_count(local_type = nil) ⇒ Object



264
265
266
# File 'lib/coverband/adapters/hash_redis_store.rb', line 264

def file_count(local_type = nil)
  files_set(local_type).count { |filename| !Coverband.configuration.ignore.any? { |i| filename.match(i) } }
end

#raw_storeObject



272
273
274
# File 'lib/coverband/adapters/hash_redis_store.rb', line 272

def raw_store
  @redis
end

#save_report(report) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/coverband/adapters/hash_redis_store.rb', line 147

def save_report(report)
  report_time = Time.now.to_i
  updated_time = (type == Coverband::EAGER_TYPE) ? nil : report_time
  keys = []
  report.each_slice(@save_report_batch_size) do |slice|
    files_data = slice.map do |(file, data)|
      relative_file = @relative_file_converter.convert(file)
      file_hash = file_hash(relative_file)
      key = key(relative_file, file_hash: file_hash)
      keys << key
      script_input(
        key: key,
        file: relative_file,
        file_hash: file_hash,
        data: data,
        report_time: report_time,
        updated_time: updated_time
      )
    end
    next unless files_data.any?

    arguments_key = [@redis_namespace, SecureRandom.uuid].compact.join(".")
    @redis.set(arguments_key, {ttl: @ttl, files_data: files_data}.to_json, ex: JSON_PAYLOAD_EXPIRATION)
    @redis.evalsha(hash_incr_script, [arguments_key], [report_time])
  end
  @redis.sadd(files_key, keys) if keys.any?
end

#short_name(filename) ⇒ Object



259
260
261
262
# File 'lib/coverband/adapters/hash_redis_store.rb', line 259

def short_name(filename)
  filename.sub(/^#{Coverband.configuration.root}/, ".")
    .gsub(%r{^\./}, "")
end

#sizeObject



276
277
278
# File 'lib/coverband/adapters/hash_redis_store.rb', line 276

def size
  "not available"
end

#size_in_mibObject



280
281
282
# File 'lib/coverband/adapters/hash_redis_store.rb', line 280

def size_in_mib
  "not available"
end

#split_coverage(types, coverage_cache, options = {}) ⇒ Object



207
208
209
210
211
212
213
214
215
216
# File 'lib/coverband/adapters/hash_redis_store.rb', line 207

def split_coverage(types, coverage_cache, options = {})
  if types.is_a?(Array) && !options[:filename] && options[:page]
    data = coverage_for_types(types, options)
    coverage_cache[Coverband::RUNTIME_TYPE] = data[Coverband::RUNTIME_TYPE]
    coverage_cache[Coverband::EAGER_TYPE] = data[Coverband::EAGER_TYPE]
    data
  else
    super
  end
end

#supported?Boolean

Returns:

  • (Boolean)


117
118
119
120
121
122
# File 'lib/coverband/adapters/hash_redis_store.rb', line 117

def supported?
  Gem::Version.new(@redis.info["redis_version"]) >= Gem::Version.new("2.6.0")
rescue Redis::CannotConnectError => e
  Coverband.configuration.logger.info "Redis is not available (#{e}), Coverband not configured"
  Coverband.configuration.logger.info "If this is a setup task like assets:precompile feel free to ignore"
end