andresusandi-storage
Version:
storage utilities for distributed systems
127 lines (104 loc) • 5.96 kB
JavaScript
const { name: packageName } = require('./package.json');
const ConsistentHashShard = require('./ConsistentHashShard');
const SHARDS_MAX_COUNT = 100000;
const ITEMS_MAX_PER_SHARD = 1000;
class ConsistentHashStore {
constructor(storageConstructor, maxShardCount, maxItemsPerShard, counterConstructor) {
this.maxShardCount = maxShardCount || SHARDS_MAX_COUNT;
this.maxItemsPerShard = maxItemsPerShard || ITEMS_MAX_PER_SHARD;
this.storageConstructor = storageConstructor;
this.counterConstructor = counterConstructor;
this.circle = new Map();
console.log(`${packageName}/${this.constructor.name}: Created Consistant Hash Store with max ${this.maxShardCount} shards and ${this.maxItemsPerShard} items`);
}
async init(existingShardIndices) {
console.log(`${packageName}/${this.constructor.name}: Initializing Consistent Hash Store`);
if (existingShardIndices && existingShardIndices.length > 0) {
for (var i = 0; i < existingShardIndices.length; i++) {
this.circle.set(existingShardIndices[i], (await this._instantiateShard(existingShardIndices[i])));
console.log(`${packageName}/${this.constructor.name}: Adding existing shard ${existingShardIndices[i]}`);
}
} else {
console.log(`${packageName}/${this.constructor.name}: Adding default first shard`);
this.circle.set(0, (await this._instantiateShard(0)));
}
}
async save(item) {
if (!item)
throw new Error("item cannot be a null or empty string");
const shardIndex = this._getShardIndex(item);
const shard = this.circle.get(shardIndex);
await shard.save(item, item);
await shard.modifyCount(1);
console.log(`${packageName}/${this.constructor.name}: Saved item ${item} to shard ${shardIndex}`);
const shardCount = await shard.getCount();
if (shardCount >= this.maxItemsPerShard) {
console.log(`${packageName}/${this.constructor.name}: Item count for shard ${shardIndex} is ${shardCount} which is greater than ${this.maxItemsPerShard}, splitting...`);
await this._splitShard(shardIndex);
}
}
async exists(item) {
const shardIndex = this._getShardIndex(item);
const shard = this.circle.get(shardIndex);
const storedItem = await shard.get(item);
return (storedItem !== null && storedItem !== undefined);
}
async _splitShard(shardIndex) {
var shardIndexNumber = Number(shardIndex);
var newShardIndex = Array.from(this.circle.keys()).find(k => Number(k) > shardIndexNumber);
newShardIndex = !newShardIndex ? this.maxShardCount : Number(newShardIndex);
newShardIndex = shardIndexNumber + Math.floor((Number(newShardIndex) - shardIndexNumber) / 2);
console.log(`${packageName}/${this.constructor.name}: Splitting shard ${shardIndex} with ${newShardIndex}`);
this.circle.set(newShardIndex, (await this._instantiateShard(newShardIndex)));
await this._recalculateShardLocation(shardIndex, newShardIndex);
console.log(`${packageName}/${this.constructor.name}: Finished Shard Split`);
}
async _recalculateShardLocation(currentShardIndex, newShardIndex) {
const currentShard = this.circle.get(currentShardIndex);
const newShard = this.circle.get(newShardIndex);
const items = await currentShard.getAllItems();
console.log(`${packageName}/${this.constructor.name}: Moving ${items.length} items from ${currentShardIndex} to ${newShardIndex}`);
for (var i = 0; i < items.length; i++) {
const item = items[i].value;
const calculatedShardIndex = this._getShardIndex(item);
if (calculatedShardIndex != currentShardIndex && calculatedShardIndex != newShardIndex)
console.log("throw new Error(`Shard index ${calculatedShardIndex} was not expected, ${newShardIndex} was`)");
if (calculatedShardIndex == newShardIndex) {
await newShard.save(item, item);
await newShard.modifyCount(1);
console.log(`${packageName}/${this.constructor.name}: Copied item ${item} from ${currentShardIndex} to ${newShardIndex}`);
}
}
const newItems = await newShard.getAllItems();
console.log(`${packageName}/${this.constructor.name}: Deleting ${newItems.length} items from ${currentShardIndex}`);
for (var i = 0; i < newItems.length; i++) {
await currentShard.delete(newItems[i].value);
await currentShard.modifyCount(-1);
console.log(`${packageName}/${this.constructor.name}: Deleted item ${newItems[i].value} from ${currentShardIndex}`);
}
}
async _instantiateShard(shardIndex) {
const shard = new ConsistentHashShard(shardIndex, this.storageConstructor, this.counterConstructor);
await shard.init();
return (shard);
}
_getShardIndex(item) {
const rawIndex = this._gethashCode(item) % this.maxShardCount;
//rawIndex = rawIndex < 0 ? rawIndex * -1 : rawIndex;
if (rawIndex < 0)
throw new Error("hash result is less than 0.");
const keysDesc = [...Array.from(this.circle.keys())].sort((a, b) => b - a);
const actualIndex = keysDesc.find(k => k <= rawIndex);
return (actualIndex);
}
_gethashCode(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
var character = str.charCodeAt(i);
hash = ((hash << 5) - hash) + character;
hash = hash & hash; // Convert to 32bit integer
}
return hash + 0x7FFFFFFF; // make it unsigned
}
}
module.exports = ConsistentHashStore;