1 | /*
|
2 | * Copyright DataStax, Inc.
|
3 | *
|
4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | * you may not use this file except in compliance with the License.
|
6 | * You may obtain a copy of the License at
|
7 | *
|
8 | * http://www.apache.org/licenses/LICENSE-2.0
|
9 | *
|
10 | * Unless required by applicable law or agreed to in writing, software
|
11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | * See the License for the specific language governing permissions and
|
14 | * limitations under the License.
|
15 | */
|
16 | ;
|
17 |
|
18 | const types = require('./types');
|
19 | const util = require('util');
|
20 |
|
21 | const _Murmur3TokenType = types.dataTypes.getByName('bigint');
|
22 | const _RandomTokenType = types.dataTypes.getByName('varint');
|
23 | const _OrderedTokenType = types.dataTypes.getByName('blob');
|
24 |
|
25 | /**
|
26 | * Represents a token on the Cassandra ring.
|
27 | */
|
28 | class Token {
|
29 | constructor(value) {
|
30 | this._value = value;
|
31 | }
|
32 |
|
33 | /**
|
34 | * @returns {{code: number, info: *|Object}} The type info for the
|
35 | * type of the value of the token.
|
36 | */
|
37 | getType() {
|
38 | throw new Error('You must implement a getType function for this Token instance');
|
39 | }
|
40 |
|
41 | /**
|
42 | * @returns {*} The raw value of the token.
|
43 | */
|
44 | getValue() {
|
45 | return this._value;
|
46 | }
|
47 |
|
48 | toString() {
|
49 | return this._value.toString();
|
50 | }
|
51 |
|
52 | /**
|
53 | * Returns 0 if the values are equal, 1 if greater than other, -1
|
54 | * otherwise.
|
55 | *
|
56 | * @param {Token} other
|
57 | * @returns {Number}
|
58 | */
|
59 | compare(other) {
|
60 | return this._value.compare(other._value);
|
61 | }
|
62 |
|
63 | equals(other) {
|
64 | return this.compare(other) === 0;
|
65 | }
|
66 |
|
67 | inspect() {
|
68 | return this.constructor.name + ' { ' + this.toString() + ' }';
|
69 | }
|
70 | }
|
71 |
|
72 | /**
|
73 | * Represents a token from a Cassandra ring where the partitioner
|
74 | * is Murmur3Partitioner.
|
75 | *
|
76 | * The raw token type is a varint (represented by MutableLong).
|
77 | */
|
78 | class Murmur3Token extends Token {
|
79 | constructor(value) {
|
80 | super(value);
|
81 | }
|
82 |
|
83 | getType() {
|
84 | return _Murmur3TokenType;
|
85 | }
|
86 | }
|
87 |
|
88 | /**
|
89 | * Represents a token from a Cassandra ring where the partitioner
|
90 | * is RandomPartitioner.
|
91 | *
|
92 | * The raw token type is a bigint (represented by Number).
|
93 | */
|
94 | class RandomToken extends Token {
|
95 | constructor(value) {
|
96 | super(value);
|
97 | }
|
98 |
|
99 | getType() {
|
100 | return _RandomTokenType;
|
101 | }
|
102 | }
|
103 |
|
104 | /**
|
105 | * Represents a token from a Cassandra ring where the partitioner
|
106 | * is ByteOrderedPartitioner.
|
107 | *
|
108 | * The raw token type is a blob (represented by Buffer or Array).
|
109 | */
|
110 | class ByteOrderedToken extends Token {
|
111 | constructor(value) {
|
112 | super(value);
|
113 | }
|
114 |
|
115 | getType() {
|
116 | return _OrderedTokenType;
|
117 | }
|
118 |
|
119 | toString() {
|
120 | return this._value.toString('hex').toUpperCase();
|
121 | }
|
122 | }
|
123 |
|
124 | /**
|
125 | * Represents a range of tokens on a Cassandra ring.
|
126 | *
|
127 | * A range is start-exclusive and end-inclusive. It is empty when
|
128 | * start and end are the same token, except if that is the minimum
|
129 | * token, in which case the range covers the whole ring (this is
|
130 | * consistent with the behavior of CQL range queries).
|
131 | *
|
132 | * Note that CQL does not handle wrapping. To query all partitions
|
133 | * in a range, see {@link unwrap}.
|
134 | */
|
135 | class TokenRange {
|
136 | constructor(start, end, tokenizer) {
|
137 | this.start = start;
|
138 | this.end = end;
|
139 | Object.defineProperty(this, '_tokenizer', { value: tokenizer, enumerable: false});
|
140 | }
|
141 |
|
142 | /**
|
143 | * Splits this range into a number of smaller ranges of equal "size"
|
144 | * (referring to the number of tokens, not the actual amount of data).
|
145 | *
|
146 | * Splitting an empty range is not permitted. But not that, in edge
|
147 | * cases, splitting a range might produce one or more empty ranges.
|
148 | *
|
149 | * @param {Number} numberOfSplits Number of splits to make.
|
150 | * @returns {TokenRange[]} Split ranges.
|
151 | * @throws {Error} If splitting an empty range.
|
152 | */
|
153 | splitEvenly(numberOfSplits) {
|
154 | if (numberOfSplits < 1) {
|
155 | throw new Error(util.format("numberOfSplits (%d) must be greater than 0.", numberOfSplits));
|
156 | }
|
157 | if (this.isEmpty()) {
|
158 | throw new Error("Can't split empty range " + this.toString());
|
159 | }
|
160 |
|
161 | const tokenRanges = [];
|
162 | const splitPoints = this._tokenizer.split(this.start, this.end, numberOfSplits);
|
163 | let splitStart = this.start;
|
164 | let splitEnd;
|
165 | for (let splitIndex = 0; splitIndex < splitPoints.length; splitIndex++) {
|
166 | splitEnd = splitPoints[splitIndex];
|
167 | tokenRanges.push(new TokenRange(splitStart, splitEnd, this._tokenizer));
|
168 | splitStart = splitEnd;
|
169 | }
|
170 | tokenRanges.push(new TokenRange(splitStart, this.end, this._tokenizer));
|
171 | return tokenRanges;
|
172 | }
|
173 |
|
174 | /**
|
175 | * A range is empty when start and end are the same token, except if
|
176 | * that is the minimum token, in which case the range covers the
|
177 | * whole ring. This is consistent with the behavior of CQL range
|
178 | * queries.
|
179 | *
|
180 | * @returns {boolean} Whether this range is empty.
|
181 | */
|
182 | isEmpty() {
|
183 | return this.start.equals(this.end) && !this.start.equals(this._tokenizer.minToken());
|
184 | }
|
185 |
|
186 | /**
|
187 | * A range wraps around the end of the ring when the start token
|
188 | * is greater than the end token and the end token is not the
|
189 | * minimum token.
|
190 | *
|
191 | * @returns {boolean} Whether this range wraps around.
|
192 | */
|
193 | isWrappedAround() {
|
194 | return this.start.compare(this.end) > 0 && !this.end.equals(this._tokenizer.minToken());
|
195 | }
|
196 |
|
197 | /**
|
198 | * Splits this range into a list of two non-wrapping ranges.
|
199 | *
|
200 | * This will return the range itself if it is non-wrapped, or two
|
201 | * ranges otherwise.
|
202 | *
|
203 | * This is useful for CQL range queries, which do not handle
|
204 | * wrapping.
|
205 | *
|
206 | * @returns {TokenRange[]} The list of non-wrapping ranges.
|
207 | */
|
208 | unwrap() {
|
209 | if (this.isWrappedAround()) {
|
210 | return [
|
211 | new TokenRange(this.start, this._tokenizer.minToken(), this._tokenizer),
|
212 | new TokenRange(this._tokenizer.minToken(), this.end, this._tokenizer)
|
213 | ];
|
214 | }
|
215 | return [this];
|
216 | }
|
217 |
|
218 | /**
|
219 | * Whether this range contains a given Token.
|
220 | *
|
221 | * @param {*} token Token to check for.
|
222 | * @returns {boolean} Whether or not the Token is in this range.
|
223 | */
|
224 | contains(token) {
|
225 | if (this.isEmpty()) {
|
226 | return false;
|
227 | }
|
228 | const minToken = this._tokenizer.minToken();
|
229 | if (this.end.equals(minToken)) {
|
230 | if (this.start.equals(minToken)) {
|
231 | return true; // ]minToken, minToken] === full ring
|
232 | } else if (token.equals(minToken)) {
|
233 | return true;
|
234 | }
|
235 | return token.compare(this.start) > 0;
|
236 | }
|
237 |
|
238 | const isAfterStart = token.compare(this.start) > 0;
|
239 | const isBeforeEnd = token.compare(this.end) <= 0;
|
240 | // if wrapped around ring, token is in ring if its after start or before end.
|
241 | // otherwise, token is in ring if its after start and before end.
|
242 | return this.isWrappedAround()
|
243 | ? isAfterStart || isBeforeEnd
|
244 | : isAfterStart && isBeforeEnd;
|
245 | }
|
246 |
|
247 | /**
|
248 | * Determines if the input range is equivalent to this one.
|
249 | *
|
250 | * @param {TokenRange} other Range to compare with.
|
251 | * @returns {boolean} Whether or not the ranges are equal.
|
252 | */
|
253 | equals(other) {
|
254 | if (other === this) {
|
255 | return true;
|
256 | } else if (other instanceof TokenRange) {
|
257 | return this.compare(other) === 0;
|
258 | }
|
259 | return false;
|
260 | }
|
261 |
|
262 | /**
|
263 | * Returns 0 if the values are equal, otherwise compares against
|
264 | * start, if start is equal, compares against end.
|
265 | *
|
266 | * @param {TokenRange} other Range to compare with.
|
267 | * @returns {Number}
|
268 | */
|
269 | compare(other) {
|
270 | const compareStart = this.start.compare(other.start);
|
271 | return compareStart !== 0 ? compareStart : this.end.compare(other.end);
|
272 | }
|
273 |
|
274 | toString() {
|
275 | return util.format(']%s, %s]',
|
276 | this.start.toString(),
|
277 | this.end.toString()
|
278 | );
|
279 | }
|
280 | }
|
281 |
|
282 | exports.Token = Token;
|
283 | exports.TokenRange = TokenRange;
|
284 | exports.ByteOrderedToken = ByteOrderedToken;
|
285 | exports.Murmur3Token = Murmur3Token;
|
286 | exports.RandomToken = RandomToken; |
\ | No newline at end of file |