snowflake-sdk
Version:
Node.js driver for Snowflake
282 lines (247 loc) • 10.3 kB
JavaScript
/*
* Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved.
*/
const Logger = require('./logger');
/**
*
* @param {String} id
* @param {Number} timestamp
* @param {Number} priority
* @param {String} context
*/
function QueryContextElement(id, timestamp, priority, context) {
this.id = id;
this.timestamp = timestamp;
this.priority = priority;
this.context = context;
}
/**
* Most Recently Used and Priority based cache. A separate cache for each connection in the driver.
*/
/**
* @param {Number} capacity Maximum capacity of the cache.
* @param {Number} sessionId Session for which the cache is created.
*/
function QueryContextCache(capacity, sessionId) {
Logger.getInstance().debug(`Creating new QueryContextCache with capacity ${capacity} for session ${sessionId}`);
this.sessionId = sessionId;
this.capacity = capacity;
this.idMap = new Map(); // Map for id and QCC
this.treeSet = new Set(); // Order data as per priority
this.priorityMap = new Map(); // Map for priority and QCC
}
QueryContextCache.prototype.sortTreeSet = function () {
this.treeSet = new Set(Array.from(this.treeSet).sort((a, b) => a.priority - b.priority));
};
QueryContextCache.prototype.addQCE = function (qce) {
this.idMap.set(qce.id, qce);
this.priorityMap.set(qce.priority, qce);
this.treeSet.add(qce);
this.sortTreeSet();
Logger.getInstance().trace(`QCC session ${this.sessionId} - Added QCE: ${JSON.stringify(qce)}`);
};
/**
* Remove an element from the cache.
*
* @param {Object} qce element to remove.
*/
QueryContextCache.prototype.removeQCE = function (qce) {
this.idMap.delete(qce.id);
this.priorityMap.delete(qce.priority);
this.treeSet.delete(qce);
Logger.getInstance().trace(`QCC session ${this.sessionId} - Removed QCE: ${JSON.stringify(qce)}`);
};
/**
* Replace the cache element with a new response element. Remove old element exist in the cache
* and add a new element received.
*
* @param {Object} oldQCE an element exist in the cache
* @param {Object} newQCE a new element just received.
*/
QueryContextCache.prototype.replaceQCE = function (oldQCE, newQCE) {
// Remove old element from the cache
this.removeQCE(oldQCE);
// Add new element in the cache
this.addQCE(newQCE);
Logger.getInstance().debug(`QCC session ${this.sessionId} - Replaced QCE: ${JSON.stringify(oldQCE)} with ${JSON.stringify(newQCE)}`);
};
/**
* Merge a new element comes from the server with the existing cache. Merge is based on read time
* stamp for the same id and based on priority for two different ids.
*
* @param {Number} id
* @param {Number} timestamp
* @param {Number} priority
* @param {String} context
*
*/
QueryContextCache.prototype.merge = function (newQCE) {
Logger.getInstance().debug(`QCC session ${this.sessionId} - Merging QCE: ${JSON.stringify(newQCE)}`);
if (this.idMap.has(newQCE.id)) {
Logger.getInstance().debug(`QCC session ${this.sessionId} - Element id ${newQCE.id} found in cache`);
// ID found in the cache
const qce = this.idMap.get(newQCE.id);
if (newQCE.timestamp > qce.timestamp) {
Logger.getInstance().trace(`QCC session ${this.sessionId} - New element is more recent. Current timestamp: ${qce.timestamp}, new timestamp: ${newQCE.timestamp}`);
if (qce.priority === newQCE.priority) {
Logger.getInstance().trace(`QCC session ${this.sessionId} - Element priority (${qce.priority}) is the same`);
// Same priority, overwrite new data at same place
qce.timestamp = newQCE.timestamp;
qce.context = newQCE.context;
} else {
Logger.getInstance().trace(`QCC session ${this.sessionId} - Element priority changed. Current priority: ${qce.priority}, new priority: ${newQCE.priority}`);
// Change in priority
this.replaceQCE(qce, newQCE);
}
} else if (newQCE.timestamp === qce.timestamp && qce.priority !== newQCE.priority) {
Logger.getInstance().trace(`QCC session ${this.sessionId} - Element timestamp is the same, but priority changes. Current priority: ${qce.priority}, new priority: ${newQCE.priority}`);
// Same read timestamp but change in priority
this.replaceQCE(qce, newQCE);
} else {
Logger.getInstance().trace(`QCC session ${this.sessionId} - Element is the same. Doing nothing.`);
}
} else {
Logger.getInstance().trace(`QCC session ${this.sessionId} - New element`);
// new id
if (this.priorityMap.has(newQCE.priority)) {
// Same priority with different id
const qce = this.priorityMap.get(newQCE.priority);
Logger.getInstance().trace(`QCC session ${this.sessionId} - Element with the same priority found: ${JSON.stringify(qce)}. Replacing with new element: ${JSON.stringify(newQCE)}`);
// Replace with new data
this.replaceQCE(qce, newQCE);
} else {
Logger.getInstance().debug(`QCC session ${this.sessionId} - Adding new element to the cache: ${JSON.stringify(newQCE)}`);
// new priority
// Add new element in the cache
this.addQCE(newQCE, newQCE);
}
}
};
/**
* After the merge, loop through priority list and make sure cache is at most capacity. Remove all
* other elements from the list based on priority.
*/
QueryContextCache.prototype.checkCacheCapacity = function () {
Logger.getInstance().trace(
`QCC session ${this.sessionId} - checkCacheCapacity() called. treeSet size ${this.treeSet.size}, cache capacity ${this.capacity}`);
// remove elements based on priority
while (this.treeSet.size > this.capacity) {
const qce = Array.from(this.treeSet).pop();
this.removeQCE(qce);
}
Logger.getInstance().trace(
`QCC session ${this.sessionId} - checkCacheCapacity() returns. treeSet size ${this.treeSet.size}, cache capacity ${this.capacity}`);
};
/** Clear the cache. */
QueryContextCache.prototype.clearCache = function () {
Logger.getInstance().trace(`QCC session ${this.sessionId} - clearCache() called`);
this.idMap.clear();
this.priorityMap.clear();
this.treeSet.clear();
Logger.getInstance().trace(`QCC session ${this.sessionId} - clearCache() returns. Number of entries in cache now ${this.treeSet.size}`);
};
QueryContextCache.prototype.getElements = function () {
return this.treeSet;
};
/**
* @param data: the QueryContext Object serialized as a JSON format string
*/
QueryContextCache.prototype.deserializeQueryContext = function (data) {
const stringifyData = JSON.stringify(data);
Logger.getInstance().debug(`QCC session ${this.sessionId} - deserializeQueryContext() called: data from server: ${stringifyData}`);
if (!data || stringifyData === '{}' || data.entries === null) {
this.clearCache();
Logger.getInstance().trace(`QCC session ${this.sessionId} - deserializeQueryContext() returns`);
this.logCacheEntries();
return;
}
try {
// Deserialize the entries. The first entry with priority is the main entry. An example JSON is:
// {
// "entries": [
// {
// "id": 0,
// "readtimestamp": 123456789,
// "priority": 0,
// "context": "base64 encoded context"
// },
// {
// "id": 1,
// "readtimestamp": 123456789,
// "priority": 1,
// "context": "base64 encoded context"
// },
// {
// "id": 2,
// "readtimestamp": 123456789,
// "priority": 2,
// "context": "base64 encoded context"
// }
// ]
const entries = data.entries;
if (entries !== null && Array.isArray(entries)) {
for (const entryNode of entries) {
const entry = this.deserializeQueryContextElement(entryNode);
if (entry != null) {
this.merge(entry);
} else {
Logger.getInstance().warn(
`QCC session ${this.sessionId} - deserializeQueryContextJson: deserializeQueryContextElement meets mismatch field type. Clear the QueryContextCache.`);
this.clearCache();
return;
}
}
}
} catch (e) {
Logger.getInstance().debug(`QCC session ${this.sessionId} - deserializeQueryContextJson: Exception = ${e.getMessage}`);
// Not rethrowing. clear the cache as incomplete merge can lead to unexpected behavior.
this.clearCache();
}
this.checkCacheCapacity();
this.logCacheEntries();
};
QueryContextCache.prototype.deserializeQueryContextElement = function (node) {
const { id, timestamp, priority, context } = node;
const entry = new QueryContextElement (id, timestamp, priority, null);
if (typeof context === 'string'){
entry.context = context;
} else if (context === null || context === undefined) {
entry.context = null;
Logger.getInstance().debug(`QCC session ${this.sessionId} - deserializeQueryContextElement \`context\` field is empty`);
} else {
Logger.getInstance().warn(`QCC session ${this.sessionId} - deserializeQueryContextElement: \`context\` field is not String type`);
return null;
}
return entry;
};
QueryContextCache.prototype.logCacheEntries = function () {
this.treeSet.forEach(function (elem) {
Logger.getInstance().debug(
`QCC session ${this.sessionId} - Cache Entry: id: ${elem.id} timestamp: ${elem.timestamp} priority: ${elem.priority}`);
}, this);
};
QueryContextCache.prototype.getSize = function () {
return this.treeSet.size;
};
QueryContextCache.prototype.getQueryContextDTO = function () {
const arr = [];
const querycontexts = Array.from(this.getElements());
for (let i = 0; i < this.treeSet.size; i++) {
arr.push({ id: querycontexts[i].id, timestamp: querycontexts[i].timestamp,
priority: querycontexts[i].priority, context: { base64Data: querycontexts[i].context } || null });
}
return {
entries: arr
};
};
QueryContextCache.prototype.getSerializeQueryContext = function () {
const arr = [];
const querycontexts = Array.from(this.getElements());
for (let i = 0; i < this.treeSet.size; i++) {
arr.push({ id: querycontexts[i].id, timestamp: querycontexts[i].timestamp, priority: querycontexts[i].priority, context: querycontexts[i].context || null });
}
return {
entries: arr
};
};
module.exports = QueryContextCache;