You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

286 lines
7.7 KiB
JavaScript

/*
* 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;