Skip to content

[BUG] Pull-ingestion IndexRouter murmur3 hash does not match OpenSearch shard routing #6993

Description

@srikanthpadakanti

Describe the bug
The IndexRouter used by the pull-based ingestion path (RFC #6796, phase 1) computes the Murmur3 hash over the UTF-8 bytes of the routing value, but OpenSearch Core's Murmur3HashFunction.hash(String) computes it over the UTF-16 little-endian bytes (low byte then high byte per char). Because the two byte sequences differ, the hashes differ, so Data Prepper can route a document to a different partition than the shard OpenSearch would place it on. This breaks the 1:1 shard-to-partition mapping that pull-based ingestion depends on, and documents land on the wrong shard.

The Javadoc on the method even asserts parity with OpenSearch, which makes the mismatch easy to miss (data-prepper-plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/pull_ingestion/IndexRouter.java:61-68):

/**
   * Murmur3 hash matching OpenSearch's Murmur3HashFunction.hash(String).
   * 32-bit Murmur3 with seed 0, operating on the UTF-8 bytes of the input.
   */
  static int murmur3Hash(final String routing) {
      final byte[] bytes = routing.getBytes(StandardCharsets.UTF_8);
      return murmur3_32(bytes, 0, bytes.length, 0);
  }

OpenSearch's implementation:

byte[] bytesToHash = new byte[routing.length() * 2];
for (int i = 0; i < routing.length(); i++) {
    char c = routing.charAt(i);
    bytesToHash[i * 2]     = (byte) c;
    bytesToHash[i * 2 + 1] = (byte) (c >>> 8);
}
return StringHelper.murmurhash3_x86_32(bytesToHash, 0, bytesToHash.length, 0);

For "abc" the byte arrays are [0x61, 0x62, 0x63] (Data Prepper) vs [0x61, 0x00, 0x62, 0x00, 0x63, 0x00] (OpenSearch), so the Murmur3 outputs, and therefore the target partitions, differ.

There is also a secondary mismatch in the shard calculation formula. IndexRouter.calculateShard uses Math.floorMod(hash, numberOfShards), while OpenSearch's OperationRouting.generateShardId() uses Math.floorMod(hash, routingNumShards) / routingFactor. These are equivalent only for a fresh index where routingNumShards == numberOfShards. They diverge after _split or when index.number_of_routing_shards is set, again producing incorrect partition assignment.

To Reproduce
Steps to reproduce the behavior:

  1. Configure the opensearch sink with pull_indexing enabled against an index (RFC [RFC] Pull-Based Ingestion Support in the OpenSearch Sink #6796 phase 1).
  2. Send a document whose routing value (document ID) is any string, for example "abc".
  3. Compute the target partition with IndexRouter and, separately, with OpenSearch's Murmur3HashFunction.hash(String) plus OperationRouting.generateShardId() for the same routing value and shard count.
  4. Observe that the two partition numbers differ, so the document is produced to a partition that does not correspond to the shard OpenSearch routes it to.

Expected behavior
Partition assignment on the producer side must match OpenSearch's internal document routing. The Murmur3 hash should operate on UTF-16 little-endian bytes to match Murmur3HashFunction.hash(String), and the shard calculation should use Math.floorMod(hash, routingNumShards) / routingFactor (sourcing number_of_routing_shards from the index settings) so that a document always lands on the partition mapped to its correct shard.

Additional context
Introduced with the pull-ingestion phase 1 work under RFC #6796 in data-prepper plugins/opensearch/src/main/java/org/opensearch/dataprepper/plugins/sink/opensearch/pull_ingestion/IndexRouter.java. A cross-check unit test that asserts equality against OpenSearch's Murmur3HashFunction / OperationRouting for a spread of routing values and shard/routing-shard configurations would prevent regressions, and this strengthens the case for extracting a shared routing "library" so the producer and OpenSearch Core cannot drift.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Unplanned

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions