Index: src/test/org/apache/lucene/index/TestIndexWriter.java
===================================================================
--- src/test/org/apache/lucene/index/TestIndexWriter.java	(revision 1061490)
+++ src/test/org/apache/lucene/index/TestIndexWriter.java	(working copy)
@@ -135,7 +135,7 @@
         dir.close();
     }
 
-    private void addDoc(IndexWriter writer) throws IOException
+    public static void addDoc(IndexWriter writer) throws IOException
     {
         Document doc = new Document();
         doc.add(newField("content", "aaa", Field.Store.NO, Field.Index.ANALYZED));
Index: src/test/org/apache/lucene/index/TestIndexEvents.java
===================================================================
--- src/test/org/apache/lucene/index/TestIndexEvents.java	(revision 0)
+++ src/test/org/apache/lucene/index/TestIndexEvents.java	(revision 0)
@@ -0,0 +1,220 @@
+package org.apache.lucene.index;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+
+public class TestIndexEvents extends LuceneTestCase {
+  
+  public void testFlush() throws Exception {
+    Directory dir = newDirectory();
+    IndexWriterConfig iwc = newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer());
+    
+    FlushEventHandler eventHandler = new FlushEventHandler();
+    IndexWriter writer = new IndexWriter(dir, iwc);
+    writer.addIndexEventListener(eventHandler);
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+    writer.commit();
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+    writer.commit();
+    
+    assertEquals(2, eventHandler.startCount);
+    assertEquals(2, eventHandler.completeCount);
+    
+    writer.close();
+    dir.close();
+  }
+  
+  public class FlushEventHandler extends IndexEventListener {
+    int completeCount = 0;
+    int startCount = 0;
+    
+    public void flushEvent(FlushEvent event) {
+      //System.out.println(""+event);
+      if (event.type.equals(FlushEvent.Type.START)) {
+        startCount++;
+      } else if (event.type.equals(FlushEvent.Type.COMPLETE)) {
+        completeCount++;
+      }
+    }
+    
+    public void mergeEvent(MergeEvent event) {}
+  }
+  
+  public void testFlushAbort() throws Exception {
+    Directory dir = newDirectory();
+    IndexWriterConfig iwc = newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer());
+    ((LogMergePolicy)iwc.getMergePolicy()).setMergeFactor(2);
+    iwc.setMergeScheduler(new SerialMergeScheduler());
+    FlushAbortHandler eventHandler = new FlushAbortHandler();
+    MockIndexWriterFlushAbort writer = new MockIndexWriterFlushAbort(dir, iwc);
+    writer.addIndexEventListener(eventHandler);
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+  
+    boolean hitEx = false;
+    try {
+      writer.commit();
+      fail();
+    } catch (RuntimeException th) {
+      hitEx = true;
+    }
+    assertTrue(hitEx);
+    assertTrue(eventHandler.abortCount > 0);
+    writer.close();
+    dir.close();
+  }
+  
+  /**
+   * The first flush will fail.
+   */
+  private static final class MockIndexWriterFlushAbort extends IndexWriter {
+    public MockIndexWriterFlushAbort(Directory dir, IndexWriterConfig conf) throws IOException {
+      super(dir, conf);
+    }
+
+    int timesFailed = 0;
+
+    @Override
+    boolean testPoint(String name) {
+      if (timesFailed == 0 && name.equals("startDoFlush")) {
+        //System.out.println("testPoint: startDoFlush");
+        timesFailed++;
+        throw new RuntimeException("intentionally failing at startDoFlush");
+      } return true;
+    }
+  }
+  
+  public class FlushAbortHandler extends IndexEventListener {
+    int abortCount = 0;
+    
+    public void flushEvent(FlushEvent event) {
+      //System.out.println(""+event);
+      if (event.type.equals(FlushEvent.Type.ABORT)) {
+        abortCount++;
+      }
+    }
+    
+    public void mergeEvent(MergeEvent event) {}
+  }
+  
+  public void testMerge() throws Exception {
+    Directory dir = newDirectory();
+    IndexWriterConfig iwc = newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer());
+    
+    MergeEventHandler eventHandler = new MergeEventHandler();
+    IndexWriter writer = new IndexWriter(dir, iwc);
+    writer.addIndexEventListener(eventHandler);
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+    writer.commit();
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+    writer.commit();
+    writer.optimize();
+    
+    assertTrue(eventHandler.init);
+    assertTrue(eventHandler.start);
+    assertTrue(eventHandler.complete);
+    
+    writer.close();
+    dir.close();
+  }
+  
+  public void testMergeAbort() throws Exception {
+    Directory dir = newDirectory();
+    IndexWriterConfig iwc = newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer());
+    ((LogMergePolicy)iwc.getMergePolicy()).setMergeFactor(2);
+    iwc.setMergeScheduler(new SerialMergeScheduler());
+    MergeEventHandler eventHandler = new MergeEventHandler();
+    MockIndexWriterMergeAbort writer = new MockIndexWriterMergeAbort(dir, iwc);
+    writer.addIndexEventListener(eventHandler);
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+    writer.commit();
+    for (int x=0; x < 10; x++) {
+      TestIndexWriter.addDoc(writer);
+    }
+  
+    boolean hitEx = false;
+    try {
+      writer.commit();
+      fail();
+    } catch (RuntimeException th) {
+      hitEx = true;
+    }
+    assertTrue(hitEx);
+    assertTrue(eventHandler.init);
+    assertTrue(eventHandler.start);
+    assertTrue(eventHandler.abort);
+    assertFalse(eventHandler.complete);
+    
+    writer.close();
+    dir.close();
+  }
+
+  private static final class MockIndexWriterMergeAbort extends IndexWriter {
+    public MockIndexWriterMergeAbort(Directory dir, IndexWriterConfig conf) throws IOException {
+      super(dir, conf);
+    }
+
+    @Override
+    boolean testPoint(String name) {
+      if (name.equals("startCommitMerge"))
+        throw new RuntimeException("intentionally failing at startCommitMerge");
+      return true;
+    }
+  }
+  
+  public class MergeEventHandler extends IndexEventListener {
+    boolean init = false;
+    boolean start = false;
+    boolean complete = false;
+    boolean abort = false;
+    
+    public void flushEvent(FlushEvent event) {}
+    
+    public void mergeEvent(MergeEvent event) {
+      //System.out.println("mergeEvent:"+event);
+      if (event.type.equals(MergeEvent.Type.INIT)) {
+        init = true;
+      }
+      if (event.type.equals(MergeEvent.Type.START)) {
+        start = true;
+      }
+      if (event.type.equals(MergeEvent.Type.COMPLETE)) {
+        complete = true;
+      }
+      if (event.type.equals(MergeEvent.Type.ABORT)) {
+        abort = true;
+      }
+    }
+  }
+}
Index: src/java/org/apache/lucene/index/IndexEventListener.java
===================================================================
--- src/java/org/apache/lucene/index/IndexEventListener.java	(revision 0)
+++ src/java/org/apache/lucene/index/IndexEventListener.java	(revision 0)
@@ -0,0 +1,124 @@
+package org.apache.lucene.index;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+
+/**
+ * A custom listener that's invoked when IndexWriter
+ * merges or flushes segments.
+ * 
+ * For flushes, the start and end timestamps are registered.
+ * 
+ * <p>For merges, the init, start, and end timestamps are registered.
+ * The init time for a merge is the when the merge is actually registered 
+ * to IndexWriter.  The start time is when the merge begins running, 
+ * eg, consuming CPU and IO.  The end time is when the merge completed or 
+ * was aborted.</p>
+ *
+ * @lucene.experimental
+ */
+public abstract class IndexEventListener {
+  public abstract void mergeEvent(MergeEvent event);
+  
+  public abstract void flushEvent(FlushEvent event);
+  
+  public static class FlushEvent {
+    public enum Type {
+      START, COMPLETE, ABORT
+    }
+    public final Type type;
+    public final Long startTime;
+    public final Long completeTime;
+    public final Throwable throwable;
+    public final boolean deletesApplied;
+    // nocommit: add reason for flush, eg, RAM buffer full, deletes full, etc
+    
+    public FlushEvent(Type type, Long startTime, Long completeTime, Throwable throwable,
+        boolean deletesApplied) {
+      this.type = type;
+      this.startTime = startTime;
+      this.completeTime = completeTime;
+      this.throwable = throwable;
+      this.deletesApplied = deletesApplied;
+    }
+
+    @Override
+    public String toString() {
+      return "FlushEvent [type=" + type + ", startTime=" + startTime
+          + ", completeTime=" + completeTime + ", throwable=" + throwable
+          + ", deletesApplied=" + deletesApplied + "]";
+    }
+  }
+  
+  public static class MergeEvent {
+    public enum Type {
+      INIT, START, COMPLETE, ABORT
+    }
+    public final Type type;
+    public final long initTime;
+    public final Long startTime;
+    public final Long endTime;
+    public final SegmentInfos segments;
+    public final SegmentInfo mergeInfo;
+    final MergePolicy.OneMerge merge;
+    public final Throwable throwable;
+    
+    MergeEvent(Type type, MergePolicy.OneMerge merge) {
+      this.type = type;
+      this.merge = merge;
+      this.initTime = merge.initTime;
+      this.startTime = merge.startTime;
+      this.endTime = merge.endTime;
+      this.segments = merge.segments;
+      if (type.equals(Type.COMPLETE)) {
+        this.mergeInfo = merge.info;
+      } else {
+        this.mergeInfo = null;
+      }
+      this.throwable = merge.getException();
+    }
+    
+    @Override
+    public String toString() {
+      try {
+        return "MergeEvent [type=" + type + ", initTime=" + initTime
+            + ", startTime=" + startTime + ", endTime=" + endTime
+            + ", segments=" + segments + ", mergeInfo=" + mergeInfo
+            + ", merge=" + merge+", sourceSegmentsSize="+getSourceSegmentsSize(true)
+            + ", mergeSegmentsSize="+getMergeSegmentSize(true)+"]";
+      } catch (IOException ioe) {
+        throw new RuntimeException(ioe);
+      }
+    }
+    
+    public long getSourceSegmentsSize(boolean includeDocStores)
+        throws IOException {
+      return segments.totalSizeInBytes(includeDocStores);
+    }
+    
+    public long getMergeSegmentSize(boolean includeDocStores)
+        throws IOException {
+      if (mergeInfo != null) {
+        return mergeInfo.sizeInBytes(includeDocStores);
+      } else {
+        return 0;
+      }
+    }
+  }
+}
Index: src/java/org/apache/lucene/index/SegmentInfo.java
===================================================================
--- src/java/org/apache/lucene/index/SegmentInfo.java	(revision 1061490)
+++ src/java/org/apache/lucene/index/SegmentInfo.java	(working copy)
@@ -229,9 +229,11 @@
       if (sizeInBytesWithStore != -1) return sizeInBytesWithStore;
       sizeInBytesWithStore = 0;
       for (final String fileName : files()) {
-        // We don't count bytes used by a shared doc store against this segment
-        if (docStoreOffset == -1 || !IndexFileNames.isDocStoreFile(fileName)) {
-          sizeInBytesWithStore += dir.fileLength(fileName);
+        if (dir.fileExists(fileName)) {
+          // We don't count bytes used by a shared doc store against this segment
+          if (docStoreOffset == -1 || !IndexFileNames.isDocStoreFile(fileName)) {
+            sizeInBytesWithStore += dir.fileLength(fileName);
+          }
         }
       }
       return sizeInBytesWithStore;
@@ -509,7 +511,9 @@
       for(String ext : IndexFileNames.NON_STORE_INDEX_EXTENSIONS) {
         addIfExists(fileSet, IndexFileNames.segmentFileName(name, "", ext));
       }
-      segmentCodecs.files(dir, this, fileSet);
+      if (segmentCodecs != null) {
+        segmentCodecs.files(dir, this, fileSet);
+      }
     }
 
     if (docStoreOffset != -1) {
Index: src/java/org/apache/lucene/index/MergePolicy.java
===================================================================
--- src/java/org/apache/lucene/index/MergePolicy.java	(revision 1061490)
+++ src/java/org/apache/lucene/index/MergePolicy.java	(working copy)
@@ -75,6 +75,10 @@
     SegmentReader[] readers;        // used by IndexWriter
     SegmentReader[] readersClone;   // used by IndexWriter
     public final SegmentInfos segments;
+    public long sourceSegmentsSizeBytes;
+    Long startTime;
+    Long initTime;
+    Long endTime;
     boolean aborted;
     Throwable error;
     boolean paused;
Index: src/java/org/apache/lucene/index/SegmentInfos.java
===================================================================
--- src/java/org/apache/lucene/index/SegmentInfos.java	(revision 1061490)
+++ src/java/org/apache/lucene/index/SegmentInfos.java	(working copy)
@@ -887,4 +887,12 @@
   public void changed() {
     version++;
   }
+  
+  public long totalSizeInBytes(boolean includeDocStores) throws IOException {
+    long total = 0;
+    for (SegmentInfo info : this) {
+      total += info.sizeInBytes(includeDocStores);
+    }
+    return total;
+  }
 }
Index: src/java/org/apache/lucene/index/IndexWriter.java
===================================================================
--- src/java/org/apache/lucene/index/IndexWriter.java	(revision 1061490)
+++ src/java/org/apache/lucene/index/IndexWriter.java	(working copy)
@@ -251,6 +251,7 @@
 
   final ReaderPool readerPool = new ReaderPool();
   final BufferedDeletes bufferedDeletes;
+  private List<IndexEventListener> indexEventListeners = new ArrayList<IndexEventListener>();
   
   // This is a "write once" variable (like the organic dye
   // on a DVD-R that may or may not be heated by a laser and
@@ -567,7 +568,6 @@
         // synchronized
         // Returns a ref, which we xfer to readerMap:
         sr = SegmentReader.get(false, info.dir, info, readBufferSize, doOpenStores, termsIndexDivisor);
-
         if (info.dir == directory) {
           // Only pool if reader is not external
           readerMap.put(info, sr);
@@ -2445,22 +2445,66 @@
 
     // We can be called during close, when closing==true, so we must pass false to ensureOpen:
     ensureOpen(false);
-    if (doFlush(flushDeletes) && triggerMerge) {
+    if (doFlushWithEvents(flushDeletes) && triggerMerge) {
       maybeMerge();
     }
   }
 
+  private void notifyFlushEvent(IndexEventListener.FlushEvent event) {
+    for (IndexEventListener listener : indexEventListeners) {
+      listener.flushEvent(event);
+    }
+  }
+  
+  private void notifyMergeEvent(IndexEventListener.MergeEvent event) {
+    for (IndexEventListener listener : indexEventListeners) {
+      listener.mergeEvent(event);
+    }
+  }
+  
+  private synchronized boolean doFlushWithEvents(boolean applyAllDeletes) throws CorruptIndexException, IOException {
+    long startTime = System.currentTimeMillis();
+    notifyFlushEvent(new IndexEventListener.FlushEvent(IndexEventListener.FlushEvent.Type.START, 
+            startTime, null, null, false));
+    boolean success = false;
+    try {
+      FlushStatus status = doFlush(applyAllDeletes);
+      success = true;
+      long completeTime = System.currentTimeMillis();
+      
+      notifyFlushEvent(new IndexEventListener.FlushEvent(IndexEventListener.FlushEvent.Type.COMPLETE, 
+              startTime, completeTime, null,
+              status.deletesApplied));
+      return status.flushed;
+    } finally {
+      if (!success) {
+        // nocommit: we need to capture the exception
+        notifyFlushEvent(new IndexEventListener.FlushEvent(IndexEventListener.FlushEvent.Type.ABORT, 
+              startTime, null, null, false));
+      }
+    }
+  }
+
+  private static class FlushStatus {
+    boolean flushed = false;
+    boolean deletesApplied = false;
+    
+    public FlushStatus(boolean flushed, boolean deletesApplied) {
+      this.flushed = flushed;
+      this.deletesApplied = deletesApplied;
+    }
+  }
+  
   // TODO: this method should not have to be entirely
   // synchronized, ie, merges should be allowed to commit
   // even while a flush is happening
-  private synchronized boolean doFlush(boolean applyAllDeletes) throws CorruptIndexException, IOException {
+  private synchronized FlushStatus doFlush(boolean applyAllDeletes) throws CorruptIndexException, IOException {
 
     if (hitOOM) {
       throw new IllegalStateException("this writer hit an OutOfMemoryError; cannot flush");
     }
-
     doBeforeFlush();
-
+    
     assert testPoint("startDoFlush");
 
     // We may be flushing because it was triggered by doc
@@ -2502,6 +2546,8 @@
         }
       }
 
+      boolean deletesApplied = false;
+      
       if (applyAllDeletes) {
         if (infoStream != null) {
           message("apply all deletes during flush");
@@ -2509,6 +2555,7 @@
         flushDeletesCount.incrementAndGet();
         if (bufferedDeletes.applyDeletes(readerPool, segmentInfos, segmentInfos)) {
           checkpoint();
+          deletesApplied = true;
         }
         flushControl.clearDeletes();
       } else if (infoStream != null) {
@@ -2516,16 +2563,17 @@
       }
 
       doAfterFlush();
+      
       flushCount.incrementAndGet();
 
       success = true;
 
-      return newSegment != null;
+      return new FlushStatus(newSegment != null, deletesApplied);
 
     } catch (OutOfMemoryError oom) {
       handleOOM(oom, "doFlush");
       // never hit
-      return false;
+      return new FlushStatus(false, false);
     } finally {
       flushControl.clearFlushPending();
       if (!success && infoStream != null)
@@ -2917,6 +2965,10 @@
     // after our merge is done but while we are building the
     // CFS:
     mergingSegments.add(merge.info);
+    
+    merge.initTime = System.currentTimeMillis();
+    notifyMergeEvent(new IndexEventListener.MergeEvent(IndexEventListener.MergeEvent.Type.INIT, 
+        merge));
   }
 
   private void setDiagnostics(SegmentInfo info, String source) {
@@ -2958,6 +3010,19 @@
     }
 
     runningMerges.remove(merge);
+    
+    merge.endTime = System.currentTimeMillis();
+    
+    // successful merge
+    if (!merge.aborted && merge.getException() == null) {
+      notifyMergeEvent(new IndexEventListener.MergeEvent(IndexEventListener.MergeEvent.Type.COMPLETE, merge));
+    } else {
+      // merge aborted
+      if (merge != null) {
+        merge.endTime = System.currentTimeMillis();
+        notifyMergeEvent(new IndexEventListener.MergeEvent(IndexEventListener.MergeEvent.Type.ABORT, merge));
+      }
+    }
   }
 
   private synchronized void setMergeDocStoreIsCompoundFile(MergePolicy.OneMerge merge) {
@@ -3079,7 +3144,10 @@
       }
 
       merge.checkAborted(directory);
-
+      merge.startTime = System.currentTimeMillis();
+      
+      notifyMergeEvent(new IndexEventListener.MergeEvent(IndexEventListener.MergeEvent.Type.START, merge));
+      
       // This is where all the work happens:
       mergedDocCount = merge.info.docCount = merger.merge();
 
@@ -3519,6 +3587,20 @@
     return payloadProcessorProvider;
   }
 
+  /** Expert: adds a {@link ReaderFinishedListener}.
+   *
+   * @lucene.experimental */
+  public void addIndexEventListener(IndexEventListener listener) {
+    indexEventListeners.add(listener);
+  }
+  
+  /** Expert: remove a previously added {@link IndexEventListener}.
+   *
+   * @lucene.experimental */
+  public void removeIndexEventListener(IndexEventListener listener) {
+    indexEventListeners.remove(listener);
+  }
+  
   // decides when flushes happen
   final class FlushControl {
 
