lib/defog/proxy.rb in defog-0.1.1 vs lib/defog/proxy.rb in defog-0.2.0

- old
+ new

@@ -1,19 +1,22 @@ require "hash_keyword_args" -require "tmpdir" require "pathname" +require "set" +require "tmpdir" module Defog class Proxy - attr_reader :proxy_root # + attr_reader :proxy_root + attr_reader :persist + attr_reader :max_cache_size attr_reader :fog_wrapper # :nodoc: # Opens a <code>Fog</code> cloud storage connection to map to a corresponding proxy # directory. Use via, e.g., # - # Defog::Proxy.new(:provider => :AWS, :aws_access_key_id => access_key, ...) + # defog = Defog::Proxy.new(:provider => :AWS, :aws_access_key_id => access_key, ...) # # The <code>:provider</code> and its corresponding options must be # specified as per <code>Fog::Storage.new</code>. Currently, only # <code>:local</code> and <code>:AWS</code> are supported. When using # <code>:AWS</code>, an additional option <code>:bucket</code> must be @@ -29,14 +32,31 @@ # # The upshot is that if you have no special constraints you don't need # to worry about it. But if you do care, you can specify the option: # :proxy_root => "/root/for/this/proxy/files" # + # You can turn on persistence of local proxy files by specifying + # :persist => true + # The persistence behavior can be overriden on a per-file basis when + # opening a proxy (see Defog::Handle#open) + # + # You can enable cache management by specifying a max cache size in + # bytes, e.g. + # :max_cache_size => 3.gigabytes + # See the README for discussion. [Number#gigabytes is defined in + # Rails' ActiveSupport core extensions] def initialize(opts={}) - opts = opts.keyword_args(:provider => :required, :proxy_root => :optional, :OTHERS => :optional) + opts = opts.keyword_args(:provider => :required, + :proxy_root => :optional, + :persist => :optional, + :max_cache_size => :optional, + :OTHERS => :optional) @proxy_root = Pathname.new(opts.delete(:proxy_root)) if opts.proxy_root + @persist = opts.delete(:persist) + @max_cache_size = opts.delete(:max_cache_size) + @open_proxy_paths = Set.new @fog_wrapper = FogWrapper.connect(opts) @proxy_root ||= case when defined?(Rails) then Rails.root + "tmp" @@ -67,17 +87,18 @@ # Returns the Fog directory object for the root of the cloud files def fog_directory @fog_wrapper.fog_directory end - # Proxy a remote cloud file. Returns a Defog::Handle object that + # Proxy a remote cloud file. Returns or yields a Defog::Handle object that # represents the file. # - # If a <code>mode</code> is specified given opens a proxy file via + # If a <code>mode</code> is given, opens a proxy file via # Defog::Handle#open (passing it the mode and other options and - # optional block), returning instead the Defog::File object. + # optional block), returning or yielding instead the Defog::File object. # + # # Thus # proxy.file("key", mode, options, &block) # is shorthand for # proxy.file("key").open(mode, options, &block) # @@ -86,9 +107,58 @@ case when mode then handle.open(mode, opts, &block) if mode when block then block.call(handle) else handle end + end + + def open_proxy_file(handle) #:nodoc: + manage_cache(handle) if max_cache_size + @open_proxy_paths << handle.proxy_path + end + + def close_proxy_file(handle) #:nodoc: + @open_proxy_paths.delete handle.proxy_path + end + + private + + def manage_cache(handle) + remote_size = handle.size + proxy_path = handle.proxy_path + + # find available space (not counting current proxy) + available = max_cache_size + proxy_root.find { |path| available -= path.size if path.file? and path != proxy_path} + return if available >= remote_size + + space_needed = remote_size - available + + # find all paths in the cache that aren't currently open (not + # counting current proxy) + candidates = [] + proxy_root.find { |path| candidates << path if path.file? and not @open_proxy_paths.include?(path) and path != proxy_path} + + # take candidates in LRU order until that would be enough space + would_free = 0 + candidates = Set.new(candidates.sort_by(&:atime).take_while{|path| (would_free < space_needed).tap{|condition| would_free += path.size}}) + + # still not enough...? + raise Error::CacheFull, "No room in cache for #{handle.key.inspect}: size=#{remote_size} available=#{available} can_free=#{would_free}, max_cache_size=#{max_cache_size}" if would_free < space_needed + + # LRU order may have taken more than needed, if last file was a big + # chunk. So take another pass, eliminating files that aren't needed. + # Do this in reverse size order, since we want to keep big files in + # the cache if possible since they're most expensive to replace. + candidates.sort_by(&:size).reverse.each do |path| + if (would_free - path.size) > space_needed + candidates.delete path + would_free -= path.size + end + end + + # free the remaining candidates + candidates.each(&:unlink) end end end