Author: slebresne
Date: Mon Aug  1 14:22:50 2011
New Revision: 1152793

URL: http://svn.apache.org/viewvc?rev=1152793&view=rev
Log:
fix bug where dirty commit logs were removed (and avoid keeping segment with no 
post-flush activity permanently dirty)
patch by slebresne; reviewed by jbellis for CASSANDRA-2829

Modified:
    cassandra/branches/cassandra-0.8/CHANGES.txt
    
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
    
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
    
cassandra/branches/cassandra-0.8/test/unit/org/apache/cassandra/db/CommitLogTest.java

Modified: cassandra/branches/cassandra-0.8/CHANGES.txt
URL: 
http://svn.apache.org/viewvc/cassandra/branches/cassandra-0.8/CHANGES.txt?rev=1152793&r1=1152792&r2=1152793&view=diff
==============================================================================
--- cassandra/branches/cassandra-0.8/CHANGES.txt (original)
+++ cassandra/branches/cassandra-0.8/CHANGES.txt Mon Aug  1 14:22:50 2011
@@ -17,6 +17,8 @@
    (CASSANDRA-2958)
  * improved POSIX compatibility of start scripts (CASsANDRA-2965)
  * add counter support to Hadoop InputFormat (CASSANDRA-2981)
+ * fix bug where dirty commit logs were removed (and avoid keeping segment
+   with no post-flush activity permanently dirty) (CASSANDRA-2829)
 
 
 0.8.2

Modified: 
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
URL: 
http://svn.apache.org/viewvc/cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLog.java?rev=1152793&r1=1152792&r2=1152793&view=diff
==============================================================================
--- 
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
 (original)
+++ 
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLog.java
 Mon Aug  1 14:22:50 2011
@@ -385,6 +385,12 @@ public class CommitLog
         }
     }
 
+    // for tests mainly
+    public int segmentsCount()
+    {
+        return segments.size();
+    }
+
     /*
      * Adds the specified row to the commit log. This method will reset the
      * file offset to what it is before the start of the operation in case
@@ -450,30 +456,36 @@ public class CommitLog
             CommitLogSegment segment = iter.next();
             if (segment.id == context.segment)
             {
-                // we can't just mark the segment where the flush happened 
clean,
-                // since there may have been writes to it between when the 
flush
-                // started and when it finished.
-                segment.turnOn(id);
+                // Only unmark this segment if there were not write since the
+                // ReplayPosition was grabbed.
+                segment.turnOffIfNotWritten(id, context.position);
+                maybeDiscardSegment(segment, iter);
                 break;
             }
 
             segment.turnOff(id);
-            if (segment.isSafeToDelete() && iter.hasNext())
-            {
-                logger.info("Discarding obsolete commit log:" + segment);
-                segment.close();
-                DeletionService.executeDelete(segment.getPath());
-                // usually this will be the first (remaining) segment, but not 
always, if segment A contains
-                // writes to a CF that is unflushed but is followed by segment 
B whose CFs are all flushed.
-                iter.remove();
-            }
-            else
-            {
-                if (logger.isDebugEnabled())
-                    logger.debug("Not safe to delete commit log " + segment + 
"; dirty is " + segment.dirtyString() + "; hasNext: " + iter.hasNext());
-            }
+            maybeDiscardSegment(segment, iter);
         }
     }
+
+    private void maybeDiscardSegment(CommitLogSegment segment, 
Iterator<CommitLogSegment> iter)
+    {
+        if (segment.isSafeToDelete() && iter.hasNext())
+        {
+            logger.info("Discarding obsolete commit log:" + segment);
+            segment.close();
+            DeletionService.executeDelete(segment.getPath());
+            // usually this will be the first (remaining) segment, but not 
always, if segment A contains
+            // writes to a CF that is unflushed but is followed by segment B 
whose CFs are all flushed.
+            iter.remove();
+        }
+        else
+        {
+            if (logger.isDebugEnabled())
+                logger.debug("Not safe to delete commit log " + segment + "; 
dirty is " + segment.dirtyString() + "; hasNext: " + iter.hasNext());
+        }
+    }
+
     
     void sync() throws IOException
     {

Modified: 
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
URL: 
http://svn.apache.org/viewvc/cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java?rev=1152793&r1=1152792&r2=1152793&view=diff
==============================================================================
--- 
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
 (original)
+++ 
cassandra/branches/cassandra-0.8/src/java/org/apache/cassandra/db/commitlog/CommitLogSegment.java
 Mon Aug  1 14:22:50 2011
@@ -23,8 +23,8 @@ package org.apache.cassandra.db.commitlo
 import java.io.File;
 import java.io.IOError;
 import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.CRC32;
@@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.RowMutation;
+import org.apache.cassandra.db.ColumnFamily;
 import org.apache.cassandra.io.util.BufferedRandomAccessFile;
 
 public class CommitLogSegment
@@ -48,7 +49,7 @@ public class CommitLogSegment
     private final BufferedRandomAccessFile logWriter;
 
     // cache which cf is dirty in this segment to avoid having to lookup all 
ReplayPositions to decide if we could delete this segment
-    private Set<Integer> cfDirty = new HashSet<Integer>();
+    private Map<Integer, Integer> cfLastWrite = new HashMap<Integer, 
Integer>();
 
     public CommitLogSegment()
     {
@@ -102,6 +103,21 @@ public class CommitLogSegment
             assert currentPosition <= Integer.MAX_VALUE;
             ReplayPosition cLogCtx = new ReplayPosition(id, (int) 
currentPosition);
 
+            for (ColumnFamily columnFamily : rowMutation.getColumnFamilies())
+            {
+                // check for null cfm in case a cl write goes through after 
the cf is
+                // defined but before a new segment is created.
+                CFMetaData cfm = 
DatabaseDescriptor.getCFMetaData(columnFamily.id());
+                if (cfm == null)
+                {
+                    logger.error("Attempted to write commit log entry for 
unrecognized column family: " + columnFamily.id());
+                }
+                else
+                {
+                    turnOn(cfm.cfId, (int) currentPosition);
+                }
+            }
+
             // write mutation, w/ checksum on the size and data
             Checksum checksum = new CRC32();
             byte[] serializedRow = 
rowMutation.getSerializedBuffer(MessagingService.version_);
@@ -168,21 +184,32 @@ public class CommitLogSegment
         }
     }
 
-    void turnOn(Integer cfId)
+    void turnOn(Integer cfId, Integer position)
+    {
+        cfLastWrite.put(cfId, position);
+    }
+
+    /**
+     * Turn the dirty bit off only if there has been no write since the flush
+     * position was grabbed.
+     */
+    void turnOffIfNotWritten(Integer cfId, Integer flushPosition)
     {
-        cfDirty.add(cfId);
+        Integer lastWritten = cfLastWrite.get(cfId);
+        if (lastWritten == null || lastWritten < flushPosition)
+            cfLastWrite.remove(cfId);
     }
 
     void turnOff(Integer cfId)
     {
-        cfDirty.remove(cfId);
+        cfLastWrite.remove(cfId);
     }
 
     // For debugging, not fast
     String dirtyString()
     {
         StringBuilder sb = new StringBuilder();
-        for (Integer cfId : cfDirty)
+        for (Integer cfId : cfLastWrite.keySet())
         {
             CFMetaData m = DatabaseDescriptor.getCFMetaData(cfId);
             sb.append(m == null ? m.cfName : "<deleted>").append(" 
(").append(cfId).append("), ");
@@ -192,7 +219,7 @@ public class CommitLogSegment
 
     boolean isSafeToDelete()
     {
-        return cfDirty.isEmpty();
+        return cfLastWrite.isEmpty();
     }
 
     @Override

Modified: 
cassandra/branches/cassandra-0.8/test/unit/org/apache/cassandra/db/CommitLogTest.java
URL: 
http://svn.apache.org/viewvc/cassandra/branches/cassandra-0.8/test/unit/org/apache/cassandra/db/CommitLogTest.java?rev=1152793&r1=1152792&r2=1152793&view=diff
==============================================================================
--- 
cassandra/branches/cassandra-0.8/test/unit/org/apache/cassandra/db/CommitLogTest.java
 (original)
+++ 
cassandra/branches/cassandra-0.8/test/unit/org/apache/cassandra/db/CommitLogTest.java
 Mon Aug  1 14:22:50 2011
@@ -20,6 +20,7 @@
 package org.apache.cassandra.db;
 
 import java.io.*;
+import java.nio.ByteBuffer;
 import java.util.zip.CRC32;
 import java.util.zip.Checksum;
 
@@ -29,6 +30,7 @@ import org.apache.cassandra.CleanupHelpe
 import org.apache.cassandra.db.commitlog.CommitLog;
 import org.apache.cassandra.db.filter.QueryPath;
 import org.apache.cassandra.utils.Pair;
+import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
 
 public class CommitLogTest extends CleanupHelper
 {
@@ -87,6 +89,74 @@ public class CommitLogTest extends Clean
         testRecoveryWithBadSizeArgument(-10, 10); // negative size, but no EOF
     }
 
+    @Test
+    public void testDontDeleteIfDirty() throws Exception
+    {
+        CommitLog.instance.resetUnsafe();
+        // Roughly 32 MB mutation
+        RowMutation rm = new RowMutation("Keyspace1", bytes("k"));
+        rm.add(new QueryPath("Standard1", null, bytes("c1")), 
ByteBuffer.allocate(32 * 1024 * 1024), 0);
+
+        // Adding it 5 times
+        CommitLog.instance.add(rm);
+        CommitLog.instance.add(rm);
+        CommitLog.instance.add(rm);
+        CommitLog.instance.add(rm);
+        CommitLog.instance.add(rm);
+
+        // Adding new mutation on another CF
+        RowMutation rm2 = new RowMutation("Keyspace1", bytes("k"));
+        rm2.add(new QueryPath("Standard2", null, bytes("c1")), 
ByteBuffer.allocate(4), 0);
+        CommitLog.instance.add(rm2);
+
+        assert CommitLog.instance.segmentsCount() == 2 : "Expecting 2 
segments, got " + CommitLog.instance.segmentsCount();
+
+        int cfid2 = rm2.getColumnFamilyIds().iterator().next();
+        CommitLog.instance.discardCompletedSegments(cfid2, 
CommitLog.instance.getContext());
+
+        // Assert we still have both our segment
+        assert CommitLog.instance.segmentsCount() == 2 : "Expecting 2 
segments, got " + CommitLog.instance.segmentsCount();
+    }
+
+    @Test
+    public void testDeleteIfNotDirty() throws Exception
+    {
+        CommitLog.instance.resetUnsafe();
+        // Roughly 32 MB mutation
+        RowMutation rm = new RowMutation("Keyspace1", bytes("k"));
+        rm.add(new QueryPath("Standard1", null, bytes("c1")), 
ByteBuffer.allocate(32 * 1024 * 1024), 0);
+
+        // Adding it twice (won't change segment)
+        CommitLog.instance.add(rm);
+        CommitLog.instance.add(rm);
+
+        assert CommitLog.instance.segmentsCount() == 1 : "Expecting 1 segment, 
got " + CommitLog.instance.segmentsCount();
+
+        // "Flush": this won't delete anything
+        int cfid1 = rm.getColumnFamilyIds().iterator().next();
+        CommitLog.instance.discardCompletedSegments(cfid1, 
CommitLog.instance.getContext());
+
+        assert CommitLog.instance.segmentsCount() == 1 : "Expecting 1 segment, 
got " + CommitLog.instance.segmentsCount();
+
+        // Adding new mutation on another CF so that a new segment is created
+        RowMutation rm2 = new RowMutation("Keyspace1", bytes("k"));
+        rm2.add(new QueryPath("Standard2", null, bytes("c1")), 
ByteBuffer.allocate(64 * 1024 * 1024), 0);
+        CommitLog.instance.add(rm2);
+        CommitLog.instance.add(rm2);
+
+        assert CommitLog.instance.segmentsCount() == 2 : "Expecting 2 
segments, got " + CommitLog.instance.segmentsCount();
+
+
+        // "Flush" second cf: The first segment should be deleted since we
+        // didn't write anything on cf1 since last flush (and we flush cf2)
+
+        int cfid2 = rm2.getColumnFamilyIds().iterator().next();
+        CommitLog.instance.discardCompletedSegments(cfid2, 
CommitLog.instance.getContext());
+
+        // Assert we still have both our segment
+        assert CommitLog.instance.segmentsCount() == 1 : "Expecting 1 segment, 
got " + CommitLog.instance.segmentsCount();
+    }
+
     protected void testRecoveryWithBadSizeArgument(int size, int dataSize) 
throws Exception
     {
         Checksum checksum = new CRC32();


Reply via email to