Skip to content

Commit

Permalink
FAB-1108: Sample query handler and tutorial
Browse files Browse the repository at this point in the history
Change-Id: I52844ab283a9e8d5bd60ccb7cde2dd4e6822cfd2
Signed-off-by: Mark S. Lewis <[email protected]>
  • Loading branch information
bestbeforetoday committed Jan 18, 2019
1 parent 07956d6 commit 309722a
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 7 deletions.
74 changes: 74 additions & 0 deletions docs/tutorials/query-peers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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.

### 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
in `DefaultQueryHandlerStrategies`. 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.

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

const connectOptions = {
queryHandlerOptions: {
strategy: DefaultQueryHandlerStrategies.MSPID_SCOPE_SINGLE
}
}

const gateway = new Gateway();
await gateway.connect(connectionProfile, connectOptions);
```

### Plug-in query handlers

If behavior is required that is not provided by the default query handling
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`

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

```javascript
function createQueryHandler(network) {
/* Your implementation here */
return new MyQueryHandler(peers);
}

const connectOptions = {
queryHandlerOptions: {
strategy: createQueryHandler
}
}

const gateway = new Gateway();
await gateway.connect(connectionProfile, connectOptions);
```

The *query handler* object returned must implement the following functions.

```javascript
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.
* @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).
5 changes: 4 additions & 1 deletion docs/tutorials/tutorials.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
"sign-transaction-offline": {
"title": "Working with an offline private key"
},
"transaction-commit-events" : {
"transaction-commit-events": {
"title": "fabric-network: How to wait for transactions to be committed to the ledger"
},
"query-peers": {
"title": "fabric-network: How to select peers for evaluating transactions (queries)"
},
"grpc-settings": {
"title": "fabric-client: How to set gRPC settings"
}
Expand Down
5 changes: 2 additions & 3 deletions fabric-network/lib/impl/query/roundrobinqueryhandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,21 @@ class RoundRobinQueryHandler {
}

async evaluate(query) {
const errors = [];
const errorMessages = [];

for (let i = 0; i < this._peers.length; i++) {
const index = (this._startPeerIndex + i) % this._peers.length;
const peer = this._peers[index];
const results = await query.evaluate([peer]);
const result = results[peer.getName()];
if (result instanceof Error) {
errors.push(result);
errorMessages.push(result.message);
} else {
this._startPeerIndex = (index + 1) % this._peers.length;
return result;
}
}

const errorMessages = errors.map((error) => error.message);
const message = util.format('No peers available to query. Errors: %j', errorMessages);
const error = new FabricError(message);
logger.error('evaluate:', error);
Expand Down
5 changes: 2 additions & 3 deletions fabric-network/lib/impl/query/singlequeryhandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,21 @@ class SingleQueryHandler {
}

async evaluate(query) {
const errors = [];
const errorMessages = [];

for (let i = 0; i < this._peers.length; i++) {
const index = (this._startPeerIndex + i) % this._peers.length;
const peer = this._peers[index];
const results = await query.evaluate([peer]);
const result = results[peer.getName()];
if (result instanceof Error) {
errors.push(result);
errorMessages.push(result);
} else {
this._startPeerIndex = index;
return result;
}
}

const errorMessages = errors.map((error) => error.message);
const message = util.format('No peers available to query. Errors: %j', errorMessages);
const error = new FabricError(message);
logger.error('evaluate:', error);
Expand Down
60 changes: 60 additions & 0 deletions test/typescript/integration/network-e2e/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
X509WalletMixin,
} from 'fabric-network';

import sampleQueryStrategy = require('./sample-query-handler');

import e2eUtils = require('../../../integration/e2e/e2eUtils.js');
import testUtils = require('../../../unit/util.js');

Expand Down Expand Up @@ -238,6 +240,64 @@ test('\n\n***** Network End-to-end flow: evaluate transaction with MSPID_SCOPE_S
t.end();
});

test('\n\n***** Network End-to-end flow: evaluate transaction with sample query handler *****\n\n', async (t: any) => {
const tmpdir = path.join(os.tmpdir(), 'integration-network-test988');
const gateway = new Gateway();

try {
const wallet = await createWallet(t, tmpdir);
const ccp: Buffer = fs.readFileSync(fixtures + '/network.json');
const ccpObject = JSON.parse(ccp.toString());

await gateway.connect(ccpObject, {
clientTlsIdentity: tlsLabel,
discovery: {
enabled: false,
},
identity: identityLabel,
queryHandlerOptions: {
strategy: sampleQueryStrategy,
},
wallet,
});
t.pass('Connected to the gateway');

const channel = await gateway.getNetwork(channelName);
t.pass('Initialized the channel, ' + channelName);

const contract = await channel.getContract(chaincodeId);
t.pass('Got the contract, about to evaluate (query) transaction');

// try a standard query
let response = await contract.evaluateTransaction('query', 'a');

if (!isNaN(parseInt(response.toString(), 10))) {
t.pass('Successfully got back a value');
} else {
t.fail('Unexpected response from transaction chaincode: ' + response);
}

// check we deal with an error returned.
try {
response = await contract.evaluateTransaction('throwError', 'a', 'b', '100');
t.fail('Transaction "throwError" should have thrown an error. Got response: ' + response.toString());
} catch (expectedErr) {
if (expectedErr.message.includes('throwError: an error occurred')) {
t.pass('Successfully handled invocation errors');
} else {
t.fail('Unexpected exception: ' + expectedErr.message);
}
}
} catch (err) {
t.fail('Failed to invoke transaction chaincode on channel. ' + err.stack ? err.stack : err);
} finally {
await deleteWallet(tmpdir);
gateway.disconnect();
}

t.end();
});

test('\n\n***** Network End-to-end flow: evaluate transaction with transient data *****\n\n', async (t: any) => {
const tmpdir = path.join(os.tmpdir(), 'integration-network-test988');
const gateway = new Gateway();
Expand Down
71 changes: 71 additions & 0 deletions test/typescript/integration/network-e2e/sample-query-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright 2018 IBM All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

// Sample query handler that will use all queryable peers within the network to evaluate transactions, with preference
// given to peers within the same organization.

import {
Network,
Query,
QueryHandler,
QueryHandlerFactory,
} from 'fabric-network';

import {
ChannelPeer,
} from 'fabric-client';

import util = require('util');

/**
* Query handler implementation that simply tries all the peers it is given in order until it gets a result.
*/
class SampleQueryHandler implements QueryHandler {
private peers: ChannelPeer[];

constructor(peers: ChannelPeer[]) {
this.peers = peers;
}

public async evaluate(query: Query): Promise<Buffer> {
const errorMessages: string[] = [];

for (const peer of this.peers) {
const results = await query.evaluate([peer]);
const result = results[peer.getName()];

if (result instanceof Error) {
errorMessages.push(result.message);
} else {
return result;
}
}

const message = util.format('Evaluate failed with the following errors: %j', errorMessages);
throw new Error(message);
}
}

function filterQueryablePeers(peers: ChannelPeer[]): ChannelPeer[] {
return peers.filter((peer) => peer.isInRole('chaincodeQuery'));
}

/**
* Factory function for creating sample query handlers.
* @param {Network} network The network where transactions are to be evaluated.
* @returns {QueryHandler} A query handler implementation.
*/
const createQueryHandler: QueryHandlerFactory = (network: Network) => {
const channel = network.getChannel();
const orgPeers = filterQueryablePeers(channel.getPeersForOrg());
const networkPeers = filterQueryablePeers(channel.getChannelPeers())
.filter((peer) => !orgPeers.includes(peer)); // Exclude peers already in the orgPeer array

const allPeers = orgPeers.concat(networkPeers); // Peers in our organization first
return new SampleQueryHandler(allPeers);
};

export = createQueryHandler; // Plain JavaScript compatible node module export

0 comments on commit 309722a

Please sign in to comment.