Skip to content
Browse files

make object writes atomic

When Grit writes a loose object via the LooseStorage class,
it just opens the object file and starts writing. This works
most of the time, but can be a problem in some corner cases,
including:

  1. If another process tries to write the same object
     simultaneously, the writes may be interleaved and the
     object can be corrupted.

  2. If another process tries to read the object
     simultaneously, it may see the object in a half-written
     state.

  3. If the process or machine crashes during the write, we
     may leave a half-written corrupt object.

This can be solved by writing the object to a temporary file
and linking it into place. This is the same strategy used by
git itself.
  • Loading branch information...
1 parent 75cf040 commit 00a00543a7df025b6503de66b3b5aca4ddaa799b @peff peff committed Feb 21, 2013
Showing with 19 additions and 3 deletions.
  1. +19 −3 lib/grit/git-ruby/internal/loose.rb
View
22 lib/grit/git-ruby/internal/loose.rb
@@ -12,6 +12,7 @@
require 'zlib'
require 'digest/sha1'
require 'grit/git-ruby/internal/raw_object'
+require 'tempfile'
module Grit
module GitRuby
@@ -60,6 +61,23 @@ def get_raw_object(buf)
return RawObject.new(type, content)
end
+ # write an object to a temporary file, then atomically rename it
+ # into place; this ensures readers never see a half-written file
+ def safe_write(path, content)
+ Tempfile.open("tmp_obj_", File.dirname(path), :opt => "wb") do |f|
+ f.write content
+ f.close
+ begin
+ File.link(f.path, path)
+ rescue Errno::EEXIST
+ # The path already exists; we raced with another process,
+ # but it's OK, because by definition the content is the
+ # same. So we can just ignore the error.
+ end
+ f.unlink
+ end
+ end
+
# currently, I'm using the legacy format because it's easier to do
# this function takes content and a type and writes out the loose object and returns a sha
def put_raw_object(content, type)
@@ -76,9 +94,7 @@ def put_raw_object(content, type)
content = Zlib::Deflate.deflate(store)
FileUtils.mkdir_p(@directory+'/'+sha1[0...2])
- File.open(path, 'wb') do |f|
- f.write content
- end
+ safe_write(path, content)
end
return sha1
end

0 comments on commit 00a0054

Please sign in to comment.
Something went wrong with that request. Please try again.