Dealing with Data Corruption with GIT, or, I’m Famous!
I store all the data I care about in git. Not so recently I had updated a few files in one of my repositories when my computer kernel panic‘d. Right before the panic I was getting ready to push my changes into a backup repository on another machine. Upon reboot I went to do this when git noticed what turned out to be data corruption.
One minor aside: if I had been using any of some other VCSs, one that did not hash whatever I put into it, I probably wouldn’t have to deal with this problem. But then I would only notice the corruption much later, when I’ve forgotten all the changes, and I run into some hard-to-diagnose problem. Dealing with this up front is definitely better.
This prompted a thread on the git mailing list (the post contains a detailed description of the symptoms). The problem was that two git objects corresponding to two different, recent commits in my repository had been corrupted. Now, there are several ways one can proceed when dealing with corruption, in ascending order of simplicity:
- Copy the corrupted objects from a backup repository.
- Replace the offending commit with a new commit altogether, re-creating approximately the same changes.
- Re-create exactly the changes that turn the commit-before-offending-commit into the offending commit.
I couldn’t take option 1, because I didn’t have a backup; and I couldn’t take option 3 because I didn’t remember the exact changes between the previous and offending commits. So I had to go with 2. This option, incidentally, is an option that is not covered explicitly in the relevant section of the manual. Also, as I usually commit early and often, it was pretty easy for me to reproduce these changes.
Replacing a Corrupted Commit
As noted in the thread, my git repository was in the following state:
$ git fsck --full
error: 320bd6e82267b71dd2ca7043ea3f61dbbca16109: object corrupt or missing
missing blob 320bd6e82267b71dd2ca7043ea3f61dbbca16109
Jakub Narebski was kind enough to explain with diagrams how this is done. Assume that A is the SHA1 ID of the commit preceding the corrupt commit, and B is the ID of the commit immediately following. First, create a new branch, here called corruption, whose head is the commit before the corrupt commit.
$ git checkout -b corruption A $ ... edit edit edit ... $ git commit -a -m <something-like-corrupted-commit-msg>
The next step is teaching the git repository to ignore the corrupted commit. To accomplish this we use the undocumented grafts file. Conceptually, this file is extremely simple. It consists of any number of entries, one per line. Each entry is of the form:
<sha1-id> <sha1-id>*
that is, a SHA1 ID followed by zero or more IDs. The effect of this is that in your local repository, git will treat the first named object as having the parents given. In this way we can trick git, by adding the entry:
$ echo B '<new-commit>' >> .git/info/grafts
At this point, switch back to master.
$ git checkout master $ git fsck --full
There should be no errors. (There might be warnings.)
Unfortunately, this is not the whole story. The grafts file is purely a local measure. Every clone of this repository will still have the corruption. So we have to teach git to write the grafts information directly into the history. Enter git filter-branch.
Replacing a Corrupted Commit For All Time
git filter-branch rewrites history while allowing filters to alter the history. We’ll use it to carve the grafts file into an actual git history.
(on master) $ git filter-branch HEAD Rewrite (3/3) Ref 'refs/heads/master' was rewritten
Now the clones should use the new history. Voilà!
You shouldn’t lie about being famous
Behold, recorded for all time in the git source tree:
commit e9039dd35194b7c1cf4ecd479928638166b8458f
Author: Linus Torvalds
Date: Tue Jun 10 18:47:18 2008 -0700
Consolidate SHA1 object file close
This consolidates the common operations for closing the new
temporary file that we have written, before we move it into
place with the final name.
There's some common code there (make it read-only and check
for errors on close), but more importantly, this also gives a
single place to add an fsync_or_die() call if we want to add
a safe mode.
This was triggered due to Denis Bueno apparently twice being
able to corrupt his git repository on OS X due to an unlucky
combination of kernel crashes and a not-very-robust
filesystem.
Signed-off-by: Linus Torvalds
Signed-off-by: Junio C Hamano
Also, I think I’m the reason for a recent patch adding a new git configuration option, core.fsyncobjectfiles, described as:
This is a total waste of time and effort on a filesystem that orders data writes properly, but can be useful for filesystems that do not use journalling (traditional UNIX filesystems) or that only journal metadata and not file contents (OS X’s HFS+, or Linux ext3 with “data=writeback”).
Linear-time First UIP calculation
In a previous post I mentioned I was using a super-linear algorithm for calculating the first unique implication point (UIP) learned clause in funsat. The algorithm basically uses the definition of first UIP and requires calculating graph dominators of an explicitly-constructed conflict graph. By contrast, the linear-time algorithm described in the Minisat paper never explicitly constructs the graph, it merely inspects the trail in reverse, figuring out which literals should be inserted in the conflict clause using the reasons for each assignment. The former algorithm has the advantage that it implements what it means to calculate the first UIP clause; in other words, it’s easily seen as a correct implementation. The latter isn’t, but when it works, it’s faster and leaner.
The Minisat paper only gives lightly documented pseudocode for the algorithm. There are no data structure invariants nor proof of correctness. Here’s my attempt at explaining how and why it works.
The implication graph is described well in this handbook chapter. Basically, it is a directed graph in which the nodes are literals from the assignment, and an edge x → y indicates the assignment x helped propagate the assignment y.
A UIP of an implication graph is a node at the current decision level d such that every path from the decision variable at level d to the conflict variable or its negation must go through it. Intuitively, it is a single reason at level d that causes the conflict. (This paragraph is from the same chapter.)
There may be many UIPs for the current decision level. The last decision variable is always a UIP. The first UIP is one with the shortest path to the conflict node.
From the UIP definition it is clear why graph dominators are involved: every UIP is a dominator of the conflict variables with respect to the last decision variable. My first implementation explicitly calculated those dominators, and chose the one closest to the conflict nodes.
Once the desired UIP is found, we have to calculate the corresponding learned clause. It turns out that good learned clauses correspond to cuts of the implication graph during a conflict (this is often called the conflict graph). The learned clause corresponding to a cut (S,T) is the set of nodes that are cut edge sources. Formally, this is the set . To tie the knot, one only need know that the UIP u determines the cut (S,T) where T =
. This information is sufficient to calculate the learned clause corresponding to a UIP.
The trail is the current assignment arranged in reverse chronological unit-propagation order (last assigned first out). The reason for a literal q is the set .
Algorithm
The algorithm outputs a learned clause (sequence of literals). There are a few important variables and conventions for the following pseudocode:
- p — Invariant: literal from the current decision level, initially the propagated literal that caused the conflict. The top of the trail is not p.
- c — Invariant: number of unprocessed but seen variables from current decision level, initially 0.
- We can mark a variable as seen. All variables are initially unseen.
- Every literal included in the learned clause has sign opposite what it does under the current assignment. (In the case of the conflicting literal, we include its negation.)
Onto the pseudocode:
Process literals starting with p until we process all the literals we see at the current decision level.
do
Process literal p:
foreach literal q in the reason for p
if var(q)1 is unseen
mark var(q) seen
if q is from the current decision level
increment c
else if q is from a lower decision level
add q to the learned clauseSelect the next interesting literal to follow:
do
assign p to head of trail
undo head of trail
decrement c
while p is unseenwhile c > 0
By now, p is the first UIP node of the current decision level. Add the negation of p to the learned clause and output it.
1 var(x) is the variable corresponding to the literal x.
Correctness
The algorithm performs a backwards breadth-first search for the first UIP node. The trail is the BFS queue. The counter allows us to deduce when p is the closest dominator of the conflict variable. Recall that a node’s being seen means having been discovered as a reason during another node’s processing. At the bottom of the loop, the counter contains the number of unprocessed but seen nodes we know about which end a reverse path from the conflict variable backwards consisting only of current-level nodes (say it three times fast). When c reaches zero, it means there are no seen reverse paths back from the conflict node to the decision variable. Since p is from the current decision level, however, it must have a path from the decision variable. Therefore p must dominate all paths from the decision variable to the conflict variable. p is a UIP node. Moreover, since p is the first such node, it must be the first UIP node.
The first UIP learned clause is determined by the literals that cross the cut (S,T) determined by p, as indicated above. Every proper descendant of p is on the T side of the cut. Therefore, any lower-decision-level node must be on the S side of the cut. (If such a node x were on the T side, there would be a path from the decision node for d to x, and x would have level d, contradicting the assumption that x has a lower decision level.) The first such nodes encountered during traversal, as well as p, cross the cut. The algorithm includes exactly these variables in the learned clause.
-
Archives
- November 2009 (1)
- July 2009 (1)
- March 2009 (2)
- July 2008 (1)
- May 2008 (1)
- April 2008 (1)
- March 2008 (1)
- February 2008 (1)
- October 2007 (1)
- August 2007 (1)
- July 2007 (2)
- April 2007 (4)
-
Categories
-
RSS
Entries RSS
Comments RSS