SOLR-17176: Fix log history V2 API serialization (#2293)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index e485de5..a9c2054 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -174,6 +174,8 @@
* SOLR-17213: Fix spurious warnings about solr url format in Solr CLI when users aren't providing a deprecated solr url. (Eric Pugh)
+* SOLR-17176: Fix log history V2 API serialization (Michael Gibney)
+
Dependency Upgrades
---------------------
diff --git a/solr/core/src/java/org/apache/solr/logging/LogWatcher.java b/solr/core/src/java/org/apache/solr/logging/LogWatcher.java
index 86ca1b8..30f4f6e 100644
--- a/solr/core/src/java/org/apache/solr/logging/LogWatcher.java
+++ b/solr/core/src/java/org/apache/solr/logging/LogWatcher.java
@@ -21,10 +21,13 @@
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.CollectionUtil;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.logging.jul.JulWatcher;
import org.apache.solr.logging.log4j2.Log4j2Watcher;
@@ -75,7 +78,16 @@
* instance to multiple callers, we should guard against modification.
*/
private static SolrDocument unmodifiable(SolrDocument doc) {
- return new SolrDocument(Collections.unmodifiableMap(doc.getFieldValueMap()));
+ LinkedHashMap<String, Object> map = CollectionUtil.newLinkedHashMap(doc.size());
+ for (Map.Entry<String, Object> e : doc) {
+ Object v = e.getValue();
+ if (v instanceof Collection) {
+ map.put(e.getKey(), Collections.unmodifiableCollection((Collection<?>) v));
+ } else {
+ map.put(e.getKey(), v);
+ }
+ }
+ return new SolrDocument(Collections.unmodifiableMap(map));
}
public long getLastEvent() {
diff --git a/solr/core/src/test/org/apache/solr/logging/TestLogWatcher.java b/solr/core/src/test/org/apache/solr/logging/TestLogWatcher.java
index 11cdf8e..34cd05e 100644
--- a/solr/core/src/test/org/apache/solr/logging/TestLogWatcher.java
+++ b/solr/core/src/test/org/apache/solr/logging/TestLogWatcher.java
@@ -16,17 +16,31 @@
*/
package org.apache.solr.logging;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.MapWriter;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.TimeSource;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequestBase;
+import org.apache.solr.response.BinaryQueryResponseWriter;
+import org.apache.solr.response.JSONResponseWriter;
+import org.apache.solr.response.JacksonJsonWriter;
+import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.TimeOut;
import org.junit.Before;
import org.junit.Test;
+import org.noggit.CharArr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,7 +59,7 @@
// All we really care about is that new watchers get the new messages, so test for that
// explicitly. See SOLR-12732.
@Test
- public void testLog4jWatcher() throws InterruptedException {
+ public void testLog4jWatcher() throws InterruptedException, IOException {
LogWatcher<?> watcher = null;
int lim = random().nextInt(3) + 2;
// Every time through this loop, ensure that, of all the test messages that have been logged,
@@ -65,9 +79,12 @@
boolean foundNewMsg = false;
boolean foundOldMessage = false;
// In local testing this loop usually succeeds 1-2 tries, so it's not very expensive to loop.
+ QueryResponseWriter responseWriter =
+ random().nextBoolean() ? new JacksonJsonWriter() : new JSONResponseWriter();
do {
// Returns an empty (but non-null) list even if there are no messages yet.
SolrDocumentList events = watcher.getHistory(-1, null);
+ validateWrite(responseWriter, events, msg);
for (SolrDocument doc : events) {
String oneMsg = (String) doc.get("message");
if (oneMsg.equals(msg)) {
@@ -104,4 +121,56 @@
oldMessages.add(msg);
}
}
+
+ /**
+ * Here we validate that serialization works as expected for several different methods. Ideally we
+ * would use actual serialization from Jersey/Jackson, since this is what really happens in V2
+ * APIs. But this is simpler, and should give us roughly equivalent assurances.
+ */
+ private static void validateWrite(
+ QueryResponseWriter responseWriter, SolrDocumentList docs, String expectMsg)
+ throws IOException {
+ SolrQueryRequest req = new SolrQueryRequestBase(null, new ModifiableSolrParams()) {};
+ SolrQueryResponse rsp = new SolrQueryResponse();
+ rsp.addResponse(docs);
+ String output;
+ if (responseWriter instanceof BinaryQueryResponseWriter) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ((BinaryQueryResponseWriter) responseWriter).write(baos, req, rsp);
+ baos.close();
+ output = baos.toString(StandardCharsets.UTF_8);
+ } else {
+ StringWriter writer = new StringWriter();
+ responseWriter.write(writer, req, rsp);
+ writer.close();
+ output = writer.toString();
+ }
+ assertTrue("found: " + output, output.contains(expectMsg));
+ validateWrite(docs, expectMsg);
+ }
+
+ private static void validateWrite(SolrDocumentList docs, String expectMsg) throws IOException {
+ CharArr arr = new CharArr();
+ org.noggit.JSONWriter w = new org.noggit.JSONWriter(arr, 2);
+ docs.writeMap(
+ new MapWriter.EntryWriter() {
+ boolean first = true;
+
+ @Override
+ public MapWriter.EntryWriter put(CharSequence k, Object v) {
+ if (first) {
+ first = false;
+ } else {
+ w.writeValueSeparator();
+ }
+ w.indent();
+ w.writeString(k.toString());
+ w.writeNameSeparator();
+ w.write(v);
+ return this;
+ }
+ });
+ String output = arr.toString();
+ assertTrue("found: " + output, output.contains(expectMsg));
+ }
}