Skip to content

Commit

Permalink
FABN-1464 NodeSDK update queryHandling (#105)
Browse files Browse the repository at this point in the history
Update queryHandler's to use a highlevel wrapper on the
low level query sending. This will sheild the users that
wish to create their own handlers from the sending and
handling of the responses. The handlers will focus on peer
selection. Updated the sample and doc.

Signed-off-by: Bret Harrison <beharrison@nc.rr.com>
  • Loading branch information
harrisob authored Feb 13, 2020
1 parent 854dba2 commit 5d61cfd
Show file tree
Hide file tree
Showing 13 changed files with 874 additions and 188 deletions.
88 changes: 73 additions & 15 deletions docs/tutorials/query-peers.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
This tutorial describes how peers are selected to evaluate transactions
that will not then be written to the ledger, which may also be considered
as queries.
This tutorial describes how peers are selected when a transaction is evaluated
and the results are not written to the ledger. The is considered to be a
query.

### Query handling strategies

The SDK provides several selectable strategies for how it should evaluate
transactions on peers in the network. The available strategies are defined
The SDK provides two strategies to evaluate transactions.
The available strategies are defined
in `QueryHandlerStrategies`. The desired strategy is (optionally)
specified as an argument to `connect()` on the `Gateway`, and is used for
all transaction evaluations on Contracts obtained from that Gateway
instance.

If no query handling strategy is specified, `MSPID_SCOPE_SINGLE` is used
by default. This will evaluate all transactions on the first peer from
which is can obtain a response, and only switch to another peer if this
peer fails.
which it can obtain a response, and only switch to another peer if this
peer fails. The list of peers will be all peers in the contract's `Network`
that belong to the gateway's organization.

There is another query handling strategy provided called `MSPID_SCOPE_ROUND_ROBIN`.
This will evaluate a transaction starting with the first peer on the list.
It will try the peers in order until a response is received or all peers
have been tried. On the next call the second peer will be tried first and then
continue on in the list until a response is received. The starting point within
the list is incremented on each call, this will distribute the work load among all
responding peers. The list of peers will be all peers in the contract's `Network`
that belong to the gateway's organization.

```javascript
const { Gateway, QueryHandlerStrategies } = require('fabric-network');

const connectOptions = {
query: {
timeout: 3,
timeout: 3, // timeout in seconds
strategy: QueryHandlerStrategies.MSPID_SCOPE_SINGLE
}
}
Expand All @@ -37,20 +47,28 @@ strategies, it is possible to implement your own query handling. This is
achieved by specifying your own factory function as the query handling
strategy. The factory function should return a *query handler*
object and take one parameter:
1. Blockchain network: `Network`
1. Blockchain network: `Network` - {@link fabric-network.Network}

The Network provides access to peers on which transactions should be
The Network instance provides access to peers on which transactions should be
evaluated.

```javascript
// factory function will return the handler
function createQueryHandler(network) {
/* Your implementation here */
// use the network to get all endorsing peers
// of all organizations
const peers = network.getEndorsers();
// use the network to get endorsing peers
// of my organization (MSPID of the organization)
const peers = network.getEndorsers('mymspid');

// build and return the query handler
return new MyQueryHandler(peers);
}

const connectOptions = {
query: {
timeout: 3,
timeout: 3, // timeout in seconds (optional will default to 3)
strategy: createQueryHandler
}
}
Expand All @@ -65,12 +83,52 @@ The *query handler* object returned must implement the following functions.
class MyQueryHandler {
/**
* Evaluate the supplied query on appropriate peers.
* @param {Query} query A query object that provides an evaluate()
* function to invoke itself on specified peers.
* @param {Query} query - A query object that will send the
* query proposal to the peers and format the responses for this query handler
* @returns {Buffer} Query result.
*/
async evaluate(query) { /* Your implementation here */ }
}
```

For a complete sample plug-in query handler implementation, see [sample-query-handler.ts](https://github.com/hyperledger/fabric-sdk-node/blob/master/test/typescript/integration/network-e2e/sample-query-handler.ts).
Use the `query` instance provided to the `evaluate` method to make the query call
to the peer or peers of your Fabric network. The query instance will process
the peer responses of the endorsement and provide your handler with the results.
The results will be keyed by peer name and may contain either a `QueryResult`
or an `Error`.

The QueryResult:
```
export interface QueryResponse {
isEndorsed: boolean; // indicates a good endorsement, required to have query results
payload: Buffer; // The query results
status: number; // status of the query, 200 successful, 500 failed
message: string; // failed reason message
}
```

The following sample code is in TypeScript to show the object types involved.
```javascript
public async evaluate(query: Query): Promise<Buffer> {
const errorMessages: string[] = [];

for (const peer of this.peers) {
const results: QueryResults = await query.evaluate([peer]);
const result = results[peer.name];
if (result instanceof Error) {
errorMessages.push(result.toString());
} else {
if (result.isEndorsed) {
return result.payload;
}
errorMessages.push(result.message);
}
}

const message = util.format('Query failed. Errors: %j', errorMessages);
const error = new Error(message);
throw error;
}
```

For a complete sample plug-in query handler implementation, see [sample-query-handler.ts](https://github.com/hyperledger/fabric-sdk-node/blob/master/test/ts-scenario/config/handlers/sample-query-handler.ts).
6 changes: 3 additions & 3 deletions fabric-common/lib/Proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ message Endorsement {
const signedEnvelope = this.getSignedProposal();
this._proposalResponses = [];
this._proposalErrors = [];
this._queryResults = [];

if (handler) {
logger.debug('%s - endorsing with a handler', method);
Expand Down Expand Up @@ -389,13 +390,12 @@ message Endorsement {
};

if (this.type === 'Query') {
this._queryResults = [];
this._proposalResponses.forEach((response) => {
if (response.response && response.response.payload && response.response.payload.length > 0) {
if (response.endorsement && response.response && response.response.payload) {
logger.debug('%s - have payload', method);
this._queryResults.push(response.response.payload);
} else {
logger.error('%s - unknown or missing results in query', method);
logger.debug('%s - no payload in query', method);
}
});
return_results.queryResults = this._queryResults;
Expand Down
1 change: 1 addition & 0 deletions fabric-common/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class DiscoveryHandler extends ServiceHandler {
}

export class ServiceEndpoint {
public readonly name: string;
constructor(name: string, client: Client, mspid?: string);
public connect(endpoint: Endpoint, options: ConnectOptions): Promise<void>;
public disconnect(): void;
Expand Down
103 changes: 103 additions & 0 deletions fabric-network/src/impl/query/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright 2020 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const logger = require('fabric-network/lib/logger').getLogger('Query');

/**
* @typedef {Object} Query~QueryResponse
* @memberof module:fabric-network
* @property {number} status - The status value from the endorsement. This attriibute
* will be set by the chaincode.
* @property {Buffer} payload - The payload value from the endorsement. This attribute
* may be considered the query value if the status code is exceptable.
* @property {Buffer} payload - The message value from the endorsement. This attribute
* will have a value when there is not a payload and status value indicates an issue
* with determining the payload (the query value).
*/
/**
* Used by query handler implementations to evaluate transactions on peers of their choosing.
* @memberof module:fabric-network
*/
class Query {
/**
* Builds a Query instance to send and then work with the results returned
* by the fabric-common/Query.
* @param {module:fabric-common.Query} query - The query instance of the proposal
* @returns {Object} options - options to be used when sending the request to
* fabric-common service endpoint {Endorser} peer.
*/
constructor(query, options = {}) {
this.query = query;
this.requestTimeout = 3000; // default 3 seconds
if (Number.isInteger(options.timeout)) {
this.requestTimeout = options.timeout * 1000; // need ms;
}
}

/**
* Sends a signed proposal to the specified peers. The peer endorsment
* responses are
* @param {Endorser[]} peers - The peers to query
* @returns {Object.<String, (QueryResponse | Error)>} Object with peer name keys and associated values that are either
* QueryResponse objects or Error objects.
*/
async evaluate(peers) {
const method = 'evaluate';
logger.debug('%s - start', method);

const results = {};
try {
const responses = await this.query.send({targets: peers, requestTimeout: this.requestTimeout});
if (responses) {
if (responses.errors) {
for (const resultError of responses.errors) {
results[resultError.connection.name] = resultError;
logger.debug('%s - problem with query to peer %s error:%s', method, resultError.connection.name, resultError);
}
}
if (responses.responses) {
for (const peer_response of responses.responses) {
if (peer_response.response) {
const response = {};
response.status = peer_response.response.status;
response.payload = peer_response.response.payload;
response.message = peer_response.response.message;
response.isEndorsed = peer_response.endorsement ? true : false;
results[peer_response.connection.name] = response;
logger.debug('%s - have results - peer: %s with status:%s',
method,
peer_response.connection.name,
response.status);
}
}
}

// check to be sure we got results for each peer requested
for (const peer of peers) {
if (!results[peer.name]) {
logger.error('%s - no results for peer: %s', method, peer.name);
results[peer.name] = new Error('Missing response from peer');
}
}
} else {
throw Error('No responses returned for query');
}
} catch (error) {
// if we get an error, return this error for each peer
for (const peer of peers) {
results[peer.name] = error;
logger.error('%s - problem with query to peer %s error:%s', method, peer.name, error);
}
}

logger.debug('%s - end', method);
return results;
}
}

module.exports = Query;
13 changes: 2 additions & 11 deletions fabric-network/src/impl/query/queryhandlerstrategies.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,14 @@ function getOrganizationPeers(network) {
return network.channel.getEndorsers(network.mspid);
}

function getTimeout(network) {
const queryOptions = network.gateway.getOptions().query;
let timeout = 3000; // default 3 seconds
if (Number.isInteger(queryOptions.timeout)) {
timeout = queryOptions.timeout * 1000; // need ms;
}
return {timeout};
}

function MSPID_SCOPE_SINGLE(network) {
const peers = getOrganizationPeers(network);
return new SingleQueryHandler(peers, getTimeout(network));
return new SingleQueryHandler(peers);
}

function MSPID_SCOPE_ROUND_ROBIN(network) {
const peers = getOrganizationPeers(network);
return new RoundRobinQueryHandler(peers, getTimeout(network));
return new RoundRobinQueryHandler(peers);
}

/**
Expand Down
49 changes: 19 additions & 30 deletions fabric-network/src/impl/query/roundrobinqueryhandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,41 @@ const util = require('util');
const logger = require('fabric-network/lib/logger').getLogger('RoundRobinQueryHandler');

class RoundRobinQueryHandler {
constructor(peers, options = {}) {
constructor(peers) {
logger.debug('constructor: peers=%j', peers.map((peer) => peer.name));
this._peers = peers;
this._currentPeerIndex = 0;
this._options = options;
}

async evaluate(query) {
const method = 'evaluate';
logger.debug('%s - start', method);

const startPeerIndex = this._currentPeerIndex;

this._currentPeerIndex = (this._currentPeerIndex + 1) % this._peers.length;

const errorMessages = [];
const options = {requestTimeout: 3000};
// use the timeout as the requestTimeout or let default
if (Number.isInteger(this._options.timeout)) {
options.requestTimeout = this._options.timeout * 1000; // in ms;
}

for (let i = 0; i < this._peers.length; i++) {
const peerIndex = (startPeerIndex + i) % this._peers.length;
this._currentPeerIndex = peerIndex;
const peer = this._peers[peerIndex];

logger.debug('%s - query sending to peer %s', method, peer.name);
const results = await query.send({targets:[peer]}, options);

if (results.errors.length > 0) {
logger.error('%s - problem with query to peer %s error:%s', method, peer.name, results.errors[0]);
// since only one peer, only one error
errorMessages.push(results.errors[0].message);
continue;
}

const endorsementResponse = results.responses[0];

if (!endorsementResponse.endorsement) {
logger.debug('%s - peer response status: %s message: %s',
method,
endorsementResponse.response.status,
endorsementResponse.response.message);
throw new Error(endorsementResponse.response.message);
const peer = this._peers[peerIndex];
logger.debug('%s - sending to peer %s', method, peer.name);

const results = await query.evaluate([peer]);
const result = results[peer.name];
if (result instanceof Error) {
errorMessages.push(result.toString());
} else {
if (result.isEndorsed) {
logger.debug('%s - return peer response status: %s', method, result.status);
return result.payload;
} else {
logger.debug('%s - throw peer response status: %s message: %s', method, result.status, result.message);
throw Error(result.message);
}
}

logger.debug('%s - peer response status %s', method, endorsementResponse.response.status);
return endorsementResponse.response.payload;
}

const message = util.format('Query failed. Errors: %j', errorMessages);
Expand Down
Loading

0 comments on commit 5d61cfd

Please sign in to comment.