From 2ab800006c7ebf2859ce5199d579115741fecabe Mon Sep 17 00:00:00 2001 From: Jan Hecking Date: Thu, 1 Feb 2018 18:38:41 +0800 Subject: [PATCH 1/4] Refactor AerospikeError; add inDoubt property --- lib/aerospike.js | 9 +- lib/commands/command.js | 9 +- lib/error.js | 217 ++++++++++++++++++----------------- src/main/util/conversions.cc | 1 + test/error.js | 89 ++++++++++---- 5 files changed, 192 insertions(+), 133 deletions(-) diff --git a/lib/aerospike.js b/lib/aerospike.js index f056f35f..3bca08b7 100644 --- a/lib/aerospike.js +++ b/lib/aerospike.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -17,7 +17,8 @@ 'use strict' const as = require('bindings')('aerospike.node') -const asEventLoop = require('./event_loop') +const AerospikeError = require('./error') +const EventLoop = require('./event_loop') const utils = require('./utils') /** @@ -120,7 +121,7 @@ exports.status = require('./status') * * @summary {@link AerospikeError} class */ -exports.AerospikeError = require('./error') +exports.AerospikeError = AerospikeError /** * The main interface of the Aerospike client. Through the Client class, @@ -319,7 +320,7 @@ exports.indexType = as.indexType // ======================================================================== exports.print = utils.print -exports.releaseEventLoop = asEventLoop.releaseEventLoop +exports.releaseEventLoop = EventLoop.releaseEventLoop let Client = exports.Client diff --git a/lib/commands/command.js b/lib/commands/command.js index 315b3070..04238519 100644 --- a/lib/commands/command.js +++ b/lib/commands/command.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ 'use strict' const AerospikeError = require('../error') -const status = require('../status') class Command { constructor (client, name, args, callback) { @@ -30,7 +29,7 @@ class Command { captureStackTrace () { if (this.captureStackTraces) { - this.stackTrace = AerospikeError.captureStackTrace() + AerospikeError.captureStackTrace(this, this.captureStackTrace) } } @@ -39,7 +38,7 @@ class Command { } convertError (error) { - return AerospikeError.fromASError(error, this.stackTrace) + return AerospikeError.fromASError(error, this) } convertResult (arg1, arg2, arg3) { @@ -109,7 +108,7 @@ class Command { } sendError (message) { - let error = new AerospikeError(status.ERR_CLIENT, message) + let error = new AerospikeError(message, this) if (this.expectsPromise()) { return Promise.reject(error) } else { diff --git a/lib/error.js b/lib/error.js index ba6e2089..12b9c24a 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -17,128 +17,137 @@ 'use strict' const status = require('./status') -const util = require('util') /** - * @class AerospikeError - * @extends Error - * @classdesc Error status returned by the server. - * - * @summary Construct a new AerospikeError instance. - * - * @param {number} code - The status code of the error. - * @param {string} [message] - A message describing the status code. - * @param {string} [func] - The name of the function in which the error occurred. - * @param {string} [file] - The file name in which the error occurred. - * @param {string} [line] - The line number on which the error occurred. + * Error raised by the client when execution of a database command fails. This + * may be either due to an error status code returned by the server, or caused + * by an error condition that occured on the client side. * - * @see Aerospike.status contains the full list of possible status codes. + * @extends Error * - * @example + * @example Expected output: "Error: 127.0.0.1:3000 Record does not exist in database. May be returned by read, or write with policy Aerospike.policy.exists.UPDATE [2]" * * const Aerospike = require('aerospike') - * var key = new Aerospike.Key('test', 'key', 'does_not_exist') - * Aerospike.connect((error, client) => { - * if (error) throw error - * client.get(key, (error, record) => { - * console.log(error) // => { [AerospikeError: ERR_RECORD_NOT_FOUND] - * // code: 2, - * // message: 'ERR_RECORD_NOT_FOUND', - * // func: 'as_event_command_parse_result', - * // file: 'src/main/aerospike/as_event.c', - * // line: 614, - * // name: 'AerospikeError' } + * let key = new Aerospike.Key('test', 'key', 'does_not_exist') + * Aerospike.connect() + * .then(client => { + * client.get(key) + * .then(record => console.info(record)) + * .catch(error => console.error(`Error: ${error.message} [${error.code}]`)) + * .then(() => client.close()) * }) - * client.close() - * }) */ -function AerospikeError (code, message, func, file, line, stack) { - /** - * Error name - * - * @name AerospikeError#name - * @type {string} - * @readonly - */ - Object.defineProperty(this, 'name', {value: 'AerospikeError'}) +class AerospikeError extends Error { + /** @private */ + constructor (message, command) { + super(message) + this.name = this.constructor.name - /** - * Error message - * - * @name AerospikeError#message - * @type {string} - * @readonly - */ - this.message = message || 'Aerospike Error' + /** + * Numeric status code returned by the server or the client. + * + * @type {number} + * @readonly + * + * @see {@link module:aerospike.status} contains the full list of possible status codes. + */ + this.code = status.ERR_CLIENT - /** - * Status code. - * - * @name AerospikeError#code - * @type {number} - * @readonly - * - * @see List of status codes defined at {@link module:aerospike.status} - */ - this.code = code + /** + * Command during which the error occurred. + * + * @type {?Command} + * @readonly + */ + this.command = command || null - /** - * C/C++ function name where the error occurred. - * - * @name AerospikeError#func - * @type {?string} - * @readonly - */ - this.func = func + /** + * C/C++ function name in which the error occurred. + * + * @type {?string} + * @readonly + */ + this.func = null - /** - * File name of the C/C++ source file in which the error occurred. - * - * @name AerospikeError#file - * @type {?string} - * @readonly - */ - this.file = file + /** + * File name of the C/C++ source file in which the error occurred. + * + * @type {?string} + * @readonly + */ + this.file = null - /** - * Line number in the C/C++ source file in which the error occurred. - * - * @name AerospikeError#file - * @type {?string} - * @readonly - */ - this.line = line + /** + * Line number in the C/C++ source file in which the error occurred. + * + * @type {?number} + * @readonly + */ + this.line = null - if (stack) { - stack = stack.replace(/^.*$/m, util.format('%s: %s', this.name, this.message)) - Object.defineProperty(this, 'stack', {value: stack}) - } else { - Error.captureStackTrace(this, this.constructor) + /** + * It is possible that a write transaction completed even though the client + * returned this error. This may be the case when a client error occurs + * (like timeout) after the command was sent to the server. + * + * @type {boolean} + * @readonly + */ + this.inDoubt = false + + if (command && command.stack) { + this.setStackTrace(command.stack) + } } -} -/** @private */ -AerospikeError.fromASError = function (err, stackTrace) { - if (!err) { - return null - } else if (err.code === status.OK) { - return null - } else if (err instanceof AerospikeError) { - return err - } else { - let message = err.message - if (message.startsWith('AEROSPIKE_')) { - message = status.getMessage(err.code) + /** @private */ + static fromASError (asError, command) { + if (!asError) { + return null + } else if (asError.code === status.OK) { + return null + } else if (asError instanceof AerospikeError) { + return asError + } else { + let message = this.formatMessage(asError.message, asError.code) + let error = new AerospikeError(message, command) + this.copyASErrorProperties(error, asError) + return error } - return new AerospikeError(err.code, message, err.func, err.file, err.line, stackTrace) } -} -/** @private */ -AerospikeError.captureStackTrace = function () { - return new AerospikeError().stack -} + /** @private */ + static copyASErrorProperties (target, source) { + target.code = source.code + target.inDoubt = source.inDoubt + target.func = source.func + target.file = source.file + target.line = Number.parseInt(source.line) + } + + /** @private */ + static formatMessage (message, code) { + if (message) { + message = message.replace(/AEROSPIKE_[A-Z_]+/, () => status.getMessage(code)) + } + return message + } + + /** @private */ + setStackTrace (stack) { + let firstLine = `${this.name}: ${this.message}` + stack = stack.replace(/^.*$/m, firstLine) + Object.defineProperty(this, 'stack', {value: stack}) + } -util.inherits(AerospikeError, Error) + /** + * Indicates whether the error originated on the database server. + * + * @returns {boolean} - true if the server raised the error, false otherwise. + */ + isServerError () { + return this.code > status.OK + } +} module.exports = AerospikeError diff --git a/src/main/util/conversions.cc b/src/main/util/conversions.cc index 27845e05..7dbde1ba 100644 --- a/src/main/util/conversions.cc +++ b/src/main/util/conversions.cc @@ -542,6 +542,7 @@ Local error_to_jsobject(as_error* error, const LogInfo* log) err->Set(Nan::New("func").ToLocalChecked(), error->func ? Nan::New(error->func).ToLocalChecked() : Nan::New("\0").ToLocalChecked() ); err->Set(Nan::New("file").ToLocalChecked(), error->file ? Nan::New(error->file).ToLocalChecked() : Nan::New("\0").ToLocalChecked() ); err->Set(Nan::New("line").ToLocalChecked(), error->line ? Nan::New(error->line) : Nan::New((uint32_t)0) ); + err->Set(Nan::New("inDoubt").ToLocalChecked(), error->in_doubt ? Nan::True() : Nan::False()); return scope.Escape(err); } diff --git a/test/error.js b/test/error.js index c94fe3e9..e35987d2 100644 --- a/test/error.js +++ b/test/error.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -19,10 +19,12 @@ /* global expect, describe, it */ const AerospikeError = require('../lib/error') +const status = require('../lib/status') + require('./test_helper.js') describe('AerospikeError', function () { - describe('new AerospikeError()', function () { + describe('constructor', function () { it('creates a new AerospikeError instance', function () { expect(new AerospikeError()).to.be.a(AerospikeError) }) @@ -31,44 +33,91 @@ describe('AerospikeError', function () { expect(new AerospikeError()).to.be.a(Error) }) - it('sets the error code, message, function, file and line information', function () { - var subject = new AerospikeError(-1, 'client error', 'connect', 'lib/client.js', 101) - expect(subject.code).to.be(-1) - expect(subject.message).to.be('client error') - expect(subject.func).to.be('connect') - expect(subject.file).to.be('lib/client.js') - expect(subject.line).to.be(101) + it('initializes the error with default values', function () { + let subject = new AerospikeError() + expect(subject.message).to.be('') + expect(subject.code).to.be(status.ERR_CLIENT) + expect(subject.command).to.be(null) + expect(subject.func).to.be(null) + expect(subject.file).to.be(null) + expect(subject.line).to.be(null) + expect(subject.inDoubt).to.be(false) + }) + + it('sets an error message', function () { + let subject = new AerospikeError('Dooh!') + expect(subject.message).to.be('Dooh!') + }) + + it('keeps a reference to the command', function () { + let cmd = {} + let subject = new AerospikeError('Dooh!', cmd) + expect(subject.command).to.be(cmd) }) - it('captures a stack trace', function () { - var subject = new AerospikeError(-1, 'client error', 'connect', 'lib/client.js', 101) - var stack = subject.stack.split('\n') + it('captures a stacktrace', function () { + let subject = new AerospikeError('Dooh!') + let stack = subject.stack.split('\n') expect(stack).to.not.be.empty() - expect(stack.shift()).to.be('AerospikeError: client error') + expect(stack.shift()).to.be('AerospikeError: Dooh!') + }) + + it('copies the stacktrace of the command', function () { + let cmd = {} + Error.captureStackTrace(cmd) + let subject = new AerospikeError('Dooh!', cmd) + let expected = ['AerospikeError: Dooh!'].concat(cmd.stack.split('\n').slice(1)).join('\n') + expect(subject.stack).to.equal(expected) }) }) describe('.fromASError', function () { it('copies the info from a AerospikeClient error instance', function () { - var error = {code: -1, message: 'client error', func: 'connect', file: 'lib/client.js', line: 101} - var subject = AerospikeError.fromASError(error) - expect(subject.code).to.be(-1) - expect(subject.message).to.be('client error') + let error = { + code: -11, + message: 'Dooh!', + func: 'connect', + file: 'lib/client.js', + line: 101, + inDoubt: true + } + let subject = AerospikeError.fromASError(error) + expect(subject.code).to.be(-11) + expect(subject.message).to.be('Dooh!') expect(subject.func).to.be('connect') expect(subject.file).to.be('lib/client.js') expect(subject.line).to.be(101) + expect(subject.inDoubt).to.be(true) + }) + + it('replaces error codes with descriptive messages', function () { + let error = { + code: status.ERR_RECORD_NOT_FOUND, + message: '127.0.0.1:3000 AEROSPIKE_ERR_RECORD_NOT_FOUND' + } + let subject = AerospikeError.fromASError(error) + expect(subject.message).to.be('127.0.0.1:3000 Record does not exist in database. May be returned by read, or write with policy Aerospike.policy.exists.UPDATE') }) it('returns an AerospikeError instance unmodified', function () { - let error = new AerospikeError(-1, 'client error', 'connect', 'lib/client.js', 101) + let error = new AerospikeError('Dooh!') expect(AerospikeError.fromASError(error)).to.equal(error) }) + + it('returns null if the status code is OK', function () { + let error = { code: status.OK } + expect(AerospikeError.fromASError(error)).to.be(null) + }) + + it('returns null if no error is passed', function () { + expect(AerospikeError.fromASError(null)).to.be(null) + }) }) describe('#toString()', function () { it('sets an informative error message', function () { - var subject = new AerospikeError(-1, 'client error', 'connect', 'lib/client.js', 101) - expect(subject.toString()).to.eql('AerospikeError: client error') + let subject = new AerospikeError('Dooh!') + expect(subject.toString()).to.eql('AerospikeError: Dooh!') }) }) }) From 032a18d9e12e24b1370a0f0f721163332e38cb2d Mon Sep 17 00:00:00 2001 From: Jan Hecking Date: Thu, 1 Feb 2018 23:30:36 +0800 Subject: [PATCH 2/4] Document public Command interface Since Command instances are not referenced from AerospikeError, we need to document the public interface. --- lib/client.js | 28 ++++++++++++++++++-- lib/commands/command.js | 57 ++++++++++++++++++++++++++++++++++++++++- test/command.js | 42 ++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 test/command.js diff --git a/lib/client.js b/lib/client.js index 97076d20..1373aca1 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -143,7 +143,31 @@ function Client (config) { /** @private */ this.connected = false - /** @private */ + /** + * @name Client#captureStackTraces + * + * @summary Set to true to enable capturing of debug stacktraces for + * every database command. + * + * @description The client will capture a stacktrace before each database + * command is executed, instead of capturing the stacktrace only when an + * error is raised. This generally results in much more useful stacktraces + * that include stackframes from the calling application issuing the database + * command. + * + * **Note:** Enabling this feature incurs a significant performance overhead for + * every database command. It is recommended to leave this feature disabled + * in production environments. + * + * By default, the client will set this flag to true, if the + * AEROSPIKE_DEBUG_STACKTRACES environment variable is set (to + * any value). + * + * @type {boolean} + * @default true, if + * process.env.AEROSPIKE_DEBUG_STACKTRACES is set; + * false otherwise. + */ this.captureStackTraces = !!process.env.AEROSPIKE_DEBUG_STACKTRACES } diff --git a/lib/commands/command.js b/lib/commands/command.js index 04238519..a46709f3 100644 --- a/lib/commands/command.js +++ b/lib/commands/command.js @@ -18,39 +18,89 @@ const AerospikeError = require('../error') +// Command is an abstract template (aka "mix-in") for concrete command +// subclasses, that execute a specific database command method on the native +// Aerospike add-on. A concrete command subclass should be defined like this, +// where "getAsync" is the native command method to call: +// +// class GetCommand extends Command("getAsync") { +// // ... +// } + +/** + * Database command. + */ class Command { + /** @private */ constructor (client, name, args, callback) { + /** + * Client instance used to execute this command. + * + * @name Command#client + * @type {Client} + * @readonly + */ this.client = client this.name = name + + /** @private */ this.args = args - if (callback) this.callback = callback + + if (callback) { + /** @private */ + this.callback = callback + } + + /** + * Whether debug stacktraces are enabled. + * + * @name Command#captureStackTraces + * @type {boolean} + * @readonly + * @see {@link Client#captureStackTraces} + */ this.captureStackTraces = client.captureStackTraces + + /** + * The record key for which the command was issued. (Single-key commands only.) + * + * @name Command#key + * @type {?Key} + * @readonly + */ + this.key = undefined } + /** @private */ captureStackTrace () { if (this.captureStackTraces) { AerospikeError.captureStackTrace(this, this.captureStackTrace) } } + /** @private */ connected () { return this.client.isConnected(false) } + /** @private */ convertError (error) { return AerospikeError.fromASError(error, this) } + /** @private */ convertResult (arg1, arg2, arg3) { return arg1 } + /** @private */ convertResponse (err, arg1, arg2, arg3) { let error = this.convertError(err) let result = this.convertResult(arg1, arg2, arg3) return [error, result] } + /** @private */ execute () { if (!this.connected()) { return this.sendError('Not connected.') @@ -65,6 +115,7 @@ class Command { } } + /** @private */ executeWithCallback (callback) { // C client will call the callback function synchronously under certain error // conditions; if we detect a synchronous callback we need to schedule the JS @@ -80,6 +131,7 @@ class Command { sync = false // if we get here before the cb was called the cb is async } + /** @private */ executeAndReturnPromise () { return new Promise((resolve, reject) => { this.process((error, result) => { @@ -92,10 +144,12 @@ class Command { }) } + /** @private */ expectsPromise () { return !this.callback } + /** @private */ process (cb) { let asCallback = (err, arg1, arg2, arg3) => { let tmp = this.convertResponse(err, arg1, arg2, arg3) @@ -107,6 +161,7 @@ class Command { this.client.asExec(this.name, asArgs) } + /** @private */ sendError (message) { let error = new AerospikeError(message, this) if (this.expectsPromise()) { diff --git a/test/command.js b/test/command.js new file mode 100644 index 00000000..ad2f1d11 --- /dev/null +++ b/test/command.js @@ -0,0 +1,42 @@ +// ***************************************************************************** +// Copyright 2013-2018 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. +// ***************************************************************************** + +'use strict' + +/* eslint-env mocha */ +/* global expect */ + +require('./test_helper') +const Command = require('../lib/commands/command') + +describe('Command', function () { + context('Extend Command', function () { + class TestCommand extends Command('testCmd') { + foo () { return 'bar' } + } + + it('creates subclasses with informative constructor names', function () { + let cmd = new TestCommand({}) + expect(cmd.constructor.name).to.be('TestCommand') + }) + + it('keeps a reference to the client instance', function () { + let client = {} + let cmd = new TestCommand(client) + expect(cmd.client).to.be(client) + }) + }) +}) From e1403723a33b1d4ad47a28ea48f912344728e471 Mon Sep 17 00:00:00 2001 From: Jan Hecking Date: Fri, 2 Feb 2018 00:00:29 +0800 Subject: [PATCH 3/4] Refactor commands Turn the Command class into an abstract template to generate concrete subclasses for every individual database command. --- lib/client.js | 50 ++++++++++++-------------- lib/commands/batch_command.js | 6 ++-- lib/commands/command.js | 17 +++++---- lib/commands/exists_command.js | 6 ++-- lib/commands/index.js | 52 ++++++++++++++++++++++++++++ lib/commands/read_record_command.js | 13 ++++--- lib/commands/stream_command.js | 10 +++--- lib/commands/write_record_command.js | 13 ++++--- lib/job.js | 6 ++-- lib/query.js | 17 +++++---- lib/scan.js | 9 +++-- 11 files changed, 122 insertions(+), 77 deletions(-) create mode 100644 lib/commands/index.js diff --git a/lib/client.js b/lib/client.js index 1373aca1..860206f4 100644 --- a/lib/client.js +++ b/lib/client.js @@ -22,17 +22,13 @@ const EventEmitter = require('events') const as = require('bindings')('aerospike.node') const AerospikeError = require('./error') -const BatchCommand = require('./commands/batch_command') -const Command = require('./commands/command') +const Commands = require('./commands') const Config = require('./config') -const ExistsCommand = require('./commands/exists_command') +const EventLoop = require('./event_loop') const IndexJob = require('./index_job') const Query = require('./query') -const ReadRecordCommand = require('./commands/read_record_command') const Scan = require('./scan') const UdfJob = require('./udf_job') -const WriteRecordCommand = require('./commands/write_record_command') -const asEventLoop = require('./event_loop') const operations = require('./operations') const utils = require('./utils') @@ -259,7 +255,7 @@ Client.prototype.batchExists = function (keys, policy, callback) { policy = null } - let cmd = new BatchCommand(this, 'batchExists', [keys, policy], callback) + let cmd = new Commands.BatchExists(this, [keys, policy], callback) return cmd.execute() } @@ -312,7 +308,7 @@ Client.prototype.batchGet = function (keys, policy, callback) { policy = null } - let cmd = new BatchCommand(this, 'batchGet', [keys, policy], callback) + let cmd = new Commands.BatchGet(this, [keys, policy], callback) return cmd.execute() } @@ -367,7 +363,7 @@ Client.prototype.batchRead = function (records, policy, callback) { policy = null } - let cmd = new BatchCommand(this, 'batchRead', [records, policy], callback) + let cmd = new Commands.BatchRead(this, [records, policy], callback) return cmd.execute() } @@ -422,7 +418,7 @@ Client.prototype.batchSelect = function (keys, bins, policy, callback) { policy = null } - let cmd = new BatchCommand(this, 'batchSelect', [keys, bins, policy], callback) + let cmd = new Commands.BatchSelect(this, [keys, bins, policy], callback) return cmd.execute() } @@ -454,7 +450,7 @@ Client.prototype.close = function (releaseEventLoop) { _connectedClients -= 1 } if (releaseEventLoop && _connectedClients === 0) { - asEventLoop.releaseEventLoop() + EventLoop.releaseEventLoop() } } @@ -505,7 +501,7 @@ Client.prototype.close = function (releaseEventLoop) { * }) */ Client.prototype.connect = function (callback) { - asEventLoop.registerASEventLoop() + EventLoop.registerASEventLoop() let eventsCb = eventsCallback.bind(this) this.as_client.setupEventCb(eventsCb) @@ -651,7 +647,7 @@ Client.prototype.createIndex = function (options, policy, callback) { policy ] - let cmd = new Command(this, 'indexCreate', args, callback) + let cmd = new Commands.IndexCreate(this, args, callback) cmd.convertResult = () => new IndexJob(this, options.ns, options.index) return cmd.execute() } @@ -849,7 +845,7 @@ Client.prototype.apply = function (key, udfArgs, policy, callback) { policy = null } - let cmd = new Command(this, 'applyAsync', [key, udfArgs, policy], callback) + let cmd = new Commands.Apply(this, [key, udfArgs, policy], callback) return cmd.execute() } @@ -893,7 +889,7 @@ Client.prototype.exists = function exists (key, policy, callback) { policy = null } - let cmd = new ExistsCommand(this, 'existsAsync', [key, policy], callback) + let cmd = new Commands.Exists(this, [key, policy], callback) return cmd.execute() } @@ -929,7 +925,7 @@ Client.prototype.get = function (key, policy, callback) { policy = null } - let cmd = new ReadRecordCommand(this, 'getAsync', [key, policy], callback) + let cmd = new Commands.Get(this, key, [policy], callback) return cmd.execute() } @@ -963,7 +959,7 @@ Client.prototype.indexRemove = function (namespace, index, policy, callback) { policy = null } - let cmd = new Command(this, 'indexRemove', [namespace, index, policy], callback) + let cmd = new Commands.IndexRemove(this, [namespace, index, policy], callback) return cmd.execute() } @@ -1009,7 +1005,7 @@ Client.prototype.info = function (request, host, policy, callback) { host = utils.parseHostString(host) } - let cmd = new Command(this, 'info', [request, host, policy], callback) + let cmd = new Commands.Info(this, [request, host, policy], callback) return cmd.execute() } @@ -1051,7 +1047,7 @@ Client.prototype.infoAny = function (request, policy, callback) { policy = null } - let cmd = new Command(this, 'info', [request, null, policy], callback) + let cmd = new Commands.Info(this, [request, null, policy], callback) return cmd.execute() } @@ -1096,7 +1092,7 @@ Client.prototype.infoAll = function (request, policy, callback) { policy = null } - let cmd = new Command(this, 'infoForeach', [request, policy], callback) + let cmd = new Commands.InfoForeach(this, [request, policy], callback) return cmd.execute() } @@ -1169,7 +1165,7 @@ Client.prototype.operate = function (key, operations, metadata, policy, callback metadata = null } - let cmd = new ReadRecordCommand(this, 'operateAsync', [key, operations, metadata, policy], callback) + let cmd = new Commands.Operate(this, key, [operations, metadata, policy], callback) return cmd.execute() } @@ -1312,7 +1308,7 @@ Client.prototype.put = function (key, bins, meta, policy, callback) { policy = null } - let cmd = new WriteRecordCommand(this, 'putAsync', [key, bins, meta, policy], callback) + let cmd = new Commands.Put(this, key, [bins, meta, policy], callback) return cmd.execute() } @@ -1373,7 +1369,7 @@ Client.prototype.remove = function (key, policy, callback) { policy = null } - let cmd = new WriteRecordCommand(this, 'removeAsync', [key, policy], callback) + let cmd = new Commands.Remove(this, key, [policy], callback) return cmd.execute() } @@ -1430,7 +1426,7 @@ Client.prototype.select = function (key, bins, policy, callback) { policy = null } - let cmd = new ReadRecordCommand(this, 'selectAsync', [key, bins, policy], callback) + let cmd = new Commands.Select(this, key, [bins, policy], callback) return cmd.execute() } @@ -1470,7 +1466,7 @@ Client.prototype.truncate = function (ns, set, beforeNanos, policy, callback) { policy = null } - let cmd = new Command(this, 'truncate', [ns, set, beforeNanos, policy], callback) + let cmd = new Commands.Truncate(this, [ns, set, beforeNanos, policy], callback) return cmd.execute() } @@ -1530,7 +1526,7 @@ Client.prototype.udfRegister = function (udfPath, udfType, policy, callback) { udfType = null } - let cmd = new Command(this, 'udfRegister', [udfPath, udfType, policy], callback) + let cmd = new Commands.UdfRegister(this, [udfPath, udfType, policy], callback) cmd.convertResult = () => { let module = path.basename(udfPath) return new UdfJob(this, module, UdfJob.REGISTER) @@ -1583,7 +1579,7 @@ Client.prototype.udfRemove = function (udfModule, policy, callback) { policy = null } - let cmd = new Command(this, 'udfRemove', [udfModule, policy], callback) + let cmd = new Commands.UdfRemove(this, [udfModule, policy], callback) cmd.convertResult = () => new UdfJob(this, udfModule, UdfJob.UNREGISTER) return cmd.execute() } diff --git a/lib/commands/batch_command.js b/lib/commands/batch_command.js index c9650529..d177b46d 100644 --- a/lib/commands/batch_command.js +++ b/lib/commands/batch_command.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ class BatchResult { } } -class BatchCommand extends Command { +module.exports = asCommand => class BatchCommand extends Command(asCommand) { convertResult (results) { if (!results) return [] @@ -36,5 +36,3 @@ class BatchCommand extends Command { }) } } - -module.exports = BatchCommand diff --git a/lib/commands/command.js b/lib/commands/command.js index a46709f3..af5a1833 100644 --- a/lib/commands/command.js +++ b/lib/commands/command.js @@ -28,11 +28,12 @@ const AerospikeError = require('../error') // } /** - * Database command. + * @class Command + * @classdesc Database command. */ -class Command { +module.exports = asCommand => class Command { /** @private */ - constructor (client, name, args, callback) { + constructor (client, args, callback) { /** * Client instance used to execute this command. * @@ -41,7 +42,6 @@ class Command { * @readonly */ this.client = client - this.name = name /** @private */ this.args = args @@ -149,6 +149,11 @@ class Command { return !this.callback } + /** @private */ + asCommand () { + return asCommand + } + /** @private */ process (cb) { let asCallback = (err, arg1, arg2, arg3) => { @@ -158,7 +163,7 @@ class Command { return cb(error, result) } let asArgs = this.args.concat([asCallback]) - this.client.asExec(this.name, asArgs) + this.client.asExec(this.asCommand(), asArgs) } /** @private */ @@ -171,5 +176,3 @@ class Command { } } } - -module.exports = Command diff --git a/lib/commands/exists_command.js b/lib/commands/exists_command.js index d26d9cd9..93a08c32 100644 --- a/lib/commands/exists_command.js +++ b/lib/commands/exists_command.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ const status = require('../status') const Command = require('./command') -class ExistsCommand extends Command { +module.exports = asCommand => class ExistsCommand extends Command(asCommand) { convertResponse (error) { error = this.convertError(error) if (error && error.code === status.ERR_RECORD_NOT_FOUND) { @@ -31,5 +31,3 @@ class ExistsCommand extends Command { } } } - -module.exports = ExistsCommand diff --git a/lib/commands/index.js b/lib/commands/index.js new file mode 100644 index 00000000..1174cabb --- /dev/null +++ b/lib/commands/index.js @@ -0,0 +1,52 @@ +// ***************************************************************************** +// Copyright 2013-2018 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. +// ***************************************************************************** + +'use strict' + +const BatchCommand = require('./batch_command') +const Command = require('./command') +const ExistsCommandBase = require('./exists_command') +const ReadRecordCommand = require('./read_record_command') +const StreamCommand = require('./stream_command') +const WriteRecordCommand = require('./write_record_command') + +module.exports = { + Apply: class ApplyCommand extends Command('applyAsync') { }, + BatchExists: class BatchExistsCommand extends BatchCommand('batchExists') { }, + BatchGet: class BatchGetCommand extends BatchCommand('batchGet') { }, + BatchRead: class BatchReadCommand extends BatchCommand('batchRead') { }, + BatchSelect: class BatchSelectCommand extends BatchCommand('batchSelect') { }, + Exists: class ExistsCommand extends ExistsCommandBase('existsAsync') { }, + Get: class GetCommand extends ReadRecordCommand('getAsync') { }, + IndexCreate: class IndexCreateCommand extends Command('indexCreate') { }, + IndexRemove: class IndexRemoveCommand extends Command('indexRemove') { }, + Info: class InfoCommand extends Command('info') { }, + InfoForeach: class InfoForeachCommand extends Command('infoForeach') { }, + JobInfo: class JobInfoCommand extends Command('jobInfo') { }, + Operate: class OperateCommand extends ReadRecordCommand('operateAsync') { }, + Put: class PutCommand extends WriteRecordCommand('putAsync') { }, + Query: class QueryCommand extends StreamCommand('queryAsync') { }, + QueryApply: class QueryApplyCommand extends Command('queryApply') { }, + QueryBackground: class QueryBackgroundCommand extends Command('queryBackground') { }, + QueryForeach: class QueryForeachCommand extends StreamCommand('queryForeach') { }, + Remove: class RemoveCommand extends WriteRecordCommand('removeAsync') { }, + Scan: class ScanCommand extends StreamCommand('scanAsync') { }, + ScanBackground: class ScanBackgroundCommand extends Command('scanBackground') { }, + Select: class SelectCommand extends ReadRecordCommand('selectAsync') { }, + Truncate: class TruncateCommand extends Command('truncate') { }, + UdfRegister: class UdfRegisterCommand extends Command('udfRegister') { }, + UdfRemove: class UdfRemoveCommand extends Command('udfRemove') { } +} diff --git a/lib/commands/read_record_command.js b/lib/commands/read_record_command.js index 1f232808..a89d28f2 100644 --- a/lib/commands/read_record_command.js +++ b/lib/commands/read_record_command.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -19,15 +19,14 @@ const Command = require('./command') const Record = require('../record') -class ReadRecordCommand extends Command { - constructor (client, name, args, callback) { - super(client, name, args, callback) - this.key = args[0] +module.exports = asCommand => class ReadRecordCommand extends Command(asCommand) { + constructor (client, key, args, callback) { + args = [key].concat(args) + super(client, args, callback) + this.key = key } convertResult (bins, metadata) { return new Record(this.key, bins, metadata) } } - -module.exports = ReadRecordCommand diff --git a/lib/commands/stream_command.js b/lib/commands/stream_command.js index 6a21e984..24a7c8a2 100644 --- a/lib/commands/stream_command.js +++ b/lib/commands/stream_command.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -22,9 +22,9 @@ const Record = require('../record') const EndOfStream = {} -class StreamCommand extends Command { - constructor (stream, name, args) { - super(stream.client, name, args) +module.exports = asCommand => class StreamCommand extends Command(asCommand) { + constructor (stream, args) { + super(stream.client, args) this.stream = stream } @@ -45,5 +45,3 @@ class StreamCommand extends Command { return new Record(key, bins, meta) } } - -module.exports = StreamCommand diff --git a/lib/commands/write_record_command.js b/lib/commands/write_record_command.js index 2ce7f456..b6eee7f1 100644 --- a/lib/commands/write_record_command.js +++ b/lib/commands/write_record_command.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -18,15 +18,14 @@ const Command = require('./command') -class WriteRecordCommand extends Command { - constructor (client, name, args, callback) { - super(client, name, args, callback) - this.key = args[0] +module.exports = asCommand => class WriteRecordCommand extends Command(asCommand) { + constructor (client, key, args, callback) { + args = [key].concat(args) + super(client, args, callback) + this.key = key } convertResult () { return this.key } } - -module.exports = WriteRecordCommand diff --git a/lib/job.js b/lib/job.js index 6df0d50c..ce81916d 100644 --- a/lib/job.js +++ b/lib/job.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ 'use strict' const as = require('bindings')('aerospike.node') -const Command = require('./commands/command') +const Commands = require('./commands') const DEFAULT_POLL_INTERVALL = 1000 @@ -130,7 +130,7 @@ Job.prototype.info = function (policy, callback) { policy = null } - let cmd = new Command(this.client, 'jobInfo', [this.jobID, this.module, policy], callback) + let cmd = new Commands.JobInfo(this.client, [this.jobID, this.module, policy], callback) return cmd.execute() } diff --git a/lib/query.js b/lib/query.js index bbb3acec..808a5ad1 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -16,10 +16,9 @@ 'use strict' -const Command = require('./commands/command') +const Commands = require('./commands') const Job = require('./job') const RecordStream = require('./record_stream') -const StreamCommand = require('./commands/stream_command') const assert = require('assert') const util = require('util') @@ -324,9 +323,13 @@ Query.prototype.foreach = function (policy, dataCb, errorCb, endCb) { if (errorCb) stream.on('error', errorCb) if (endCb) stream.on('end', endCb) - let cmdName = this.udf ? 'queryForeach' : 'queryAsync' let args = [this.ns, this.set, this, policy] - let cmd = new StreamCommand(stream, cmdName, args) + let cmd + if (this.udf) { + cmd = new Commands.QueryForeach(stream, args) + } else { + cmd = new Commands.Query(stream, args) + } cmd.execute() return stream @@ -363,7 +366,7 @@ Query.prototype.apply = function (udfModule, udfFunction, udfArgs, policy, callb args: udfArgs } - let cmd = new Command(this.client, 'queryApply', [this.ns, this.set, this, policy], callback) + let cmd = new Commands.QueryApply(this.client, [this.ns, this.set, this, policy], callback) return cmd.execute() } @@ -407,7 +410,7 @@ Query.prototype.background = function (udfModule, udfFunction, udfArgs, policy, } queryID = queryID || Job.safeRandomJobID() - let cmd = new Command(this.client, 'queryBackground', [this.ns, this.set, this, policy, queryID], callback) + let cmd = new Commands.QueryBackground(this.client, [this.ns, this.set, this, policy, queryID], callback) cmd.convertResult = () => { let module = this.filters.length > 0 ? 'query' : 'scan' return new Job(this.client, queryID, module) diff --git a/lib/scan.js b/lib/scan.js index 5d9a5d64..d56029bd 100644 --- a/lib/scan.js +++ b/lib/scan.js @@ -1,5 +1,5 @@ // ***************************************************************************** -// Copyright 2013-2017 Aerospike, Inc. +// Copyright 2013-2018 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. @@ -16,10 +16,9 @@ 'use strict' -const Command = require('./commands/command') +const Commands = require('./commands') const Job = require('./job') const RecordStream = require('./record_stream') -const StreamCommand = require('./commands/stream_command') /** * @class Scan @@ -221,7 +220,7 @@ Scan.prototype.background = function (udfModule, udfFunction, udfArgs, policy, s } scanID = scanID || Job.safeRandomJobID() - let cmd = new Command(this.client, 'scanBackground', [this.ns, this.set, this, policy, scanID], callback) + let cmd = new Commands.ScanBackground(this.client, [this.ns, this.set, this, policy, scanID], callback) cmd.convertResult = () => new Job(this.client, scanID, 'scan') return cmd.execute() } @@ -246,7 +245,7 @@ Scan.prototype.foreach = function (policy, dataCb, errorCb, endCb) { var scanID = Job.safeRandomJobID() let args = [this.ns, this.set, this, policy, scanID] - let cmd = new StreamCommand(stream, 'scanAsync', args) + let cmd = new Commands.Scan(stream, args) cmd.execute() stream.job = new Job(this.client, scanID, 'scan') From 83e7448b18022f5f2118e76c730286b5d1b1f97d Mon Sep 17 00:00:00 2001 From: Jan Hecking Date: Fri, 2 Feb 2018 11:53:39 +0800 Subject: [PATCH 4/4] Add test cases for isServerError() --- test/error.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/error.js b/test/error.js index e35987d2..93139a2f 100644 --- a/test/error.js +++ b/test/error.js @@ -114,6 +114,20 @@ describe('AerospikeError', function () { }) }) + describe('#isServerError()', function () { + it('returns true if the error code indicates a server error', function () { + let error = { code: status.ERR_RECORD_NOT_FOUND } + let subject = AerospikeError.fromASError(error) + expect(subject.isServerError()).to.be(true) + }) + + it('returns false if the error code indicates a client error', function () { + let error = { code: status.ERR_PARAM } + let subject = AerospikeError.fromASError(error) + expect(subject.isServerError()).to.be(false) + }) + }) + describe('#toString()', function () { it('sets an informative error message', function () { let subject = new AerospikeError('Dooh!')