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.

170 lines
5.8 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 util = require('util');
const { Long } = require('../types');
const errors = require('../errors');
/** @module policies/timestampGeneration */
/**
* Defines the maximum date in milliseconds that can be represented in microseconds using Number ((2 ^ 53) / 1000)
* @const
* @private
*/
const _maxSafeNumberDate = 9007199254740;
/**
* A long representing the value 1000
* @const
* @private
*/
const _longOneThousand = Long.fromInt(1000);
/**
* Creates a new instance of {@link TimestampGenerator}.
* @classdesc
* Generates client-side, microsecond-precision query timestamps.
* <p>
* Given that Cassandra uses those timestamps to resolve conflicts, implementations should generate
* monotonically increasing timestamps for successive invocations of {@link TimestampGenerator.next()}.
* </p>
* @constructor
*/
function TimestampGenerator() {
}
/**
* Returns the next timestamp.
* <p>
* Implementors should enforce increasing monotonicity of timestamps, that is,
* a timestamp returned should always be strictly greater that any previously returned
* timestamp.
* <p/>
* <p>
* Implementors should strive to achieve microsecond precision in the best possible way,
* which is usually largely dependent on the underlying operating system's capabilities.
* </p>
* @param {Client} client The {@link Client} instance to generate timestamps to.
* @returns {Long|Number|null} the next timestamp (in microseconds). If it's equals to <code>null</code>, it won't be
* sent by the driver, letting the server to generate the timestamp.
* @abstract
*/
TimestampGenerator.prototype.next = function (client) {
throw new Error('next() must be implemented');
};
/**
* A timestamp generator that guarantees monotonically increasing timestamps and logs warnings when timestamps
* drift in the future.
* <p>
* {@link Date} has millisecond precision and client timestamps require microsecond precision. This generator
* keeps track of the last generated timestamp, and if the current time is within the same millisecond as the last,
* it fills the microsecond portion of the new timestamp with the value of an incrementing counter.
* </p>
* @param {Number} [warningThreshold] Determines how far in the future timestamps are allowed to drift before a
* warning is logged, expressed in milliseconds. Default: <code>1000</code>.
* @param {Number} [minLogInterval] In case of multiple log events, it determines the time separation between log
* events, expressed in milliseconds. Use 0 to disable. Default: <code>1000</code>.
* @extends {TimestampGenerator}
* @constructor
*/
function MonotonicTimestampGenerator(warningThreshold, minLogInterval) {
if (warningThreshold < 0) {
throw new errors.ArgumentError('warningThreshold can not be lower than 0');
}
this._warningThreshold = warningThreshold || 1000;
this._minLogInterval = 1000;
if (typeof minLogInterval === 'number') {
// A value under 1 will disable logging
this._minLogInterval = minLogInterval;
}
this._micros = -1;
this._lastDate = 0;
this._lastLogDate = 0;
}
util.inherits(MonotonicTimestampGenerator, TimestampGenerator);
/**
* Returns the current time in milliseconds since UNIX epoch
* @returns {Number}
*/
MonotonicTimestampGenerator.prototype.getDate = function () {
return Date.now();
};
MonotonicTimestampGenerator.prototype.next = function (client) {
let date = this.getDate();
let drifted = 0;
if (date > this._lastDate) {
this._micros = 0;
this._lastDate = date;
return this._generateMicroseconds();
}
if (date < this._lastDate) {
drifted = this._lastDate - date;
date = this._lastDate;
}
if (++this._micros === 1000) {
this._micros = 0;
if (date === this._lastDate) {
// Move date 1 millisecond into the future
date++;
drifted++;
}
}
const lastDate = this._lastDate;
this._lastDate = date;
const result = this._generateMicroseconds();
if (drifted >= this._warningThreshold) {
// Avoid logging an unbounded amount of times within a clock-skew event or during an interval when more than 1
// query is being issued by microsecond
const currentLogDate = Date.now();
if (this._minLogInterval > 0 && this._lastLogDate + this._minLogInterval <= currentLogDate){
const message = util.format(
'Timestamp generated using current date was %d milliseconds behind the last generated timestamp (which ' +
'millisecond portion was %d), the returned value (%s) is being artificially incremented to guarantee ' +
'monotonicity.',
drifted, lastDate, result);
this._lastLogDate = currentLogDate;
client.log('warning', message);
}
}
return result;
};
/**
* @private
* @returns {Number|Long}
*/
MonotonicTimestampGenerator.prototype._generateMicroseconds = function () {
if (this._lastDate < _maxSafeNumberDate) {
// We are safe until Jun 06 2255, its faster to perform this operations on Number than on Long
// We hope to have native int64 by then :)
return this._lastDate * 1000 + this._micros;
}
return Long
.fromNumber(this._lastDate)
.multiply(_longOneThousand)
.add(Long.fromInt(this._micros));
};
exports.TimestampGenerator = TimestampGenerator;
exports.MonotonicTimestampGenerator = MonotonicTimestampGenerator;