aerospike
Version:
Aerospike Client Library
367 lines (304 loc) • 11.6 kB
text/typescript
// *****************************************************************************
// Copyright 2013-2024 Aerospike, 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.
// ****************************************************************************
import Aerospike, {Client, Config, Job, IndexJob, indexDataType, indexType, cdt, InfoAllResponse, AerospikeExp} from 'aerospike';
import options from './util/options';
import * as semver from 'semver';
import { SemVer } from 'semver';
import * as path from 'path';
import { runInNewProcessFn } from './util/run_in_new_process';
import { Suite } from 'mocha';
import * as chai from 'chai';
const expect: any = chai.expect;
(global as any).expect = expect;
export {options}
export const namespace = options.namespace
export const set = options.set
import * as keygen from './generators/key';
import * as metagen from './generators/metadata';
import * as recgen from './generators/record';
import * as valgen from './generators/value';
import * as putgen from './generators/put';
import * as util from './util';
export { keygen, metagen, recgen, valgen, putgen, util };
let testConfigs = options.getConfig()
const config: Config = testConfigs.config
const helper_client_exists = testConfigs.omitHelperClient
let client: any;
client = Aerospike.client(config)
export {client, config}
Aerospike.setDefaultLogging(config.log ?? {})
class UDFHelper {
private client: Client;
constructor(client: Client) {
this.client = client;
}
register(filename: string) {
const script = path.join(__dirname, filename);
return this.client.udfRegister(script)
.then((job: Job) => job.wait(50));
}
remove(filename: string) {
return this.client.udfRemove(filename)
.then((job: Job) => job.wait(50))
.catch((error: any) => {
if (error.code !== Aerospike.status.ERR_UDF) {
return Promise.reject(error);
}
});
}
}
class IndexHelper {
private client: Client;
constructor(client: Client) {
this.client = client;
}
async create(indexName: string, setName: string, binName: string, dataType: indexDataType, indexType: indexType, context?: cdt.Context) {
const index = {
ns: options.namespace,
set: setName,
bin: binName,
index: indexName,
type: indexType || Aerospike.indexType.DEFAULT,
datatype: dataType,
context
}
await this.createIndex(index, false)
}
async createExpIndex(indexName: string, setName: string, exp: AerospikeExp | string, dataType: indexDataType, indexType: indexType) {
const index = {
ns: options.namespace,
set: setName,
exp: exp,
index: indexName,
type: indexType || Aerospike.indexType.DEFAULT,
datatype: dataType,
}
await this.createIndex(index, true)
}
async createIndex(index: any, has_exp: boolean) {
const retries = 3;
for (let attempt = 0; attempt < retries; attempt++) {
try {
let job: IndexJob;
if(has_exp){
job = await this.client.createExpIndex(index)
}
else{
job = await this.client.createIndex(index)
}
await job.wait()
return
}
catch (error: any) {
if (error.code === Aerospike.status.ERR_INDEX_FOUND) {
return;
}
if (attempt === retries - 1) {
throw new Error(`IndexHelper.createIndex function failed with the following error: ${error}`, { cause: error });
}
}
}
}
async remove(indexName: string) {
try {
await this.client.indexRemove(options.namespace, indexName)
}
catch(error: any) {
if (error.code === Aerospike.status.ERR_INDEX_NOT_FOUND) {
// ignore - index does not exist
} else {
throw new Error('IndexHelper.remove function failed with the following error: ', { cause: error });
}
}
}
}
class ServerInfoHelper {
private features: Set<string>;
private edition: string;
private build: string;
private namespaceInfo: { [key: string]: any };
private cluster: any[];
private client: Client;
constructor(client: Client) {
this.features = new Set();
this.edition = 'community';
this.build = '';
this.namespaceInfo = {};
this.cluster = [];
this.client = client;
}
hasFeature(feature: string) {
return this.features.has(feature);
}
isEnterprise() {
return this.edition.match('Enterprise');
}
isVersionInRange(versionRange: string) {
const version: string = process.env.AEROSPIKE_VERSION_OVERRIDE || this.build;
const semverVersion: SemVer | null = semver.coerce(version); // truncate a build number like "4.3.0.2-28-gdd9f506" to just "4.3.0"
return semver.satisfies(semverVersion!, versionRange);
}
supportsTtl() {
const { config } = this.namespaceInfo;
return config['nsup-period'] > 0 || config['allow-ttl-without-nsup'] === 'true';
}
fetchInfo() {
return this.client.infoAll('build\nedition\nfeatures')
.then((results: InfoAllResponse[]) => {
results.forEach((response: InfoAllResponse) => {
const info = Aerospike.info.parse(response.info);
this.edition = info.edition;
this.build = info.build;
const features = info.features;
if (Array.isArray(features)) {
features.forEach(feature => this.features.add(feature));
}
});
});
}
fetchNamespaceInfo(ns: string) {
const nsKey = `namespace/${ns}`;
const cfgKey = `get-config:context=namespace;id=${ns}`;
return this.client.infoAny([nsKey, cfgKey].join('\n'))
.then((results: string) => {
const info = Aerospike.info.parse(results);
this.namespaceInfo = {
info: info[nsKey],
config: info[cfgKey],
};
});
}
randomNode() {
const nodes = this.client.getNodes();
const i = Math.floor(Math.random() * nodes.length);
return nodes[i];
}
}
const udfHelper = new UDFHelper(client)
const indexHelper = new IndexHelper(client)
const serverInfoHelper = new ServerInfoHelper(client)
export const udf = udfHelper
export const index = indexHelper
export const cluster = serverInfoHelper
export function runInNewProcess(fn: Function, data: any) {
if (data === undefined) {
data = null
}
const env = {
NODE_PATH: path.join(process.cwd(), 'node_modules')
}
return runInNewProcessFn(fn, env, data)
}
export function skip(this: any, ctx: Suite, message: string) {
ctx.beforeEach(function (this: any) {
this.skip(message)
})
}
export function skipIf (this: any, ctx: Suite, condition: any, message: string) {
ctx.beforeEach(function (this: any) {
let skip = condition
if (typeof condition === 'function') {
skip = condition()
}
if (skip) {
this.skip(message)
}
})
}
export function skipUnless (ctx: Suite, condition: any, message: string) {
if (typeof condition === 'function') {
skipIf(ctx, () => !condition(), message)
} else {
skipIf(ctx, !condition, message)
}
}
export function skipUnlessSupportsFeature (this: any, feature: string, ctx: Suite) {
skipUnless(ctx, () => this.cluster.hasFeature(feature), `requires server feature "${feature}"`)
}
export function skipUnlessEnterprise(this: any, ctx: Suite) {
skipUnless(ctx, () => this.cluster.isEnterprise(), 'requires enterprise edition')
}
export function skipUnlessVersion(this: any, versionRange: any, ctx: Suite) {
skipUnless(ctx, () => this.cluster.isVersionInRange(versionRange), `cluster version does not meet requirements: "${versionRange}"`)
}
export function skipUnlessVersionAndEnterprise (this: any, versionRange: any, ctx: Suite) {
skipUnless(ctx, () => {
return (this.cluster.isVersionInRange(versionRange) && (this.cluster.isEnterprise())) }, `cluster version does not meet requirements: "${versionRange} and/or requires enterprise"`)
}
export function skipUnlessVersionAndCommunity (this: any, versionRange: any, ctx: Suite) {
skipUnless(ctx, () => {
return (this.cluster.isVersionInRange(versionRange) && (!this.cluster.isEnterprise()))
}, `cluster version does not meet requirements: "${versionRange} and/or requires enterprise"`)
}
export function skipUnlessSupportsTtl(this: any, ctx: Suite) {
skipUnless(ctx, () => this.cluster.supportsTtl(), 'test namespace does not support record TTLs')
}
export function skipUnlessXDR(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testXDR, 'XDR tests disabled')
return options.testXDR
}
export function skipUnlessAdvancedMetrics(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testMetrics, 'Advanced metrics tests disabled')
}
export function skipUnlessStrongConsistency(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testStrongConsistency, 'Advanced metrics tests disabled')
}
export function skipUnlessDynamicConfig(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testDynamicConfig, 'Dynamic config tests disabled')
}
export function skipUnlessMRT(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testMRT, 'MRT tests disabled')
}
export function skipUnlessPreferRack(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testPreferRack, 'Prefer rack tests disabled')
}
export function skipUnlessMetricsKeyBusy(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testMetricsKeyBusy, 'Metrics key busy test disabled')
}
export function skipUnlessTimeoutDelay(this: any, ctx: Suite) {
skipUnless(ctx, () => options.testTimeoutDelay, 'timeout delay test disabled')
}
if (process.env.GLOBAL_CLIENT !== 'false') {
/* global before */
before(() => {
if(helper_client_exists){
client.connect()
.catch((error: any) => {
console.error('ERROR:', error)
console.error('CONFIG:', client.config)
console.error("Client connection failed.")
console.error("Without a valid connection, tests cannot be run.")
console.error("Testing failed, exiting with error")
process.exit(1)
})
.then(() => serverInfoHelper.fetchInfo())
.then(() => serverInfoHelper.fetchNamespaceInfo(options.namespace))
.catch((error: any) => {
console.error('ERROR:', error)
console.error('CONFIG:', client.config)
console.error("Client connection failed, tests cannot be executed. Tests failed, exiting with error.")
process.exit(1)
})
}
})
/* global after */
after(async function () {
if(helper_client_exists){
await client.close()
}
})
}