/* * Copyright DataStax, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; const types = require('./types'); const util = require('util'); const _Murmur3TokenType = types.dataTypes.getByName('bigint'); const _RandomTokenType = types.dataTypes.getByName('varint'); const _OrderedTokenType = types.dataTypes.getByName('blob'); /** * Represents a token on the Cassandra ring. */ class Token { constructor(value) { this._value = value; } /** * @returns {{code: number, info: *|Object}} The type info for the * type of the value of the token. */ getType() { throw new Error('You must implement a getType function for this Token instance'); } /** * @returns {*} The raw value of the token. */ getValue() { return this._value; } toString() { return this._value.toString(); } /** * Returns 0 if the values are equal, 1 if greater than other, -1 * otherwise. * * @param {Token} other * @returns {Number} */ compare(other) { return this._value.compare(other._value); } equals(other) { return this.compare(other) === 0; } inspect() { return this.constructor.name + ' { ' + this.toString() + ' }'; } } /** * Represents a token from a Cassandra ring where the partitioner * is Murmur3Partitioner. * * The raw token type is a varint (represented by MutableLong). */ class Murmur3Token extends Token { constructor(value) { super(value); } getType() { return _Murmur3TokenType; } } /** * Represents a token from a Cassandra ring where the partitioner * is RandomPartitioner. * * The raw token type is a bigint (represented by Number). */ class RandomToken extends Token { constructor(value) { super(value); } getType() { return _RandomTokenType; } } /** * Represents a token from a Cassandra ring where the partitioner * is ByteOrderedPartitioner. * * The raw token type is a blob (represented by Buffer or Array). */ class ByteOrderedToken extends Token { constructor(value) { super(value); } getType() { return _OrderedTokenType; } toString() { return this._value.toString('hex').toUpperCase(); } } /** * Represents a range of tokens on a Cassandra ring. * * A range is start-exclusive and end-inclusive. It is empty when * start and end are the same token, except if that is the minimum * token, in which case the range covers the whole ring (this is * consistent with the behavior of CQL range queries). * * Note that CQL does not handle wrapping. To query all partitions * in a range, see {@link unwrap}. */ class TokenRange { constructor(start, end, tokenizer) { this.start = start; this.end = end; Object.defineProperty(this, '_tokenizer', { value: tokenizer, enumerable: false}); } /** * Splits this range into a number of smaller ranges of equal "size" * (referring to the number of tokens, not the actual amount of data). * * Splitting an empty range is not permitted. But not that, in edge * cases, splitting a range might produce one or more empty ranges. * * @param {Number} numberOfSplits Number of splits to make. * @returns {TokenRange[]} Split ranges. * @throws {Error} If splitting an empty range. */ splitEvenly(numberOfSplits) { if (numberOfSplits < 1) { throw new Error(util.format("numberOfSplits (%d) must be greater than 0.", numberOfSplits)); } if (this.isEmpty()) { throw new Error("Can't split empty range " + this.toString()); } const tokenRanges = []; const splitPoints = this._tokenizer.split(this.start, this.end, numberOfSplits); let splitStart = this.start; let splitEnd; for (let splitIndex = 0; splitIndex < splitPoints.length; splitIndex++) { splitEnd = splitPoints[splitIndex]; tokenRanges.push(new TokenRange(splitStart, splitEnd, this._tokenizer)); splitStart = splitEnd; } tokenRanges.push(new TokenRange(splitStart, this.end, this._tokenizer)); return tokenRanges; } /** * A range is empty when start and end are the same token, except if * that is the minimum token, in which case the range covers the * whole ring. This is consistent with the behavior of CQL range * queries. * * @returns {boolean} Whether this range is empty. */ isEmpty() { return this.start.equals(this.end) && !this.start.equals(this._tokenizer.minToken()); } /** * A range wraps around the end of the ring when the start token * is greater than the end token and the end token is not the * minimum token. * * @returns {boolean} Whether this range wraps around. */ isWrappedAround() { return this.start.compare(this.end) > 0 && !this.end.equals(this._tokenizer.minToken()); } /** * Splits this range into a list of two non-wrapping ranges. * * This will return the range itself if it is non-wrapped, or two * ranges otherwise. * * This is useful for CQL range queries, which do not handle * wrapping. * * @returns {TokenRange[]} The list of non-wrapping ranges. */ unwrap() { if (this.isWrappedAround()) { return [ new TokenRange(this.start, this._tokenizer.minToken(), this._tokenizer), new TokenRange(this._tokenizer.minToken(), this.end, this._tokenizer) ]; } return [this]; } /** * Whether this range contains a given Token. * * @param {*} token Token to check for. * @returns {boolean} Whether or not the Token is in this range. */ contains(token) { if (this.isEmpty()) { return false; } const minToken = this._tokenizer.minToken(); if (this.end.equals(minToken)) { if (this.start.equals(minToken)) { return true; // ]minToken, minToken] === full ring } else if (token.equals(minToken)) { return true; } return token.compare(this.start) > 0; } const isAfterStart = token.compare(this.start) > 0; const isBeforeEnd = token.compare(this.end) <= 0; // if wrapped around ring, token is in ring if its after start or before end. // otherwise, token is in ring if its after start and before end. return this.isWrappedAround() ? isAfterStart || isBeforeEnd : isAfterStart && isBeforeEnd; } /** * Determines if the input range is equivalent to this one. * * @param {TokenRange} other Range to compare with. * @returns {boolean} Whether or not the ranges are equal. */ equals(other) { if (other === this) { return true; } else if (other instanceof TokenRange) { return this.compare(other) === 0; } return false; } /** * Returns 0 if the values are equal, otherwise compares against * start, if start is equal, compares against end. * * @param {TokenRange} other Range to compare with. * @returns {Number} */ compare(other) { const compareStart = this.start.compare(other.start); return compareStart !== 0 ? compareStart : this.end.compare(other.end); } toString() { return util.format(']%s, %s]', this.start.toString(), this.end.toString() ); } } exports.Token = Token; exports.TokenRange = TokenRange; exports.ByteOrderedToken = ByteOrderedToken; exports.Murmur3Token = Murmur3Token; exports.RandomToken = RandomToken;