class Condenser::Cache::FileStore

Constants

GITKEEP_FILES

Public Class Methods

new(root, size: 26_214_400, logger: nil) click to toggle source

Public: Initialize the cache store.

root - A String path to a directory to persist cached values to. size - A Integer of the maximum size the store will hold (in bytes).

(default: 25MB).

logger - The logger to which some info will be printed.

(default logger level is FATAL and won't output anything).
# File lib/condenser/cache/file_store.rb, line 13
def initialize(root, size: 26_214_400, logger: nil)
  @root     = root
  @max_size = size
  @gc_size  = size * 0.75
  @logger   = logger
end

Public Instance Methods

clear(options=nil) click to toggle source

Public: Clear the cache

adapted from ActiveSupport::Cache::FileStore#clear

Deletes all items from the cache. In this case it deletes all the entries in the specified file store directory except for .keep or .gitkeep. Be careful which directory is specified as @root because everything in that directory will be deleted.

Returns true

# File lib/condenser/cache/file_store.rb, line 93
def clear(options=nil)
  Dir.children(@root).each do |f|
    next if GITKEEP_FILES.include?(f)
    FileUtils.rm_r(File.join(@root, f))
  end
  true
end
get(key) click to toggle source
# File lib/condenser/cache/file_store.rb, line 20
def get(key)
  path = File.join(@root, "#{key}.cache")
  
  value = safe_open(path) do |f|
    begin
      unmarshaled_deflated(f.read, Zlib::MAX_WBITS)
    rescue Exception => e
      # @logger.error do
        puts "#{self.class}[#{path}] could not be unmarshaled: #{e.class}: #{e.message}"
      # end
      nil
    end
  end

  FileUtils.touch(path) if value
  
  value
end
inspect() click to toggle source

Public: Pretty inspect

Returns String.

# File lib/condenser/cache/file_store.rb, line 80
def inspect
  "#<#{self.class}>"
end
set(key, value) click to toggle source
# File lib/condenser/cache/file_store.rb, line 39
def set(key, value)
  path = File.join(@root, "#{key}.cache")
  
  # Ensure directory exists
  FileUtils.mkdir_p File.dirname(path)
  
  # Check if cache exists before writing
  exists = File.exist?(path)

  # Serialize value
  marshaled = Marshal.dump(value)

  # Compress if larger than 4KB
  if marshaled.bytesize > 4_096
    deflater = Zlib::Deflate.new(
      Zlib::BEST_COMPRESSION,
      Zlib::MAX_WBITS,
      Zlib::MAX_MEM_LEVEL,
      Zlib::DEFAULT_STRATEGY
    )
    deflater << marshaled
    raw = deflater.finish
  else
    raw = marshaled
  end

  # Write data
  Condenser::Utils.atomic_write(path) do |f|
    f.write(raw)
    @size = size + f.size unless exists
  end

  # GC if necessary
  gc! if size > @max_size

  value
end
size() click to toggle source
# File lib/condenser/cache/file_store.rb, line 101
def size
  @size ||= find_caches.inject(0) { |sum, (_, stat)| sum + stat.size }
end

Private Instance Methods

find_caches() click to toggle source

Returns an Array of [String filename, File::Stat] pairs sorted by mtime.

# File lib/condenser/cache/file_store.rb, line 114
def find_caches
  Dir.glob(File.join(@root, '**/*.cache')).reduce([]) { |stats, filename|
    stat = safe_stat(filename)
    # stat maybe nil if file was removed between the time we called
    # dir.glob and the next stat
    stats << [filename, stat] if stat
    stats
  }.sort_by { |_, stat| stat.mtime.to_i }
end
gc!() click to toggle source
# File lib/condenser/cache/file_store.rb, line 130
def gc!
  start_time = Time.now

  caches = find_caches
  size = caches.inject(0) { |sum, (_, stat)| sum + stat.size }

  delete_caches, keep_caches = caches.partition { |filename, stat|
    deleted = size > @gc_size
    size -= stat.size
    deleted
  }

  return if delete_caches.empty?

  FileUtils.remove(delete_caches.map(&:first), force: true)
  @size = keep_caches.inject(0) { |sum, (_, stat)| sum + stat.size }

  @logger.warn do
    secs = Time.now.to_f - start_time.to_f
    "#{self.class}[#{@root}] garbage collected " +
      "#{delete_caches.size} files (#{(secs * 1000).to_i}ms)"
  end
end
safe_open(path, &block) click to toggle source
# File lib/condenser/cache/file_store.rb, line 107
def safe_open(path, &block)
  File.open(path, 'rb', &block) if File.exist?(path)
rescue Errno::ENOENT
end
safe_stat(fn) click to toggle source
# File lib/condenser/cache/file_store.rb, line 124
def safe_stat(fn)
  File.stat(fn)
rescue Errno::ENOENT
  nil
end
unmarshaled_deflated(str, window_bits = -Zlib::MAX_WBITS) click to toggle source

Internal: Unmarshal optionally deflated data.

Checks leading marshal header to see if the bytes are uncompressed otherwise inflate the data an unmarshal.

str - Marshaled String window_bits - Integer deflate window size. See ZLib::Inflate.new()

Returns unmarshaled Object or raises an Exception.

# File lib/condenser/cache/file_store.rb, line 163
def unmarshaled_deflated(str, window_bits = -Zlib::MAX_WBITS)
  major, minor = str[0], str[1]
  if major && major.ord == Marshal::MAJOR_VERSION &&
      minor && minor.ord <= Marshal::MINOR_VERSION
    marshaled = str
  else
    begin
      marshaled = Zlib::Inflate.new(window_bits).inflate(str)
    rescue Zlib::DataError
      marshaled = str
    end
  end
  Marshal.load(marshaled)
end