Skip to content
This repository was archived by the owner on Apr 22, 2025. It is now read-only.

Commit 5d61cfd

Browse files
authored
FABN-1464 NodeSDK update queryHandling (#105)
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>
1 parent 854dba2 commit 5d61cfd

File tree

13 files changed

+874
-188
lines changed

13 files changed

+874
-188
lines changed

docs/tutorials/query-peers.md

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
1-
This tutorial describes how peers are selected to evaluate transactions
2-
that will not then be written to the ledger, which may also be considered
3-
as queries.
1+
This tutorial describes how peers are selected when a transaction is evaluated
2+
and the results are not written to the ledger. The is considered to be a
3+
query.
44

55
### Query handling strategies
66

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

1414
If no query handling strategy is specified, `MSPID_SCOPE_SINGLE` is used
1515
by default. This will evaluate all transactions on the first peer from
16-
which is can obtain a response, and only switch to another peer if this
17-
peer fails.
16+
which it can obtain a response, and only switch to another peer if this
17+
peer fails. The list of peers will be all peers in the contract's `Network`
18+
that belong to the gateway's organization.
19+
20+
There is another query handling strategy provided called `MSPID_SCOPE_ROUND_ROBIN`.
21+
This will evaluate a transaction starting with the first peer on the list.
22+
It will try the peers in order until a response is received or all peers
23+
have been tried. On the next call the second peer will be tried first and then
24+
continue on in the list until a response is received. The starting point within
25+
the list is incremented on each call, this will distribute the work load among all
26+
responding peers. The list of peers will be all peers in the contract's `Network`
27+
that belong to the gateway's organization.
1828

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

2232
const connectOptions = {
2333
query: {
24-
timeout: 3,
34+
timeout: 3, // timeout in seconds
2535
strategy: QueryHandlerStrategies.MSPID_SCOPE_SINGLE
2636
}
2737
}
@@ -37,20 +47,28 @@ strategies, it is possible to implement your own query handling. This is
3747
achieved by specifying your own factory function as the query handling
3848
strategy. The factory function should return a *query handler*
3949
object and take one parameter:
40-
1. Blockchain network: `Network`
50+
1. Blockchain network: `Network` - {@link fabric-network.Network}
4151

42-
The Network provides access to peers on which transactions should be
52+
The Network instance provides access to peers on which transactions should be
4353
evaluated.
4454

4555
```javascript
56+
// factory function will return the handler
4657
function createQueryHandler(network) {
47-
/* Your implementation here */
58+
// use the network to get all endorsing peers
59+
// of all organizations
60+
const peers = network.getEndorsers();
61+
// use the network to get endorsing peers
62+
// of my organization (MSPID of the organization)
63+
const peers = network.getEndorsers('mymspid');
64+
65+
// build and return the query handler
4866
return new MyQueryHandler(peers);
4967
}
5068

5169
const connectOptions = {
5270
query: {
53-
timeout: 3,
71+
timeout: 3, // timeout in seconds (optional will default to 3)
5472
strategy: createQueryHandler
5573
}
5674
}
@@ -65,12 +83,52 @@ The *query handler* object returned must implement the following functions.
6583
class MyQueryHandler {
6684
/**
6785
* Evaluate the supplied query on appropriate peers.
68-
* @param {Query} query A query object that provides an evaluate()
69-
* function to invoke itself on specified peers.
86+
* @param {Query} query - A query object that will send the
87+
* query proposal to the peers and format the responses for this query handler
7088
* @returns {Buffer} Query result.
7189
*/
7290
async evaluate(query) { /* Your implementation here */ }
7391
}
7492
```
7593

76-
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).
94+
Use the `query` instance provided to the `evaluate` method to make the query call
95+
to the peer or peers of your Fabric network. The query instance will process
96+
the peer responses of the endorsement and provide your handler with the results.
97+
The results will be keyed by peer name and may contain either a `QueryResult`
98+
or an `Error`.
99+
100+
The QueryResult:
101+
```
102+
export interface QueryResponse {
103+
isEndorsed: boolean; // indicates a good endorsement, required to have query results
104+
payload: Buffer; // The query results
105+
status: number; // status of the query, 200 successful, 500 failed
106+
message: string; // failed reason message
107+
}
108+
```
109+
110+
The following sample code is in TypeScript to show the object types involved.
111+
```javascript
112+
public async evaluate(query: Query): Promise<Buffer> {
113+
const errorMessages: string[] = [];
114+
115+
for (const peer of this.peers) {
116+
const results: QueryResults = await query.evaluate([peer]);
117+
const result = results[peer.name];
118+
if (result instanceof Error) {
119+
errorMessages.push(result.toString());
120+
} else {
121+
if (result.isEndorsed) {
122+
return result.payload;
123+
}
124+
errorMessages.push(result.message);
125+
}
126+
}
127+
128+
const message = util.format('Query failed. Errors: %j', errorMessages);
129+
const error = new Error(message);
130+
throw error;
131+
}
132+
```
133+
134+
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).

fabric-common/lib/Proposal.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ message Endorsement {
330330
const signedEnvelope = this.getSignedProposal();
331331
this._proposalResponses = [];
332332
this._proposalErrors = [];
333+
this._queryResults = [];
333334

334335
if (handler) {
335336
logger.debug('%s - endorsing with a handler', method);
@@ -389,13 +390,12 @@ message Endorsement {
389390
};
390391

391392
if (this.type === 'Query') {
392-
this._queryResults = [];
393393
this._proposalResponses.forEach((response) => {
394-
if (response.response && response.response.payload && response.response.payload.length > 0) {
394+
if (response.endorsement && response.response && response.response.payload) {
395395
logger.debug('%s - have payload', method);
396396
this._queryResults.push(response.response.payload);
397397
} else {
398-
logger.error('%s - unknown or missing results in query', method);
398+
logger.debug('%s - no payload in query', method);
399399
}
400400
});
401401
return_results.queryResults = this._queryResults;

fabric-common/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export class DiscoveryHandler extends ServiceHandler {
112112
}
113113

114114
export class ServiceEndpoint {
115+
public readonly name: string;
115116
constructor(name: string, client: Client, mspid?: string);
116117
public connect(endpoint: Endpoint, options: ConnectOptions): Promise<void>;
117118
public disconnect(): void;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright 2020 IBM All Rights Reserved.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
'use strict';
8+
9+
const logger = require('fabric-network/lib/logger').getLogger('Query');
10+
11+
/**
12+
* @typedef {Object} Query~QueryResponse
13+
* @memberof module:fabric-network
14+
* @property {number} status - The status value from the endorsement. This attriibute
15+
* will be set by the chaincode.
16+
* @property {Buffer} payload - The payload value from the endorsement. This attribute
17+
* may be considered the query value if the status code is exceptable.
18+
* @property {Buffer} payload - The message value from the endorsement. This attribute
19+
* will have a value when there is not a payload and status value indicates an issue
20+
* with determining the payload (the query value).
21+
*/
22+
/**
23+
* Used by query handler implementations to evaluate transactions on peers of their choosing.
24+
* @memberof module:fabric-network
25+
*/
26+
class Query {
27+
/**
28+
* Builds a Query instance to send and then work with the results returned
29+
* by the fabric-common/Query.
30+
* @param {module:fabric-common.Query} query - The query instance of the proposal
31+
* @returns {Object} options - options to be used when sending the request to
32+
* fabric-common service endpoint {Endorser} peer.
33+
*/
34+
constructor(query, options = {}) {
35+
this.query = query;
36+
this.requestTimeout = 3000; // default 3 seconds
37+
if (Number.isInteger(options.timeout)) {
38+
this.requestTimeout = options.timeout * 1000; // need ms;
39+
}
40+
}
41+
42+
/**
43+
* Sends a signed proposal to the specified peers. The peer endorsment
44+
* responses are
45+
* @param {Endorser[]} peers - The peers to query
46+
* @returns {Object.<String, (QueryResponse | Error)>} Object with peer name keys and associated values that are either
47+
* QueryResponse objects or Error objects.
48+
*/
49+
async evaluate(peers) {
50+
const method = 'evaluate';
51+
logger.debug('%s - start', method);
52+
53+
const results = {};
54+
try {
55+
const responses = await this.query.send({targets: peers, requestTimeout: this.requestTimeout});
56+
if (responses) {
57+
if (responses.errors) {
58+
for (const resultError of responses.errors) {
59+
results[resultError.connection.name] = resultError;
60+
logger.debug('%s - problem with query to peer %s error:%s', method, resultError.connection.name, resultError);
61+
}
62+
}
63+
if (responses.responses) {
64+
for (const peer_response of responses.responses) {
65+
if (peer_response.response) {
66+
const response = {};
67+
response.status = peer_response.response.status;
68+
response.payload = peer_response.response.payload;
69+
response.message = peer_response.response.message;
70+
response.isEndorsed = peer_response.endorsement ? true : false;
71+
results[peer_response.connection.name] = response;
72+
logger.debug('%s - have results - peer: %s with status:%s',
73+
method,
74+
peer_response.connection.name,
75+
response.status);
76+
}
77+
}
78+
}
79+
80+
// check to be sure we got results for each peer requested
81+
for (const peer of peers) {
82+
if (!results[peer.name]) {
83+
logger.error('%s - no results for peer: %s', method, peer.name);
84+
results[peer.name] = new Error('Missing response from peer');
85+
}
86+
}
87+
} else {
88+
throw Error('No responses returned for query');
89+
}
90+
} catch (error) {
91+
// if we get an error, return this error for each peer
92+
for (const peer of peers) {
93+
results[peer.name] = error;
94+
logger.error('%s - problem with query to peer %s error:%s', method, peer.name, error);
95+
}
96+
}
97+
98+
logger.debug('%s - end', method);
99+
return results;
100+
}
101+
}
102+
103+
module.exports = Query;

fabric-network/src/impl/query/queryhandlerstrategies.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,14 @@ function getOrganizationPeers(network) {
1313
return network.channel.getEndorsers(network.mspid);
1414
}
1515

16-
function getTimeout(network) {
17-
const queryOptions = network.gateway.getOptions().query;
18-
let timeout = 3000; // default 3 seconds
19-
if (Number.isInteger(queryOptions.timeout)) {
20-
timeout = queryOptions.timeout * 1000; // need ms;
21-
}
22-
return {timeout};
23-
}
24-
2516
function MSPID_SCOPE_SINGLE(network) {
2617
const peers = getOrganizationPeers(network);
27-
return new SingleQueryHandler(peers, getTimeout(network));
18+
return new SingleQueryHandler(peers);
2819
}
2920

3021
function MSPID_SCOPE_ROUND_ROBIN(network) {
3122
const peers = getOrganizationPeers(network);
32-
return new RoundRobinQueryHandler(peers, getTimeout(network));
23+
return new RoundRobinQueryHandler(peers);
3324
}
3425

3526
/**

fabric-network/src/impl/query/roundrobinqueryhandler.js

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,52 +13,41 @@ const util = require('util');
1313
const logger = require('fabric-network/lib/logger').getLogger('RoundRobinQueryHandler');
1414

1515
class RoundRobinQueryHandler {
16-
constructor(peers, options = {}) {
16+
constructor(peers) {
1717
logger.debug('constructor: peers=%j', peers.map((peer) => peer.name));
1818
this._peers = peers;
1919
this._currentPeerIndex = 0;
20-
this._options = options;
2120
}
2221

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

2726
const startPeerIndex = this._currentPeerIndex;
27+
28+
this._currentPeerIndex = (this._currentPeerIndex + 1) % this._peers.length;
29+
2830
const errorMessages = [];
29-
const options = {requestTimeout: 3000};
30-
// use the timeout as the requestTimeout or let default
31-
if (Number.isInteger(this._options.timeout)) {
32-
options.requestTimeout = this._options.timeout * 1000; // in ms;
33-
}
3431

3532
for (let i = 0; i < this._peers.length; i++) {
3633
const peerIndex = (startPeerIndex + i) % this._peers.length;
37-
this._currentPeerIndex = peerIndex;
38-
const peer = this._peers[peerIndex];
39-
40-
logger.debug('%s - query sending to peer %s', method, peer.name);
41-
const results = await query.send({targets:[peer]}, options);
4234

43-
if (results.errors.length > 0) {
44-
logger.error('%s - problem with query to peer %s error:%s', method, peer.name, results.errors[0]);
45-
// since only one peer, only one error
46-
errorMessages.push(results.errors[0].message);
47-
continue;
48-
}
49-
50-
const endorsementResponse = results.responses[0];
51-
52-
if (!endorsementResponse.endorsement) {
53-
logger.debug('%s - peer response status: %s message: %s',
54-
method,
55-
endorsementResponse.response.status,
56-
endorsementResponse.response.message);
57-
throw new Error(endorsementResponse.response.message);
35+
const peer = this._peers[peerIndex];
36+
logger.debug('%s - sending to peer %s', method, peer.name);
37+
38+
const results = await query.evaluate([peer]);
39+
const result = results[peer.name];
40+
if (result instanceof Error) {
41+
errorMessages.push(result.toString());
42+
} else {
43+
if (result.isEndorsed) {
44+
logger.debug('%s - return peer response status: %s', method, result.status);
45+
return result.payload;
46+
} else {
47+
logger.debug('%s - throw peer response status: %s message: %s', method, result.status, result.message);
48+
throw Error(result.message);
49+
}
5850
}
59-
60-
logger.debug('%s - peer response status %s', method, endorsementResponse.response.status);
61-
return endorsementResponse.response.payload;
6251
}
6352

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

0 commit comments

Comments
 (0)