This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_10x by this push:
     new 33949044b2c SOLR-18415: CrossDC Common: improve serialization for 
large docs. (#4887)
33949044b2c is described below

commit 33949044b2c92bb137d02546ac0cbd030e8733a2
Author: Andrzej BiaƂecki <[email protected]>
AuthorDate: Wed Sep 9 17:29:08 2026 +0200

    SOLR-18415: CrossDC Common: improve serialization for large docs. (#4887)
    
    (cherry picked from commit ba8f866d05371bc54898ad93ec06cf2f998d05ac)
---
 changelog/unreleased/solr-18415.yml                |   8 ++
 .../common/MirroredSolrRequestSerializer.java      |  27 ++++-
 .../common/MirroredSolrRequestSerializerTest.java  | 115 +++++++++++++++++++++
 3 files changed, 147 insertions(+), 3 deletions(-)

diff --git a/changelog/unreleased/solr-18415.yml 
b/changelog/unreleased/solr-18415.yml
new file mode 100644
index 00000000000..3aba47c964a
--- /dev/null
+++ b/changelog/unreleased/solr-18415.yml
@@ -0,0 +1,8 @@
+title: CrossDC Common optimize serialization for large docs
+  
+type: changed
+authors:
+  - name: Andrzej Bialecki
+links:
+  - name: SOLR-18415
+    url: https://issues.apache.org/jira/browse/SOLR-18415
diff --git 
a/solr/modules/cross-dc/src/java/org/apache/solr/crossdc/common/MirroredSolrRequestSerializer.java
 
b/solr/modules/cross-dc/src/java/org/apache/solr/crossdc/common/MirroredSolrRequestSerializer.java
index 4bac11fea92..137d46f106e 100644
--- 
a/solr/modules/cross-dc/src/java/org/apache/solr/crossdc/common/MirroredSolrRequestSerializer.java
+++ 
b/solr/modules/cross-dc/src/java/org/apache/solr/crossdc/common/MirroredSolrRequestSerializer.java
@@ -16,6 +16,7 @@
  */
 package org.apache.solr.crossdc.common;
 
+import com.google.common.annotations.VisibleForTesting;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -202,7 +203,7 @@ public class MirroredSolrRequestSerializer
 
     try (JavaBinCodec codec = new JavaBinCodec(null)) {
 
-      ExposedByteArrayOutputStream baos = new ExposedByteArrayOutputStream();
+      ExposedByteArrayOutputStream baos = newExposedByteArrayOutputStream();
       Map<String, Object> map = CollectionUtil.newHashMap(8);
       map.put("attempt", request.getAttempt());
       map.put("submitTimeNanos", request.getSubmitTimeNanos());
@@ -219,7 +220,9 @@ public class MirroredSolrRequestSerializer
           map.put("deletes", deletes.keySet());
           map.put("deletesParams", deletes.values());
         }
-        map.put("deleteQuery", update.getDeleteQuery());
+        if (update.getDeleteQuery() != null && 
!update.getDeleteQuery().isEmpty()) {
+          map.put("deleteQuery", update.getDeleteQuery());
+        }
       } else if (solrRequest instanceof 
MirroredSolrRequest.MirroredConfigSetRequest config) {
         map.put("method", config.getMethod().toString());
         if (config.getContentStreams() != null) {
@@ -248,6 +251,11 @@ public class MirroredSolrRequestSerializer
     }
   }
 
+  @VisibleForTesting
+  ExposedByteArrayOutputStream newExposedByteArrayOutputStream() {
+    return new ExposedByteArrayOutputStream();
+  }
+
   /**
    * Close this serializer.
    *
@@ -258,12 +266,25 @@ public class MirroredSolrRequestSerializer
     Serializer.super.close();
   }
 
-  private static final class ExposedByteArrayOutputStream extends 
ByteArrayOutputStream {
+  @VisibleForTesting
+  static final class ExposedByteArrayOutputStream extends 
ByteArrayOutputStream {
     ExposedByteArrayOutputStream() {
       super();
     }
 
     byte[] byteArray() {
+      // padding exceeds 1/10th of the buffer size
+      // it's worth it to do an extra array copy to avoid sending
+      // the padding bytes over the wire
+      if (buf.length - count > buf.length / 10) {
+        return toByteArray();
+      } else {
+        return buf;
+      }
+    }
+
+    @VisibleForTesting
+    byte[] getBuffer() {
       return buf;
     }
   }
diff --git 
a/solr/modules/cross-dc/src/test/org/apache/solr/crossdc/common/MirroredSolrRequestSerializerTest.java
 
b/solr/modules/cross-dc/src/test/org/apache/solr/crossdc/common/MirroredSolrRequestSerializerTest.java
index 0b211b8bc32..598ef314846 100644
--- 
a/solr/modules/cross-dc/src/test/org/apache/solr/crossdc/common/MirroredSolrRequestSerializerTest.java
+++ 
b/solr/modules/cross-dc/src/test/org/apache/solr/crossdc/common/MirroredSolrRequestSerializerTest.java
@@ -16,8 +16,11 @@
  */
 package org.apache.solr.crossdc.common;
 
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import org.apache.lucene.tests.util.TestUtil;
 import org.apache.solr.SolrTestCase;
@@ -26,12 +29,19 @@ import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
+import org.junit.BeforeClass;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 public class MirroredSolrRequestSerializerTest extends SolrTestCase {
 
   private static final byte[] EMPTY_ARR = new byte[3];
 
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
   @Test
   public void testSerializationBufferOptimization() {
     MirroredSolrRequestSerializer serializer = new 
MirroredSolrRequestSerializer();
@@ -166,4 +176,109 @@ public class MirroredSolrRequestSerializerTest extends 
SolrTestCase {
 
     assertEquals(List.of("field:value"), deserializedReq.getDeleteQuery());
   }
+
+  /**
+   * Confirms the waste pattern from {@link 
MirroredSolrRequestSerializer#serialize(String,
+   * MirroredSolrRequest)} testing both small and large documents around the 
8192 bytes boundary.
+   */
+  @Test
+  public void testSerializationWasteForRealDocumentSizes() {
+    MirroredSolrRequest<?> emptyRequest = new MirroredSolrRequest<>(new 
UpdateRequest());
+    Sizes emptySizes = serializeAndMeasure(emptyRequest);
+    assertTrue(
+        "expected empty-request framing to exceed the 32-byte floor, was "
+            + emptySizes.actualSize(),
+        emptySizes.actualSize() > 32);
+    assertTrue(
+        "expected empty-request framing to land in the 32-64 zone, was " + 
emptySizes.actualSize(),
+        emptySizes.actualSize() <= 64);
+    logWaste(emptySizes);
+
+    // 64B-8K zone: single doc, single flush -> exact-fit growth, zero padding 
waste
+    for (int fieldLen = 50; fieldLen < 8000; fieldLen += 
random().nextInt(1000) + 100) {
+      Sizes sizes = serializeAndMeasure(docWithField(fieldLen));
+      assertTrue("expected to stay under 8k for fieldLen=" + fieldLen, 
sizes.actualSize() < 8192);
+      assertEquals(
+          "expected zero padding waste in the 64B-8K zone, actualSize=" + 
sizes.actualSize(),
+          sizes.actualSize(),
+          sizes.bufferSize());
+      logWaste(sizes);
+    }
+
+    // 8k+ zone: a single large doc forces FastOutputStream to flush in 
8192-byte chunks,
+    // re-triggering the buffer growth and real, measurable waste. This waste 
is clamped down at 10%
+    for (int fieldLen = 8192; fieldLen < 100000; fieldLen += 
random().nextInt(8000) + 1000) {
+      Sizes sizes = serializeAndMeasure(docWithField(fieldLen));
+      assertTrue("expected to exceed 8k for fieldLen=" + fieldLen, 
sizes.actualSize() > 8192);
+      double wasteRatio = sizes.waste() / (double) sizes.bufferSize();
+      logWaste(sizes);
+      if (sizes.waste() == 0) {
+        // expect no waste but only due to clamped down padding
+        assertTrue(
+            "allocated buffer size "
+                + sizes.bufferSize()
+                + " should be larger than the returned buffer size "
+                + sizes.returnedSize(),
+            sizes.bufferSize() > sizes.returnedSize());
+      }
+      assertTrue(
+          "waste should never exceed ~10% of the transmitted bufferSize, 
ratio=" + wasteRatio,
+          wasteRatio < 0.11);
+    }
+  }
+
+  private static void logWaste(Sizes sizes) {
+    System.err.println(
+        "actualSize="
+            + sizes.actualSize()
+            + "\treturnedSize="
+            + sizes.returnedSize()
+            + "\t(allocatedBufferSize="
+            + sizes.bufferSize()
+            + ")"
+            + "\twaste="
+            + sizes.waste()
+            + "\t("
+            + String.format(Locale.ROOT, "%.1f%%", 100.0 * sizes.waste() / 
sizes.bufferSize())
+            + " of transmitted bytes)");
+  }
+
+  private static MirroredSolrRequest<?> docWithField(int fieldLen) {
+    UpdateRequest req = new UpdateRequest();
+    SolrInputDocument doc = new SolrInputDocument();
+    doc.setField("id", "1");
+    doc.setField("body", "a".repeat(fieldLen));
+    req.add(doc);
+    return new MirroredSolrRequest<>(req);
+  }
+
+  private record Sizes(int actualSize, int returnedSize, int bufferSize) {
+    int waste() {
+      return returnedSize - actualSize;
+    }
+  }
+
+  /**
+   * Serializes {@code request} and measures both the true, unpadded size and 
the padded transmitted
+   * bufferSize.
+   */
+  private static Sizes serializeAndMeasure(MirroredSolrRequest<?> request) {
+    MirroredSolrRequestSerializer serializer = Mockito.spy(new 
MirroredSolrRequestSerializer());
+    // capture the actual instance of the ExposedByteArrayOutputStream, so we 
can measure
+    // both the raw buffer size and the size() of just the serialized data in 
it
+    MirroredSolrRequestSerializer.ExposedByteArrayOutputStream[] captured =
+        new MirroredSolrRequestSerializer.ExposedByteArrayOutputStream[1];
+    Mockito.doAnswer(
+            invocation -> {
+              captured[0] =
+                  (MirroredSolrRequestSerializer.ExposedByteArrayOutputStream)
+                      invocation.callRealMethod();
+              return captured[0];
+            })
+        .when(serializer)
+        .newExposedByteArrayOutputStream();
+
+    byte[] data = serializer.serialize("test", request);
+    return new Sizes(captured[0].size(), data.length, 
captured[0].getBuffer().length);
+  }
 }

Reply via email to