You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2016/03/12 01:27:10 UTC

[29/50] [abbrv] lucene-solr git commit: SOLR-445: refactored KnownErr -> ToleratedUpdateError in solr-common

SOLR-445: refactored KnownErr -> ToleratedUpdateError in solr-common

Also added some nehnacments to that class, and replaced the hack in CloudSolrClient with usage of this class


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/c37d5a86
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/c37d5a86
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/c37d5a86

Branch: refs/heads/jira/SOLR-445
Commit: c37d5a8618c1af41a5cda45463ab1288ea73b99b
Parents: 5b405b6
Author: Chris Hostetter <ho...@apache.org>
Authored: Thu Mar 10 11:22:08 2016 -0700
Committer: Chris Hostetter <ho...@apache.org>
Committed: Thu Mar 10 11:22:08 2016 -0700

----------------------------------------------------------------------
 .../processor/TolerantUpdateProcessor.java      | 124 +++------------
 .../cloud/TestTolerantUpdateProcessorCloud.java |  25 ++--
 .../processor/TolerantUpdateProcessorTest.java  |  37 +----
 .../solr/client/solrj/impl/CloudSolrClient.java |   8 +-
 .../solr/common/ToleratedUpdateError.java       | 150 +++++++++++++++++++
 .../solr/common/TestToleratedUpdateError.java   |  89 +++++++++++
 6 files changed, 281 insertions(+), 152 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c37d5a86/solr/core/src/java/org/apache/solr/update/processor/TolerantUpdateProcessor.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/update/processor/TolerantUpdateProcessor.java b/solr/core/src/java/org/apache/solr/update/processor/TolerantUpdateProcessor.java
index dc07082..30e5f80 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/TolerantUpdateProcessor.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/TolerantUpdateProcessor.java
@@ -29,6 +29,8 @@ import org.apache.lucene.util.CharsRefBuilder;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.ToleratedUpdateError;
+import org.apache.solr.common.ToleratedUpdateError.CmdType;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.params.ShardParams;
@@ -53,7 +55,7 @@ import org.slf4j.LoggerFactory;
  * will receive a 200 response, but gets a list of errors (keyed by
  * unique key) unless <code>maxErrors</code> is reached. 
  * If <code>maxErrors</code> occur, the first exception caught will be re-thrown, 
- * Solr will respond with 5XX or 4XX (depending on the exception) and
+ * Solr will respond with 5XX or 4XX (depending on the underlying exceptions) and
  * it won't finish processing the batch. This means that the last docs
  * in the batch may not be added in this case even if they are valid. 
  * </p>
@@ -94,7 +96,7 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
    * Known errors that occurred in this batch, in order encountered (may not be the same as the 
    * order the commands were originally executed in due to the async distributed updates).
    */
-  private final List<KnownErr> knownErrors = new ArrayList<KnownErr>();
+  private final List<ToleratedUpdateError> knownErrors = new ArrayList<ToleratedUpdateError>();
 
   // Kludge: Because deleteByQuery updates are forwarded to every leader, we can get identical
   // errors reported by every leader for the same underlying problem.
@@ -109,13 +111,13 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
   //   dbq: foo:bar
   //
   // ...but i can't figure out a way to accurately identify & return duplicate 
-  // KnownErrs from duplicate identical underlying requests w/o erroneously returning identical 
-  // KnownErrs for the *same* underlying request but from diff shards.
+  // ToleratedUpdateErrors from duplicate identical underlying requests w/o erroneously returning identical 
+  // ToleratedUpdateErrors for the *same* underlying request but from diff shards.
   //
   // So as a kludge, we keep track of them for deduping against identical remote failures
   //
   // :nocommit: probably need to use this for "commit" as well?
-  private Set<KnownErr> knownDBQErrors = new HashSet<>();
+  private Set<ToleratedUpdateError> knownDBQErrors = new HashSet<>();
         
   private final FirstErrTracker firstErrTracker = new FirstErrTracker();
   private final DistribPhase distribPhase;
@@ -153,9 +155,10 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
       if (isLeader || distribPhase.equals(DistribPhase.NONE)) {
         // nocommit: should we skip if condition and always do this? see comment in else...
         
-        knownErrors.add(new KnownErr(CmdType.ADD,
-                                     getPrintableId(id, cmd.getReq().getSchema().getUniqueKeyField()),
-                                     t.getMessage()));
+        knownErrors.add(new ToleratedUpdateError
+                        (CmdType.ADD,
+                         getPrintableId(id, cmd.getReq().getSchema().getUniqueKeyField()),
+                         t.getMessage()));
         if (knownErrors.size() > maxErrors) {
           firstErrTracker.throwFirst();
         }
@@ -185,9 +188,9 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
 
       // nocommit: do we need isLeader type logic like processAdd ? does processAdd even need it?
       
-      KnownErr err = new KnownErr(cmd.isDeleteById() ? CmdType.DELID : CmdType.DELQ,
-                                  cmd.isDeleteById() ? cmd.id : cmd.query,
-                                  t.getMessage());
+      ToleratedUpdateError err = new ToleratedUpdateError(cmd.isDeleteById() ? CmdType.DELID : CmdType.DELQ,
+                                                          cmd.isDeleteById() ? cmd.id : cmd.query,
+                                                          t.getMessage());
       knownErrors.add(err);
 
       // NOTE: we're not using this to dedup before adding to knownErrors.
@@ -241,8 +244,9 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
         }
 
         for (int i = 0; i < remoteErrMetadata.size(); i++) {
-          KnownErr err = KnownErr.parseMetadataIfKnownErr(remoteErrMetadata.getName(i),
-                                                          remoteErrMetadata.getVal(i));
+          ToleratedUpdateError err =
+            ToleratedUpdateError.parseMetadataIfToleratedUpdateError(remoteErrMetadata.getName(i),
+                                                                     remoteErrMetadata.getVal(i));
           if (null == err) {
             // some metadata unrelated to this update processor
             continue;
@@ -262,7 +266,7 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
       }
     }
 
-    header.add("errors", KnownErr.formatForResponseHeader(knownErrors));
+    header.add("errors", ToleratedUpdateError.formatForResponseHeader(knownErrors));
     // include in response so client knows what effective value was (may have been server side config)
     header.add("maxErrors", maxErrors);
 
@@ -364,7 +368,7 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
      * Annotates the first exception (which may already have been thrown, or be thrown in the future) with 
      * the metadata from this update processor.  For use in {@link TolerantUpdateProcessor#finish}
      */
-    public void annotate(List<KnownErr> errors) {
+    public void annotate(List<ToleratedUpdateError> errors) {
 
       if (null == first) {
         return; // no exception to annotate
@@ -378,8 +382,8 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
         first.setMetadata(firstErrMetadata);
       }
 
-      for (KnownErr ke : errors) {
-        firstErrMetadata.add(ke.getMetadataKey(), ke.getMetadataValue());
+      for (ToleratedUpdateError te : errors) {
+        firstErrMetadata.add(te.getMetadataKey(), te.getMetadataValue());
       }
     }
     
@@ -388,91 +392,5 @@ public class TolerantUpdateProcessor extends UpdateRequestProcessor {
     public SolrException getFirst() {
       return first;
     }
-    
-  }
-
-  /**
-   * Helper class for dealing with SolrException metadata (String) keys 
-   */
-  public static final class KnownErr {
-    
-    private final static String META_PRE =  TolerantUpdateProcessor.class.getName() + "--";
-    private final static int META_PRE_LEN = META_PRE.length();
-
-    /** returns a map of simple objects suitable for putting in a SolrQueryResponse */
-    public static List<SimpleOrderedMap<String>> formatForResponseHeader(List<KnownErr> errs) {
-      List<SimpleOrderedMap<String>> result = new ArrayList<>(errs.size());
-      for (KnownErr e : errs) {
-        SimpleOrderedMap<String> entry = new SimpleOrderedMap<String>();
-        entry.add("type", e.type.toString());
-        entry.add("id", e.id);
-        entry.add("message", e.errorValue);
-        result.add(entry);
-      }
-      return result;
-    }
-    
-    /** returns a KnownErr instance if this metadataKey is one we care about, else null */
-    public static KnownErr parseMetadataIfKnownErr(String metadataKey, String metadataVal) {
-      if (! metadataKey.startsWith(META_PRE)) {
-        return null; // not a key we care about
-      }
-      final int typeEnd = metadataKey.indexOf(':', META_PRE_LEN);
-      assert 0 < typeEnd; // nocommit: better error handling
-      return new KnownErr(CmdType.valueOf(metadataKey.substring(META_PRE_LEN, typeEnd)),
-                          metadataKey.substring(typeEnd+1), metadataVal);
-    }
-
-    public final CmdType type;
-    /** may be null depending on type */
-    public final String id;
-    public final String errorValue; // nocommit: refactor: rename errMessage?
-    
-    public KnownErr(CmdType type, String id, String errorValue) {
-      this.type = type;
-      assert null != type;
-      
-      assert null != id;
-      this.id = id;
-      
-      assert null != errorValue;
-      this.errorValue = errorValue;
-    }
-    
-    public String getMetadataKey() {
-      return META_PRE + type + ":" + id;
-    }
-    public String getMetadataValue() {
-      return errorValue.toString();
-    }
-    public String toString() {
-      return getMetadataKey() + "=>" + getMetadataValue();
-    }
-    public int hashCode() {
-      int h = this.getClass().hashCode();
-      h = h * 31 + type.hashCode();
-      h = h * 31 + id.hashCode();
-      h = h * 31 + errorValue.hashCode();
-      return h;
-    }
-    public boolean equals(Object o) {
-      if (o instanceof KnownErr) {
-        KnownErr that = (KnownErr)o;
-        return that.type.equals(this.type)
-          && that.id.equals(this.id)
-          && that.errorValue.equals(this.errorValue);
-      }
-      return false;
-    }
-  }
-  
-  /**
-   * Helper class for dealing with SolrException metadata (String) keys 
-   */
-  public static enum CmdType {
-    ADD, DELID, DELQ; // nocommit: others supported types? (commit?) ..
-
-    // if we add support for things like commit, parsing/toString/hashCode logic
-    // needs to be smarter to account for 'id' being null ... "usesId" should be a prop of enum instances
   }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c37d5a86/solr/core/src/test/org/apache/solr/cloud/TestTolerantUpdateProcessorCloud.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestTolerantUpdateProcessorCloud.java b/solr/core/src/test/org/apache/solr/cloud/TestTolerantUpdateProcessorCloud.java
index bee23a7..93200d8 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestTolerantUpdateProcessorCloud.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestTolerantUpdateProcessorCloud.java
@@ -39,6 +39,8 @@ import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.SolrInputField;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.ToleratedUpdateError;
+import org.apache.solr.common.ToleratedUpdateError.CmdType;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
@@ -49,9 +51,6 @@ import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.util.RevertDefaultThreadHandlerRule;
 
-import org.apache.solr.update.processor.TolerantUpdateProcessor.KnownErr;
-import org.apache.solr.update.processor.TolerantUpdateProcessor.CmdType;
-
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -536,11 +535,13 @@ public class TestTolerantUpdateProcessorCloud extends SolrCloudTestCase {
       // verify that the Exceptions metadata can tell us what failed.
       NamedList<String> remoteErrMetadata = e.getMetadata();
       assertNotNull("no metadata in: " + e.toString(), remoteErrMetadata);
-      Set<KnownErr> actualKnownErrs = new LinkedHashSet<KnownErr>(remoteErrMetadata.size());
+      Set<ToleratedUpdateError> actualKnownErrs
+        = new LinkedHashSet<ToleratedUpdateError>(remoteErrMetadata.size());
       int actualKnownErrsCount = 0;
       for (int i = 0; i < remoteErrMetadata.size(); i++) {
-        KnownErr err = KnownErr.parseMetadataIfKnownErr(remoteErrMetadata.getName(i),
-                                                        remoteErrMetadata.getVal(i));
+        ToleratedUpdateError err =
+          ToleratedUpdateError.parseMetadataIfToleratedUpdateError(remoteErrMetadata.getName(i),
+                                                                   remoteErrMetadata.getVal(i));
         if (null == err) {
           // some metadata unrelated to this update processor
           continue;
@@ -552,7 +553,7 @@ public class TestTolerantUpdateProcessorCloud extends SolrCloudTestCase {
                    11, actualKnownErrsCount);
       assertEquals("at least one dup error in metadata: " + remoteErrMetadata.toString(),
                    actualKnownErrsCount, actualKnownErrs.size());
-      for (KnownErr err : actualKnownErrs) {
+      for (ToleratedUpdateError err : actualKnownErrs) {
         assertEquals("only expected type of error is ADD: " + err,
                      CmdType.ADD, err.type);
         assertTrue("failed err msg didn't match expected value: " + err,
@@ -609,11 +610,13 @@ public class TestTolerantUpdateProcessorCloud extends SolrCloudTestCase {
       // verify that the Exceptions metadata can tell us what failed.
       NamedList<String> remoteErrMetadata = e.getMetadata();
       assertNotNull("no metadata in: " + e.toString(), remoteErrMetadata);
-      Set<KnownErr> actualKnownErrs = new LinkedHashSet<KnownErr>(remoteErrMetadata.size());
+      Set<ToleratedUpdateError> actualKnownErrs
+        = new LinkedHashSet<ToleratedUpdateError>(remoteErrMetadata.size());
       int actualKnownErrsCount = 0;
       for (int i = 0; i < remoteErrMetadata.size(); i++) {
-        KnownErr err = KnownErr.parseMetadataIfKnownErr(remoteErrMetadata.getName(i),
-                                                        remoteErrMetadata.getVal(i));
+        ToleratedUpdateError err =
+          ToleratedUpdateError.parseMetadataIfToleratedUpdateError(remoteErrMetadata.getName(i),
+                                                                   remoteErrMetadata.getVal(i));
         if (null == err) {
           // some metadata unrelated to this update processor
           continue;
@@ -625,7 +628,7 @@ public class TestTolerantUpdateProcessorCloud extends SolrCloudTestCase {
                    11, actualKnownErrsCount);
       assertEquals("at least one dup error in metadata: " + remoteErrMetadata.toString(),
                    actualKnownErrsCount, actualKnownErrs.size());
-      for (KnownErr err : actualKnownErrs) {
+      for (ToleratedUpdateError err : actualKnownErrs) {
         assertEquals("only expected type of error is ADD: " + err,
                      CmdType.ADD, err.type);
         assertTrue("failed id had unexpected prefix: " + err,

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c37d5a86/solr/core/src/test/org/apache/solr/update/processor/TolerantUpdateProcessorTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/update/processor/TolerantUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TolerantUpdateProcessorTest.java
index 61dac44..7470ea0 100644
--- a/solr/core/src/test/org/apache/solr/update/processor/TolerantUpdateProcessorTest.java
+++ b/solr/core/src/test/org/apache/solr/update/processor/TolerantUpdateProcessorTest.java
@@ -30,6 +30,8 @@ import javax.xml.xpath.XPathExpressionException;
 import org.apache.solr.client.solrj.util.ClientUtils;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.ToleratedUpdateError;
+import org.apache.solr.common.ToleratedUpdateError.CmdType;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.SimpleOrderedMap;
@@ -40,8 +42,6 @@ import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.servlet.DirectSolrConnection;
 import org.apache.solr.update.AddUpdateCommand;
-import org.apache.solr.update.processor.TolerantUpdateProcessor.KnownErr;
-import org.apache.solr.update.processor.TolerantUpdateProcessor.CmdType;
 import org.apache.solr.util.BaseTestHarness;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -333,39 +333,6 @@ public class TolerantUpdateProcessorTest extends UpdateProcessorTestBase {
     
   }
 
-  public void testKnownErrClass() {
-
-    assertNull(KnownErr.parseMetadataIfKnownErr("some other key", "some value"));
-
-    for (KnownErr in : new KnownErr[] {
-        new KnownErr(CmdType.ADD, "doc1", "some error"),
-        new KnownErr(CmdType.DELID, "doc1", "some diff error"),
-        new KnownErr(CmdType.DELQ, "-field:yakko other_field:wakko", "some other error"),
-      }) {
-      KnownErr out = KnownErr.parseMetadataIfKnownErr(in.getMetadataKey(), in.getMetadataValue());
-      assertNotNull(out);
-      assertEquals(out.type, in.type);
-      assertEquals(out.id, in.id);
-      assertEquals(out.errorValue, in.errorValue);
-      assertEquals(out.hashCode(), in.hashCode());
-      assertEquals(out.toString(), in.toString());
-      
-      assertEquals(out, in);
-      assertEquals(in, out);
-
-    }
-    
-    assertFalse((new KnownErr(CmdType.ADD, "doc1", "some error")).equals
-                (new KnownErr(CmdType.ADD, "doc2", "some error")));
-    assertFalse((new KnownErr(CmdType.ADD, "doc1", "some error")).equals
-                (new KnownErr(CmdType.ADD, "doc1", "some errorxx")));
-    assertFalse((new KnownErr(CmdType.ADD, "doc1", "some error")).equals
-                (new KnownErr(CmdType.DELID, "doc1", "some error")));
-    
-
-    // nocommit: add randomized testing, particularly with non-trivial 'id' values
-    
-  }
 
   
   public String update(String chain, String xml) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c37d5a86/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
index 655844f..febb56f 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudSolrClient.java
@@ -54,6 +54,7 @@ import org.apache.solr.client.solrj.request.UpdateRequest;
 import org.apache.solr.client.solrj.util.ClientUtils;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.ToleratedUpdateError;
 import org.apache.solr.common.cloud.Aliases;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
@@ -776,13 +777,14 @@ public class CloudSolrClient extends SolrClient {
     if (null != toleratedErrors) {
       cheader.add("errors", toleratedErrors);
       if (maxToleratedErrors < toleratedErrors.size()) {
+        // cumulative errors are too high, we need to throw a client exception w/correct metadata
+        
         NamedList metadata = new NamedList<String>();
         SolrException toThrow = new SolrException(ErrorCode.BAD_REQUEST, "nocommit: need better msg");
         toThrow.setMetadata(metadata);
         for (SimpleOrderedMap<String> err : toleratedErrors) {
-          // nocommit: hack, refactor KnownErr into solr-common and re-use here...
-          metadata.add("org.apache.solr.update.processor.TolerantUpdateProcessor--" +
-                       err.get("type") + ":" + err.get("id"), err.get("message"));
+          ToleratedUpdateError te = ToleratedUpdateError.parseMap(err);
+          metadata.add(te.getMetadataKey(), te.getMetadataValue());
         }
         throw toThrow;
       }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c37d5a86/solr/solrj/src/java/org/apache/solr/common/ToleratedUpdateError.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/common/ToleratedUpdateError.java b/solr/solrj/src/java/org/apache/solr/common/ToleratedUpdateError.java
new file mode 100644
index 0000000..7261a21
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/common/ToleratedUpdateError.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+package org.apache.solr.common;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+
+/**
+ * nocommit: more javadocs, mention (but obviously no link) to TolerantUpdateProcessor
+ */
+public final class ToleratedUpdateError {
+    
+  private final static String META_PRE =  ToleratedUpdateError.class.getName() + "--";
+  private final static int META_PRE_LEN = META_PRE.length();
+  
+  /** 
+   * returns a list of maps of simple objects suitable for putting in a SolrQueryResponse header 
+   * @see #getSimpleMap
+   * @see #parseMap
+   */
+  public static List<SimpleOrderedMap<String>> formatForResponseHeader(List<ToleratedUpdateError> errs) {
+    List<SimpleOrderedMap<String>> result = new ArrayList<>(errs.size());
+    for (ToleratedUpdateError e : errs) {
+      result.add(e.getSimpleMap());
+    }
+    return result;
+  }
+  
+  /** 
+   * returns a ToleratedUpdateError instance from the data in this Map 
+   * @see #getSimpleMap
+   */
+  public static ToleratedUpdateError parseMap(SimpleOrderedMap<String> data) {
+    // nocommit: error handling and clean exception reporting if data is bogus
+    return new ToleratedUpdateError(CmdType.valueOf(data.get("type")), data.get("id"), data.get("message"));
+  }
+  
+  /** 
+   * returns a ToleratedUpdateError instance if this metadataKey is one we care about, else null 
+   * @see #getMetadataKey
+   * @see #getMetadataValue
+   */
+  public static ToleratedUpdateError parseMetadataIfToleratedUpdateError(String metadataKey,
+                                                                         String metadataVal) {
+    if (! metadataKey.startsWith(META_PRE)) {
+      return null; // not a key we care about
+    }
+    final int typeEnd = metadataKey.indexOf(':', META_PRE_LEN);
+    assert 0 < typeEnd; // nocommit: better error handling
+    return new ToleratedUpdateError(CmdType.valueOf(metadataKey.substring(META_PRE_LEN, typeEnd)),
+                                    metadataKey.substring(typeEnd+1), metadataVal);
+  }
+
+  // nocommit: make these private & provide getter methods
+  public final CmdType type;
+  public final String id; // may be null depending on type
+  public final String errorValue; // nocommit: refactor: rename message?
+  
+  public ToleratedUpdateError(CmdType type, String id, String errorValue) {
+    this.type = type;
+    assert null != type;
+    
+    assert null != id;
+    this.id = id;
+    
+    assert null != errorValue;
+    this.errorValue = errorValue;
+  }
+
+  /**
+   * returns a string suitable for use as a key in {@link SolrException#setMetadata}
+   *
+   * @see #parseMetadataIfToleratedUpdateError
+   */
+  public String getMetadataKey() {
+    return META_PRE + type + ":" + id;
+  }
+  
+  /**
+   * returns a string suitable for use as a value in {@link SolrException#setMetadata}
+   *
+   * @see #parseMetadataIfToleratedUpdateError
+   */
+  public String getMetadataValue() {
+    return errorValue.toString();
+  }
+  
+  /** 
+   * returns a map of simple objects suitable for putting in a SolrQueryResponse header 
+   * @see #formatForResponseHeader
+   * @see #parseMap
+   */
+  public SimpleOrderedMap<String> getSimpleMap() {
+    SimpleOrderedMap<String> entry = new SimpleOrderedMap<String>();
+    entry.add("type", type.toString());
+    entry.add("id", id);
+    entry.add("message", errorValue);
+    return entry;
+  }
+  
+  public String toString() {
+    return getMetadataKey() + "=>" + getMetadataValue();
+  }
+  
+  public int hashCode() {
+    int h = this.getClass().hashCode();
+    h = h * 31 + type.hashCode();
+    h = h * 31 + id.hashCode();
+    h = h * 31 + errorValue.hashCode();
+    return h;
+  }
+  
+  public boolean equals(Object o) {
+    if (o instanceof ToleratedUpdateError) {
+      ToleratedUpdateError that = (ToleratedUpdateError)o;
+      return that.type.equals(this.type)
+        && that.id.equals(this.id)
+        && that.errorValue.equals(this.errorValue);
+    }
+    return false;
+  }
+  
+  /**
+   * Helper class for dealing with SolrException metadata (String) keys 
+   */
+  public static enum CmdType {
+    ADD, DELID, DELQ; 
+
+    // if we add support for things like commit, parsing/toString/hashCode logic
+    // needs to be smarter to account for 'id' being null ... "usesId" should be a prop of enum instances
+  }
+}
+
+  

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/c37d5a86/solr/solrj/src/test/org/apache/solr/common/TestToleratedUpdateError.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/common/TestToleratedUpdateError.java b/solr/solrj/src/test/org/apache/solr/common/TestToleratedUpdateError.java
new file mode 100644
index 0000000..6759b79
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/common/TestToleratedUpdateError.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.solr.common;
+
+import org.apache.solr.common.ToleratedUpdateError;
+import org.apache.solr.common.ToleratedUpdateError.CmdType;
+
+import org.apache.lucene.util.LuceneTestCase;
+
+/** Basic testing of the serialization/encapsulation code in ToleratedUpdateError */
+public class TestToleratedUpdateError extends LuceneTestCase {
+  
+  // nocommit: add randomized testing, particularly with non-trivial 'id' values
+
+  public void checkRoundTripComparisons(Coppier coppier) {
+
+    assertNull(ToleratedUpdateError.parseMetadataIfToleratedUpdateError("some other key", "some value"));
+    
+    for (ToleratedUpdateError in : new ToleratedUpdateError[] {
+        new ToleratedUpdateError(CmdType.ADD, "doc1", "some error"),
+        new ToleratedUpdateError(CmdType.DELID, "doc1", "some diff error"),
+        new ToleratedUpdateError(CmdType.DELQ, "-field:yakko other_field:wakko", "some other error"),
+      }) {
+      
+      ToleratedUpdateError out = coppier.copy(in);
+      
+      assertNotNull(out);
+      assertEquals(out.type, in.type);
+      assertEquals(out.id, in.id);
+      assertEquals(out.errorValue, in.errorValue);
+      assertEquals(out.hashCode(), in.hashCode());
+      assertEquals(out.toString(), in.toString());
+
+      assertEquals(in.getMetadataKey(), out.getMetadataKey());
+      assertEquals(in.getMetadataValue(), out.getMetadataValue());
+      
+      assertEquals(out, in);
+      assertEquals(in, out);
+
+    }
+    
+    assertFalse((new ToleratedUpdateError(CmdType.ADD, "doc1", "some error")).equals
+                (new ToleratedUpdateError(CmdType.ADD, "doc2", "some error")));
+    assertFalse((new ToleratedUpdateError(CmdType.ADD, "doc1", "some error")).equals
+                (new ToleratedUpdateError(CmdType.ADD, "doc1", "some errorxx")));
+    assertFalse((new ToleratedUpdateError(CmdType.ADD, "doc1", "some error")).equals
+                (new ToleratedUpdateError(CmdType.DELID, "doc1", "some error")));
+    
+  }
+  
+  public void testMetadataRoundTripComparisons(Coppier coppier) {
+    checkRoundTripComparisons(new Coppier() {
+      public ToleratedUpdateError copy(ToleratedUpdateError in) {
+        return ToleratedUpdateError.parseMetadataIfToleratedUpdateError
+          (in.getMetadataKey(), in.getMetadataValue());
+      }
+    });
+  }
+  
+  public void testMapRoundTripComparisons() {
+    checkRoundTripComparisons(new Coppier() {
+      public ToleratedUpdateError copy(ToleratedUpdateError in) {
+        return ToleratedUpdateError.parseMap(in.getSimpleMap());
+      }
+    });
+  }
+
+  private static abstract class Coppier {
+    public abstract ToleratedUpdateError copy(ToleratedUpdateError in);
+  }
+}
+
+
+
+