Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ To get the binaries of this library as distributed by Microsoft, ready for use w
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-documentdb</artifactId>
<version>1.5.0</version>
<version>1.5.1</version>
</dependency>

###Option 2: Source Via Git
Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Changes in 1.5.1 : ##

- Fixed a bug in HashPartitionResolver to generate hash values in little-endian order to be consistent with other SDKs.

## Changes in 1.5.0 : ##

- Added Client-side sharding framework to the SDK. Implemented HashPartionResolver and RangePartitionResolver classes.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-documentdb</artifactId>
<version>1.5.0</version>
<version>1.5.1</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>Java SDK for Microsoft Azure DocumentDB</description>
<url>http://azure.microsoft.com/en-us/services/documentdb/</url>
Expand Down
48 changes: 32 additions & 16 deletions src/com/microsoft/azure/documentdb/ConsistentHashRing.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,31 @@ of this software and associated documentation files (the "Software"), to deal
package com.microsoft.azure.documentdb;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
* The ConsistentHashRing class internally implements a consistent hash ring using the hash function specified
* The ConsistentHashRing class implements a consistent hash ring using the hash function specified
*/
final class ConsistentHashRing {
private HashGenerator hashGenerator;
private Partition[] partitions;
private ArrayList<String> collectionLinks = new ArrayList<String>();

/**
* ConsistentHashRing constructor taking in the collection links, total number of partitions
* ConsistentHashRing constructor taking in the collection links, number of partitions per node
* and hash generator to initialize the ring.
*
* @param collectionLinks the links of collections participating in partitioning.
* @param totalPartitions the total number of partitions.
* @param partitionsPerNode number of partitions per node.
* @param hashGenerator the hash generator to be used for hashing algorithm.
*/
public ConsistentHashRing(Iterable<String> collectionLinks, int totalPartitions, HashGenerator hashGenerator) {
public ConsistentHashRing(Iterable<String> collectionLinks, int partitionsPerNode, HashGenerator hashGenerator) {
if(collectionLinks == null) {
throw new IllegalArgumentException("collectionLinks");
}
Expand All @@ -52,16 +57,16 @@ public ConsistentHashRing(Iterable<String> collectionLinks, int totalPartitions,
this.collectionLinks.add(collectionLink);
}

if(totalPartitions < this.collectionLinks.size()) {
throw new IllegalArgumentException("The total number of partitions must be at least the number of collections.");
if(partitionsPerNode <= 0) {
throw new IllegalArgumentException("The partitions per node must greater than 0.");
}

if(hashGenerator == null) {
throw new IllegalArgumentException("hashGenerator");
}

this.hashGenerator = hashGenerator;
this.partitions = this.constructPartitions(this.collectionLinks, totalPartitions);
this.partitions = this.constructPartitions(this.collectionLinks, partitionsPerNode);
}

/**
Expand Down Expand Up @@ -112,23 +117,18 @@ private static byte[] getBytes(Object partitionKey) {
* using the hashing algorithm and then finally sorting the partitions based on the hash value.
*
*/
private Partition[] constructPartitions(ArrayList<String> collectionLinks, int totalPartitions) {
private Partition[] constructPartitions(ArrayList<String> collectionLinks, int partitionsPerNode) {
int collectionsNodeCount = collectionLinks.size();
Partition[] partitions = new Partition[totalPartitions];

int partitionsPerNode = totalPartitions/collectionsNodeCount;
int extraPartitions = totalPartitions - (partitionsPerNode * collectionsNodeCount);
Partition[] partitions = new Partition[partitionsPerNode * collectionsNodeCount];

int index = 0;
for(String collectionNode : collectionLinks) {
byte[] hashValue = this.hashGenerator.computeHash(getBytes(collectionNode));

for(int i=0; i < partitionsPerNode + (extraPartitions > 0 ? 1 : 0); ++i) {
for(int i=0; i < partitionsPerNode; ++i) {
partitions[index++] = new Partition(hashValue, collectionNode);
hashValue = this.hashGenerator.computeHash(hashValue);
}

extraPartitions--;
}

Arrays.sort(partitions);
Expand Down Expand Up @@ -157,5 +157,21 @@ private static int lowerBoundSearch(Partition[] partitions, byte[] hashValue) {
}

return partitions.length-1;
}
}

/**
* Gets the serialized version of the consistentRing. Added this helper for the test code.
*
*/
List<Map.Entry<String,Long>> getSerializedPartitionList() {
List<Map.Entry<String,Long>> partitionList= new ArrayList<>();

for(int i=0; i<this.partitions.length; i++) {
ByteBuffer wrapped = ByteBuffer.wrap(partitions[i].getHashValue()).order(ByteOrder.LITTLE_ENDIAN);
int num = wrapped.getInt();
partitionList.add(new AbstractMap.SimpleEntry<>(partitions[i].getNode(), (long)num & 0x0FFFFFFFFL));
}

return partitionList;
}
}
17 changes: 12 additions & 5 deletions src/com/microsoft/azure/documentdb/HashPartitionResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ of this software and associated documentation files (the "Software"), to deal
package com.microsoft.azure.documentdb;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* HashPartitionResolver implements partitioning based on the value of a hash function, allowing you to evenly
Expand Down Expand Up @@ -105,7 +107,7 @@ public HashPartitionResolver(PartitionKeyExtractor partitionKeyExtractor, Iterab
}

// Initialize the consistent ring to be used for the hashing algorithm by placing the virtual nodes along the ring
this.consistentHashRing = new ConsistentHashRing(this.collectionLinks, this.collectionLinks.size() * numberOfVirtualNodesPerCollection, hashGenerator);
this.consistentHashRing = new ConsistentHashRing(this.collectionLinks, numberOfVirtualNodesPerCollection, hashGenerator);
}

/**
Expand All @@ -121,10 +123,6 @@ public String resolveForCreate(Object document) {
throw new IllegalArgumentException("document");
}

if(this.partitionKeyExtractor == null) {
throw new IllegalStateException("Unable to extract partition key from document. Ensure that you have provided a valid PartitionKeyExtractor function.");
}

Object partitionKey = this.partitionKeyExtractor.getPartitionKey(document);
return this.consistentHashRing.getCollectionNode(partitionKey);
}
Expand All @@ -147,4 +145,13 @@ public Iterable<String> resolveForRead(Object partitionKey) {
collectionLinks.add(this.consistentHashRing.getCollectionNode(partitionKey));
return collectionLinks;
}

/**
* Gets the serialized version of the consistentRing. Added this helper for the test code.
*
*/
@SuppressWarnings("unused") // used only by test code
private List<Map.Entry<String,Long>> getSerializedPartitionList() {
return this.consistentHashRing.getSerializedPartitionList();
}
}
2 changes: 1 addition & 1 deletion src/com/microsoft/azure/documentdb/HttpConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public static class HttpHeaders {

public static class Versions {
public static String CURRENT_VERSION = "2015-08-06";
public static String USER_AGENT = "documentdb-java-sdk-1.5.0";
public static String USER_AGENT = "documentdb-java-sdk-1.5.1";
}

public static class StatusCodes {
Expand Down
6 changes: 5 additions & 1 deletion src/com/microsoft/azure/documentdb/MurmurHash.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal
package com.microsoft.azure.documentdb;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
* The MurmurHash3 algorithm was created by Austin Appleby and placed in the public domain.
Expand All @@ -46,7 +47,10 @@ public byte[] computeHash(byte[] data) {
}

int hashValue = computeHash(data, data.length, 0);
return ByteBuffer.allocate(4).putInt(hashValue).array();
// Java's default "Endianess" is BigEndian but for all other SDKs
// the default is LittleEndian, so changing the ByteOrder to be LittleEndian
// here so that we can be consistent across all SDKs.
return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(hashValue).array();
}

/** Returns the MurmurHash3_x86_32 hash. */
Expand Down
9 changes: 7 additions & 2 deletions src/com/microsoft/azure/documentdb/Partition.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ private static int CompareHashValues(byte[] hash1, byte[] hash2) {
if(hash1.length != hash2.length)
throw new IllegalArgumentException("Length of hashes doesn't match.");

// Casting "byte" which is 8-bit signed data type to a "char" which is a 16 bit unsigned data type in Java,
// so that they are compared the same way in all SDKs which have the native 8-bit types as unsigned.

// The hash byte array that is returned from ComputeHash method has the MSB at the end of the array
// so comparing the bytes from the end for compare operations.
for (int i = 0; i < hash1.length; i++) {
if (hash1[i] < hash2[i]) {
if ((char)hash1[hash1.length - i - 1] < (char)hash2[hash1.length - i - 1]) {
return -1;
}
else if (hash1[i] > hash2[i]) {
else if ((char)hash1[hash1.length - i - 1] > (char)hash2[hash1.length - i - 1]) {
return 1;
}
}
Expand Down
20 changes: 6 additions & 14 deletions src/com/microsoft/azure/documentdb/RangePartitionResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ public class RangePartitionResolver<T extends Comparable<T>> implements Partitio
private Map<Range<T>, String> partitionMap;

/**
* RangePartitionResolver constructor taking in the PartitionKeyExtractor, a map of Ranges to collection links
* with the type of class used as the partition key.
* RangePartitionResolver constructor taking in the PartitionKeyExtractor, a map of Ranges to collection links.
*
* @param partitionKeyExtractor an instance of class that implements PartitionKeyExtractor interface.
* @param partitionMap the map of ranges to collection links.
Expand Down Expand Up @@ -69,10 +68,6 @@ public String resolveForCreate(Object document) {
throw new IllegalArgumentException("document");
}

if(this.partitionKeyExtractor == null) {
throw new UnsupportedOperationException("Unable to extract partition key from document. Ensure that you have provided a valid PartitionKeyExtractor function.");
}

Object partitionKey = this.partitionKeyExtractor.getPartitionKey(document);
Range<T> containingRange = this.getContainingRange(partitionKey);

Expand All @@ -92,14 +87,7 @@ public String resolveForCreate(Object document) {
*/
@Override
public Iterable<String> resolveForRead(Object partitionKey) {
Set<Range<T>> intersectingRanges = null;

if (partitionKey == null) {
intersectingRanges = this.partitionMap.keySet();
}
else {
intersectingRanges = this.getIntersectingRanges(partitionKey);
}
Set<Range<T>> intersectingRanges = this.getIntersectingRanges(partitionKey);

ArrayList<String> collectionsLinks = new ArrayList<String>();
for(Range<T> range : intersectingRanges) {
Expand Down Expand Up @@ -137,6 +125,10 @@ private Set<Range<T>> getIntersectingRanges(Object partitionKey) {
Set<Range<T>> intersectingRanges = new HashSet<Range<T>>();
Set<Range<T>> partitionKeyRanges = new HashSet<Range<T>>();

if (partitionKey == null) {
return this.partitionMap.keySet();
}

try {
// Check the type of partitionKey to be Range<T>. In Java the type information is erased at runtime due to type erasure.
// We can only check for Range<?> in this case
Expand Down
Loading