lib/httpimagestore/configuration/s3.rb in httpimagestore-1.3.0 vs lib/httpimagestore/configuration/s3.rb in httpimagestore-1.4.0
- old
+ new
@@ -81,36 +81,78 @@
def initialize(root_dir)
super "S3 object cache directory '#{root_dir}' is not readable"
end
end
+ class CacheFile < Pathname
+ def initialize(path)
+ super
+ @header = nil
+ end
+
+ def header
+ begin
+ read(0)
+ rescue
+ @header = {}
+ end unless @header
+ @header or fail 'no header data'
+ end
+
+ def read(max_bytes = nil)
+ open('rb') do |io|
+ io.flock(File::LOCK_SH)
+ @header = read_header(io)
+ return io.read(max_bytes)
+ end
+ end
+
+ def write(data)
+ dirname.directory? or dirname.mkpath
+ open('ab') do |io|
+ # opened but not truncated before lock can be obtained
+ io.flock(File::LOCK_EX)
+
+ # now get rid of the old content if any
+ io.seek 0, IO::SEEK_SET
+ io.truncate 0
+
+ begin
+ header = MessagePack.pack(@header)
+ io.write [header.length].pack('L') # header length
+ io.write header
+ io.write data
+ rescue => error
+ unlink # remove broken cache file
+ raise
+ end
+ end
+ end
+
+ private
+
+ def read_header_length(io)
+ head_length = io.read(4)
+ fail 'no header length' unless head_length and head_length.length == 4
+ head_length.unpack('L').first
+ end
+
+ def read_header(io)
+ MessagePack.unpack(io.read(read_header_length(io)))
+ end
+ end
+
def initialize(root_dir)
@root = Pathname.new(root_dir)
@root.directory? or raise CacheRootNotDirError.new(root_dir)
@root.executable? or raise CacheRootNotAccessibleError.new(root_dir)
@root.writable? or raise CacheRootNotWritableError.new(root_dir)
end
def cache_file(bucket, key)
- File.join(Digest::SHA2.new.update("#{bucket}/#{key}").to_s[0,32].match(/(..)(..)(.*)/).captures)
+ CacheFile.new(File.join(@root.to_s, *Digest::SHA2.new.update("#{bucket}/#{key}").to_s[0,32].match(/(..)(..)(.*)/).captures))
end
-
- def open(bucket, key)
- # TODO: locking
- file = @root + cache_file(bucket, key)
-
- file.dirname.directory? or file.dirname.mkpath
- if file.exist?
- file.open('r+') do |io|
- yield io
- end
- else
- file.open('w+') do |io|
- yield io
- end
- end
- end
end
class S3Object
def initialize(client, bucket, key)
@client = client
@@ -145,95 +187,78 @@
s3_object.head[:content_type]
end
end
class CacheObject < S3Object
+ extend Stats
+ def_stats(
+ :total_s3_cache_hits,
+ :total_s3_cache_misses,
+ :total_s3_cache_errors,
+ )
+
include ClassLogging
- def initialize(io, client, bucket, key)
- @io = io
+ def initialize(cache_file, client, bucket, key)
super(client, bucket, key)
- @header = {}
- @have_cache = false
+ @cache_file = cache_file
@dirty = false
- begin
- head_length = @io.read(4)
-
- if head_length and head_length.length == 4
- head_length = head_length.unpack('L').first
- @header = MessagePack.unpack(@io.read(head_length))
- @have_cache = true
-
- log.debug{"S3 object cache hit; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]: header: #{@header}"}
- else
- log.debug{"S3 object cache miss; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]"}
- end
- rescue => error
- log.warn "cannot use cached S3 object; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]: #{error}"
- # not usable
- io.seek 0
- io.truncate 0
- end
-
yield self
- # save object as was used if no error happened and there were changes
+ # save object if new data was read/written to/from S3 and no error happened
write_cache if dirty?
end
def read(max_bytes = nil)
- if @have_cache
- data_location = @io.seek(0, IO::SEEK_CUR)
- begin
- return @data = @io.read(max_bytes)
- ensure
- @io.seek(data_location, IO::SEEK_SET)
- end
- else
- dirty! :read
- return @data = super
+ begin
+ @data = @cache_file.read(max_bytes)
+ CacheObject.stats.incr_total_s3_cache_hits
+ log.debug{"S3 object cache hit for bucket: '#{@bucket}' key: '#{@key}' [#{@cache_file}]: header: #{@cache_file.header}"}
+ return @data
+ rescue Errno::ENOENT
+ CacheObject.stats.incr_total_s3_cache_misses
+ log.debug{"S3 object cache miss for bucket: '#{@bucket}' key: '#{@key}' [#{@cache_file}]"}
+ rescue => error
+ CacheObject.stats.incr_total_s3_cache_errors
+ log.warn "cannot use cached S3 object for bucket: '#{@bucket}' key: '#{@key}' [#{@cache_file}]", error
end
+ @data = super
+ dirty! :read
+ return @data
end
def write(data, options = {})
- out = super
+ super
@data = data
+ @cache_file.header['content_type'] = options[:content_type] if options[:content_type]
dirty! :write
- out
end
def private_url
- @header['private_url'] ||= (dirty! :private_url; super)
+ @cache_file.header['private_url'] ||= (dirty! :private_url; super)
end
def public_url
- @header['public_url'] ||= (dirty! :public_url; super)
+ @cache_file.header['public_url'] ||= (dirty! :public_url; super)
end
def content_type
- @header['content_type'] ||= (dirty! :content_type; super)
+ @cache_file.header['content_type'] ||= (dirty! :content_type; super)
end
private
def write_cache
begin
- log.debug{"S3 object is dirty, wirting cache file; bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]; header: #{@header}"}
+ log.debug{"S3 object is dirty, wirting cache file for bucket: '#{@bucket}' key: '#{@key}' [#{@cache_file}]; header: #{@cache_file.header}"}
raise 'nil data!' unless @data
- # rewrite
- @io.seek(0, IO::SEEK_SET)
- @io.truncate 0
-
- header = MessagePack.pack(@header)
- @io.write [header.length].pack('L') # header length
- @io.write header
- @io.write @data
+ @cache_file.write(@data)
rescue => error
- log.warn "cannot store S3 object in cache: bucket: '#{@bucket}' key: '#{@key}' [#{@io.path}]: #{error}"
+ log.warn "cannot store S3 object in cache for bucket: '#{@bucket}' key: '#{@key}' [#{@cache_file}]", error
ensure
@dirty = false
end
end
@@ -294,11 +319,11 @@
log.info "using S3 object cache directory '#{cache_root}' for image '#{image_name}'"
else
log.info "S3 object cache not configured (no cache-root) for image '#{image_name}'"
end
rescue CacheRoot::CacheRootNotDirError => error
- log.warn "not using S3 object cache for image '#{image_name}': #{error}"
+ log.warn "not using S3 object cache for image '#{image_name}'", error
end
local :bucket, @bucket
end
@@ -319,30 +344,27 @@
key = @prefix + path
image = nil
if @cache_root
begin
- @cache_root.open(@bucket, key) do |cahce_file_io|
- CacheObject.new(cahce_file_io, client, @bucket, key) do |obj|
- image = yield obj
- end
+ cache_file = @cache_root.cache_file(@bucket, key)
+ CacheObject.new(cache_file, client, @bucket, key) do |obj|
+ image = yield obj
end
- rescue IOError => error
- log.warn "cannot use S3 object cache '#{@cache_root.cache_file(@bucket, key)}': #{error}"
- image = yield obj
+ return image
+ rescue Errno::EACCES, IOError => error
+ log.warn "cannot use S3 object cache for bucket: '#{@bucket}' key: '#{key}' [#{cache_file}]", error
end
- else
- image = yield S3Object.new(client, @bucket, key)
end
+ return yield S3Object.new(client, @bucket, key)
rescue AWS::S3::Errors::AccessDenied
raise S3AccessDenied.new(@bucket, path)
rescue AWS::S3::Errors::NoSuchBucket
raise S3NoSuchBucketError.new(@bucket)
rescue AWS::S3::Errors::NoSuchKey
raise S3NoSuchKeyError.new(@bucket, path)
end
- image
end
S3SourceStoreBase.logger = Handler.logger_for(S3SourceStoreBase)
CacheObject.logger = S3SourceStoreBase.logger_for(CacheObject)
end
@@ -394,11 +416,11 @@
object(rendered_path) do |object|
image.mime_type or log.warn "storing '#{image_name}' in S3 '#{@bucket}' bucket under '#{rendered_path}' key with unknown mime type"
options = {}
options[:single_request] = true
- options[:content_type] = image.mime_type
+ options[:content_type] = image.mime_type if image.mime_type
options[:acl] = acl
options[:cache_control] = @cache_control if @cache_control
object.write(image.data, options)
S3SourceStoreBase.stats.incr_total_s3_store
@@ -409,7 +431,8 @@
end
end
end
Handler::register_node_parser S3Store
StatsReporter << S3SourceStoreBase.stats
+ StatsReporter << S3SourceStoreBase::CacheObject.stats
end