You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Send a document whose routing value (document ID) is any string, for example "abc".
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.
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.
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):
OpenSearch's implementation:
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.calculateShardusesMath.floorMod(hash, numberOfShards), while OpenSearch'sOperationRouting.generateShardId()usesMath.floorMod(hash, routingNumShards) / routingFactor. These are equivalent only for a fresh index whereroutingNumShards == numberOfShards. They diverge after_splitor whenindex.number_of_routing_shardsis set, again producing incorrect partition assignment.To Reproduce
Steps to reproduce the behavior:
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 useMath.floorMod(hash, routingNumShards) / routingFactor(sourcingnumber_of_routing_shardsfrom 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'sMurmur3HashFunction / OperationRoutingfor 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.