"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.
*
* Given that Cassandra uses those timestamps to resolve conflicts, implementations should generate
* monotonically increasing timestamps for successive invocations of {@link TimestampGenerator.next()}.
*
* @constructor
*/
function TimestampGenerator() {}
/**
* Returns the next timestamp.
*
* Implementors should enforce increasing monotonicity of timestamps, that is,
* a timestamp returned should always be strictly greater that any previously returned
* timestamp.
*
* Implementors should strive to achieve microsecond precision in the best possible way,
* which is usually largely dependent on the underlying operating system's capabilities.
* @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 `null`, 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.
*
* {@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.
* @param {Number} [warningThreshold] Determines how far in the future timestamps are allowed to drift before a
* warning is logged, expressed in milliseconds. Default: `1000`.
* @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: `1000`.
* @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;