diff --git a/docs/tutorials/event-checkpointer.md b/docs/tutorials/event-checkpointer.md new file mode 100644 index 0000000000..53e3aa377e --- /dev/null +++ b/docs/tutorials/event-checkpointer.md @@ -0,0 +1,75 @@ +This tutorial describes the approaches that can be selected by users of the fabric-network module for replaying missed events emitted by peers. + +### Overview + +Events are emitted by peers when blocks are committed. Two types of events support checkpointing: +1. Contract events (also known as chaincode events) - Defined in transactions to be emitted. E.g. an event emitted when a commercial paper is sold +2. Block Events - Emitted when a block is committed + +In the case of an application crashing and events being missed, applications may still want to execute the event callback for the event it missed. Peers in a Fabric network support event replay, and to support this, the fabric-network module supports checkpointing strategies that track the last block and transactions in that block, that have been seen by the client. + +### Checkpointers + +The `BaseCheckpoint` class is an interface that is to be used by all Checkpoint classes. fabric-network has one default class, `FileSystemCheckpointer` that is exported as a factory in the `CheckpointFactories`. The `FILE_SYSTEM_CHECKPOINTER` is the default checkpointer. + +A checkpoint factory is a function that returns an instance with `BaseCheckpointer` as a parent class. These classes implement the `async save(channelName, listenerName)` and `async load()` functions. + +A checkpointer is called each time the event callback is triggered. + +The checkpointer can be set when connecting to a gateway or when creating the event listener. +```javascript +const { Gateway, CheckpointFactories } = require('fabric-network'); + +const connectOptions = { + checkpointer: { + factory: CheckpointFactories.FILE_SYSTEM_CHECKPOINTER, + options: {} // Options usable by the factory + } +}; + +const gateway = new Gateway() +await gateway.connect(connectionProfile, connectOptions); +``` + +Configuring a listener to be checkpointed required two properties: +1. `replay : boolean` - Tells the listener to record a checkpoint. Required if checkpointing is desired +2. `checkpointer : BaseCheckpointer` - If a checkpointer is not specified in the gateway, it must be specified here +```javascript +const listener = await contract.addContractListener('saleEventListener', 'sale', (err, event, blockNumber, txId) => { + if (err) { + console.error(err); + return; + } + // -- Do something +}, {replay: true, checkpointer: {factory: MyCheckpointer}); +``` + +### Custom Checkpointer + +Users can configure their own checkpointer. This requires two components to be created: +1. The Checkpointer class +2. The Factory + +```javascript +class DbCheckpointer extends BaseCheckpointer { + constructor(channelName, listenerName, dbOptions) { + super(channelName, listenerName); + this.db = new Db(dbOptions); + } + + async save(transactionId, blockNumber) { /* Your implementation using a database */ } + + async load() { /* Your implementation using a database*/ } +} + +function BD_CHECKPOINTER_FACTORY(channelName, listenerName, options) { + return new DbCheckpointer(channelName, listenerName, options); +} + +const gateway = new Gateway(); +await gateway.connect({ + checkpointer: { + factory: DB_CHECKPOINTER_FACTORY, + options: {host: 'http://localhost'} +}); +``` diff --git a/docs/tutorials/event-hub-management.md b/docs/tutorials/event-hub-management.md new file mode 100644 index 0000000000..c6071dd6dc --- /dev/null +++ b/docs/tutorials/event-hub-management.md @@ -0,0 +1,43 @@ +This tutorial describes how to define the behavior of the event hub selection strategy used when event hubs disconnect or new event hubs are required. + +The `ChannelEventHub` is a fabric-client class that receives contract, commit and block events from the event hub within a peer. The `fabric-network` abstracts the event hub away, and instead uses an event hub selection strategy to create new event hub instances or reuse existing instances. + +The interface for an event hub selection strategy is as follows: + +```javascript +class BaseEventHubSelectionStrategy { + /** + * Returns the next peer in the list per the strategy implementation + * @returns {ChannelPeer} + */ + getNextPeer() { + // Peer selection logic. Called whenever an event hub is required + } + + /** + * Updates the status of a peers event hub + * @param {ChannelPeer} deadPeer The peer that needs its status updating + */ + updateEventHubAvailability(deadPeer) { + // Peer availability update logic. Called whenever the event hub disconnects. + } +} +``` + +The event hub strategy exists at a gateway level, and is included in the `GatewayOptions` in the form of a factory function. The factory gives the event hub selection strategy instance a list of peers that it can select event hubs from. + +```javascript +function EXAMPLE_EVENT_HUB_SELECTION_FACTORY(network) { + const orgPeers = getOrganizationPeers(network); + const eventEmittingPeers = filterEventEmittingPeers(orgPeers); + return new ExampleEventHubSelectionStrategy(eventEmittingPeers); +} + +const gateway = new Gateway(); +await gateway.connect(connectionProfile, { + ... + eventHubSelectionOptions: { + strategy: EXAMPLE_EVENT_HUB_SELECTION_FACTORY + } +}) +``` diff --git a/docs/tutorials/listening-to-events.md b/docs/tutorials/listening-to-events.md new file mode 100644 index 0000000000..e01044e7b6 --- /dev/null +++ b/docs/tutorials/listening-to-events.md @@ -0,0 +1,111 @@ +This tutorial describes the different ways to listen to events emitted by a network using the fabric-network module. + +### Overview + +There are three event types that can be subscribed to: +1. Contract events - Those emitted explicitly by the chaincode developer within a transaction +2. Transaction (Commit) events - Those emitted automatically when a transaction is committed after an invoke +3. Block events - Those emitted automatically when a block is committed + +Listening for these events allows the application to react without directly calling a transaction. This is ideal in use cases such as tracking network analytics. + +### Usage + +Each listener type takes at least one parameter, the event callback. This is the function that is called when an event is detected. This callback is overridden by the `fabric-network` in order to support `Checkpointing`. + +#### Contract events + +```javascript +const gateway = new Gateway(); +await gateway.connect(connectionProfile, gatewayOptions); +const network = await gateway.getNetwork('my-channel'); +const contract = network.getContract('my-contract'); + +/** + * @param {String} listenerName the name of the event listener + * @param {String} eventName the name of the event being listened to + * @param {Function} callback the callback function with signature (error, event, blockNumber, transactionId, status) + * @param {Object} options +**/ +const listener = await contract.addContractListener('my-contract-listener', 'sale', (error, event, blockNumber, transactionId, status) => { + if (err) { + console.error(err); + return; + } + console.log(`Block Number: ${blockNumber} Transaction ID: ${transactionId} Status: ${status}`); +}) +``` +Notice that there is no need to specify an event hub, as the `EventHubSelectionStrategy` will select it automatically. + +#### Block events + +```javascript +const gateway = new Gateway(); +await gateway.connect(connectionProfile, gatewayOptions); +const network = await gateway.getNetwork('my-channel'); + +/** + * @param {String} listenerName the name of the event listener + * @param {Function} callback the callback function with signature (error, blockNumber, transactionId, status) + * @param {Object} options +**/ +const listener = await network.addBlockListener('my-block-listener', (error, block) => { + if (err) { + console.error(err); + return; + } + console.log(`Block: ${block}`); +}, {filtered: true /*false*/}) +``` +When listening for block events, it is important to specify if you want a filtered or none filtered event, as this determines which event hub is compatible with the request. + +#### Commit events + +Option 1: +```javascript +const gateway = new Gateway(); +await gateway.connect(connectionProfile, gatewayOptions); +const network = await gateway.getNetwork('my-channel'); +const contract = network.getContract('my-contract'); + +const transaction = contract.newTransaction('sell'); +/** + * @param {String} transactionId the name of the event listener + * @param {Function} callback the callback function with signature (error, transactionId, status, blockNumber) + * @param {Object} options +**/ +const listener = await network.addCommitListener(transaction.getTransactionID().getTransactionID(), (error, transactionId, status, blockNumber) => { + if (err) { + console.error(err); + return; + } + console.log(`Transaction ID: ${transactionId} Status: ${status} Block number: ${blockNumber}`); +}, {}); +``` + +Option 2: +```javascript +const gateway = new Gateway(); +await gateway.connect(connectionProfile, gatewayOptions); +const network = await gateway.getNetwork('my-channel'); +const contract = network.getContract('my-contract'); + +const transaction = contract.newTransaction('sell'); +/** + * @param {String} transactionId the name of the event listener + * @param {Function} callback the callback function with signature (error, transactionId, status, blockNumber) + * @param {Object} options +**/ +const listener = await transaction.addCommitListener((error, transactionId, status, blockNumber) => { + if (err) { + console.error(err); + return; + } + console.log(`Transaction ID: ${transactionId} Status: ${status} Block number: ${blockNumber}`); +}); +``` + + + + + diff --git a/docs/tutorials/transaction-commit-events.md b/docs/tutorials/transaction-commit-events.md index 721af0a9ac..2d5b9b504f 100644 --- a/docs/tutorials/transaction-commit-events.md +++ b/docs/tutorials/transaction-commit-events.md @@ -1,5 +1,5 @@ This tutorial describes the approaches that can be selected by users of the -fabric-network module for ensuring that submitted transactions are commited +fabric-network module for ensuring that submitted transactions are committed on peers. ### Overview @@ -61,7 +61,7 @@ strategies, it is possible to implement your own event handling. This is achieved by specifying your own factory function as the event handling strategy. The factory function should return a *transaction event handler* object and take two parameters: -1. Transaction ID: `String` +1. Transaction: `Transaction` 2. Blockchain network: `Network` The Network provides access to peers and event hubs from which events should diff --git a/docs/tutorials/tutorials.json b/docs/tutorials/tutorials.json index 455eacdf90..5d8ead971e 100644 --- a/docs/tutorials/tutorials.json +++ b/docs/tutorials/tutorials.json @@ -35,6 +35,15 @@ "query-peers": { "title": "fabric-network: How to select peers for evaluating transactions (queries)" }, + "event-checkpointer": { + "title": "fabric-network: How to replay missed events" + }, + "event-hub-management": { + "title": "fabric-network: How to automatically select and reconnect to event hubs" + }, + "listening-to-events": { + "title": "fabric-network: How to listen to events" + }, "grpc-settings": { "title": "fabric-client: How to set gRPC settings" }, diff --git a/fabric-client/lib/ChannelEventHub.js b/fabric-client/lib/ChannelEventHub.js index e7edd91502..12419f3bca 100644 --- a/fabric-client/lib/ChannelEventHub.js +++ b/fabric-client/lib/ChannelEventHub.js @@ -21,6 +21,7 @@ const logger = utils.getLogger('ChannelEventHub.js'); const {Identity} = require('./msp/identity'); const TransactionID = require('./TransactionID'); const util = require('util'); +const EventHubDisconnectError = require('./errors/EventHubDisconnectError'); const BlockDecoder = require('./BlockDecoder.js'); @@ -588,7 +589,7 @@ class ChannelEventHub { /** * Disconnects the ChannelEventHub from the peer event source. - * Will close all event listeners and send an Error object + * Will close all event listeners and send an EventHubDisconnectError object * with the message "ChannelEventHub has been shutdown" to * all listeners that provided an "onError" callback. */ @@ -597,14 +598,14 @@ class ChannelEventHub { logger.debug('disconnect - disconnect is running'); } else { this._disconnect_running = true; - this._disconnect(new Error('ChannelEventHub has been shutdown')); + this._disconnect(new EventHubDisconnectError('ChannelEventHub has been shutdown')); this._disconnect_running = false; } } /** * Disconnects the ChannelEventHub from the fabric peer service. - * Will close all event listeners and send an Error object + * Will close all event listeners and send an EventHubDisconnectError object * with the message "ChannelEventHub has been shutdown" to * all listeners that provided an "onError" callback. */ @@ -1387,6 +1388,10 @@ class ChannelEventHub { } } + isFiltered() { + return !!this._filtered_stream; + } + /* * private internal method for processing block events * @param {Object} block protobuf object @@ -1462,7 +1467,7 @@ class ChannelEventHub { } if (trans_reg.disconnect) { logger.debug('_callTransactionListener - automatically disconnect'); - this._disconnect(new Error('Shutdown due to disconnect on transaction id registration')); + this._disconnect(new EventHubDisconnectError('Shutdown due to disconnect on transaction id registration')); } } @@ -1556,7 +1561,7 @@ class ChannelEventHub { logger.debug('_callChaincodeListener - automatically unregister tx listener for %s', tx_id); } if (chaincode_reg.event_reg.disconnect) { - this._disconnect(new Error('Shutdown due to disconnect on transaction id registration')); + this._disconnect(new EventHubDisconnectError('Shutdown due to disconnect on transaction id registration')); } } else { logger.debug('_callChaincodeListener - NOT calling chaincode listener callback'); @@ -1577,7 +1582,7 @@ class ChannelEventHub { this._start_stop_registration.unregister_action(); } if (this._start_stop_registration.disconnect) { - this._disconnect(new Error('Shutdown due to end block number has been seen')); + this._disconnect(new EventHubDisconnectError('Shutdown due to end block number has been seen')); } } } diff --git a/fabric-client/lib/errors/EventHubDisconnectError.js b/fabric-client/lib/errors/EventHubDisconnectError.js new file mode 100644 index 0000000000..e1086a501b --- /dev/null +++ b/fabric-client/lib/errors/EventHubDisconnectError.js @@ -0,0 +1,22 @@ +/* + Copyright 2019, 2018 IBM All Rights Reserved. + + SPDX-License-Identifier: Apache-2.0 + +*/ + +'use strict'; + +/** + * Error when an event hub is disconnected. + * @interface + * @memberof module:fabric-network + * @property {String} [message] The error message + */ +class EventHubDisconnectError extends Error { + constructor(message) { + super(message); + } +} + +module.exports = EventHubDisconnectError; diff --git a/fabric-network/index.js b/fabric-network/index.js index 937e0c4b06..bcfa81b9c3 100644 --- a/fabric-network/index.js +++ b/fabric-network/index.js @@ -60,4 +60,6 @@ module.exports.FileSystemWallet = require('./lib/impl/wallet/filesystemwallet'); module.exports.CouchDBWallet = require('./lib/impl/wallet/couchdbwallet'); module.exports.DefaultEventHandlerStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies'); module.exports.DefaultQueryHandlerStrategies = require('fabric-network/lib/impl/query/defaultqueryhandlerstrategies'); +module.exports.CheckpointFactories = require('fabric-network/lib/impl/event/checkpointfactories'); +module.exports.EventHubSelectionStrategies = require('fabric-network/lib/impl/event/defaulteventhubselectionstrategies'); module.exports.TimeoutError = require('fabric-network/lib/errors/timeouterror'); diff --git a/fabric-network/lib/contract.js b/fabric-network/lib/contract.js index 1bb62a0516..dd6c2449c6 100644 --- a/fabric-network/lib/contract.js +++ b/fabric-network/lib/contract.js @@ -7,6 +7,7 @@ 'use strict'; const Transaction = require('fabric-network/lib/transaction'); +const ContractEventListener = require('./impl/event/contracteventlistener'); const logger = require('./logger').getLogger('Contract'); const util = require('util'); @@ -47,7 +48,7 @@ function verifyNamespace(namespace) { * @hideconstructor */ class Contract { - constructor(network, chaincodeId, gateway, namespace) { + constructor(network, chaincodeId, gateway, checkpointer, namespace) { logger.debug('in Contract constructor'); verifyNamespace(namespace); @@ -57,6 +58,7 @@ class Contract { this.chaincodeId = chaincodeId; this.gateway = gateway; this.namespace = namespace; + this.checkpointer = checkpointer; } /** @@ -86,6 +88,23 @@ class Contract { return this.chaincodeId; } + getCheckpointer(options) { + if (options) { + if (typeof options.checkpointer === 'undefined') { + return this.checkpointer; + } else if (typeof options.checkpointer === 'function') { + return options.checkpointer; + } else if (options.checkpointer === false) { + return null; + } + } + return this.checkpointer; + } + + getEventHubSelectionStrategy() { + return this.network.eventHubSelectionStrategy; + } + /** * Get event handler options specified by the user when creating the gateway. * @private @@ -152,6 +171,32 @@ class Contract { async evaluateTransaction(name, ...args) { return this.createTransaction(name).evaluate(...args); } + + /** + * Create a commit event listener for this transaction. + * @param {string} listenerName The name of the listener + * @param {Function} callback - This callback will be triggered when + * a transaction commit event is emitted. It takes parameters + * of error, event payload, block number, transaction ID and status + * @param {RegistrationOptions} [options] - Optional. Options on + * registrations allowing start and end block numbers. + * @param {ChannelEventHub} [eventHub] - Optional. Used to override the event hub selection + * @returns {CommitEventListener} + * @async + */ + async addContractListener(listenerName, eventName, callback, options) { + if (!options) { + options = {}; + } + options.replay = options.replay ? true : false; + options.checkpointer = this.getCheckpointer(options); + const listener = new ContractEventListener(this, listenerName, eventName, callback, options); + const network = this.getNetwork(); + network.saveListener(listenerName, listener); + await listener.register(); + return listener; + } + } module.exports = Contract; diff --git a/fabric-network/lib/gateway.js b/fabric-network/lib/gateway.js index 013e1e81da..f02f215549 100644 --- a/fabric-network/lib/gateway.js +++ b/fabric-network/lib/gateway.js @@ -11,6 +11,8 @@ const Client = require('fabric-client'); const Network = require('./network'); const EventStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies'); const QueryStrategies = require('fabric-network/lib/impl/query/defaultqueryhandlerstrategies'); +const EventHubSelectionStrategies = require('fabric-network/lib/impl/event/defaulteventhubselectionstrategies'); +const CheckpointFactories = require('fabric-network/lib/impl/event/checkpointfactories'); const logger = require('./logger').getLogger('Gateway'); @@ -25,6 +27,8 @@ const logger = require('./logger').getLogger('Gateway'); * @property {module:fabric-network.Gateway~DefaultQueryHandlerOptions} [queryHandlerOptions] Options for the inbuilt * default query handler capability. * @property {module:fabric-network.Gateway~DiscoveryOptions} [discovery] Discovery options. + * @property {module:fabric-network.Gateway~DefaultEventHubSelectionOptions} [eventHubSelectionOptions] Event hub selection options. + * @property {module:fabric-network.Network~CheckpointerFactory} [checkpointer] Event hub selection options. */ /** @@ -57,6 +61,38 @@ const logger = require('./logger').getLogger('Gateway'); * fails. */ +/** + * @typedef {Object} Gateway~CheckpointerFactory + * @memberof module:fabric-network + * @param {String} channelName the name of the channel the checkpoint exists in + * @param {String} listenerName the name of the listener being checkpointed + * @param {Object} [options] Optional. Options to configure behaviour of customer checkpointers i.e. + * Supplying database connection details + * @returns {BaseCheckpointer} + */ + +/** + * @typedef {Object} Gateway~DefaultEventHubSelectionOptions + * @memberof module:fabric-network + * @property {?module:fabric-network.Gateway~DefaultEventHubSelectionFactory} [strategy=MSPID_SCOPE_ROUND_ROBIN] Selects the next + * event hub in the event of a new listener being created or an event hub disconnect + */ + +/** + * @typedef {Object} Gateway~DefaultEventHubSelectionFactory + * @memberof module:fabric-network + * @param {module:fabric-network.Network} network The network the event hub is being selected for + * @returns {module:fabric-network.Gateway~BaseEventHubSelectionStrategy} + */ + +/** + * @typedef {Object} Gateway~BaseEventHubSelectionStrategy + * @memberof module:fabric-network + * @property {Function} getNextPeer Function that returns the next peer in the list of available peers + * @property {Function} updateEventHubAvailability Function that updates the availability of an event hub + */ + + /** * @typedef {Object} Gateway~DefaultQueryHandlerOptions * @memberof module:fabric-network @@ -129,6 +165,13 @@ class Gateway { }, discovery: { enabled: Client.getConfigSetting('initialize-with-discovery', true) + }, + checkpointer: { + factory: CheckpointFactories.FILE_SYSTEM_CHECKPOINTER, + options: {} + }, + eventHubSelectionOptions: { + strategy: EventHubSelectionStrategies.MSPID_SCOPE_ROUND_ROBIN, } }; } diff --git a/fabric-network/lib/impl/event/abstracteventhubselectionstrategy.js b/fabric-network/lib/impl/event/abstracteventhubselectionstrategy.js new file mode 100644 index 0000000000..cd8ff04935 --- /dev/null +++ b/fabric-network/lib/impl/event/abstracteventhubselectionstrategy.js @@ -0,0 +1,41 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +/** + * An abstract selection strategy that provides an interface for other selection + * strategies + * + * @memberof module:fabric-network + * @class + */ +class AbstractEventHubSelectionStrategy { + constructor(peers) { + this.peers = peers; + } + /** + * Gets the next peer + * @returns {Peer} + */ + getNextPeer() { + throw new Error('Abstract method called.'); + } + + /** + * Updates the availability of the peer + * @param {Peer} deadPeer Disconnected peer + */ + updateEventHubAvailability(deadPeer) { + return; + } + + getPeers() { + return this.peers; + } +} + +module.exports = AbstractEventHubSelectionStrategy; diff --git a/fabric-network/lib/impl/event/abstracteventlistener.js b/fabric-network/lib/impl/event/abstracteventlistener.js new file mode 100644 index 0000000000..1f4888b5fb --- /dev/null +++ b/fabric-network/lib/impl/event/abstracteventlistener.js @@ -0,0 +1,158 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ +'use strict'; + +const Long = require('long'); + +const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError'); +const BaseCheckpointer = require('./basecheckpointer'); +const logger = require('fabric-client/lib/utils').getLogger('AbstractEventListener'); + +/** + * @typedef {Object} module:fabric-network.Network~ListenerOptions + * @memberof module:fabric-network + * @property {Object} checkpointer The checkpointer factory and options + * @property {Gateway~CheckpointerFactory} checkpointer.factory The checkpointer factory + * @property {Object} checkpointer.options The checkpoint configuration options + * @property {boolean} replay event replay and checkpointing on listener + * @extends RegistrationOpts + */ + +/** + * Event listener base class handles initializing common properties across contract, transaction + * and block event listeners. + * + * Instances of the event listener are stateful and must only be used for one listener + * @private + * @class + */ +class AbstractEventListener { + /** + * Constructor + * @param {module:fabric-network.Network} network The network + * @param {string} listenerName The name of the listener being created + * @param {function} eventCallback The function called when the event is triggered. + * It has signature (err, ...args) where args changes depending on the event type + * @param {module:fabric-network.Network~ListenerOptions} options Event handler options + */ + constructor(network, listenerName, eventCallback, options) { + if (!options) { + options = {}; + } + this.channel = network.getChannel(); + this.network = network; + this.listenerName = listenerName; + this.eventCallback = eventCallback; + this.options = options; + + this._registered = false; + this._firstCheckpoint = {}; + this._registration = null; + this._filtered = typeof options.filtered === 'boolean' ? options.filtered : true; + } + + /** + * Called by the super classes register function. Saves information needed to start + * listening, and disconnects an event hub if it is the incorrect type + */ + async register(chaincodeId) { + logger.debug(`Register event listener: ${this.listenerName}`); + if (this.getEventHubManager().getPeers().length === 0) { + logger.error('No peers available to register a listener'); + return; + } + if (this._registered) { + throw new Error('Listener already registered'); + } + if (this.eventHub && this.eventHub.isconnected() && !!this.eventHub.isFiltered() !== this._filtered) { + this.eventHub.disconnect(); + this.eventHub = null; + } + + if (this.options.checkpointer) { + if (typeof this.options.checkpointer.factory === 'function') { + this.checkpointer = this.options.checkpointer.factory( + this.channel.getName(), + this.listenerName, + this.options.checkpointer.options + ); + this.checkpointer.setChaincodeId(chaincodeId); + } + } + if (this.useEventReplay()) { + if (!this.getCheckpointer()) { + logger.error('Opted to use checkpointing without defining a checkpointer'); + } + } + + let checkpoint; + if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) { + this._firstCheckpoint = checkpoint = await this.checkpointer.load(); + const blockchainInfo = await this.channel.queryInfo(); + if (checkpoint && checkpoint.blockNumber && blockchainInfo.height - 1 <= Number(checkpoint.blockNumber)) { + logger.debug(`Requested Block Number: ${checkpoint.blockNumber} Latest Block Number: ${blockchainInfo.height - 1}`); + this.options.startBlock = Long.fromValue(checkpoint.blockNumber); + } + } + } + + /** + * Called by the super classes unregister function. Removes state from the listener so it + * can be reregistered at a later time + */ + unregister() { + logger.debug(`Unregister event listener: ${this.listenerName}`); + this._registered = false; + delete this.options.startBlock; + delete this.options.endBlock; + delete this.options.disconnect; + this._firstCheckpoint = {}; + } + + /** + * @returns {boolean} Listeners registration status + */ + isregistered() { + return this._registered; + } + + /** + * Returns the checkpoint instance created by the checkpoint factory + * @returns {BaseCheckpointer} Checkpointer instance specific to this listener + */ + getCheckpointer() { + return this.checkpointer; + } + + /** + * Returns the event hub manager from the network + * @returns {EventHubManager} Event hub manager + */ + getEventHubManager() { + const network = this.network; + return network.getEventHubManager(); + } + + useEventReplay() { + return this.options.replay; + } + + /** + * Check if the event hub error is a disconnect message + * @param {Error} error The error emitted by the event hub + * @returns {boolean} is shutdown message + * @private + */ + _isShutdownMessage(error) { + if (error) { + logger.debug('Event hub shutdown.'); + return error instanceof EventHubDisconnectError; + } + return false; + } +} + +module.exports = AbstractEventListener; diff --git a/fabric-network/lib/impl/event/basecheckpointer.js b/fabric-network/lib/impl/event/basecheckpointer.js new file mode 100644 index 0000000000..c221cc5e28 --- /dev/null +++ b/fabric-network/lib/impl/event/basecheckpointer.js @@ -0,0 +1,50 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +/** + * Base checkpointer providing an interface for checkpointers + * @class + */ +class BaseCheckpointer { + /** + * The constructor + * @param {Object} options The options to configure the checkpointer + */ + constructor(options) { + this.options = options || {}; + this._chaincodeId = null; + } + + /** + * Updates the storage mechanism + * @param {String} transactionId the transaction ID + * @param {Number} blockNumber the block number + * @async + */ + async save(transactionId, blockNumber) { + throw new Error('Method has not been implemented'); + } + + /** + * Loads the latest checkpoint + * @async + */ + async load() { + throw new Error('Method has not been implemented'); + } + + /** + * Sets the chaincode ID to group together listeners + * @param {String} chaincodeId the chaincodeId + */ + async setChaincodeId(chaincodeId) { + this._chaincodeId = chaincodeId; + } +} + +module.exports = BaseCheckpointer; diff --git a/fabric-network/lib/impl/event/blockeventlistener.js b/fabric-network/lib/impl/event/blockeventlistener.js new file mode 100644 index 0000000000..b667d0595e --- /dev/null +++ b/fabric-network/lib/impl/event/blockeventlistener.js @@ -0,0 +1,112 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const BaseCheckpointer = require('./basecheckpointer'); +const AbstractEventListener = require('./abstracteventlistener'); + +const logger = require('fabric-network/lib/logger').getLogger('BlockEventListener'); +const util = require('util'); + +/** + * The Block Event listener class handles block events from the channel. + * + * + * @private + * @class + */ +class BlockEventListener extends AbstractEventListener { + constructor(network, listenerName, eventCallback, options) { + super(network, listenerName, eventCallback, options); + } + + /** + * Finds and connects to an event hub then creates the listener registration + */ + async register() { + await super.register(); + if (!this.eventHub) { + return this._registerWithNewEventHub(); + } + this._registration = this.eventHub.registerBlockEvent( + this._onEvent.bind(this), + this._onError.bind(this), + this.options + ); + this.eventHub.connect(!this._filtered); + this._registered = true; + } + + /** + * Unregisters the registration from the event hub + */ + unregister() { + super.unregister(); + if (this.eventHub) { + this.eventHub.unregisterBlockEvent(this._registration); + } + } + + /** + * The callback triggered when the event was successful. Checkpoints the last + * block and transaction seen once the callback has run and unregisters the + * listener if the unregister flag was provided + * @param {*} block Either a full or filtered block + * @private + */ + _onEvent(block) { + const blockNumber = Number(block.number); + + try { + this.eventCallback(null, block); + } catch (err) { + logger.error(util.format('Error executing callback: %s', err)); + } + if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) { + this.checkpointer.save(null, blockNumber); + } + if (this._registration.unregister) { + this.unregister(); + } + } + + /** + * This callback is triggered when the event was unsuccessful. If the error indicates + * that the event hub shutdown and the listener is still registered, it updates the + * {@link EventHubSelectionStrategy} status of event hubs (if implemented) and finds a + * new event hub to connect to + * @param {Error} error The error emitted + * @private + */ + async _onError(error) { + logger.debug('_onError:', util.format('received error from peer %s: %j', this.eventHub.getPeerAddr(), error)); + if (error) { + if (this._isShutdownMessage(error) && this.isregistered()) { + this.getEventHubManager().updateEventHubAvailability(this.eventHub._peer); + await this._registerWithNewEventHub(); + } + } + this.eventCallback(error); + } + + /** + * Finds a new event hub for the listener in the event of one shutting down. Will + * create a new instance if checkpointer is being used, or reuse one if not + * @private + */ + async _registerWithNewEventHub() { + this.unregister(); + if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) { + this.eventHub = this.getEventHubManager().getReplayEventHub(); + } else { + this.eventHub = this.getEventHubManager().getEventHub(); + } + await this.register(); + } +} + +module.exports = BlockEventListener; diff --git a/fabric-network/lib/impl/event/checkpointfactories.js b/fabric-network/lib/impl/event/checkpointfactories.js new file mode 100644 index 0000000000..2769fae0d6 --- /dev/null +++ b/fabric-network/lib/impl/event/checkpointfactories.js @@ -0,0 +1,17 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FileSystemCheckpointer = require('./filesystemcheckpointer'); + +function FILE_SYSTEM_CHECKPOINTER(channelName, listenerName, options) { + return new FileSystemCheckpointer(channelName, listenerName, options); +} + +module.exports = { + FILE_SYSTEM_CHECKPOINTER +}; diff --git a/fabric-network/lib/impl/event/commiteventlistener.js b/fabric-network/lib/impl/event/commiteventlistener.js new file mode 100644 index 0000000000..39ca67bb4e --- /dev/null +++ b/fabric-network/lib/impl/event/commiteventlistener.js @@ -0,0 +1,124 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const AbstractEventListener = require('./abstracteventlistener'); +const logger = require('fabric-network/lib/logger').getLogger('CommitEventListener'); +const util = require('util'); + +/** + * The Commit Event Listener handles transaction commit events + * + * @private + * @class + */ +class CommitEventListener extends AbstractEventListener { + /** + * + * @param {module:fabric-network.Network} network The fabric network + * @param {string} transactionId the transaction id being listened to + * @param {Function} eventCallback The event callback called when a transaction is commited. + * It has signature (err, transactionId, status, blockNumber) + * @param {*} options + */ + constructor(network, transactionId, eventCallback, options) { + const listenerName = transactionId + Math.random(); + super(network, listenerName, eventCallback, options); + this.transactionId = transactionId; + } + + async register() { + await super.register(); + if (!this.eventHub) { + logger.debug('No event hub. Retrieving new one.'); + return await this._registerWithNewEventHub(); + } + if (this._isAlreadyRegistered(this.eventHub)) { // Prevents a transaction being registered twice with the same event hub + logger.debug('Event hub already has registrations. Generating new event hub instance.'); + if (!this.options.fixedEventHub) { + this.eventHub = this.getEventHubManager().getEventHub(this.eventHub._peer, true); + } else { + this.eventHub = this.getEventHubManager().getFixedEventHub(this.eventHub._peer); + } + } + const txid = this.eventHub.registerTxEvent( + this.transactionId, + this._onEvent.bind(this), + this._onError.bind(this), + Object.assign({unregister: true}, this.options) + ); + this._registration = this.eventHub._transactionRegistrations[txid]; + this.eventHub.connect(!this._filtered); + this._registered = true; + } + + /** + * @param {module:fabric-client.ChannelEventHub} eventhub Event hub. + * @param {boolean} isFixed If true only this peers event hub will be used + */ + setEventHub(eventHub, isFixed) { + this.eventHub = eventHub; + this.options.fixedEventHub = isFixed; + } + + unregister() { + super.unregister(); + if (this.eventHub) { + this.eventHub.unregisterTxEvent(this.transactionId); + } + } + + _onEvent(txid, status, blockNumber) { + logger.debug('_onEvent:', util.format('success for transaction %s', txid)); + blockNumber = Number(blockNumber); + + try { + this.eventCallback(null, txid, status, blockNumber); + } catch (err) { + logger.debug(util.format('_onEvent error from callback: %s', err)); + } + if (this._registration.unregister) { + this.unregister(); + } + } + + _onError(error) { + logger.debug('_onError:', util.format('received error from peer %s: %j', this.eventHub.getPeerAddr(), error)); + this.eventCallback(error); + } + + + async _registerWithNewEventHub() { + if (this.isregistered()) { + this.unregister(); + } + if (this.options.fixedEventHub && !this.eventHub) { + throw new Error(`Cannot use a fixed event hub without setting an event hub ${this.listenerName}`); + } + if (!this.options.fixedEventHub) { + this.eventHub = this.getEventHubManager().getReplayEventHub(); + } else { + this.eventHub = this.getEventHubManager().getFixedEventHub(this.eventHub._peer); + } + + this.options.disconnect = true; + await this.register(); + } + + _isAlreadyRegistered(eventHub) { + if (!eventHub) { + throw new Error('Event hub not given'); + } + const registrations = eventHub._transactionRegistrations; + if (registrations[this.transactionId]) { + return true; + } + return false; + } +} + +module.exports = CommitEventListener; diff --git a/fabric-network/lib/impl/event/contracteventlistener.js b/fabric-network/lib/impl/event/contracteventlistener.js new file mode 100644 index 0000000000..f6b9372090 --- /dev/null +++ b/fabric-network/lib/impl/event/contracteventlistener.js @@ -0,0 +1,129 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const AbstractEventListener = require('./abstracteventlistener'); +const BaseCheckpointer = require('./basecheckpointer'); +const logger = require('fabric-network/lib/logger').getLogger('ContractEventListener'); +const util = require('util'); + +/** + * The Contract Event Listener handles contract events from the chaincode. + * + * @private + * @class + */ +class ContractEventListener extends AbstractEventListener { + /** + * Constructor. + * @param {Contract} contract The contract instance + * @param {string} listenerName The name of the listener + * @param {string} eventName The name of the contract event being listened for + * @param {function} eventCallback The event callback called when an event is recieved. + * It has signature (err, BlockEvent, blockNumber, transactionId) + * @param {*} options + */ + constructor(contract, listenerName, eventName, eventCallback, options) { + super(contract.getNetwork(), listenerName, eventCallback, options); + this.contract = contract; + this.eventName = eventName; + } + + /** + * Finds and connects to an event hub then creates the listener registration + */ + async register() { + await super.register(this.contract.getChaincodeId()); + if (!this.eventHub) { + return await this._registerWithNewEventHub(); + } + this._registration = this.eventHub.registerChaincodeEvent( + this.contract.getChaincodeId(), + this.eventName, + this._onEvent.bind(this), + this._onError.bind(this), + this.options + ); + this._registered = true; + this.eventHub.connect(!this._filtered); + } + + /** + * Unregisters the registration from the event hub + */ + unregister() { + super.unregister(); + if (this.eventHub) { + this.eventHub.unregisterChaincodeEvent(this._registration); + } + } + + /** + * The callback triggered when the event was successful. Checkpoints the last + * block and transaction seen once the callback has run and unregisters the + * listener if the unregister flag was provided + * @param {ChaincodeEvent} event the event emitted + * @param {number} blockNumber the block number this transaction was commited inside + * @param {string} transactionId the transaction ID of the transaction this event was emitted by + * @param {string} status the status of the the transaction + */ + async _onEvent(event, blockNumber, transactionId, status) { + logger.debug(`_onEvent[${this.listenerName}]:`, util.format('success for transaction %s', transactionId)); + blockNumber = Number(blockNumber); + if (this.useEventReplay() && this.checkpointer instanceof BaseCheckpointer) { + const checkpoint = await this.checkpointer.load(); + if (checkpoint && checkpoint.transactionIds && checkpoint.transactionIds.includes(transactionId)) { + logger.debug(util.format('_onEvent skipped transaction: %s', transactionId)); + return; + } + await this.checkpointer.save(transactionId, blockNumber); + } + + try { + this.eventCallback(null, event, blockNumber, transactionId, status); + } catch (err) { + logger.debug(util.format('_onEvent error from callback: %s', err)); + } + if (this._registration.unregister) { + this.unregister(); + } + } + + /** + * This callback is triggerend when the event was unsuccessful. If the error indicates + * that the event hub shutdown and the listener is still registered, it updates the + * {@link EventHubSelectionStrategy} status of event hubs (if implemented) and finds a + * new event hub to connect to + * @param {Error} error The error emitted + */ + async _onError(error) { + logger.debug('_onError:', util.format('received error from peer %s: %j', this.eventHub.getPeerAddr(), error)); + if (error) { + if (this._isShutdownMessage(error) && this.isregistered()) { + this.getEventHubManager().updateEventHubAvailability(this.eventHub._peer); + await this._registerWithNewEventHub(); + } + } + this.eventCallback(error); + } + + /** + * Finds a new event hub for the listener in the event of one shutting down. Will + * create a new instance if checkpointer is being used, or reuse one if not + */ + async _registerWithNewEventHub() { + this.unregister(); + if (this.checkpointer instanceof BaseCheckpointer && this.useEventReplay()) { + this.eventHub = this.getEventHubManager().getReplayEventHub(); + } else { + this.eventHub = this.getEventHubManager().getEventHub(); + } + await this.register(this.contract.getChaincodeId()); + } +} + +module.exports = ContractEventListener; diff --git a/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js b/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js index b3e16be2dd..9cee8533a9 100644 --- a/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js +++ b/fabric-network/lib/impl/event/defaulteventhandlerstrategies.js @@ -12,32 +12,36 @@ const TransactionEventHandler = require('fabric-network/lib/impl/event/transacti function getOrganizationEventHubs(network) { const peers = network.getChannel().getPeersForOrg(); - return network.getEventHubFactory().getEventHubs(peers); + return network.getEventHubManager().getEventHubs(peers); } function getNetworkEventHubs(network) { const peers = network.getChannel().getPeers(); - return network.getEventHubFactory().getEventHubs(peers); + return network.getEventHubManager().getEventHubs(peers); } -function MSPID_SCOPE_ALLFORTX(transactionId, network, options) { +function MSPID_SCOPE_ALLFORTX(transaction, options) { + const network = transaction.getNetwork(); const eventStrategy = new AllForTxStrategy(getOrganizationEventHubs(network)); - return new TransactionEventHandler(transactionId, eventStrategy, options); + return new TransactionEventHandler(transaction, eventStrategy, options); } -function MSPID_SCOPE_ANYFORTX(transactionId, network, options) { +function MSPID_SCOPE_ANYFORTX(transaction, options) { + const network = transaction.getNetwork(); const eventStrategy = new AnyForTxStrategy(getOrganizationEventHubs(network)); - return new TransactionEventHandler(transactionId, eventStrategy, options); + return new TransactionEventHandler(transaction, eventStrategy, options); } -function NETWORK_SCOPE_ALLFORTX(transactionId, network, options) { +function NETWORK_SCOPE_ALLFORTX(transaction, options) { + const network = transaction.getNetwork(); const eventStrategy = new AllForTxStrategy(getNetworkEventHubs(network)); - return new TransactionEventHandler(transactionId, eventStrategy, options); + return new TransactionEventHandler(transaction, eventStrategy, options); } -function NETWORK_SCOPE_ANYFORTX(transactionId, network, options) { +function NETWORK_SCOPE_ANYFORTX(transaction, options) { + const network = transaction.getNetwork(); const eventStrategy = new AnyForTxStrategy(getNetworkEventHubs(network)); - return new TransactionEventHandler(transactionId, eventStrategy, options); + return new TransactionEventHandler(transaction, eventStrategy, options); } /** diff --git a/fabric-network/lib/impl/event/defaulteventhubselectionstrategies.js b/fabric-network/lib/impl/event/defaulteventhubselectionstrategies.js new file mode 100644 index 0000000000..1cefec242d --- /dev/null +++ b/fabric-network/lib/impl/event/defaulteventhubselectionstrategies.js @@ -0,0 +1,34 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const FabricConstants = require('fabric-client/lib/Constants'); +const RoundRobinEventHubSelectionStrategy = require('fabric-network/lib/impl/event/roundrobineventhubselectionstrategy'); + +function getOrganizationPeers(network) { + return network.getChannel().getPeersForOrg(); +} + +function filterEventEmittingPeers(peers) { + return peers.filter((peer) => peer.isInRole(FabricConstants.NetworkConfig.EVENT_SOURCE_ROLE)); +} + +function MSPID_SCOPE_ROUND_ROBIN(network) { + const orgPeers = getOrganizationPeers(network); + const eventEmittingPeers = filterEventEmittingPeers(orgPeers); + return new RoundRobinEventHubSelectionStrategy(eventEmittingPeers); +} + +/** + * @typedef DefaultEventHubSelectionStrategies + * @memberof module:fabric-network + * @property {function} MSPID_SCOPE_ROUND_ROBIN Reconnect to any of the event emitting peers in the org after + * a disconnect occurs + */ +module.exports = { + MSPID_SCOPE_ROUND_ROBIN +}; diff --git a/fabric-network/lib/impl/event/eventhubmanager.js b/fabric-network/lib/impl/event/eventhubmanager.js new file mode 100644 index 0000000000..3a5f9bba13 --- /dev/null +++ b/fabric-network/lib/impl/event/eventhubmanager.js @@ -0,0 +1,127 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const EventHubFactory = require('./eventhubfactory'); + +/** + * The Event Hub Manager is responsible for creating and distributing event hubs. + * It uses the event hub factory to reuse event hubs that exists, and maintains + * its own list of new event hubs that are used for event replay + * @private + * @class + */ +class EventHubManager { + /** + * Constructor + * @param {module:fabric-network.Network} network The network + */ + constructor(network) { + this.channel = network.getChannel(); + this.eventHubFactory = new EventHubFactory(this.channel); + this.eventHubSelectionStrategy = network.getEventHubSelectionStrategy(); + this.newEventHubs = []; + } + /** + * Gets an event hub. If given a peer, it will get that peers event hub, otherwise + * it will get the next peer defined by the {@link EventHubSelectionStrategy} + * @param {Peer} peer A peer instance + * @param {boolean} filtered Flag to decide between filtered and unfiltered events + * @returns {module:fabric-client.ChannelEventHub} The event hub + */ + getEventHub(peer, filtered) { + if (!peer) { + peer = this.eventHubSelectionStrategy.getNextPeer(); + } + peer = peer.getPeer ? peer.getPeer() : peer; + const eventHub = this.eventHubFactory.getEventHub(peer); + if (eventHub.isconnected() && eventHub.isFiltered() !== !!filtered) { + return this.getReplayEventHub(peer); + } + return eventHub; + } + + /** + * Gets a list of event hubs from the {@link EventHubFactory} for a list of peers + * @param {module:fabric-client.Peer[]} peers A list of peer instances + */ + getEventHubs(peers) { + return this.eventHubFactory.getEventHubs(peers); + } + + /** + * Gets a new event hub instance for a give peer and updates the list of new event + * hubs that have been created + * @param {module:fabric-client.Peer} peer A peer instance + * @returns {module:fabric-client.ChannelEventHub} The event hub + */ + getReplayEventHub(peer) { + for (const index in this.newEventHubs) { + const eventHub = this.newEventHubs[index]; + if (this._isNewEventHub(eventHub) && (!peer || eventHub.getName() === peer.getName())) { + this.newEventHubs.splice(index, 1); + } + } + peer = this.eventHubSelectionStrategy.getNextPeer(); + const eh = this.channel.newChannelEventHub(peer); + this.newEventHubs.push(eh); + return eh; + } + + /** + * Will get a new event hub instance without the possibility of selecting a new peer + * @param {module:fabric-client.Peer} peer A peer instance + * @returns {module:fabric-client.ChannelEventHub} The event hub + */ + getFixedEventHub(peer) { + const eventHub = this.channel.newChannelEventHub(peer); + this.newEventHubs.push(eventHub); + return eventHub; + } + + /** + * When called with a peer, it updates the {@link EventHubSelectionStategy} with the + * new status of a peer to allow for intelligent strategies + * @param {module:fabric-client:Peer} deadPeer A peer instance + */ + updateEventHubAvailability(deadPeer) { + return this.eventHubSelectionStrategy.updateEventHubAvailability(deadPeer); + } + + /** + * Disconnect from and delete all event hubs + */ + dispose() { + this.eventHubFactory.dispose(); + this.newEventHubs.forEach((eh) => eh.disconnect()); + } + + getEventHubFactory() { + return this.eventHubFactory; + } + + getPeers() { + return this.eventHubSelectionStrategy.getPeers(); + } + + /** + * Check if an event hub has any registrations + * @param {module:fabric-client.ChannelEventHub} eventHub An event hub instance + * @returns {boolean} + */ + _isNewEventHub(eventHub) { + if (!eventHub) { + throw new Error('event hub not given'); + } + const chaincodeRegistrations = Object.values(eventHub._chaincodeRegistrants).length; + const blockRegistrations = Object.values(eventHub._blockRegistrations).length; + const txRegistrations = Object.values(eventHub._transactionRegistrations).length; + return (chaincodeRegistrations + blockRegistrations + txRegistrations) === 0; + } +} + +module.exports = EventHubManager; diff --git a/fabric-network/lib/impl/event/filesystemcheckpointer.js b/fabric-network/lib/impl/event/filesystemcheckpointer.js new file mode 100644 index 0000000000..299df23d97 --- /dev/null +++ b/fabric-network/lib/impl/event/filesystemcheckpointer.js @@ -0,0 +1,79 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); +const BaseCheckpointer = require('./basecheckpointer'); + + +class FileSystemCheckpointer extends BaseCheckpointer { + constructor(channelName, listenerName, options = {}) { + super(options); + if (!options.basePath) { + options.basePath = path.join(os.homedir(), '/.hlf-checkpoint'); + } + this._basePath = path.resolve(options.basePath); // Ensure that this path is correct + this._channelName = channelName; + this._listenerName = listenerName; + } + + async _initialize() { + const checkpointPath = this._getCheckpointFileName(); + await fs.ensureDir(path.join(this._basePath, this._channelName)); + await fs.createFile(checkpointPath); + } + + async save(transactionId, blockNumber) { + const checkpointPath = this._getCheckpointFileName(this._chaincodeId); + if (!(await fs.exists(checkpointPath))) { + await this._initialize(); + } + const checkpoint = await this.load(); + if (Number(checkpoint.blockNumber) === Number(blockNumber)) { + const transactionIds = checkpoint.transactionIds; + if (transactionId) { + transactionIds.push(transactionId); + } + checkpoint.transactionIds = transactionIds; + } else { + if (transactionId) { + checkpoint.transactionIds = [transactionId]; + } else { + checkpoint.transactionIds = []; + } + checkpoint.blockNumber = blockNumber; + } + await fs.writeFile(checkpointPath, JSON.stringify(checkpoint)); + } + + async load() { + const checkpointPath = this._getCheckpointFileName(this._chaincodeId); + if (!(await fs.exists(checkpointPath))) { + await this._initialize(); + } + const checkpointBuffer = (await fs.readFile(checkpointPath)); + let checkpoint = checkpointBuffer.toString('utf8'); + if (!checkpoint) { + checkpoint = {}; + } else { + checkpoint = JSON.parse(checkpoint); + } + return checkpoint; + } + + _getCheckpointFileName() { + let filePath = path.join(this._basePath, this._channelName); + if (this._chaincodeId) { + filePath = path.join(filePath, this._chaincodeId); + } + return path.join(filePath, this._listenerName); + } +} + +module.exports = FileSystemCheckpointer; diff --git a/fabric-network/lib/impl/event/roundrobineventhubselectionstrategy.js b/fabric-network/lib/impl/event/roundrobineventhubselectionstrategy.js new file mode 100644 index 0000000000..2d91ff2cbd --- /dev/null +++ b/fabric-network/lib/impl/event/roundrobineventhubselectionstrategy.js @@ -0,0 +1,43 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const AbstractEventHubSelectionStrategy = require('./abstracteventhubselectionstrategy'); + +/** + * The Round Robin Event Hub Strategy is a basic event hub selection strategy used to + * spread out load onto different event hubs and track event hub availability + * + * @memberof module:fabric-network + * @implements {module:fabric-network.AbstractEventHubSelectionStrategy} + * @class + */ +class RoundRobinEventHubSelectionStrategy extends AbstractEventHubSelectionStrategy { + /** + * Constructor. + * @param {Peer[]} peers The list of peers that the strategy can chose from + */ + constructor(peers = []) { + super(peers); + this.lastPeerIdx = null; + } + + /** + * Gets the next peer in the list of peers + * @returns {Peer} + */ + getNextPeer() { + if (this.lastPeerIdx === null || this.lastPeerIdx === this.peers.length - 1) { + this.lastPeerIdx = 0; + } else { + this.lastPeerIdx++; + } + return this.peers[this.lastPeerIdx]; + } +} + +module.exports = RoundRobinEventHubSelectionStrategy; diff --git a/fabric-network/lib/impl/event/transactioneventhandler.js b/fabric-network/lib/impl/event/transactioneventhandler.js index cdeb827494..a04e42a87e 100644 --- a/fabric-network/lib/impl/event/transactioneventhandler.js +++ b/fabric-network/lib/impl/event/transactioneventhandler.js @@ -1,5 +1,5 @@ /** - * Copyright 2018 IBM All Rights Reserved. + * Copyright 2019 IBM All Rights Reserved. * * SPDX-License-Identifier: Apache-2.0 */ @@ -29,12 +29,13 @@ class TransactionEventHandler { /** * Constructor. * @private - * @param {String} transactionId Transaction ID. + * @param {Transaction} transaction Traneaction object. * @param {Object} strategy Event strategy implementation. * @param {TransactionEventHandlerOptions} [options] Additional options. */ - constructor(transactionId, strategy, options) { - this.transactionId = transactionId; + constructor(transaction, strategy, options) { + this.transaction = transaction; + this.transactionId = transaction.getTransactionID().getTransactionID(); this.strategy = strategy; const defaultOptions = { @@ -80,19 +81,17 @@ class TransactionEventHandler { } async _registerTxEventListeners() { - const registrationOptions = {unregister: true}; + const registrationOptions = {unregister: true, fixedEventHub: true}; const promises = this.eventHubs.map((eventHub) => { - return new Promise((resolve) => { + return new Promise(async (resolve) => { logger.debug('_registerTxEventListeners:', `registerTxEvent(${this.transactionId}) for event hub:`, eventHub.getName()); - - eventHub.registerTxEvent( - this.transactionId, - (txId, code) => this._onEvent(eventHub, txId, code), - (err) => this._onError(eventHub, err), - registrationOptions - ); - eventHub.connect(); + await this.transaction.addCommitListener((err, txId, code) => { + if (err) { + return this._onError(eventHub, err); + } + return this._onEvent(eventHub, txId, code); + }, registrationOptions, eventHub); resolve(); }); }); @@ -126,7 +125,7 @@ class TransactionEventHandler { } _onError(eventHub, err) { - logger.info('_onError:', util.format('received error from peer %s: %s', eventHub.getPeerAddr(), err)); + logger.debug('_onError:', util.format('received error from peer %s: %s', eventHub.getPeerAddr(), err)); this._receivedEventHubResponse(eventHub); this.strategy.errorReceived(this._strategySuccess.bind(this), this._strategyFail.bind(this)); @@ -141,7 +140,7 @@ class TransactionEventHandler { * @private */ _strategySuccess() { - logger.info('_strategySuccess:', util.format('strategy success for transaction %j', this.transactionId)); + logger.debug('_strategySuccess:', util.format('strategy success for transaction %j', this.transactionId)); this.cancelListening(); this._resolveNotificationPromise(); diff --git a/fabric-network/lib/network.js b/fabric-network/lib/network.js index af1ce7372b..edb26bf64e 100644 --- a/fabric-network/lib/network.js +++ b/fabric-network/lib/network.js @@ -7,7 +7,9 @@ 'use strict'; const FabricConstants = require('fabric-client/lib/Constants'); const Contract = require('./contract'); -const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); +const BlockEventListener = require('fabric-network/lib/impl/event/blockeventlistener'); +const CommitEventListener = require('fabric-network/lib/impl/event/commiteventlistener'); const logger = require('./logger').getLogger('Network'); const util = require('util'); @@ -31,8 +33,8 @@ class Network { this.gateway = gateway; this.channel = channel; this.contracts = new Map(); - this.eventHubFactory = new EventHubFactory(channel); this.initialized = false; + this.listeners = new Map(); } /** @@ -106,6 +108,12 @@ class Network { // Must be created after channel initialization to ensure discovery has located peers const queryHandlerOptions = this.gateway.getOptions().queryHandlerOptions; this.queryHandler = queryHandlerOptions.strategy(this, queryHandlerOptions); + + this.checkpointer = this.gateway.getOptions().checkpointer; + + const eventHubSelectionOptions = this.gateway.getOptions().eventHubSelectionOptions; + this.eventHubSelectionStrategy = eventHubSelectionOptions.strategy(this); + this.eventHubManager = new EventHubManager(this); } /** @@ -134,6 +142,7 @@ class Network { this, chaincodeId, this.gateway, + this.getCheckpointer(), name ); this.contracts.set(key, contract); @@ -144,33 +153,111 @@ class Network { _dispose() { logger.debug('in _dispose'); + this.listeners.forEach(listener => listener.unregister()); // Danger as this cached in gateway, and also async so how would // network._dispose() followed by network.initialize() be safe ? // make this private is the safest option. this.contracts.clear(); - this.eventHubFactory.dispose(); + this.eventHubManager.dispose(); this.channel.close(); this.initialized = false; } /** - * Get the event hub factory for this network. + * Get the query handler for this network. * @private - * @returns {EventHubFactory} An event hub factory. + * @returns {object} A query handler. */ - getEventHubFactory() { - return this.eventHubFactory; + getQueryHandler() { + return this.queryHandler; } /** - * Get the query handler for this network. + * Get the checkpoint factory * @private - * @returns {object} A query handler. + * @returns {Function} The checkpointer factory */ - getQueryHandler() { - return this.queryHandler; + getCheckpointer() { + return this.checkpointer; + } + + /** + * Get the event hub manager + * @private + * @returns {EventHubManager} An event hub manager + */ + getEventHubManager() { + return this.eventHubManager; + } + + /** + * Get the event hub selection strategy + * @private + * @returns {BaseEventHubSelectionStrategy} + */ + getEventHubSelectionStrategy() { + return this.eventHubSelectionStrategy; + } + + /** + * Save the listener to a map in Network + * @param {String} listenerName the name of the listener being saved + * @param {AbstractEventListener} listener the listener to be saved + * @private + */ + saveListener(listenerName, listener) { + if (this.listeners.has(listenerName)) { + listener.unregister(); + throw new Error(`Listener already exists with the name ${listenerName}`); + } + this.listeners.set(listenerName, listener); + } + + /** + * Create a block event listener + * @param {String} listenerName the name of the listener + * @param {Function} callback the callback called when an event is triggered with signature (error, block) + * @param {Object} [options] Optional. Tjhe event listener options + */ + async addBlockListener(listenerName, callback, options) { + if (!options) { + options = {}; + } + options.replay = options.replay ? true : false; + options.checkpointer = this.getCheckpointer(options); + const listener = new BlockEventListener(this, listenerName, callback, options); + this.saveListener(listenerName, listener); + await listener.register(); + return listener; + } + + /** + * Create a commit event listener for this transaction. + * @param {string} transactionId The transactionId being watched + * @param {Function} callback - This callback will be triggered when + * a transaction commit event is emitted. It takes parameters + * of error, transactionId, transaction status and block number + * @param {RegistrationOptions} [options] - Optional. Options on + * registrations allowing start and end block numbers. + * @param {ChannelEventHub} [eventHub] - Optional. Used to override the event hub selection + * @returns {CommitEventListener} + * @async + */ + async addCommitListener(transactionId, callback, options, eventHub) { + if (!options) { + options = {}; + } + options.replay = false; + options.checkpointer = null; + const listener = new CommitEventListener(this, transactionId, callback, options); + if (eventHub) { + listener.setEventHub(eventHub, options.fixedEventHub); + } + this.saveListener(listener.listenerName, listener); + await listener.register(); + return listener; } } diff --git a/fabric-network/lib/transaction.js b/fabric-network/lib/transaction.js index 48b678565b..5f2ba04430 100644 --- a/fabric-network/lib/transaction.js +++ b/fabric-network/lib/transaction.js @@ -100,6 +100,14 @@ class Transaction { return this; } + /** + * Returns the network from the contract + * @returns {module:fabric-network.Network} + */ + getNetwork() { + return this._contract.getNetwork(); + } + /** * Submit a transaction to the ledger. The transaction function name * will be evaluated on the endorsing peers and then submitted to the ordering service @@ -117,7 +125,7 @@ class Transaction { const network = this._contract.getNetwork(); const channel = network.getChannel(); const txId = this._transactionId.getTransactionID(); - const eventHandler = this._createTxEventHandler(txId, network, this._contract.getEventHandlerOptions()); + const eventHandler = this._createTxEventHandler(this, network, this._contract.getEventHandlerOptions()); const request = this._buildRequest(args); @@ -237,6 +245,23 @@ class Transaction { return this._queryHandler.evaluate(query); } + + /** + * Create a commit event listener for this transaction. + * @param {Function} callback - This callback will be triggered when + * a transaction commit event is emitted. It takes parameters + * of error, transactionId, transaction status and block number + * @param {RegistrationOptions} [options] - Optional. Options on + * registrations allowing start and end block numbers. + * @param {ChannelEventHub} [eventHub] - Optional. Used to override the event hub selection + * @returns {CommitEventListener} + * @async + */ + async addCommitListener(callback, options, eventHub) { + const txid = this.getTransactionID().getTransactionID(); + const network = this._contract.getNetwork(); + return network.addCommitListener(txid, callback, options, eventHub); + } } module.exports = Transaction; diff --git a/fabric-network/test/contract.js b/fabric-network/test/contract.js index 30a83e3f41..599868032a 100644 --- a/fabric-network/test/contract.js +++ b/fabric-network/test/contract.js @@ -8,10 +8,12 @@ const sinon = require('sinon'); const Channel = require('fabric-client/lib/Channel'); +const ChannelEventHub = require('fabric-client/lib/ChannelEventHub'); const Client = require('fabric-client'); const TransactionID = require('fabric-client/lib/TransactionID.js'); const chai = require('chai'); +const expect = chai.expect; chai.use(require('chai-as-promised')); chai.should(); @@ -20,6 +22,9 @@ const Gateway = require('../lib/gateway'); const Network = require('fabric-network/lib/network'); const Transaction = require('../lib/transaction'); const TransactionEventHandler = require('../lib/impl/event/transactioneventhandler'); +const BaseCheckpointer = require('./../lib/impl/event/basecheckpointer'); +const ContractEventListener = require('./../lib/impl/event/contracteventlistener'); +const EventHubManager = require('./../lib/impl/event/eventhubmanager'); describe('Contract', () => { const chaincodeId = 'CHAINCODE_ID'; @@ -29,12 +34,18 @@ describe('Contract', () => { let mockChannel, mockClient, mockGateway; let contract; let mockTransactionID; + let mockCheckpointer; + let mockEventHubManager; + let mockEventHub; beforeEach(() => { + mockEventHub = sinon.createStubInstance(ChannelEventHub); mockChannel = sinon.createStubInstance(Channel); mockClient = sinon.createStubInstance(Client); mockGateway = sinon.createStubInstance(Gateway); + mockCheckpointer = sinon.createStubInstance(BaseCheckpointer); + mockEventHubManager = sinon.createStubInstance(EventHubManager); mockGateway.getClient.returns(mockClient); mockGateway.getOptions.returns({ eventHandlerOptions: { @@ -46,15 +57,18 @@ describe('Contract', () => { } } }); - network = new Network(mockGateway, mockChannel); + mockEventHubManager.getEventHub.returns(mockEventHub); + mockEventHubManager.getReplayEventHub.returns(mockEventHub); + network.eventHubManager = mockEventHubManager; + mockEventHubManager.getPeers.returns(['peer1']); mockTransactionID = sinon.createStubInstance(TransactionID); mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000'); mockClient.newTransactionID.returns(mockTransactionID); mockChannel.getName.returns('testchainid'); - contract = new Contract(network, chaincodeId, mockGateway); + contract = new Contract(network, chaincodeId, mockGateway, mockCheckpointer); }); afterEach(() => { @@ -63,7 +77,7 @@ describe('Contract', () => { describe('#constructor', () => { it('throws if namespace is not a string', () => { - (() => new Contract(network, chaincodeId, mockGateway, 123)) + (() => new Contract(network, chaincodeId, mockGateway, mockCheckpointer, 123)) .should.throw(/namespace/i); }); }); @@ -89,6 +103,43 @@ describe('Contract', () => { }); }); + describe('#getCheckpointer', () => { + it('should return the global checkpointer if it is undefined in options', () => { + const checkpointer = contract.getCheckpointer(); + expect(checkpointer).to.equal(mockCheckpointer); + }); + + it('should return the global checkpointer if it is undefined in options object', () => { + const checkpointer = contract.getCheckpointer({}); + expect(checkpointer).to.equal(mockCheckpointer); + }); + + it('should return the global checkpointer if it is true in options', () => { + const checkpointer = contract.getCheckpointer({checkpointer: 'LOL'}); + expect(checkpointer).to.equal(mockCheckpointer); + }); + + it('should return the checkpointer passed as an option', () => { + const checkpointerFactory = () => {}; + const checkpointer = contract.getCheckpointer({checkpointer: checkpointerFactory}); + expect(checkpointer).to.equal(checkpointerFactory); + expect(checkpointer).to.not.equal(mockCheckpointer); + }); + + it('should return null if checkpointer is false', () => { + const checkpointer = contract.getCheckpointer({checkpointer: false}); + expect(checkpointer).to.be.null; + }); + }); + + describe('#getEventHubSelectionStrategy', () => { + it('should return the eventhub selection strategy', () => { + network.eventHubSelectionStrategy = 'selection-strategy'; + const strategy = contract.getEventHubSelectionStrategy(); + expect(strategy).to.equal('selection-strategy'); + }); + }); + describe('#getEventHandlerOptions', () => { it('returns event handler options from the gateway', () => { const result = contract.getEventHandlerOptions(); @@ -109,7 +160,7 @@ describe('Contract', () => { const name = 'name'; const expected = `${namespace}:${name}`; - contract = new Contract(network, chaincodeId, mockGateway, namespace); + contract = new Contract(network, chaincodeId, mockGateway, mockCheckpointer, namespace); const result = contract.createTransaction(name); result.getName().should.equal(expected); @@ -163,4 +214,39 @@ describe('Contract', () => { result.should.equal(expected); }); }); + + + describe('#addContractListener', () => { + let listenerName; + let testEventName; + let callback; + beforeEach(() => { + listenerName = 'testContractListener'; + testEventName = 'testEvent'; + callback = () => {}; + }); + it('should create options if the options param is undefined', async () => { + const listener = await contract.addContractListener(listenerName, testEventName, callback); + expect(listener).to.be.instanceof(ContractEventListener); + expect(network.listeners.get(listenerName)).to.equal(listener); + }); + + it('should create an instance of ContractEventListener and add it to the list of listeners', async () => { + const listener = await contract.addContractListener(listenerName, testEventName, callback, {}); + expect(listener).to.be.instanceof(ContractEventListener); + expect(network.listeners.get(listenerName)).to.equal(listener); + }); + + it('should change options.replay=undefined to options.replay=false', async () => { + sinon.spy(contract, 'getCheckpointer'); + await contract.addContractListener(listenerName, testEventName, callback, {replay: undefined}); + sinon.assert.calledWith(contract.getCheckpointer, {replay: false, checkpointer: sinon.match.instanceOf(BaseCheckpointer)}); + }); + + it('should change options.replay=\'true\' to options.replay=true', async () => { + sinon.spy(contract, 'getCheckpointer'); + await contract.addContractListener(listenerName, testEventName, callback, {replay: 'true'}); + sinon.assert.calledWith(contract.getCheckpointer, {checkpointer: sinon.match.instanceOf(BaseCheckpointer), replay: true}); + }); + }); }); diff --git a/fabric-network/test/gateway.js b/fabric-network/test/gateway.js index 3c534c19a5..b90b4b572d 100644 --- a/fabric-network/test/gateway.js +++ b/fabric-network/test/gateway.js @@ -397,6 +397,7 @@ describe('Gateway', () => { mockClient.getChannel.withArgs('bar').returns(mockInternalChannel); gateway.getCurrentIdentity = sinon.stub().returns({_mspId: 'MSP01'}); gateway.getOptions().queryHandlerOptions.strategy = () => {}; + gateway.getOptions().eventHubSelectionOptions.strategy = () => {}; const network2 = await gateway.getNetwork('bar'); network2.should.be.instanceof(Network); @@ -410,6 +411,7 @@ describe('Gateway', () => { mockClient.newChannel.withArgs('bar').returns(mockInternalChannel); gateway.getCurrentIdentity = sinon.stub().returns({_mspId: 'MSP01'}); gateway.getOptions().queryHandlerOptions.strategy = () => {}; + gateway.getOptions().eventHubSelectionOptions.strategy = () => {}; const network2 = await gateway.getNetwork('bar'); network2.should.be.instanceof(Network); diff --git a/fabric-network/test/impl/event/abstracteventhubselectionstrategy.js b/fabric-network/test/impl/event/abstracteventhubselectionstrategy.js new file mode 100644 index 0000000000..a57004d3c1 --- /dev/null +++ b/fabric-network/test/impl/event/abstracteventhubselectionstrategy.js @@ -0,0 +1,38 @@ +/** + * Copyright 2018 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; + +const AbstractEventHubSelectionStrategy = require('fabric-network/lib/impl/event/abstracteventhubselectionstrategy'); + +describe('AbstractEventHubStrategy', () => { + let strategy; + let peers; + + beforeEach(() => { + peers = ['peer1']; + strategy = new AbstractEventHubSelectionStrategy(peers); + }); + describe('#getNextPeer', () => { + it('should throw', () => { + expect(() => strategy.getNextPeer()).to.throw(/Abstract/); + }); + }); + describe('#updateEventHubAvailability', () => { + it('should throw', () => { + expect(strategy.updateEventHubAvailability()).to.be.undefined; + }); + }); + + describe('#getPeers', () => { + it('should return a list of peers', () => { + expect(strategy.getPeers()).to.equal(peers); + }); + }); +}); diff --git a/fabric-network/test/impl/event/abstracteventlistener.js b/fabric-network/test/impl/event/abstracteventlistener.js new file mode 100644 index 0000000000..712a71633c --- /dev/null +++ b/fabric-network/test/impl/event/abstracteventlistener.js @@ -0,0 +1,200 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); + +const Channel = require('fabric-client/lib/Channel'); +const ChannelEventHub = require('fabric-client/lib/ChannelEventHub'); +const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError'); +const Contract = require('./../../../lib/contract'); +const Network = require('./../../../lib/network'); +const EventHubManager = require('./../../../lib/impl/event/eventhubmanager'); +const AbstractEventListener = require('./../../../lib/impl/event/abstracteventlistener'); +const FileSystemCheckpointer = require('./../../../lib/impl/event/filesystemcheckpointer'); + +describe('AbstractEventListener', () => { + let sandbox; + + let testListener; + let contractStub; + let networkStub; + let checkpointerStub; + let eventHubManagerStub; + let channelStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + eventHubManagerStub = sandbox.createStubInstance(EventHubManager); + contractStub = sandbox.createStubInstance(Contract); + networkStub = sandbox.createStubInstance(Network); + networkStub.getEventHubManager.returns(eventHubManagerStub); + contractStub.getNetwork.returns(networkStub); + checkpointerStub = sandbox.createStubInstance(FileSystemCheckpointer); + checkpointerStub.setChaincodeId = sandbox.stub(); + channelStub = sandbox.createStubInstance(Channel); + networkStub.getChannel.returns(channelStub); + channelStub.getName.returns('mychannel'); + eventHubManagerStub.getPeers.returns(['peer1']); + channelStub.queryInfo.returns({height: 0}); + + contractStub.getChaincodeId.returns('ccid'); + const callback = (err) => {}; + testListener = new AbstractEventListener(networkStub, 'testListener', callback, {option: 'anoption', replay: true}); + + }); + + afterEach(() => { + sandbox.reset(); + }); + + describe('#constructor', () => { + it('should set the correct properties on instantiation', () => { + const callback = (err) => {}; + const listener = new AbstractEventListener(networkStub, 'testlistener', callback, {option: 'anoption'}); + expect(listener.network).to.equal(networkStub); + expect(listener.listenerName).to.equal('testlistener'); + expect(listener.eventCallback).to.equal(callback); + expect(listener.options).to.deep.equal({option: 'anoption'}); + expect(listener.checkpointer).to.be.undefined; + expect(listener._registered).to.be.false; + expect(listener._firstCheckpoint).to.deep.equal({}); + expect(listener._registration).to.be.null; + }); + + it('should set options if options is undefined', () => { + const callback = (err) => {}; + const listener = new AbstractEventListener(networkStub, 'testlistener', callback); + expect(listener.options).to.deep.equal({}); + }); + + it('should set this.filtered to be options.filtered', () => { + const listener = new AbstractEventListener(networkStub, 'testListener', () => {}, {filtered: false}); + expect(listener._filtered).to.be.false; + }); + }); + + describe('#register', () => { + it('should throw if the listener is already registered', () => { + testListener._registered = true; + expect(testListener.register()).to.be.rejectedWith('Listener already registered'); + }); + + it('should not call checkpointer._initialize() or checkpointer.load()', async () => { + await testListener.register(); + sinon.assert.notCalled(checkpointerStub.load); + }); + + it('should not call checkpointer.initialize()', async () => { + const checkpoint = {transactionId: 'txid', blockNumber: '10'}; + checkpointerStub.load.returns(checkpoint); + testListener.checkpointer = checkpointerStub; + await testListener.register(); + sinon.assert.called(checkpointerStub.load); + expect(testListener.options.startBlock.toNumber()).to.equal(10); // Start block is a Long + expect(testListener._firstCheckpoint).to.deep.equal(checkpoint); + }); + + it('should disconnect and reset the event hub if it emits the wrong type of events', async () => { + const eventHub = sinon.createStubInstance(ChannelEventHub); + eventHub.isFiltered.returns(true); + eventHub.isconnected.returns(true); + testListener.eventHub = eventHub; + testListener._filtered = false; + await testListener.register(); + sinon.assert.called(eventHub.disconnect); + expect(testListener.eventHub).to.be.null; + }); + + it('should return undefined and log when no peers are available', async() => { + eventHubManagerStub.getPeers.returns([]); + await testListener.register(); + }); + + it('should call the checkpointer factory if it is set', async () => { + const checkpointerFactoryStub = sinon.stub().returns(checkpointerStub); + const listener = new AbstractEventListener(networkStub, 'testlistener', () => {}, {replay: true, checkpointer: {factory: checkpointerFactoryStub}}); + await listener.register(); + sinon.assert.calledWith(checkpointerFactoryStub, 'mychannel', 'testlistener'); + sinon.assert.called(checkpointerStub.setChaincodeId); + expect(listener.checkpointer).to.equal(checkpointerStub); + }); + + it('should log an error if replay is enabled and no checkpointer is given', async () => { + const listener = new AbstractEventListener(networkStub, 'testlistener', () => {}, {replay: true}); + await listener.register(); + }); + + }); + + describe('#unregister', () => { + beforeEach(async () => { + checkpointerStub.load.returns({transactionId: 'txid', blockNumber: '10'}); + testListener.checkpointer = checkpointerStub; + await testListener.register(); + }); + it('should reset the correct variables', async () => { + await testListener.unregister(); + expect(testListener._registered).to.be.false; + expect(testListener.startBlock).to.be.undefined; + expect(testListener.options.endBlock).to.be.undefined; + expect(testListener._firstCheckpoint).to.deep.equal({}); + }); + }); + + describe('#isRegistered', () => { + it('should return false if the listener has not been registered', () => { + expect(testListener.isregistered()).to.be.false; + }); + + // Abstract listener does not change the register status + it('should return false if the listener has been registered', async () => { + await testListener.register(); + expect(testListener.isregistered()).to.be.false; + }); + + it('should return false if registered and unregistered', async () => { + await testListener.register(); + testListener.unregister(); + expect(testListener.isregistered()).to.be.false; + }); + }); + + describe('#getCheckpointer', () => { + it('should return undefined if checkpointer has not been set', () => { + expect(testListener.getCheckpointer()).to.be.undefined; + }); + + it('should return the checkpointer if it has been set', () => { + testListener.checkpointer = checkpointerStub; + expect(testListener.getCheckpointer()).to.equal(checkpointerStub); + }); + }); + + describe('#getEventHubManager', () => { + it('shouild return the event hub manager from the network', () => { + expect(testListener.getEventHubManager()).to.equal(eventHubManagerStub); + }); + }); + + describe('#_isShutdownMessage', () => { + it('should return false if an error is not given', () => { + expect(testListener._isShutdownMessage()).to.be.false; + }); + + it('should return false if error message does not match', () => { + expect(testListener._isShutdownMessage(new Error('An error'))).to.be.false; + }); + + it('should return true if the error message does match', () => { + expect(testListener._isShutdownMessage(new EventHubDisconnectError())).to.be.true; + }); + }); +}); diff --git a/fabric-network/test/impl/event/basecheckpointer.js b/fabric-network/test/impl/event/basecheckpointer.js new file mode 100644 index 0000000000..2df881e6e2 --- /dev/null +++ b/fabric-network/test/impl/event/basecheckpointer.js @@ -0,0 +1,41 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +chai.use(require('chai-as-promised')); + +const BaseCheckpointer = require('./../../../lib/impl/event/basecheckpointer'); + +describe('BaseCheckpointer', () => { + describe('#constructor', () => { + it('options should be set', () => { + const checkpointer = new BaseCheckpointer(); + expect(checkpointer.options).to.deep.equal({}); + }); + + it('options should be set to an object', () => { + const checkpointer = new BaseCheckpointer({option: 'anoption'}); + expect(checkpointer.options).to.deep.equal({option: 'anoption'}); + }); + }); + + describe('#save', () => { + it('should throw an exception', () => { + const checkpointer = new BaseCheckpointer(); + expect(checkpointer.save()).to.be.rejectedWith('Method has not been implemented'); + }); + }); + + describe('#load', () => { + it('should throw an exception', () => { + const checkpointer = new BaseCheckpointer(); + expect(checkpointer.load()).to.be.rejectedWith('Method has not been implemented'); + }); + }); +}); diff --git a/fabric-network/test/impl/event/blockeventlistener.js b/fabric-network/test/impl/event/blockeventlistener.js new file mode 100644 index 0000000000..92a7a70432 --- /dev/null +++ b/fabric-network/test/impl/event/blockeventlistener.js @@ -0,0 +1,189 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); +chai.use(require('chai-as-promised')); + +const Contract = require('fabric-network/lib/contract'); +const Network = require('fabric-network/lib/network'); +const ChannelEventHub = require('fabric-client/lib/ChannelEventHub'); +const Channel = require('fabric-client/lib/Channel'); +const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError'); +const BlockEventListener = require('fabric-network/lib/impl/event/blockeventlistener'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); +const Checkpointer = require('fabric-network/lib/impl/event/basecheckpointer'); + +describe('BlockEventListener', () => { + let sandbox; + let channelStub; + let contractStub; + let networkStub; + let eventHubStub; + let checkpointerStub; + let eventHubManagerStub; + let blockEventListener; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + channelStub = sandbox.createStubInstance(Channel); + channelStub.getName.returns('mychannel'); + contractStub = sandbox.createStubInstance(Contract); + contractStub.getChaincodeId.returns('chaincodeid'); + networkStub = sandbox.createStubInstance(Network); + networkStub.getChannel.returns(channelStub); + contractStub.getNetwork.returns(networkStub); + eventHubManagerStub = sinon.createStubInstance(EventHubManager); + eventHubManagerStub.getPeers.returns(['peer1']); + networkStub.getEventHubManager.returns(eventHubManagerStub); + eventHubStub = sandbox.createStubInstance(ChannelEventHub); + checkpointerStub = sandbox.createStubInstance(Checkpointer); + blockEventListener = new BlockEventListener(networkStub, 'test', () => {}, {replay: true}); + }); + describe('#register', () => { + it('should register a block event, connect to the event hub and set the register flag', async () => { + blockEventListener.eventHub = eventHubStub; + sandbox.spy(blockEventListener._onEvent, 'bind'); + sandbox.spy(blockEventListener._onError, 'bind'); + await blockEventListener.register(); + sinon.assert.calledWith( + eventHubStub.registerBlockEvent, + sinon.match.func, + sinon.match.func, + {replay: true} + ); + sinon.assert.calledWith(blockEventListener._onEvent.bind, blockEventListener); + sinon.assert.calledWith(blockEventListener._onError.bind, blockEventListener); + sinon.assert.called(eventHubStub.connect); + expect(blockEventListener._registered).to.be.true; + }); + + it('should call _registerWithNewEventHub', async () => { + sandbox.stub(blockEventListener, '_registerWithNewEventHub'); + await blockEventListener.register(); + sinon.assert.called(blockEventListener._registerWithNewEventHub); + }); + }); + + describe('#unregister', () => { + it('should not call ChannelEventHub.unregisterBlockEvent', async () => { + await blockEventListener.unregister(); + sinon.assert.notCalled(eventHubStub.unregisterBlockEvent); + }); + + it('should call ChannelEventHub.unregisterBlockEvent', async () => { + eventHubStub.registerBlockEvent.returns('registration'); + blockEventListener.eventHub = eventHubStub; + await blockEventListener.register(); + blockEventListener.unregister(); + sinon.assert.calledWith(eventHubStub.unregisterBlockEvent, 'registration'); + }); + }); + + describe('#_onEvent', () => { + beforeEach(() => { + blockEventListener._registration = {}; + sandbox.spy(blockEventListener, 'unregister'); + sandbox.stub(blockEventListener, 'eventCallback'); + }); + + it('should call the event callback', () => { + const block = {number: '10'}; + blockEventListener._onEvent(block); + sinon.assert.calledWith(blockEventListener.eventCallback, null, block); + sinon.assert.notCalled(checkpointerStub.save); + sinon.assert.notCalled(blockEventListener.unregister); + }); + + it('should save a checkpoint', () => { + const block = {number: '10'}; + blockEventListener.checkpointer = checkpointerStub; + blockEventListener._onEvent(block); + sinon.assert.calledWith(checkpointerStub.save, null, 10); + }); + + it('should unregister if registration.unregister is set', () => { + const block = {number: '10'}; + blockEventListener._registration.unregister = true; + blockEventListener._onEvent(block); + sinon.assert.calledWith(blockEventListener.eventCallback, null, block); + sinon.assert.called(blockEventListener.unregister); + }); + + it ('should not save a checkpoint if the callback fails', () => { + const block = {number: '10'}; + blockEventListener.eventCallback.throws(new Error()); + blockEventListener.checkpointer = checkpointerStub; + blockEventListener._onEvent(block); + sinon.assert.calledWith(blockEventListener.eventCallback, null, block); + sinon.assert.calledWith(checkpointerStub.save, null, 10); + }); + }); + + describe('#_onError', () => { + beforeEach(() => { + blockEventListener.eventHub = eventHubStub; + eventHubStub._peer = 'peer'; + blockEventListener._registration = {}; + sandbox.spy(blockEventListener, 'unregister'); + sandbox.stub(blockEventListener, 'eventCallback'); + sandbox.stub(blockEventListener, '_registerWithNewEventHub'); + }); + + it('should call the event callback with an error', async () => { + const error = new EventHubDisconnectError(); + await blockEventListener._onError(error); + sinon.assert.calledWith(blockEventListener.eventCallback, error); + }); + + it('should update event hub availability and reregister if disconnected', async () => { + const error = new EventHubDisconnectError(); + blockEventListener.eventHub = eventHubStub; + blockEventListener._registered = true; + await blockEventListener._onError(error); + sinon.assert.calledWith(eventHubManagerStub.updateEventHubAvailability, 'peer'); + sinon.assert.called(blockEventListener._registerWithNewEventHub); + sinon.assert.calledWith(blockEventListener.eventCallback, error); + }); + + it('should call the error callback if the error is null', async () => { + const error = null; + blockEventListener.eventHub = eventHubStub; + blockEventListener._registered = true; + await blockEventListener._onError(error); + sinon.assert.calledWith(blockEventListener.eventCallback, error); + }); + }); + + describe('#_registerWithNewEventHub', () => { + beforeEach(() => { + blockEventListener._registration = {}; + sandbox.spy(blockEventListener, 'unregister'); + sandbox.stub(blockEventListener, 'eventCallback'); + eventHubManagerStub.getReplayEventHub.returns(eventHubStub); + eventHubManagerStub.getEventHub.returns(eventHubStub); + sinon.stub(blockEventListener, 'register'); + }); + + it('should call unregister, get a new event hub and reregister', () => { + blockEventListener._registerWithNewEventHub(); + sinon.assert.called(blockEventListener.unregister); + sinon.assert.called(eventHubManagerStub.getEventHub); + sinon.assert.called(blockEventListener.register); + }); + + it('should get a replay event hub if a checkpointer is present', () => { + blockEventListener.checkpointer = checkpointerStub; + blockEventListener._registerWithNewEventHub(); + sinon.assert.called(blockEventListener.unregister); + sinon.assert.called(eventHubManagerStub.getReplayEventHub); + sinon.assert.called(blockEventListener.register); + }); + }); +}); diff --git a/fabric-network/test/impl/event/checkpointfactories.js b/fabric-network/test/impl/event/checkpointfactories.js new file mode 100644 index 0000000000..8df6fc5a06 --- /dev/null +++ b/fabric-network/test/impl/event/checkpointfactories.js @@ -0,0 +1,22 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; + +const CheckpointFactories = require('fabric-network/lib/impl/event/checkpointfactories'); +const FileSystemCheckpointer = require('fabric-network/lib/impl/event/filesystemcheckpointer'); + +describe('CheckpointFactories', () => { + describe('#FILE_SYSTEM_CHECKPOINTER', () => { + it('should return an instance of the file system checkpointer', () => { + const checkpointer = CheckpointFactories.FILE_SYSTEM_CHECKPOINTER('channelName', 'listnerName'); + expect(checkpointer).to.be.instanceof(FileSystemCheckpointer); + }); + }); +}); diff --git a/fabric-network/test/impl/event/commiteventlistener.js b/fabric-network/test/impl/event/commiteventlistener.js new file mode 100644 index 0000000000..143cd28a99 --- /dev/null +++ b/fabric-network/test/impl/event/commiteventlistener.js @@ -0,0 +1,228 @@ +/** + * Copyright 2019 Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +chai.use(require('chai-as-promised')); +const expect = chai.expect; +const sinon = require('sinon'); + +const Contract = require('fabric-network/lib/contract'); +const Network = require('fabric-network/lib/network'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); +const ChannelEventHub = require('fabric-client/lib/ChannelEventHub'); +const CommitEventListener = require('fabric-network/lib/impl/event/commiteventlistener'); + +describe('CommitEventListener', () => { + let sandbox; + let eventHubManagerStub; + let eventHubStub; + let contractStub; + let networkStub; + let listener; + let callback; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + eventHubStub = sandbox.createStubInstance(ChannelEventHub); + eventHubStub._transactionRegistrations = []; + contractStub = sandbox.createStubInstance(Contract); + networkStub = sandbox.createStubInstance(Network); + contractStub.getNetwork.returns(networkStub); + eventHubManagerStub = sinon.createStubInstance(EventHubManager); + eventHubManagerStub.getPeers.returns(['peer1']); + networkStub.getEventHubManager.returns(eventHubManagerStub); + + callback = () => {}; + listener = new CommitEventListener(networkStub, 'transactionId', callback, {}); + }); + + afterEach(() => { + sandbox.reset(); + }); + + describe('#constructor', () => { + it('should set the listener name and transactionId', () => { + expect(listener.transactionId).to.equal('transactionId'); + expect(listener.listenerName).to.match(/^transactionId[.0-9]+$/); + }); + }); + + describe('#register', () => { + beforeEach(() => { + sandbox.stub(listener, '_registerWithNewEventHub'); + // sandbox.stub(eventHubManagerStub, 'getEventHub'); + }); + + it('should grab a new event hub if one isnt given', async () => { + await listener.register(); + sinon.assert.called(listener._registerWithNewEventHub); + }); + + it('should assign a new event hub if given on has registrations', async () => { + const newEventHub = sandbox.createStubInstance(ChannelEventHub); + newEventHub._transactionRegistrations = {}; + eventHubManagerStub.getEventHub.returns(newEventHub); + listener.eventHub = eventHubStub; + eventHubStub._peer = 'peer'; + eventHubStub._transactionRegistrations = {transactionId: 'registration'}; + await listener.register(); + sinon.assert.calledWith(eventHubManagerStub.getEventHub, 'peer'); + }); + + it('should call registerTxEvent', async () => { + listener.eventHub = eventHubStub; + await listener.register(); + sinon.assert.calledWith( + eventHubStub.registerTxEvent, + 'transactionId', + sinon.match.func, + sinon.match.func, + {unregister: true} + ); + sinon.assert.called(eventHubStub.connect); + expect(listener._registered).to.be.true; + }); + + it('should assign an an event hub instance from the same peer if options.fixedEventHub is true', async () => { + eventHubManagerStub.getFixedEventHub.returns(eventHubStub); + eventHubStub._peer = 'peer'; + listener.setEventHub(eventHubStub, true); + eventHubStub._transactionRegistrations = {transactionId: {}}; + await listener.register(); + sinon.assert.calledWith(eventHubManagerStub.getFixedEventHub, eventHubStub._peer); + }); + }); + + describe('#unregister', () => { + it('should not call ChannelEventHub.unregisterTxEvent', () => { + listener.unregister(); + sinon.assert.notCalled(eventHubStub.unregisterTxEvent); + }); + + it('should call ChannelEventHub.unregisterBlockEvent', () => { + listener.eventHub = eventHubStub; + listener.register(); + listener.unregister(); + sinon.assert.calledWith(eventHubStub.unregisterTxEvent, 'transactionId'); + }); + }); + + describe('#_onEvent', () => { + beforeEach(() => { + listener._registration = {}; + sandbox.spy(listener, 'unregister'); + sandbox.stub(listener, 'eventCallback'); + }); + + it('should call the event callback', () => { + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + listener._onEvent(transactionId, status, blockNumber); + sinon.assert.calledWith(listener.eventCallback, null, transactionId, status, Number(blockNumber)); + sinon.assert.notCalled(listener.unregister); + }); + + it('should unregister if registration.unregister is set', () => { + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + listener._registration.unregister = true; + listener._onEvent(transactionId, status, blockNumber); + sinon.assert.calledWith(listener.eventCallback, null, transactionId, status, 10); + sinon.assert.called(listener.unregister); + }); + + it('should not fail if eventCallback throws', () => { + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + listener.eventCallback.throws(new Error('forced error')); + listener._onEvent(transactionId, status, blockNumber); + }); + }); + + describe('#_onError', () => { + beforeEach(() => { + eventHubStub._peer = 'peer'; + listener._registration = {}; + sandbox.spy(listener, 'unregister'); + sandbox.stub(listener, 'eventCallback'); + }); + it('should call eventCallback', () => { + listener.eventHub = eventHubStub; + const error = new Error(); + listener._onError(error); + sinon.assert.calledWith(listener.eventCallback, error); + }); + }); + + describe('#setEventHub', () => { + it('should set the eventhub', () => { + listener.setEventHub('new event hub'); + expect(listener.eventHub).to.equal('new event hub'); + }); + + it('should set options.fixedEventHub', () => { + listener.setEventHub('new event hub', true); + expect(listener.options.fixedEventHub).to.be.true; + }); + }); + + describe('#_registerWithNewEventHub', () => { + beforeEach(() => { + listener._registration = {}; + sandbox.spy(listener, 'unregister'); + sandbox.stub(listener, 'eventCallback'); + eventHubManagerStub.getReplayEventHub.returns(eventHubStub); + sinon.stub(listener, 'register'); + }); + + it('should call the correct methods', () => { + listener._registerWithNewEventHub(); + sinon.assert.called(eventHubManagerStub.getReplayEventHub); + expect(listener.eventHub).to.equal(eventHubStub); + expect(listener.options.disconnect).to.be.true; + sinon.assert.called(listener.register); + }); + + it('should call EventHubManager.getFixedEventHub if options.fixedEventHub', () => { + eventHubStub._peer = 'peer'; + listener.eventHub = eventHubStub; + listener.options.fixedEventHub = true; + listener._registerWithNewEventHub(); + sinon.assert.calledWith(eventHubManagerStub.getFixedEventHub, eventHubStub._peer); + }); + + it('should unregister if the listener is already registered', () => { + listener._registered = true; + listener._registerWithNewEventHub(); + sinon.assert.called(listener.unregister); + }); + + it('should throw if options.fixedEventHub is set and no event hub is given', () => { + listener.options.fixedEventHub = true; + expect(listener._registerWithNewEventHub()).to.be.rejectedWith(); + }); + }); + + describe('#_isAlreadyRegistered', () => { + it('should throw if no event hub is given', () => { + expect(() => listener._isAlreadyRegistered()).to.throw(/Event hub not given/); + }); + + it('should return false if no registration exists', () => { + expect(listener._isAlreadyRegistered(eventHubStub)).to.be.false; + }); + + it('should return true if registration exists', () => { + eventHubStub._transactionRegistrations = {transactionId: 'registration'}; + expect(listener._isAlreadyRegistered(eventHubStub)).to.be.true; + }); + }); +}); diff --git a/fabric-network/test/impl/event/contracteventlistener.js b/fabric-network/test/impl/event/contracteventlistener.js new file mode 100644 index 0000000000..03dfa0d90e --- /dev/null +++ b/fabric-network/test/impl/event/contracteventlistener.js @@ -0,0 +1,231 @@ +/* + Copyright 2019 IBM All Rights Reserved. + + SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); + +const Contract = require('fabric-network/lib/contract'); +const Network = require('fabric-network/lib/network'); +const Channel = require('fabric-client/lib/Channel'); +const ChannelEventHub = require('fabric-client/lib/ChannelEventHub'); +const EventHubDisconnectError = require('fabric-client/lib/errors/EventHubDisconnectError'); +const ContractEventListener = require('fabric-network/lib/impl/event/contracteventlistener'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); +const Checkpointer = require('fabric-network/lib/impl/event/basecheckpointer'); + +describe('ContractEventListener', () => { + let sandbox; + let contractStub; + let networkStub; + let channelStub; + let eventHubStub; + let checkpointerStub; + let eventHubManagerStub; + let contractEventListener; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + contractStub = sandbox.createStubInstance(Contract); + contractStub.getChaincodeId.returns('chaincodeid'); + networkStub = sandbox.createStubInstance(Network); + channelStub = sandbox.createStubInstance(Channel); + channelStub.getName.returns('channelName'); + networkStub.getChannel.returns(channelStub); + contractStub.getNetwork.returns(networkStub); + eventHubManagerStub = sinon.createStubInstance(EventHubManager); + networkStub.getEventHubManager.returns(eventHubManagerStub); + eventHubManagerStub.getPeers.returns(['peer1']); + eventHubStub = sandbox.createStubInstance(ChannelEventHub); + checkpointerStub = sandbox.createStubInstance(Checkpointer); + contractEventListener = new ContractEventListener(contractStub, 'test', 'eventName', () => {}, {}); + }); + + afterEach(() => { + sandbox.reset(); + }); + + describe('#contructor', () => { + it('should set the listener name', () => { + expect(contractEventListener.eventName).to.equal('eventName'); + }); + }); + + describe('#register', () => { + it('should register a block event, connect to the event hub and set the register flag', async () => { + contractEventListener.eventHub = eventHubStub; + sandbox.spy(contractEventListener._onEvent, 'bind'); + sandbox.spy(contractEventListener._onError, 'bind'); + await contractEventListener.register(); + sinon.assert.calledWith( + eventHubStub.registerChaincodeEvent, + 'chaincodeid', + 'eventName', + sinon.match.func, + sinon.match.func, + {} + ); + sinon.assert.calledWith(contractEventListener._onEvent.bind, contractEventListener); + sinon.assert.calledWith(contractEventListener._onError.bind, contractEventListener); + sinon.assert.called(eventHubStub.connect); + expect(contractEventListener._registered).to.be.true; + }); + + it('should call _registerWithNewEventHub', async () => { + sandbox.stub(contractEventListener, '_registerWithNewEventHub'); + await contractEventListener.register(); + sinon.assert.called(contractEventListener._registerWithNewEventHub); + }); + }); + + describe('#unregister', () => { + it('should not call ChannelEventHub.unregisterChaincodeEvent', () => { + contractEventListener.unregister(); + sinon.assert.notCalled(eventHubStub.unregisterChaincodeEvent); + }); + + it('should call ChannelEventHub.unregisterBlockEvent', async () => { + eventHubStub.registerChaincodeEvent.returns('registration'); + contractEventListener.eventHub = eventHubStub; + await contractEventListener.register(); + contractEventListener.unregister(); + sinon.assert.calledWith(eventHubStub.unregisterChaincodeEvent, 'registration'); + }); + }); + + describe('#_onEvent', () => { + beforeEach(() => { + contractEventListener._registration = {}; + sandbox.spy(contractEventListener, 'unregister'); + sandbox.stub(contractEventListener, 'eventCallback'); + }); + + it('should call the event callback', () => { + const event = {name: 'eventName'}; + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + contractEventListener._onEvent(event, blockNumber, transactionId, status); + sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status); + sinon.assert.notCalled(checkpointerStub.save); + sinon.assert.notCalled(contractEventListener.unregister); + }); + + it('should save a checkpoint', async () => { + const event = {name: 'eventName'}; + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + contractEventListener.checkpointer = checkpointerStub; + contractEventListener.options.replay = true; + await contractEventListener._onEvent(event, blockNumber, transactionId, status); + sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status); + sinon.assert.calledWith(checkpointerStub.save, 'transactionId', 10); + sinon.assert.notCalled(contractEventListener.unregister); + }); + + it('should unregister if registration.unregister is set', async () => { + const event = {name: 'eventName'}; + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + contractEventListener._registration.unregister = true; + await contractEventListener._onEvent(event, blockNumber, transactionId, status); + sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status); + sinon.assert.called(contractEventListener.unregister); + }); + + it ('should not save a checkpoint if the callback fails', async () => { + const event = {name: 'eventName'}; + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + contractEventListener.eventCallback.throws(new Error()); + contractEventListener.checkpointer = checkpointerStub; + contractEventListener.options.replay = true; + await contractEventListener._onEvent(event, blockNumber, transactionId, status); + sinon.assert.calledWith(contractEventListener.eventCallback, null, event, Number(blockNumber), transactionId, status); + sinon.assert.calledWith(checkpointerStub.save, 'transactionId', 10); + }); + + it('should skip a transaction if it is in the checkpoint', async () => { + contractEventListener.checkpointer = checkpointerStub; + const checkpoint = {transactionIds: ['transactionId']}; + contractEventListener._firstCheckpoint = checkpoint; + checkpointerStub.load.returns(checkpoint); + const event = {name: 'eventName'}; + const blockNumber = '10'; + const transactionId = 'transactionId'; + const status = 'VALID'; + contractEventListener.options.replay = true; + await contractEventListener._onEvent(event, blockNumber, transactionId, status); + sinon.assert.notCalled(contractEventListener.eventCallback); + }); + }); + + describe('#_onError', () => { + beforeEach(() => { + eventHubStub._peer = 'peer'; + contractEventListener._registration = {}; + sandbox.spy(contractEventListener, 'unregister'); + sandbox.stub(contractEventListener, 'eventCallback'); + sandbox.stub(contractEventListener, '_registerWithNewEventHub'); + }); + + it('should call the event callback with an error', () => { + contractEventListener.eventHub = eventHubStub; + const error = new Error(); + contractEventListener._onError(error); + sinon.assert.calledWith(contractEventListener.eventCallback, error); + }); + + it('should update event hub availability and reregister if disconnected', async () => { + const error = new EventHubDisconnectError('ChannelEventHub has been shutdown'); + contractEventListener.eventHub = eventHubStub; + contractEventListener._registered = true; + await contractEventListener._onError(error); + sinon.assert.calledWith(eventHubManagerStub.updateEventHubAvailability, 'peer'); + sinon.assert.called(contractEventListener._registerWithNewEventHub); + sinon.assert.calledWith(contractEventListener.eventCallback, error); + }); + + it('should call the error callback if the error is null', async () => { + const error = null; + contractEventListener.eventHub = eventHubStub; + contractEventListener._registered = true; + await contractEventListener._onError(error); + sinon.assert.calledWith(contractEventListener.eventCallback, error); + }); + }); + + describe('#_registerWithNewEventHub', () => { + beforeEach(() => { + contractEventListener._registration = {}; + sandbox.spy(contractEventListener, 'unregister'); + sandbox.stub(contractEventListener, 'eventCallback'); + eventHubManagerStub.getReplayEventHub.returns(eventHubStub); + eventHubManagerStub.getEventHub.returns(eventHubStub); + sinon.stub(contractEventListener, 'register'); + }); + + it('should call unregister, get a new event hub and reregister', () => { + contractEventListener._registerWithNewEventHub(); + sinon.assert.called(contractEventListener.unregister); + sinon.assert.called(eventHubManagerStub.getEventHub); + sinon.assert.called(contractEventListener.register); + }); + + it('should get a replay event hub if a checkpointer is present', () => { + contractEventListener.checkpointer = checkpointerStub; + contractEventListener.options.replay = true; + contractEventListener._registerWithNewEventHub(); + sinon.assert.called(contractEventListener.unregister); + sinon.assert.called(eventHubManagerStub.getReplayEventHub); + sinon.assert.called(contractEventListener.register); + }); + }); +}); diff --git a/fabric-network/test/impl/event/defaulteventhandlerstrategies.js b/fabric-network/test/impl/event/defaulteventhandlerstrategies.js index 7cd9af0038..c87359a476 100644 --- a/fabric-network/test/impl/event/defaulteventhandlerstrategies.js +++ b/fabric-network/test/impl/event/defaulteventhandlerstrategies.js @@ -10,9 +10,11 @@ const sinon = require('sinon'); const chai = require('chai'); const expect = chai.expect; -const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); const ChannelEventHub = require('fabric-client').ChannelEventHub; const Network = require('fabric-network/lib/network'); +const Transaction = require('fabric-network/lib/transaction'); +const TransactionID = require('fabric-client/lib/TransactionID'); const Channel = require('fabric-client').Channel; const AllForTxStrategy = require('fabric-network/lib/impl/event/allfortxstrategy'); const AnyForTxStrategy = require('fabric-network/lib/impl/event/anyfortxstrategy'); @@ -32,6 +34,7 @@ describe('DefaultEventHandlerStrategies', () => { let options; let stubNetwork; + let stubTransaction; beforeEach(() => { options = { @@ -49,8 +52,8 @@ describe('DefaultEventHandlerStrategies', () => { const stubEventHub = sinon.createStubInstance(ChannelEventHub); stubEventHub.isconnected.returns(true); - const stubEventHubFactory = sinon.createStubInstance(EventHubFactory); - stubEventHubFactory.getEventHubs.withArgs([stubPeer]).resolves([stubEventHub]); + const stubEventHubManager = sinon.createStubInstance(EventHubManager); + stubEventHubManager.getEventHubs.withArgs([stubPeer]).resolves([stubEventHub]); const channel = sinon.createStubInstance(Channel); channel.getPeers.returns([stubPeer]); @@ -58,7 +61,13 @@ describe('DefaultEventHandlerStrategies', () => { stubNetwork = sinon.createStubInstance(Network); stubNetwork.getChannel.returns(channel); - stubNetwork.getEventHubFactory.returns(stubEventHubFactory); + stubNetwork.getEventHubManager.returns(stubEventHubManager); + + stubTransaction = sinon.createStubInstance(Transaction); + const stubTransactionId = sinon.createStubInstance(TransactionID); + stubTransactionId.getTransactionID.returns(transactionId); + stubTransaction.getTransactionID.returns(stubTransactionId); + stubTransaction.getNetwork.returns(stubNetwork); }); afterEach(() => { @@ -71,13 +80,17 @@ describe('DefaultEventHandlerStrategies', () => { let eventHandler; beforeEach(() => { - eventHandler = createTxEventHandler(transactionId, stubNetwork, options); + eventHandler = createTxEventHandler(stubTransaction, options); }); it('Returns a TransactionEventHandler', () => { expect(eventHandler).to.be.an.instanceOf(TransactionEventHandler); }); + it('Sets transaction on event handler', () => { + expect(eventHandler.transaction).to.equal(stubTransaction); + }); + it('Sets transaction ID on event handler', () => { expect(eventHandler.transactionId).to.equal(transactionId); }); diff --git a/fabric-network/test/impl/event/defaulteventhubselectionstrategies.js b/fabric-network/test/impl/event/defaulteventhubselectionstrategies.js new file mode 100644 index 0000000000..113d80c6e1 --- /dev/null +++ b/fabric-network/test/impl/event/defaulteventhubselectionstrategies.js @@ -0,0 +1,42 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); + +const Channel = require('fabric-client/lib/Channel'); +const Network = require('fabric-network/lib/network'); +const RoundRobinEventSelectionStrategy = require('fabric-network/lib/impl/event/roundrobineventhubselectionstrategy'); +const DefaultEventHubSelectionStrategies = require('fabric-network/lib/impl/event/defaulteventhubselectionstrategies'); + +describe('DefaultEventHubSelectionStrategies', () => { + describe('#MSPID_SCOPE_ROUND_ROBIN', () => { + let network; + let channel; + let peer1, peer2, peer3; + beforeEach(() => { + network = sinon.createStubInstance(Network); + channel = sinon.createStubInstance(Channel); + network.getChannel.returns(channel); + peer1 = sinon.stub({isInRole() {}}); + peer2 = sinon.stub({isInRole() {}}); + peer3 = sinon.stub({isInRole() {}}); + channel.getPeersForOrg.returns([peer1, peer2, peer3]); + }); + + it('should get organization peers and filter by those that are in the correct roles', () => { + peer1.isInRole.returns(true); + peer2.isInRole.returns(false); + peer3.isInRole.returns(true); + + const strategy = DefaultEventHubSelectionStrategies.MSPID_SCOPE_ROUND_ROBIN(network); + expect(strategy).to.be.instanceof(RoundRobinEventSelectionStrategy); + }); + }); +}); diff --git a/fabric-network/test/impl/event/eventhubmanager.js b/fabric-network/test/impl/event/eventhubmanager.js new file mode 100644 index 0000000000..410d55b894 --- /dev/null +++ b/fabric-network/test/impl/event/eventhubmanager.js @@ -0,0 +1,267 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); +chai.use(require('chai-as-promised')); + +const {Channel, ChannelEventHub, Peer} = require('fabric-client'); +const Network = require('fabric-network/lib/network'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); +const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory'); +const AbstractEventHubSelectionStrategy = require('fabric-network/lib/impl/event/abstracteventhubselectionstrategy'); + + +function populateEventHub(eventHub) { + eventHub._chaincodeRegistrants = {}; + eventHub._blockRegistrations = {}; + eventHub._transactionRegistrations = {}; + return eventHub; +} + +describe('EventHubManager', () => { + let sandbox; + let networkStub; + let channelStub; + let eventHubFactoryStub; + let eventHubSelectionStrategyStub; + let eventHubManager; + let defaultNewEventHub; + let anotherEventHub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + channelStub = sandbox.createStubInstance(Channel); + networkStub = sandbox.createStubInstance(Network); + networkStub.getChannel.returns(channelStub); + eventHubFactoryStub = sandbox.createStubInstance(EventHubFactory); + eventHubSelectionStrategyStub = sandbox.createStubInstance(AbstractEventHubSelectionStrategy); + networkStub.getEventHubSelectionStrategy.returns(eventHubSelectionStrategyStub); + eventHubManager = new EventHubManager(networkStub); + eventHubManager.eventHubFactory = eventHubFactoryStub; + defaultNewEventHub = populateEventHub(sandbox.createStubInstance(ChannelEventHub)); + defaultNewEventHub.isFiltered.returns(true); + defaultNewEventHub.getName.returns('peer'); + anotherEventHub = populateEventHub(sandbox.createStubInstance(ChannelEventHub)); + anotherEventHub.isFiltered.returns(false); + anotherEventHub.getName.returns('peer'); + channelStub.newChannelEventHub.returns(defaultNewEventHub); + }); + + afterEach(() => { + sandbox.reset(); + }); + + describe('#constructor', () => { + it('should set the right parameters', () => { + const em = new EventHubManager(networkStub); + expect(em.channel).to.equal(channelStub); + expect(em.eventHubFactory).to.be.instanceOf(EventHubFactory); + expect(em.eventHubSelectionStrategy).to.equal(eventHubSelectionStrategyStub); + expect(em.newEventHubs).to.deep.equal([]); + }); + }); + + describe('#getEventHub', () => { + it('should return an event hub from the event hub factory given a peer', () => { + const peer = {getPeer: () => 'peer'}; + eventHubFactoryStub.getEventHub.returns(anotherEventHub); + + const eventHub = eventHubManager.getEventHub(peer); + sinon.assert.calledWith(eventHubFactoryStub.getEventHub, 'peer'); + expect(eventHub).to.equal(anotherEventHub); + }); + + it('should return an event hub from the event hub factory given a peer name', () => { + const peer = 'peer'; + eventHubFactoryStub.getEventHub.returns(anotherEventHub); + + const eventHub = eventHubManager.getEventHub(peer); + sinon.assert.calledWith(eventHubFactoryStub.getEventHub, 'peer'); + expect(eventHub).to.equal(anotherEventHub); + }); + + it('should return an event hub from the event hub factory without a peer', () => { + const peer = {getPeer: () => 'peer'}; + eventHubFactoryStub.getEventHub.returns(anotherEventHub); + eventHubSelectionStrategyStub.getNextPeer.returns(peer); + const eventHub = eventHubManager.getEventHub(); + sinon.assert.called(eventHubSelectionStrategyStub.getNextPeer); + sinon.assert.calledWith(eventHubFactoryStub.getEventHub, 'peer'); + expect(eventHub).to.equal(anotherEventHub); + }); + + it('should not return the unfiltered event hub and request a new isntance', () => { + eventHubFactoryStub.getEventHub.returns(anotherEventHub); + anotherEventHub.isconnected.returns(true); + eventHubManager.newEventHubs = [defaultNewEventHub]; + anotherEventHub.isFiltered.returns(false); + expect(eventHubManager.getEventHub({getName: () => 'peer'}, true)).to.equal(defaultNewEventHub); + }); + }); + + describe('#getEventHubs', () => { + it('should call the event hub factory', () => { + const peers = ['peer1', 'peer2', 'peer3']; + const ehs = ['eh1', 'eh2', 'eh3']; + eventHubFactoryStub.getEventHubs.returns(ehs); + const eventHubs = eventHubManager.getEventHubs(peers); + sinon.assert.calledWith(eventHubFactoryStub.getEventHubs, peers); + expect(eventHubs).to.deep.equal(eventHubs); + }); + }); + + describe('#getReplayEventHub', () => { + it('should return an existing event hub with no registrations and no peer is given', () => { + const newEventHub = sandbox.createStubInstance(ChannelEventHub); + newEventHub._chaincodeRegistrants = {}; + newEventHub._blockRegistrations = {}; + newEventHub._transactionRegistrations = {}; + eventHubManager.newEventHubs = [newEventHub]; + const eventHub = eventHubManager.getReplayEventHub(); + expect(eventHub).to.be.instanceof(ChannelEventHub); + expect(eventHubManager.newEventHubs).to.have.length(1); + }); + + it('should return an existing event hub with no registrations when the peer is given', () => { + const newEventHub = sandbox.createStubInstance(ChannelEventHub); + newEventHub._chaincodeRegistrants = {}; + newEventHub._blockRegistrations = {}; + newEventHub._transactionRegistrations = {}; + newEventHub.getName.returns('peer1'); + eventHubManager.newEventHubs = [newEventHub]; + const peer = sandbox.createStubInstance(Peer); + peer.getName.returns('peer1'); + const eventHub = eventHubManager.getReplayEventHub(peer); + expect(eventHub).to.instanceof(ChannelEventHub); + expect(eventHubManager.newEventHubs).to.have.length(1); + }); + + it('should return a new event hub if the peer and event hub name dont match', () => { + const newEventHub = sandbox.createStubInstance(ChannelEventHub); + newEventHub._chaincodeRegistrants = {}; + newEventHub._blockRegistrations = {}; + newEventHub._transactionRegistrations = {}; + newEventHub.getName.returns('peer2'); + eventHubManager.newEventHubs = [newEventHub]; + const peer = sandbox.createStubInstance(Peer); + peer.getName.returns('peer1'); + const eventHub = eventHubManager.getReplayEventHub(peer); + expect(eventHub).to.equal(defaultNewEventHub); + expect(eventHubManager.newEventHubs[eventHubManager.newEventHubs.length - 1]).to.equal(defaultNewEventHub); + }); + + it('should return a new event hub if the event hub isnt new', () => { + const newEventHub = sandbox.createStubInstance(ChannelEventHub); + newEventHub._chaincodeRegistrants = {'registration1': 'some registration'}; + newEventHub._blockRegistrations = {}; + newEventHub._transactionRegistrations = {}; + newEventHub.getName.returns('peer1'); + eventHubManager.newEventHubs = [newEventHub]; + const peer = sandbox.createStubInstance(Peer); + peer.getName.returns('peer1'); + const eventHub = eventHubManager.getReplayEventHub(peer); + expect(eventHub).to.equal(defaultNewEventHub); + expect(eventHubManager.newEventHubs[eventHubManager.newEventHubs.length - 1]).to.equal(defaultNewEventHub); + }); + }); + + describe('#getFixedEventHub', () => { + it('should get a new eventhub', () => { + const newEventHub = sandbox.createStubInstance(ChannelEventHub); + channelStub.newChannelEventHub.returns(newEventHub); + const eventHub = eventHubManager.getFixedEventHub('peer'); + sinon.assert.calledWith(channelStub.newChannelEventHub, 'peer'); + expect(eventHub).to.equal(newEventHub); + }); + }); + + describe('#updateEventHubAvailability', () => { + it('should call eventHubSelectionStratefy.updateEventHubAvailability', () => { + eventHubManager.updateEventHubAvailability('peer'); + sinon.assert.calledWith(eventHubSelectionStrategyStub.updateEventHubAvailability, 'peer'); + }); + }); + + describe('#dispose', () => { + it('should call dispose on the eventHubFactory', () => { + eventHubManager.dispose(); + sinon.assert.called(eventHubFactoryStub.dispose); + }); + + it('should call disconnect on the each new event hub', () => { + const eventHub1 = sandbox.createStubInstance(ChannelEventHub); + const eventHub2 = sandbox.createStubInstance(ChannelEventHub); + const eventHub3 = sandbox.createStubInstance(ChannelEventHub); + eventHubManager.newEventHubs = [eventHub1, eventHub2, eventHub3]; + eventHubManager.dispose(); + for (const eh of eventHubManager.newEventHubs) { + sinon.assert.called(eh.disconnect); + } + }); + }); + + describe('#getEventHubFactory', () => { + it ('should return the event hub factory', () => { + const eventHubFactory = eventHubManager.getEventHubFactory(); + expect(eventHubFactory).to.equal(eventHubFactoryStub); + }); + }); + + describe('#_isNewEventHub', () => { + it('should throw if no event hub is given', () => { + expect(() => eventHubManager._isNewEventHub()).to.throw('event hub not given'); + }); + + it('should return true if there are no registrations', () => { + const eventHub = sandbox.createStubInstance(ChannelEventHub); + eventHub._chaincodeRegistrants = {}; + eventHub._blockRegistrations = {}; + eventHub._transactionRegistrations = {}; + const isNew = eventHubManager._isNewEventHub(eventHub); + expect(isNew).to.be.true; + }); + + it('should return false if there is one chaincode registration', () => { + const eventHub = sandbox.createStubInstance(ChannelEventHub); + eventHub._chaincodeRegistrants = {someregistration: 'registration'}; + eventHub._blockRegistrations = {}; + eventHub._transactionRegistrations = {}; + const isNew = eventHubManager._isNewEventHub(eventHub); + expect(isNew).to.be.false; + }); + + it('should return false if there is one block registration', () => { + const eventHub = sandbox.createStubInstance(ChannelEventHub); + eventHub._chaincodeRegistrants = {}; + eventHub._blockRegistrations = {someregistration: 'registration'}; + eventHub._transactionRegistrations = {}; + const isNew = eventHubManager._isNewEventHub(eventHub); + expect(isNew).to.be.false; + }); + + it('should return false if there is one transaction registration', () => { + const eventHub = sandbox.createStubInstance(ChannelEventHub); + eventHub._chaincodeRegistrants = {}; + eventHub._blockRegistrations = {}; + eventHub._transactionRegistrations = {someregistration: 'registration'}; + const isNew = eventHubManager._isNewEventHub(eventHub); + expect(isNew).to.be.false; + }); + }); + + describe('#getPeers', () => { + it('should call EventHubSelectionStrategy.getPeers', () => { + const peers = ['peer1']; + eventHubSelectionStrategyStub.getPeers.returns(peers); + expect(eventHubManager.getPeers()).to.equal(peers); + sinon.assert.called(eventHubSelectionStrategyStub.getPeers); + }); + }); +}); diff --git a/fabric-network/test/impl/event/filesystemcheckpointer.js b/fabric-network/test/impl/event/filesystemcheckpointer.js new file mode 100644 index 0000000000..b9177f8959 --- /dev/null +++ b/fabric-network/test/impl/event/filesystemcheckpointer.js @@ -0,0 +1,142 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); + +const rewire = require('rewire'); +const FileSystemCheckpointer = rewire('fabric-network/lib/impl/event/filesystemcheckpointer'); + + +describe('FileSystemCheckpointer', () => { + const revert = []; + let sandbox; + let checkpointer; + let channelName; + let listenerName; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(os, 'homedir').returns('home'); + sandbox.spy(path, 'join'); + sandbox.stub(fs, 'ensureDir'); + sandbox.stub(fs, 'createFile'); + sandbox.stub(fs, 'exists'); + sandbox.stub(fs, 'writeFile'); + sandbox.stub(fs, 'readFile'); + sandbox.stub(path, 'resolve').callsFake(a => a); + revert.push(FileSystemCheckpointer.__set__('path', path)); + revert.push(FileSystemCheckpointer.__set__('os', os)); + revert.push(FileSystemCheckpointer.__set__('fs', fs)); + + channelName = 'mychannel'; + listenerName = 'mylistener'; + checkpointer = new FileSystemCheckpointer(channelName, listenerName); + }); + + afterEach(() => { + if (revert.length) { + revert.forEach(Function.prototype.call, Function.prototype.call); + } + sandbox.restore(); + }); + + describe('#constructor', () => { + it('should set basePath without options', () => { + const check = new FileSystemCheckpointer(channelName, listenerName); + sinon.assert.called(os.homedir); + expect(check._basePath).to.equal('home/.hlf-checkpoint'); + expect(check._channelName).to.equal(channelName); + expect(check._listenerName).to.equal(listenerName); + }); + + it('should set basePath with options', () => { + const check = new FileSystemCheckpointer(channelName, listenerName, {basePath: 'base-path'}); + sinon.assert.called(os.homedir); + expect(check._basePath).to.equal('base-path'); + }); + }); + + describe('#_initialize', () => { + it('should initialize the checkpoint file', async () => { + await checkpointer._initialize(); + sinon.assert.calledWith(fs.ensureDir, `home/.hlf-checkpoint/${channelName}`); + sinon.assert.calledWith(fs.createFile, checkpointer._getCheckpointFileName()); + }); + }); + + describe('#save', () => { + + it('should initialize the checkpointer file doesnt exist', async () => { + fs.readFile.resolves(new Buffer('')); + fs.exists.resolves(false); + sinon.spy(checkpointer, '_initialize'); + sinon.spy(checkpointer, 'load'); + await checkpointer.save('transaction1', 0); + sinon.assert.calledWith(fs.exists, checkpointer._getCheckpointFileName()); + sinon.assert.called(checkpointer._initialize); + }); + + it('should update an existing checkpoint', async () => { + fs.exists.resolves(true); + fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']})); + await checkpointer.save('transactionId1', 0); + sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':0, 'transactionIds':['transactionId', 'transactionId1']})); + }); + + it('should not update an existing checkpoint', async () => { + fs.exists.resolves(true); + fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']})); + await checkpointer.save(null, 0); + sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':0, 'transactionIds':['transactionId']})); + }); + + it('should update an existing checkpoint when blockNumber changes', async () => { + fs.exists.resolves(true); + fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']})); + await checkpointer.save('transactionId', 1); + sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':1, 'transactionIds':['transactionId']})); + }); + + it('should not add a list of transactions to a checkpoint', async () => { + fs.exists.resolves(true); + fs.readFile.resolves(JSON.stringify({blockNumber: 0, transactionIds: ['transactionId']})); + await checkpointer.save(null, 1); + sinon.assert.calledWith(fs.writeFile, checkpointer._getCheckpointFileName(), JSON.stringify({'blockNumber':1, 'transactionIds':[]})); + }); + }); + + describe('#load', () => { + it('should initialize the checkpointer if memory map doesnt exist', async () => { + fs.exists.resolves(false); + fs.readFile.resolves('{}'); + const checkpoint = await checkpointer.load(); + expect(checkpoint).to.deep.equal({}); + }); + + it('should return the checkpoint', async () => { + fs.exists.resolves(true); + const checkpoint = {blockNumber: 0, transactionIds: []}; + fs.readFile.resolves(JSON.stringify(checkpoint)); + const loadedCheckpoint = await checkpointer.load(); + expect(loadedCheckpoint).to.deep.equal(checkpoint); + }); + + it('should return an empty object if the checkpoint is empty', async () => { + fs.exists.resolves(false); + const checkpoint = ''; + fs.readFile.resolves(checkpoint); + const loadedCheckpoint = await checkpointer.load(); + expect(loadedCheckpoint).to.deep.equal({}); + }); + }); +}); diff --git a/fabric-network/test/impl/event/roundrobineventhubselectionstrategy.js b/fabric-network/test/impl/event/roundrobineventhubselectionstrategy.js new file mode 100644 index 0000000000..c3cdb09b42 --- /dev/null +++ b/fabric-network/test/impl/event/roundrobineventhubselectionstrategy.js @@ -0,0 +1,65 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); + +const RoundRobinEventHubSelectionStrategy = require('fabric-network/lib/impl/event/roundrobineventhubselectionstrategy'); +const Peer = require('fabric-client/lib/Peer'); + +describe('RoundRobinEventHubSelectionStrategy', () => { + let sandbox; + let peer1, peer2; + let strategy; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + peer1 = sandbox.createStubInstance(Peer); + peer1.getName.returns('peer1'); + peer2 = sandbox.createStubInstance(Peer); + peer2.getName.returns('peer2'); + strategy = new RoundRobinEventHubSelectionStrategy([peer1, peer2]); + }); + + afterEach(() => { + sandbox.reset(); + }); + + describe('#constructor', () => { + it('should create an of peers', () => { + expect(strategy.peers).to.be.instanceOf(Array); + expect(strategy.peers).to.deep.equal([peer1, peer2]); + }); + + it('should create an empty array of peers by default', () => { + const testStrategy = new RoundRobinEventHubSelectionStrategy(); + expect(testStrategy.peers).to.deep.equal([]); + }); + }); + + describe('#getNextPeer', () => { + let nextPeerStrategy; + before(() => { + nextPeerStrategy = new RoundRobinEventHubSelectionStrategy([peer1, peer2]); + }); + + it('should pick peer1 next', () => { + expect(nextPeerStrategy.getNextPeer()).to.deep.equal(peer1); + }); + it('should pick peer2 next', () => { + expect(nextPeerStrategy.getNextPeer()).to.deep.equal(peer2); + }); + }); + + describe('#updateEventHubAvailability', () => { + it('should not throw if a dead peer is not given', () => { + expect(() => strategy.updateEventHubAvailability()).not.to.throw(); + }); + }); +}); diff --git a/fabric-network/test/impl/event/transactioneventhandler.js b/fabric-network/test/impl/event/transactioneventhandler.js index d235a1761e..328c63ca80 100644 --- a/fabric-network/test/impl/event/transactioneventhandler.js +++ b/fabric-network/test/impl/event/transactioneventhandler.js @@ -1,5 +1,5 @@ /** - * Copyright 2018 IBM All Rights Reserved. + * Copyright 2019 IBM All Rights Reserved. * * SPDX-License-Identifier: Apache-2.0 */ @@ -13,25 +13,25 @@ const sinon = require('sinon'); const ChannelEventHub = require('fabric-client').ChannelEventHub; +const Transaction = require('fabric-network/lib/transaction'); const TransactionEventHandler = require('fabric-network/lib/impl/event/transactioneventhandler'); const TimeoutError = require('fabric-network/lib/errors/timeouterror'); +const TransactionID = require('fabric-client/lib/TransactionID'); describe('TransactionEventHandler', () => { let stubEventHub; let stubStrategy; + let stubTransaction; + let stubTransactionID; const transactionId = 'TRANSACTION_ID'; beforeEach(() => { // Include _stubInfo property on stubs to enable easier equality comparison in tests + stubTransaction = sinon.createStubInstance(Transaction); + stubTransactionID = sinon.createStubInstance(TransactionID); + stubTransactionID.getTransactionID.returns(transactionId); + stubTransaction.getTransactionID.returns(stubTransactionID); stubEventHub = sinon.createStubInstance(ChannelEventHub); - stubEventHub._stubInfo = 'eventHub'; - stubEventHub.getName.returns('eventHub'); - stubEventHub.getPeerAddr.returns('eventHubAddress'); - stubEventHub.registerTxEvent.callsFake((transaction_Id, onEventFn, onErrorFn) => { - stubEventHub._transactionId = transaction_Id; - stubEventHub._onEventFn = onEventFn; - stubEventHub._onErrorFn = onErrorFn; - }); stubStrategy = { getEventHubs: sinon.stub(), @@ -47,22 +47,28 @@ describe('TransactionEventHandler', () => { describe('#constructor', () => { it('has a default timeout of zero if no options supplied', () => { - const handler = new TransactionEventHandler(transactionId, stubStrategy); + const handler = new TransactionEventHandler(stubTransaction, stubStrategy); expect(handler.options.commitTimeout).to.equal(0); }); it('uses timeout from supplied options', () => { const options = {commitTimeout: 1}; - const handler = new TransactionEventHandler(transactionId, stubStrategy, options); + const handler = new TransactionEventHandler(stubTransaction, stubStrategy, options); expect(handler.options.commitTimeout).to.equal(options.commitTimeout); }); + + it('should set transactionID and transaction', () => { + const handler = new TransactionEventHandler(stubTransaction, stubStrategy); + expect(handler.transactionId).to.equal(transactionId); + expect(handler.transaction).to.equal(stubTransaction); + }); }); describe('event handling:', () => { let handler; beforeEach(() => { - handler = new TransactionEventHandler(transactionId, stubStrategy); + handler = new TransactionEventHandler(stubTransaction, stubStrategy); }); afterEach(() => { @@ -70,64 +76,86 @@ describe('TransactionEventHandler', () => { }); describe('#startListening', () => { - it('calls registerTxEvent() on event hub with transaction ID', async () => { + it('calls addCommitListener() on transaction with callback', async () => { await handler.startListening(); - sinon.assert.calledWith(stubEventHub.registerTxEvent, transactionId); + sinon.assert.calledWith(stubTransaction.addCommitListener, sinon.match.func); }); it('sets auto-unregister option when calling registerTxEvent() on event hub', async () => { await handler.startListening(); sinon.assert.calledWith( - stubEventHub.registerTxEvent, - sinon.match.any, - sinon.match.any, - sinon.match.any, - sinon.match.has('unregister', true) + stubTransaction.addCommitListener, + sinon.match.func, + sinon.match.has('unregister', true), + stubEventHub ); }); - it('calls connect() on event hub', async () => { - await handler.startListening(); - sinon.assert.called(stubEventHub.connect); + it('should call transaction.addCommitListener', () => { + handler = new TransactionEventHandler(stubTransaction, stubStrategy); + handler.startListening(); + sinon.assert.calledWith( + stubTransaction.addCommitListener, + sinon.match.func, + sinon.match.has('unregister', true), + stubEventHub + ); + }); + + it('should call _onError if err is set', () => { + handler = new TransactionEventHandler(stubTransaction, stubStrategy); + sinon.spy(handler, '_onError'); + handler.startListening(); + const err = new Error('an error'); + stubTransaction.addCommitListener.callArgWith(0, err); + sinon.assert.calledWith(handler._onError, stubEventHub, err); + }); + + it('should call _onEvent if err is set', () => { + handler = new TransactionEventHandler(stubTransaction, stubStrategy); + sinon.spy(handler, '_onEvent'); + handler.startListening(); + stubTransaction.addCommitListener.callArgWith(0, null, transactionId, 'VALID'); + sinon.assert.calledWith(handler._onEvent, stubEventHub, transactionId, 'VALID'); }); }); it('calls eventReceived() on strategy when event hub sends valid event', async () => { + stubTransaction.addCommitListener.yields(null, transactionId, 'VALID'); await handler.startListening(); - stubEventHub._onEventFn(transactionId, 'VALID'); sinon.assert.calledWith(stubStrategy.eventReceived, sinon.match.func, sinon.match.func); }); it('does not call errorReceived() on strategy when event hub sends valid event', async () => { + stubTransaction.addCommitListener.yields(null, transactionId, 'VALID'); await handler.startListening(); - stubEventHub._onEventFn(transactionId, 'VALID'); sinon.assert.notCalled(stubStrategy.errorReceived); }); it('calls errorReceived() on strategy when event hub sends an error', async () => { + stubTransaction.addCommitListener.yields(new Error()); await handler.startListening(); - stubEventHub._onErrorFn(new Error('EVENT HUB ERROR')); sinon.assert.calledWith(stubStrategy.errorReceived, sinon.match.func, sinon.match.func); }); it('does not call eventReceived() on strategy when event hub sends an error', async () => { + stubTransaction.addCommitListener.yields(new Error('EVENT_HUB_ERROR')); await handler.startListening(); - stubEventHub._onErrorFn(new Error('EVENT_HUB_ERROR')); sinon.assert.notCalled(stubStrategy.eventReceived); }); it('fails when event hub sends an invalid event', async () => { const code = 'ERROR_CODE'; + stubTransaction.addCommitListener.yields(null, transactionId, code); await handler.startListening(); - stubEventHub._onEventFn(transactionId, code); return expect(handler.waitForEvents()).to.be.rejectedWith(code); }); it('succeeds when strategy calls success function after event received', async () => { stubStrategy.eventReceived = ((successFn, failFn) => successFn()); // eslint-disable-line no-unused-vars + stubTransaction.addCommitListener.yields(null, transactionId, 'VALID'); await handler.startListening(); - stubEventHub._onEventFn(transactionId, 'VALID'); return expect(handler.waitForEvents()).to.be.fulfilled; }); @@ -135,16 +163,16 @@ describe('TransactionEventHandler', () => { const error = new Error('STRATEGY_FAIL'); stubStrategy.eventReceived = ((successFn, failFn) => failFn(error)); + stubTransaction.addCommitListener.yields(null, transactionId, 'VALID'); await handler.startListening(); - stubEventHub._onEventFn(transactionId, 'VALID'); return expect(handler.waitForEvents()).to.be.rejectedWith(error); }); it('succeeds when strategy calls success function after error received', async () => { stubStrategy.errorReceived = ((successFn, failFn) => successFn()); // eslint-disable-line no-unused-vars + stubTransaction.addCommitListener.yields(new Error('EVENT_HUB_ERROR')); await handler.startListening(); - stubEventHub._onErrorFn(new Error('EVENT_HUB_ERROR')); return expect(handler.waitForEvents()).to.be.fulfilled; }); @@ -152,14 +180,14 @@ describe('TransactionEventHandler', () => { const error = new Error('STRATEGY_FAIL'); stubStrategy.errorReceived = ((successFn, failFn) => failFn(error)); + stubTransaction.addCommitListener.yields(new Error('EVENT_HUB_ERROR')); await handler.startListening(); - stubEventHub._onErrorFn(new Error('EVENT_HUB_ERROR')); return expect(handler.waitForEvents()).to.be.rejectedWith(error); }); it('succeeds immediately with no event hubs', async () => { stubStrategy.getEventHubs.returns([]); - handler = new TransactionEventHandler(transactionId, stubStrategy); + handler = new TransactionEventHandler(stubTransaction, stubStrategy); await handler.startListening(); return expect(handler.waitForEvents()).to.be.fulfilled; }); @@ -180,7 +208,7 @@ describe('TransactionEventHandler', () => { it('fails on timeout if timeout set', async () => { const options = {commitTimeout: 418}; - handler = new TransactionEventHandler(transactionId, stubStrategy, options); + handler = new TransactionEventHandler(stubTransaction, stubStrategy, options); await handler.startListening(); const promise = handler.waitForEvents(); clock.runAll(); @@ -191,16 +219,16 @@ describe('TransactionEventHandler', () => { stubStrategy.eventReceived = ((successFn, failFn) => successFn()); // eslint-disable-line no-unused-vars const options = {commitTimeout: 0}; - handler = new TransactionEventHandler(transactionId, stubStrategy, options); + handler = new TransactionEventHandler(stubTransaction, stubStrategy, options); + stubTransaction.addCommitListener.yields(null, transactionId, 'VALID'); await handler.startListening(); clock.runAll(); - stubEventHub._onEventFn(transactionId, 'VALID'); return expect(handler.waitForEvents()).to.be.fulfilled; }); it('timeout failure message includes event hubs that have not responded', async () => { const options = {commitTimeout: 418}; - handler = new TransactionEventHandler(transactionId, stubStrategy, options); + handler = new TransactionEventHandler(stubTransaction, stubStrategy, options); await handler.startListening(); const promise = handler.waitForEvents(); clock.runAll(); @@ -211,7 +239,7 @@ describe('TransactionEventHandler', () => { it('does not timeout if no event hubs', async () => { stubStrategy.getEventHubs.returns([]); const options = {commitTimeout: 418}; - handler = new TransactionEventHandler(transactionId, stubStrategy, options); + handler = new TransactionEventHandler(stubTransaction, stubStrategy, options); await handler.startListening(); clock.runAll(); return expect(handler.waitForEvents()).to.be.fulfilled; @@ -219,7 +247,7 @@ describe('TransactionEventHandler', () => { it('timeout failure error has transaction ID property', async () => { const options = {commitTimeout: 418}; - handler = new TransactionEventHandler(transactionId, stubStrategy, options); + handler = new TransactionEventHandler(stubTransaction, stubStrategy, options); await handler.startListening(); const promise = handler.waitForEvents(); clock.runAll(); diff --git a/fabric-network/test/impl/wallet/basewallet.js b/fabric-network/test/impl/wallet/basewallet.js index b3ae653e5b..94f772ca60 100644 --- a/fabric-network/test/impl/wallet/basewallet.js +++ b/fabric-network/test/impl/wallet/basewallet.js @@ -27,7 +27,10 @@ describe('BaseWallet', () => { }); describe('Unimplemented methods', () => { - const wallet = new BaseWallet(); + let wallet; + beforeEach(() => { + wallet = new BaseWallet(); + }); it('throws exception calling import()', () => { return wallet.import(null, null).should.be.rejectedWith('Not implemented'); @@ -55,5 +58,4 @@ describe('BaseWallet', () => { }); }); - }); diff --git a/fabric-network/test/impl/wallet/couchdbwallet.js b/fabric-network/test/impl/wallet/couchdbwallet.js index d0878b5404..f0810be6fd 100644 --- a/fabric-network/test/impl/wallet/couchdbwallet.js +++ b/fabric-network/test/impl/wallet/couchdbwallet.js @@ -212,6 +212,18 @@ describe('CouchDBWalletKeyValueStore', () => { const deleted = await kvs.delete('key'); deleted.should.be.false; }); + + it('should return false if error occurs when finding the key', async () => { + destroyStub.yields(new Error()); + const deleted = await kvs.delete('key'); + deleted.should.be.false; + }); + + it('should return false if error occurs when finding the key', async () => { + destroyStub.yields(new Error()); + const deleted = await kvs.delete('key'); + deleted.should.be.false; + }); }); describe('#exists', () => { diff --git a/fabric-network/test/impl/wallet/inmemorywallet.js b/fabric-network/test/impl/wallet/inmemorywallet.js index f7633f1b70..423be3374f 100644 --- a/fabric-network/test/impl/wallet/inmemorywallet.js +++ b/fabric-network/test/impl/wallet/inmemorywallet.js @@ -85,6 +85,13 @@ describe('InMemoryWallet', () => { }); }); + describe('#getIdentityInfo', () => { + it('should not return null', async () => { + wallet.getAllLabels = () => Promise.resolve(['user3']); + const info = await wallet.list(); + info.should.deep.equal([{label: 'user3', mspId: 'not provided', identifier: 'not provided'}]); + }); + }); }); describe('label storage', () => { @@ -206,13 +213,23 @@ mb3MM6J+V7kciO3hSyP5OJSBPWGlsjxQj2m55aFutmlleVfr6YiaLnYd const list = await wallet.list(); list.length.should.equal(0); }); - }); + it('should return a list containing not provided', async () => { + wallet.walletMixin.getIdentityInfo = () => null; + const list = await wallet.list(); + list.length.should.equal(2); + list[0].mspId.should.equal('not provided'); + list[1].mspId.should.equal('not provided'); + }); + }); }); - describe('InMemoryKVS', async () => { - const wallet = new InMemoryWallet(); - const store = await wallet.getStateStore('test'); + describe('InMemoryKVS', () => { + let store; + beforeEach(async () => { + const wallet = new InMemoryWallet(); + store = await wallet.getStateStore('test'); + }); it('#getValue', async () => { await store.setValue('user1', 'val1'); diff --git a/fabric-network/test/network.js b/fabric-network/test/network.js index 25cd65705b..5796730344 100644 --- a/fabric-network/test/network.js +++ b/fabric-network/test/network.js @@ -12,7 +12,6 @@ const InternalChannel = rewire('fabric-client/lib/Channel'); const Peer = InternalChannel.__get__('ChannelPeer'); const Client = require('fabric-client'); const ChannelEventHub = Client.ChannelEventHub; -const EventHubFactory = require('fabric-network/lib/impl/event/eventhubfactory'); const TransactionID = require('fabric-client/lib/TransactionID.js'); const FABRIC_CONSTANTS = require('fabric-client/lib/Constants'); @@ -25,6 +24,10 @@ const Network = require('../lib/network'); const Gateway = require('../lib/gateway'); const Contract = require('../lib/contract'); const EventStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies'); +const AbstractEventHubSelectionStrategy = require('fabric-network/lib/impl/event/abstracteventhubselectionstrategy'); +const EventHubManager = require('fabric-network/lib/impl/event/eventhubmanager'); +const CommitEventListener = require('fabric-network/lib/impl/event/commiteventlistener'); +const BlockEventListener = require('fabric-network/lib/impl/event/blockeventlistener'); describe('Network', () => { let mockChannel, mockClient; @@ -33,9 +36,13 @@ describe('Network', () => { let network; let mockTransactionID, mockGateway; let stubQueryHandler; + let stubEventHubSelectionStrategy; + let mockEventHubManager; + let mockEventHub; beforeEach(() => { mockChannel = sinon.createStubInstance(InternalChannel); + mockEventHub = sinon.createStubInstance(ChannelEventHub); mockClient = sinon.createStubInstance(Client); mockTransactionID = sinon.createStubInstance(TransactionID); mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000'); @@ -73,6 +80,8 @@ describe('Network', () => { stubQueryHandler = {}; + stubEventHubSelectionStrategy = sinon.createStubInstance(AbstractEventHubSelectionStrategy); + mockGateway = sinon.createStubInstance(Gateway); mockGateway.getOptions.returns({ useDiscovery: false, @@ -85,14 +94,24 @@ describe('Network', () => { stubQueryHandler.network = theNetwork; return stubQueryHandler; } + }, + eventHubSelectionOptions: { + strategy: (theNetwork) => { + stubEventHubSelectionStrategy.network = theNetwork; + return stubEventHubSelectionStrategy; + } } }); mockGateway.getClient.returns(mockClient); mockClient.getPeersForOrg.returns([mockPeer1, mockPeer2]); - network = new Network(mockGateway, mockChannel); + mockEventHubManager = sinon.createStubInstance(EventHubManager); + mockEventHubManager.getPeers.returns([mockPeer1, mockPeer2]); + mockEventHubManager.getEventHub.returns(mockEventHub); + network = new Network(mockGateway, mockChannel); + network.eventHubManager = mockEventHubManager; }); afterEach(() => { @@ -231,16 +250,16 @@ describe('Network', () => { }); it('calls dispose() on the event hub factory', () => { - const spy = sinon.spy(network.getEventHubFactory(), 'dispose'); + const spy = network.getEventHubManager().dispose; network._dispose(); sinon.assert.called(spy); }); - }); - describe('#getEventHubFactory', () => { - it('Returns an EventHubFactory', () => { - const result = network.getEventHubFactory(); - result.should.be.an.instanceOf(EventHubFactory); + it('calls unregister on its listeners', () => { + const mockListener = sinon.createStubInstance(BlockEventListener); + network.listeners.set('mockListener', mockListener); + network._dispose(); + sinon.assert.calledOnce(mockListener.unregister); }); }); @@ -259,4 +278,86 @@ describe('Network', () => { result.network.should.equal(network); }); }); + + describe('#addCommitListener', () => { + let listenerName; + let callback; + beforeEach(() => { + listenerName = '00000000-0000-0000-0000-000000000000'; + callback = () => {}; + mockEventHub._transactionRegistrations = {}; + mockEventHub._transactionRegistrations[listenerName] = {}; // Preregister listener with eh + }); + + it('should create options if the options param is undefined', async () => { + const listener = await network.addCommitListener(listenerName, callback, null, mockEventHub); + listener.should.to.be.instanceof(CommitEventListener); + listener.eventHub.should.to.equal(mockEventHub); + network.listeners.get(listener.listenerName).should.to.equal(listener); + }); + + it('should create an instance of BlockEventListener and add it to the list of listeners', async () => { + const listener = await network.addCommitListener(listenerName, callback, {}, mockEventHub); + listener.should.to.be.instanceof(CommitEventListener); + listener.eventHub.should.to.equal(mockEventHub); + network.listeners.get(listener.listenerName).should.to.equal(listener); + }); + + it('should not set an event hub if an event hub is not given', async () => { + mockEventHubManager.getReplayEventHub.returns(mockEventHub); + const listener = await network.addCommitListener(listenerName, callback, null); + listener.eventHub.should.equal(mockEventHub); + }); + }); + + describe('#addBlockListener', () => { + let listenerName; + let callback; + beforeEach(() => { + listenerName = 'testBlockListener'; + callback = () => {}; + }); + + it('should create options if the options param is undefined', async () => { + const listener = await network.addBlockListener(listenerName, callback); + listener.should.to.be.instanceof(BlockEventListener); + network.listeners.get(listenerName).should.to.equal(listener); + }); + + it('should create an instance of BlockEventListener and add it to the list of listeners', async () => { + const listener = await network.addBlockListener(listenerName, callback, {}); + listener.should.to.be.instanceof(BlockEventListener); + network.listeners.get(listenerName).should.to.equal(listener); + }); + + it('should change options.replay=undefined to options.replay=false', async () => { + sinon.spy(network, 'getCheckpointer'); + await network.addBlockListener(listenerName, callback, {replay: undefined}); + sinon.assert.calledWith(network.getCheckpointer, {checkpointer: undefined, replay: false}); + }); + + it('should change options.replay=\'true\' to options.replay=true', async () => { + sinon.spy(network, 'getCheckpointer'); + await network.addBlockListener(listenerName, callback, {replay: 'true'}); + sinon.assert.calledWith(network.getCheckpointer, {checkpointer: undefined, replay: true}); + }); + }); + + describe('#saveListener', () => { + it ('should register a new listener if the name isnt taken', () => { + const listener = {}; + network.listeners.set('listener1', {}); + network.saveListener('listener2', listener); + network.listeners.get('listener2').should.equal(listener); + }); + + it('should throw if the listener is registered with an existing name', () => { + const listener = sinon.createStubInstance(BlockEventListener); + network.listeners.set('listener1', listener); + (() => { + network.saveListener('listener1', listener); + }).should.throw('Listener already exists with the name listener1'); + sinon.assert.called(listener.unregister); + }); + }); }); diff --git a/fabric-network/test/transaction.js b/fabric-network/test/transaction.js index 7a5511d348..0e6e062158 100644 --- a/fabric-network/test/transaction.js +++ b/fabric-network/test/transaction.js @@ -57,6 +57,7 @@ describe('Transaction', () => { let stubContract; let transaction; let channel; + let network; let stubQueryHandler; beforeEach(() => { @@ -70,7 +71,7 @@ describe('Transaction', () => { evaluate: sinon.fake.resolves(expectedResult) }; - const network = sinon.createStubInstance(Network); + network = sinon.createStubInstance(Network); network.getQueryHandler.returns(stubQueryHandler); stubContract.getNetwork.returns(network); @@ -204,11 +205,9 @@ describe('Transaction', () => { it('uses a supplied event handler strategy', async () => { const stubEventHandler = sinon.createStubInstance(TransactionEventHandler); - const txId = transaction.getTransactionID().getTransactionID(); - const network = stubContract.getNetwork(); const options = stubContract.getEventHandlerOptions(); const stubEventHandlerFactoryFn = sinon.stub(); - stubEventHandlerFactoryFn.withArgs(txId, network, options).returns(stubEventHandler); + stubEventHandlerFactoryFn.withArgs(transaction, stubContract.getNetwork(), options).returns(stubEventHandler); await transaction.setEventHandlerStrategy(stubEventHandlerFactoryFn).submit(); @@ -316,4 +315,22 @@ describe('Transaction', () => { return expect(promise).to.be.rejectedWith('Transaction has already been invoked'); }); }); + + describe('#addCommitListener', () => { + it('should call Network.addCommitlistner', async () => { + network.addCommitListener.resolves('listener'); + const callback = (err, transationId, status, blockNumber) => {}; + const listener = await transaction.addCommitListener(callback, {}, 'eventHub'); + expect(listener).to.equal('listener'); + sinon.assert.calledWith(network.addCommitListener, 'TRANSACTION_ID', callback, {}, 'eventHub'); + }); + }); + + describe('#getNetwork', () => { + it('should call Contract.getNetwork', () => { + stubContract.getNetwork.returns(network); + expect(transaction.getNetwork()).to.equal(network); + sinon.assert.called(stubContract.getNetwork); + }); + }); }); diff --git a/fabric-network/types/index.d.ts b/fabric-network/types/index.d.ts index 4993304462..d1132c404c 100644 --- a/fabric-network/types/index.d.ts +++ b/fabric-network/types/index.d.ts @@ -20,6 +20,12 @@ export interface GatewayOptions { discovery?: DiscoveryOptions; eventHandlerOptions?: DefaultEventHandlerOptions; queryHandlerOptions?: DefaultQueryHandlerOptions; + checkpointer?: CheckpointerOptions; +} + +export interface CheckpointerOptions { + factory: CheckpointerFactory; + options: object; } export interface DiscoveryOptions { @@ -39,7 +45,7 @@ export class DefaultEventHandlerStrategies { public static NETWORK_SCOPE_ANYFORTX: TxEventHandlerFactory; } -export type TxEventHandlerFactory = (transactionId: TransactionId, network: Network, options: object) => TxEventHandler; +export type TxEventHandlerFactory = (transaction: Transaction, options: object) => TxEventHandler; export interface TxEventHandler { startListening(): Promise; @@ -83,12 +89,15 @@ export class Gateway { export interface Network { getChannel(): Channel; getContract(chaincodeId: string, name?: string): Contract; + addBlockListener(listenerName: string, callback: (block: Client.Block) => void, options?: object): BlockEventListener; + addCommitListener(listenerName: string, callback: (error: Error, transactionId: string, status: string, blockNumber: string) => void, options?: object): TransactionEventListener; } export interface Contract { createTransaction(name: string): Transaction; evaluateTransaction(name: string, ...args: string[]): Promise; submitTransaction(name: string, ...args: string[]): Promise; + addContractListener(listenerName: string, eventName: string, callback: (error: Error, event: {[key: string]: any}, blockNumber: string, transactionId: string, status: string) => void, options?: object): ContractEventListener; } export interface TransientMap { @@ -98,16 +107,18 @@ export interface Transaction { evaluate(...args: string[]): Promise; getName(): string; getTransactionID(): TransactionId; + getNetwork(): Network; setTransient(transientMap: TransientMap): this; submit(...args: string[]): Promise; + addCommitListener(callback: (error: Error, transactionId: string, status: string, blockNumber: string) => void, options: object, eventHub?: Client.ChannelEventHub): void; } -export interface FabricError { +export interface FabricError extends Error { cause?: Error; transactionId?: string; } -export class TimeoutError implements FabricError {} +export interface TimeoutError extends FabricError {} // tslint:disable-line:no-empty-interface //------------------------------------------- // Wallet Management @@ -172,3 +183,54 @@ export class HSMWalletMixin implements WalletMixin { public static createIdentity(mspId: string, certificate: string): Identity; constructor(); } + +export interface Checkpoint { + blockNumber: number; + transactionIds: string[]; +} + +export class BaseCheckpointer { + public setChaincodeId(chaincodeId: string): void; +} + +export class FileSystemCheckpointer extends BaseCheckpointer { + constructor(); + public initialize(channelName: string, listenerName: string): void; + public save(transactionId: string, blockNumber: string): void; + public load(): Checkpoint; +} + +export type CheckpointerFactory = (channelName: string, listenerName: string, options: object) => BaseCheckpointer; + +export class EventHubManager { + constructor(); + public getEventHub(peer: Client.Peer): Client.ChannelEventHub; + public getEventHubs(peers: Client.Peer[]): Client.ChannelEventHub[]; + public getReplayEventHub(peer: Client.Peer): Client.ChannelEventHub; + public getReplayEventHubs(peers: Client.Peer[]): Client.ChannelEventHub[]; +} + +export class TransactionEventListener { + public register(): void; + public setEventHub(eventHub: Client.ChannelEventHub): void; + public unregister(): void; +} + +export class ContractEventListener { + public register(): void; + public unregister(): void; +} + +export class BlockEventListener { + public register(): void; + public unregister(): void; +} + +export interface BaseEventHubSelectionStrategy { + getNextPeer(): Client.Peer; + updateEventHubAvailability(deadPeer: Client.Peer): void; +} + +export class DefaultEventHubSelectionStrategies { + public static MSPID_SCOPE_ROUND_ROBIN: BaseEventHubSelectionStrategy; +} diff --git a/package.json b/package.json index abb308e2d6..aec19055b1 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,5 @@ "type": "Apache-2.0", "url": "https://github.com/hyperledger/fabric/blob/master/LICENSE" } - ], - "dependencies": {} + ] } diff --git a/test/fixtures/src/node_cc/example_cc/chaincode.js b/test/fixtures/src/node_cc/example_cc/chaincode.js index be0e972d75..bec03ff4fa 100644 --- a/test/fixtures/src/node_cc/example_cc/chaincode.js +++ b/test/fixtures/src/node_cc/example_cc/chaincode.js @@ -247,6 +247,7 @@ const Chaincode = class { } async echo(stub, args) { + stub.setEvent('echo', Buffer.from('content')); if (args.length > 0) { return shim.success(Buffer.from(args[0])); } else { diff --git a/test/integration/fabric-ca-services-tests.js b/test/integration/fabric-ca-services-tests.js index a1b2a18202..387d575c08 100644 --- a/test/integration/fabric-ca-services-tests.js +++ b/test/integration/fabric-ca-services-tests.js @@ -102,6 +102,7 @@ test('\n\n ** FabricCAServices: Test enroll() With Dynamic CSR **\n\n', (t) => { t.fail('Failed to import the public key from the enrollment certificate. ' + err.stack ? err.stack : err); t.end(); }).then(() => { + caService.setConfigSetting('socket-operation-timeout', 100000); return caService._fabricCAClient.register(enrollmentID, null, 'client', userOrg, 1, [], signingIdentity); }).then((secret) => { t.comment('secret: ' + JSON.stringify(secret)); diff --git a/test/integration/network-e2e/sample-transaction-event-handler.js b/test/integration/network-e2e/sample-transaction-event-handler.js index 1e9535262a..96de3d5ee9 100644 --- a/test/integration/network-e2e/sample-transaction-event-handler.js +++ b/test/integration/network-e2e/sample-transaction-event-handler.js @@ -23,7 +23,10 @@ class SampleTransactionEventHandler { * @param {Number} [options.commitTimeout] Time in seconds to wait for commit events to be reveived. */ constructor(transactionId, eventHubs, options) { - this.transactionId = transactionId; + if (typeof transactionId === 'object') { + this.transaction = transactionId; + } + this.transactionId = this.transaction ? this.transaction.getTransactionID().getTransactionID() : transactionId; this.eventHubs = eventHubs; const defaultOptions = { diff --git a/test/scenario/chaincode/events/node/index.js b/test/scenario/chaincode/events/node/index.js new file mode 100644 index 0000000000..5fd5828f42 --- /dev/null +++ b/test/scenario/chaincode/events/node/index.js @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const Events = require('./lib/events'); + +module.exports.Events = Events; +module.exports.contracts = [Events]; diff --git a/test/scenario/chaincode/events/node/lib/events.js b/test/scenario/chaincode/events/node/lib/events.js new file mode 100644 index 0000000000..ce6dc9147f --- /dev/null +++ b/test/scenario/chaincode/events/node/lib/events.js @@ -0,0 +1,26 @@ +/** + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-console */ + +'use strict'; +const {Contract} = require('fabric-contract-api'); + +class Events extends Contract { + async initLedger(ctx) { + console.info('Instantiated events'); + } + + async createValue(ctx) { + const {stub} = ctx; + stub.setEvent('create', Buffer.from('content')); + } + + async createValueDisconnect(ctx) { + const {stub} = ctx; + stub.setEvent('dc', Buffer.from('content')); + } +} + +module.exports = Events; diff --git a/test/scenario/chaincode/events/node/metadata/.noignore b/test/scenario/chaincode/events/node/metadata/.noignore new file mode 100644 index 0000000000..0f60d0f7c2 --- /dev/null +++ b/test/scenario/chaincode/events/node/metadata/.noignore @@ -0,0 +1,5 @@ +# +# Copyright 2019 IBM All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# diff --git a/test/scenario/chaincode/events/node/package.json b/test/scenario/chaincode/events/node/package.json new file mode 100644 index 0000000000..0143c1381f --- /dev/null +++ b/test/scenario/chaincode/events/node/package.json @@ -0,0 +1,19 @@ +{ + "name": "events", + "version": "1.0.0", + "description": "events chaincode implemented in node.js", + "main": "index.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "start": "fabric-chaincode-node start" + }, + "engine-strict": true, + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "unstable", + "fabric-shim": "unstable" + } +} diff --git a/test/scenario/chaincode/fabcar/node/package.json b/test/scenario/chaincode/fabcar/node/package.json new file mode 100644 index 0000000000..79caa7076b --- /dev/null +++ b/test/scenario/chaincode/fabcar/node/package.json @@ -0,0 +1,47 @@ +{ + "name": "fabcar", + "version": "1.0.0", + "description": "FabCar contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive", + "start": "fabric-chaincode-node start" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "unstable", + "fabric-shim": "unstable" + }, + "devDependencies": { + "chai": "^4.1.2", + "eslint": "^4.19.1", + "mocha": "^5.2.0", + "nyc": "^12.0.2", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/test/scenario/features/event.feature b/test/scenario/features/event.feature new file mode 100644 index 0000000000..469951d616 --- /dev/null +++ b/test/scenario/features/event.feature @@ -0,0 +1,34 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +@networkAPI +@clean-gateway +Feature: Listen to events using a fabric-network + Background: + Given I have deployed a tls Fabric network + And I can create and join all channels from the tls common connection profile + And I can create a gateway named test_gateway as user User1 within Org1 using the tls common connection profile + Given I can install/instantiate node chaincode at version 1.0.0 named events to the tls Fabric network for all organizations on channel mychannel with endorsement policy 1AdminOr2Other and args [initLedger] + + Scenario: Using a Contract I can listen to contract events emmited by instantiated chaincodes + When I use the gateway named test_gateway to listen for create events with listener createValueListener on chaincode events instantiated on channel mychannel + When I use the gateway named test_gateway to submit 5 transactions with args [createValue] for chaincode events instantiated on channel mychannel + Then I receive 5 events from the listener createValueListener + When I use the gateway named test_gateway to listen for dc events with listener ehDisconnectListener on chaincode events instantiated on channel mychannel + When I use the gateway named test_gateway to submit 10 transactions with args [createValueDisconnect] for chaincode events instantiated on fabric channel mychannel disconnecting the event hub on listener ehDisconnectListener every 5 transactions + Then I receive 10 events from the listener ehDisconnectListener + + Scenario: Using a Contract I can listen to block events emmited by networks + When I use the gateway named test_gateway to listen for filtered_block_events with listener filteredBlockListener on chaincode events instantiated on channel mychannel + When I use the gateway named test_gateway to submit a transaction with args [createValue] for chaincode events instantiated on channel mychannel + Then I receive at least 1 events from the listener filteredBlockListener + When I use the gateway named test_gateway to listen for unfiltered_block_events with listener unfilteredBlockListener on chaincode events instantiated on channel mychannel + When I use the gateway named test_gateway to submit a transaction with args [createValue] for chaincode events instantiated on channel mychannel + Then I receive at least 1 events from the listener unfilteredBlockListener + + Scenario: I can listen to a transaction commit event + When I use the gateway named test_gateway to create a transaction named transaction1 that calls createValue using chaincode events instantiated on channel mychannel + When I use the transaction named transaction1 to create a commit listener called transaction1Listener + When I use the transaction named transaction1 to submit a transaction with args [] + Then I receive 1 events from the listener transaction1Listener diff --git a/test/scenario/features/lib/chaincode.js b/test/scenario/features/lib/chaincode.js index 066d876226..6fb48e24f3 100644 --- a/test/scenario/features/lib/chaincode.js +++ b/test/scenario/features/lib/chaincode.js @@ -147,7 +147,7 @@ async function instantiateChaincode(ccName, ccId, ccType, args, version, upgrade Promise.reject('Unsupported test ccType: ' + ccType); } - Client.setConfigSetting('request-timeout', 120000); + Client.setConfigSetting('request-timeout', 300000); const type = upgrade ? 'upgrade' : 'instantiate'; diff --git a/test/scenario/features/lib/network.js b/test/scenario/features/lib/network.js index 0fa8d1cd58..5457b61e5b 100644 --- a/test/scenario/features/lib/network.js +++ b/test/scenario/features/lib/network.js @@ -7,10 +7,21 @@ const {Gateway, InMemoryWallet, X509WalletMixin} = require('fabric-network'); const testUtil = require('./utils.js'); const fs = require('fs'); +const chai = require('chai'); +const expect = chai.expect; // Internal Map of connected gateways const gateways = new Map(); +// Internal Map of event listenerData +// {calls, payloads} +const listeners = new Map(); +// {listener, payloads} +const transactions = new Map(); + +// transaction types +const types = ['evaluate', 'error', 'submit']; + /** * Perform an in memeory ID setup * @param {InMemoryWallet} inMemoryWallet the in memory wallet to use @@ -70,7 +81,7 @@ async function connectGateway(ccp, tls, userName, orgName, gatewayName) { } await gateway.connect(ccp.profile, opts); - gateways.set(gatewayName, gateway); + gateways.set(gatewayName, {gateway}); // Ensure that all connections have had time to process in the background await testUtil.sleep(testUtil.TIMEOUTS.SHORT_INC); @@ -84,7 +95,7 @@ async function connectGateway(ccp, tls, userName, orgName, gatewayName) { */ async function disconnectGateway(gatewayName) { try { - const gateway = gateways.get(gatewayName); + const gateway = gateways.get(gatewayName).gateway; await gateway.disconnect(); gateways.delete(gatewayName); } catch (err) { @@ -100,10 +111,12 @@ async function disconnectAllGateways() { try { for (const key of gateways.keys()) { testUtil.logMsg('disconnecting from Gateway ', key); - const gateway = gateways.get(key); + const gateway = gateways.get(key).gateway; await gateway.disconnect(); } gateways.clear(); + listeners.clear(); + transactions.clear(); } catch (err) { testUtil.logError('disconnectAllGateways() failed with error ', err); throw err; @@ -138,7 +151,7 @@ async function retrieveContractFromGateway(gateway, channelName, chaincodeId) { */ async function performGatewayTransaction(gatewayName, ccName, channelName, args, submit) { // Get contract from Gateway - const gateway = gateways.get(gatewayName); + const gateway = gateways.get(gatewayName).gateway; const contract = await retrieveContractFromGateway(gateway, channelName, ccName); // Split args @@ -162,7 +175,201 @@ async function performGatewayTransaction(gatewayName, ccName, channelName, args, } } -module.exports.connectGateway = connectGateway; -module.exports.performGatewayTransaction = performGatewayTransaction; -module.exports.disconnectGateway = disconnectGateway; -module.exports.disconnectAllGateways = disconnectAllGateways; +async function performGatewayTransactionWithListener(gatewayName, ccName, channelName, args) { + // Get contract from Gateway + const gatewayObj = gateways.get(gatewayName); + const gateway = gatewayObj.gateway; + const contract = await retrieveContractFromGateway(gateway, channelName, ccName); + + // Split args + const argArray = args.slice(1, -1).split(','); + const func = argArray[0]; + const funcArgs = argArray.slice(1); + try { + testUtil.logMsg('Submitting transaction [' + func + '] with arguments ' + args); + // const result = await contract.submitTransaction(func, ...funcArgs); + const transaction = contract.createTransaction(func); + await transaction.addCommitListener((err, ...cbArgs) => { + + }); + const result = await transaction.submit(...funcArgs); + gatewayObj.result = {type: 'submit', response: result.toString()}; + testUtil.logMsg('Successfully submitted transaction [' + func + ']'); + return Promise.resolve(); + } catch (err) { + gatewayObj.result = {type: 'error', result: err.toString()}; + testUtil.logError(err); + throw err; + } +} + +/** + * Compare the last gateway transaction response with a passed value + * @param {String} type type of resposne + * @param {*} msg the message to compare against + */ +function lastResponseCompare(gatewayName, msg) { + const gatewayObj = gateways.get(gatewayName); + return (gatewayObj.result.response.localeCompare(msg) === 0); +} + +/** + * Retrieve the last gateway transaction result + * @param {String} type type of resposne + */ +function lastResult(gatewayName) { + const gatewayObj = gateways.get(gatewayName); + return gatewayObj.result; +} + +/** + * Compare the last gateway transaction type with a passed value + * @param {String} gatewayName gateway name + * @param {String} type type of resposne + */ +function lastTypeCompare(gatewayName, type) { + const gatewayObj = gateways.get(gatewayName); + + if (!gatewayObj) { + throw new Error('Unknown gateway with name ' + gatewayName); + } + + if (!gatewayObj.result) { + throw new Error('No existing response on gateway ' + gatewayName); + } + + if (types.indexOf(type) === -1) { + throw new Error('Unknown type transaction type ' + type + ', must be one of [evaluate, error, submit]'); + } + + return gatewayObj.result.type.localeCompare(type) === 0; +} + +function getGateway(gatewayName) { + if (gateways.get(gatewayName)) { + return gateways.get(gatewayName).gateway; + } else { + return undefined; + } +} + +async function createContractListener(gatewayName, channelName, ccName, eventName, listenerName) { + const gateway = gateways.get(gatewayName).gateway; + const contract = await retrieveContractFromGateway(gateway, channelName, ccName); + if (!listeners.has(listenerName)) { + listeners.set(listenerName, {calls: 0, payloads: []}); + } + const listener = await contract.addContractListener(listenerName, eventName, (err, ...args) => { + if (err) { + testUtil.logMsg('Contract event error', err); + return err; + } + testUtil.logMsg('Received a contract event', listenerName); + const listenerInfo = listeners.get(listenerName); + listenerInfo.payloads.push(args); + listenerInfo.calls = listenerInfo.payloads.length; + }, {replay: true}); + const listenerInfo = listeners.get(listenerName); + listenerInfo.listener = listener; + listeners.set(listenerName, listenerInfo); +} + +async function createBlockListener(gatewayName, channelName, ccName, listenerName, filtered) { + const gateway = gateways.get(gatewayName).gateway; + const contract = await retrieveContractFromGateway(gateway, channelName, ccName); + const network = contract.getNetwork(); + if (!listeners.has(listenerName)) { + listeners.set(listenerName, {calls: 0, payloads: []}); + } + const listener = await network.addBlockListener(listenerName, (err, block) => { + if (err) { + testUtil.logMsg('Block event error', err); + return err; + } + testUtil.logMsg('Received a block event', listenerName); + if (filtered) { + expect(block).to.have.property('channel_id'); + expect(block).to.have.property('number'); + expect(block).to.have.property('filtered_transactions'); + } else { + expect(block).to.have.property('header'); + expect(block).to.have.property('data'); + expect(block).to.have.property('metadata'); + } + const listenerInfo = listeners.get(listenerName); + listenerInfo.payloads.push(block); + listenerInfo.calls = listenerInfo.payloads.length; + }, {filtered, replay: true}); + const listenerInfo = listeners.get(listenerName); + listenerInfo.listener = listener; + listeners.set(listenerName, listenerInfo); +} + +function getListenerInfo(listenerName) { + if (listeners.has(listenerName)) { + return listeners.get(listenerName); + } + return {}; +} + +function resetListenerCalls(listenerName) { + if (listeners.has(listenerName)) { + const listenerInfo = listeners.get(listenerName); + listenerInfo.payloads = []; + listenerInfo.calls = 0; + } +} + +async function createTransaction(gatewayName, transactionName, fcnName, chaincodeId, channelName) { + const gateway = getGateway(gatewayName); + const contract = await retrieveContractFromGateway(gateway, channelName, chaincodeId); + const transaction = contract.createTransaction(fcnName); + transactions.set(transactionName, transaction); +} + +async function createCommitListener(transactionName, listenerName) { + const transaction = transactions.get(transactionName); + if (!transaction) { + throw new Error(`Transaction with name ${transactionName} does not exist`); + } + const listener = await transaction.addCommitListener((err, ...args) => { + if (err) { + testUtil.logMsg('Commit event error', err); + return err; + } + testUtil.logMsg('Received a commit event', listenerName); + const listenerInfo = listeners.get(listenerName); + listenerInfo.payloads.push(args); + listenerInfo.calls = listenerInfo.payloads.length; + }); + listeners.set(listenerName, {listener, payloads: []}); +} + +async function submitExistingTransaction(transactionName, args) { + const transaction = transactions.get(transactionName); + if (!transaction) { + throw new Error(`Transaction with name ${transactionName} does not exist`); + } + const argsSplit = args.slice(1, -1).split(', '); + return await transaction.submit(...argsSplit); +} + + +module.exports = { + connectGateway, + performGatewayTransaction, + performGatewayTransactionWithListener, + disconnectGateway, + disconnectAllGateways, + lastResponseCompare, + lastResult, + lastTypeCompare, + getGateway, + createContractListener, + createBlockListener, + getListenerInfo, + resetListenerCalls, + createTransaction, + createCommitListener, + submitExistingTransaction +}; diff --git a/test/scenario/features/steps/admin_steps.js b/test/scenario/features/steps/admin_steps.js index 5822a6ba74..fa57557d6a 100644 --- a/test/scenario/features/steps/admin_steps.js +++ b/test/scenario/features/steps/admin_steps.js @@ -18,6 +18,9 @@ const ccpPath = configRoot + '/ccp.json'; const tlsCcpPath = configRoot + '/ccp-tls.json'; const policiesPath = configRoot + '/policies.json'; +const instantiatedChaincodesOnChannels = new Map(); +const installedChaincodesOnPeers = new Map(); + module.exports = function () { this.Then(/^I can create a channels from the (.+?) common connection profile$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (tlsType) => { @@ -96,7 +99,14 @@ module.exports = function () { tls = true; profile = new CCP(path.join(__dirname, tlsCcpPath), true); } - return chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName); + if (!installedChaincodesOnPeers.has(orgName)) { + installedChaincodesOnPeers.set(orgName, []); + } + if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.installChaincode(ccName, ccName, ccType, version, tls, profile, orgName, channelName); + installedChaincodesOnPeers.set(orgName, [...installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]); + } + return true; }); this.Then(/^I can install (.+?) chaincode named (.+?) to the (.+?) Fabric network$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (ccType, ccName, tlsType) => { @@ -119,7 +129,14 @@ module.exports = function () { // fixed version const version = '1.0.0'; - return chaincode_util.installChaincode(ccName, ccName, ccType, version, tls, profile, orgName, channelName); + if (!installedChaincodesOnPeers.has(orgName)) { + installedChaincodesOnPeers.set(orgName, []); + } + if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.installChaincode(ccName, ccName, ccType, version, tls, profile, orgName, channelName); + installedChaincodesOnPeers.set(orgName, [installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]); + } + return true; }); this.Then(/^I can install (.+?) chaincode named (.+?) as (.+?) to the (.+?) Fabric network$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (ccType, ccName, ccId, tlsType) => { @@ -142,7 +159,14 @@ module.exports = function () { // fixed version const version = '1.0.0'; - return chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName); + if (!installedChaincodesOnPeers.has(orgName)) { + installedChaincodesOnPeers.set(orgName, []); + } + if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName); + installedChaincodesOnPeers.set(orgName, [...installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]); + } + return true; }); this.Then(/^I can instantiate the (.+?) installed (.+?) chaincode at version (.+?) named (.+?) on the (.+?) Fabric network as organization (.+?) on channel (.+?) with endorsement policy (.+?) and args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (exisiting, ccType, version, ccName, tlsType, orgName, channelName, policyType, args) => { @@ -164,7 +188,14 @@ module.exports = function () { } const policy = require(path.join(__dirname, policiesPath))[policyType]; - return chaincode_util.instantiateChaincode(ccName, ccName, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy); + if (!instantiatedChaincodesOnChannels.has(channelName)) { + instantiatedChaincodesOnChannels.set(channelName, []); + } + if (!instantiatedChaincodesOnChannels.get(channelName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.instantiateChaincode(ccName, ccName, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy); + instantiatedChaincodesOnChannels.set(channelName, [...instantiatedChaincodesOnChannels.get(channelName), `${ccName}${version}${ccType}`]); + } + return true; }); this.Then(/^I can instantiate the (.+?) installed (.+?) chaincode at version (.+?) named (.+?) with identifier (.+?) on the (.+?) Fabric network as organization (.+?) on channel (.+?) with endorsement policy (.+?) and args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (exisiting, ccType, version, ccName, ccId, tlsType, orgName, channelName, policyType, args) => { @@ -186,7 +217,14 @@ module.exports = function () { } const policy = require(path.join(__dirname, policiesPath))[policyType]; - return chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy); + if (!instantiatedChaincodesOnChannels.has(channelName)) { + instantiatedChaincodesOnChannels.set(channelName, []); + } + if (!instantiatedChaincodesOnChannels.get(channelName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, upgrade, tls, profile, orgName, channelName, policy); + instantiatedChaincodesOnChannels.set(channelName, [...instantiatedChaincodesOnChannels.get(channelName), `${ccName}${version}${ccType}`]); + } + return true; }); this.Then(/^I can install\/instantiate (.+?) chaincode at version (.+?) named (.+?) to the (.+?) Fabric network for all organizations on channel (.+?) with endorsement policy (.+?) and args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (ccType, version, ccName, tlsType, channelName, policyType, args) => { @@ -234,10 +272,23 @@ module.exports = function () { try { for (const org in orgs) { const orgName = orgs[org]; - await chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName); + if (!installedChaincodesOnPeers.has(orgName)) { + installedChaincodesOnPeers.set(orgName, []); + } + if (!installedChaincodesOnPeers.get(orgName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.installChaincode(ccName, ccId, ccType, version, tls, profile, orgName, channelName); + installedChaincodesOnPeers.set(orgName, [...installedChaincodesOnPeers.get(orgName), `${ccName}${version}${ccType}`]); + } } - return chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, false, tls, profile, orgs[0], channelName, policy); + if (!instantiatedChaincodesOnChannels.has(channelName)) { + instantiatedChaincodesOnChannels.set(channelName, []); + } + if (!instantiatedChaincodesOnChannels.get(channelName).includes(`${ccName}${version}${ccType}`)) { + await chaincode_util.instantiateChaincode(ccName, ccId, ccType, args, version, false, tls, profile, orgs[0], channelName, policy); + instantiatedChaincodesOnChannels.set(channelName, [...instantiatedChaincodesOnChannels.get(channelName), `${ccName}${version}${ccType}`]); + } + return true; } catch (err) { testUtil.logError('Install/Instantiate failed with error: ', err); throw err; diff --git a/test/scenario/features/steps/docker_steps.js b/test/scenario/features/steps/docker_steps.js index 98e8caccee..b0bab5e860 100644 --- a/test/scenario/features/steps/docker_steps.js +++ b/test/scenario/features/steps/docker_steps.js @@ -10,6 +10,7 @@ const path = require('path'); module.exports = function () { this.Given(/^I have deployed a (.+?) Fabric network/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (type) => { + await testUtil.runShellCommand(undefined, 'rm -r ~/.hlf-checkpoint'); await testUtil.runShellCommand(undefined, 'docker kill $(docker ps -aq); docker rm $(docker ps -aq)'); if (type.localeCompare('non-tls') === 0) { await testUtil.runShellCommand(true, 'docker-compose -f ' + path.join(__dirname, '../../../fixtures/docker-compose/docker-compose.yaml') + ' -p node up -d'); @@ -20,6 +21,7 @@ module.exports = function () { }); this.Given(/^I have forcibly taken down all docker containers/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async () => { + await testUtil.runShellCommand(undefined, 'rm -r ~/.hlf-checkpoint'); await testUtil.runShellCommand(undefined, 'docker kill $(docker ps -aq); docker rm $(docker ps -aq)'); return await testUtil.sleep(testUtil.TIMEOUTS.SHORT_INC); }); diff --git a/test/scenario/features/steps/network_steps.js b/test/scenario/features/steps/network_steps.js index a4ef5be297..07f8fd7348 100644 --- a/test/scenario/features/steps/network_steps.js +++ b/test/scenario/features/steps/network_steps.js @@ -34,7 +34,6 @@ module.exports = function () { this.Then(/^I use the gateway named (.+?) to evaluate transaction with args (.+?) for chaincode (.+?) instantiated on channel (.+?) with the response matching (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, args, ccName, channelName, expected) => { const result = await network_util.performGatewayTransaction(gatewayName, ccName, channelName, args, false); - if (result === expected) { return Promise.resolve(); } else { @@ -43,6 +42,111 @@ module.exports = function () { }); + this.Then(/^I use the gateway named (.+?) to submit (.+?) transactions with args (.+?) for chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, numTransactions, args, ccName, channelName) => { + for (let i = 0; i < numTransactions; i++) { + await network_util.performGatewayTransaction(gatewayName, ccName, channelName, args, true); + } + }); + + this.Then(/^I use the gateway named (.+?) to submit (.+?) transactions with args (.+?) for chaincode (.+?) instantiated on fabric channel (.+?) disconnecting the event hub on listener (.+?) every (.+?) transactions$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, numTransactions, args, ccName, channelName, listenerName, disconnects) => { + const listener = network_util.getListenerInfo(listenerName).listener; + const eventHub = listener.eventHub; + for (let i = 0; i < numTransactions; i++) { + if (i % disconnects === 0) { + eventHub.disconnect(); + } + await network_util.performGatewayTransaction(gatewayName, ccName, channelName, args, true); + } + }); + + this.Then(/^I use the gateway named (.+?) to submit a transaction with args (.+?) and listen for a commit event for chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, args, ccName, channelName) => { + return await network_util.performGatewayTransactionWithListener(gatewayName, ccName, channelName, args, true); + }); + + // Events + this.Then(/^I use the gateway named (.+?) to listen for (.+?) events with listener (.+?) on chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName, eventName, listenerName, ccName, channelName) => { + return await network_util.createContractListener(gatewayName, channelName, ccName, eventName, listenerName); + }); + + this.Then(/^I use the gateway named (.+?) to listen for filtered_block_events with listener (.+?) on chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName, listenerName, ccName, channelName) => { + return await network_util.createBlockListener(gatewayName, channelName, ccName, listenerName, true); + }); + + this.Then(/^I use the gateway named (.+?) to listen for unfiltered_block_events with listener (.+?) on chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName, listenerName, ccName, channelName) => { + return await network_util.createBlockListener(gatewayName, channelName, ccName, listenerName, false); + }); + + this.Then(/^I receive ([0-9]+) events from the listener (.+?)$/, {timeout: testUtil.TIMEOUTS.SHORT_STEP}, async (calls, listenerName) => { + await new Promise(resolve => { + const interval = setInterval(() => { + const listenerInfo = network_util.getListenerInfo(listenerName); + if (Number(listenerInfo.calls) === Number(calls)) { + clearInterval(interval); + clearTimeout(timeout); + resolve(); + } + }, 1000); + const timeout = setTimeout(() => { + resolve(); + clearInterval(interval); + }, 60000); + }); + const eventListenerInfo = network_util.getListenerInfo(listenerName); + if (Number(eventListenerInfo.calls) !== Number(calls)) { + throw new Error(`Expected ${listenerName} to be called ${calls} times, but called ${eventListenerInfo.calls} times`); + } + network_util.resetListenerCalls(listenerName); + return Promise.resolve(); + }); + + this.Then(/^I use the transaction named (.+?) to create a commit listener called (.+?)$/, (transactionName, listenerName) => { + return network_util.createCommitListener(transactionName, listenerName); + }); + + this.Then(/^I use the transaction named (.+?) to submit a transaction with args (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (transactionName, args) => { + return network_util.submitExistingTransaction(transactionName, args); + }); + + this.Then(/^I use the gateway named (.+?) to submit a transaction with args (.+?) and listen for a commit event for chaincode (.+?) instantiated on channel (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, args, ccName, channelName) => { + return await network_util.performGatewayTransactionWithListener(gatewayName, ccName, channelName, args, true); + }); + + this.Then(/^I receive at least ([0-9]+) events from the listener (.+?)$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (calls, listenerName) => { + await new Promise(resolve => { + let timeout = null; + const interval = setInterval(() => { + const listenerInfo = network_util.getListenerInfo(listenerName); + if (Number(listenerInfo.calls) >= Number(calls)) { + clearInterval(interval); + clearTimeout(timeout); + resolve(); + } + }, 1000); + timeout = setTimeout(() => { + resolve(); + clearInterval(interval); + }, 60000); + }); + const eventListenerInfo = network_util.getListenerInfo(listenerName); + if (Number(eventListenerInfo.calls) < Number(calls)) { + throw new Error(`Expected ${listenerName} to be called ${calls} times, but called ${eventListenerInfo.calls} times`); + } + network_util.resetListenerCalls(listenerName); + return Promise.resolve(); + }); + + this.Then(/^I use the gateway named (.+?) to create a transaction named (.+?) that calls (.+?) using chaincode (.+?) instantiated on channel (.+?)$/, async (gatewayName, transactionName, fcnName, ccName, channelName) => { + return network_util.createTransaction(gatewayName, transactionName, fcnName, ccName, channelName); + }); + + this.Then(/^The gateway named (.+?) has a (.+?) type response$/, {timeout: testUtil.TIMEOUTS.LONG_STEP}, async (gatewayName, type) => { + if (network_util.lastTypeCompare(gatewayName, type)) { + return Promise.resolve(); + } else { + throw new Error('Expected and actual result type from previous transaction did not match. Expected [' + type + '] but had [' + network_util.lastResult(gatewayName).type + ']'); + } + }); + this.Then(/^I can disconnect from the gateway named (.+?)$/, {timeout:testUtil.TIMEOUTS.SHORT_STEP}, async (gatewayName) => { return await network_util.disconnectGateway(gatewayName); }); diff --git a/test/typescript/integration/network-e2e/invoke.ts b/test/typescript/integration/network-e2e/invoke.ts index 40178de0ba..50110e7c6a 100644 --- a/test/typescript/integration/network-e2e/invoke.ts +++ b/test/typescript/integration/network-e2e/invoke.ts @@ -82,8 +82,8 @@ async function getInternalEventHubForOrg(gateway: Gateway, orgMSP: string): Prom const orgPeer = channel.getPeersForOrg(orgMSP)[0]; // Only one peer per org in the test configuration // Using private functions to get hold of an internal event hub. Don't try this at home, kids! - const eventHubFactory = (network as any).getEventHubFactory(); - return eventHubFactory.getEventHub(orgPeer); + const eventHubManager = (network as any).getEventHubManager(); + return eventHubManager.getEventHub(orgPeer); } test('\n\n***** Network End-to-end flow: import identity into wallet and configure tls *****\n\n', async (t: any) => {