Commit 909b517a authored by Srinivasan Muralidharan's avatar Srinivasan Muralidharan
Browse files

FAB-466 integrate ledgernext with chaincode framework



The ledgernext and kvledger packages provide APIs to simulate
transactions by collecting read-write sets from invokes of
chaincodes. This change set integrates this for the Endorser
flows.  The main purpose of this code is to enable read write
sets to be propagated so end to end flow ending in a commit to
the ledger can be tested.

The chaincode unit tests continue to use the old ledger. This
allows us to (1) incrementally integrate ledger and (2) show
that the two packages can coexist from a build and runtime
point of view.

It is worth noting that the file kv_ledgers.go hosts a temporary
container for ledgers. This simple approach is expected to be
revised when (sub)ledgers are implemented.

Change-Id: I6e0bf4b9795b83d2ae869244b212c02ef9b5214a
Signed-off-by: default avatarSrinivasan Muralidharan <muralisr@us.ibm.com>
parent ea9f840c
......@@ -48,6 +48,9 @@ const (
chaincodeStartupTimeoutDefault int = 5000
chaincodeInstallPathDefault string = "/opt/gopath/bin/"
peerAddressDefault string = "0.0.0.0:7051"
//TXSimulatorKey is used to attach ledger simulation context
TXSimulatorKey string = "txsimulatorkey"
)
// chains is a map between different blockchains and their ChaincodeSupport.
......@@ -257,7 +260,7 @@ func (chaincodeSupport *ChaincodeSupport) sendInitOrReady(context context.Contex
var notfy chan *pb.ChaincodeMessage
var err error
if notfy, err = chrte.handler.initOrReady(txid, initArgs, tx, depTx); err != nil {
if notfy, err = chrte.handler.initOrReady(context, txid, initArgs, tx, depTx); err != nil {
return fmt.Errorf("Error sending %s: %s", pb.ChaincodeMessage_INIT, err)
}
if notfy != nil {
......@@ -648,7 +651,7 @@ func (chaincodeSupport *ChaincodeSupport) Execute(ctxt context.Context, chaincod
var notfy chan *pb.ChaincodeMessage
var err error
if notfy, err = chrte.handler.sendExecuteMessage(msg, tx); err != nil {
if notfy, err = chrte.handler.sendExecuteMessage(ctxt, msg, tx); err != nil {
return nil, fmt.Errorf("Error sending %s: %s", msg.Type.String(), err)
}
var ccresp *pb.ChaincodeMessage
......
......@@ -21,7 +21,6 @@ import (
"fmt"
"github.com/hyperledger/fabric/core/ledger"
"github.com/hyperledger/fabric/core/util"
pb "github.com/hyperledger/fabric/protos"
)
......@@ -40,24 +39,15 @@ func createTx(typ pb.Transaction_Type, ccname string, args [][]byte) (*pb.Transa
}
// ExecuteChaincode executes a given chaincode given chaincode name and arguments
func ExecuteChaincode(typ pb.Transaction_Type, chainname string, ccname string, args [][]byte) ([]byte, error) {
func ExecuteChaincode(ctxt context.Context, typ pb.Transaction_Type, chainname string, ccname string, args [][]byte) ([]byte, error) {
var tx *pb.Transaction
var err error
var b []byte
var lgr *ledger.Ledger
tx, err = createTx(typ, ccname, args)
lgr, err = ledger.GetLedger()
if err != nil {
return nil, fmt.Errorf("Failed to get handle to ledger: %s ", err)
}
//TODO - new ledger access will change this call to take a context
lgr.BeginTxBatch("1")
b, _, err = Execute(context.Background(), GetChain(ChainName(chainname)), tx)
b, _, err = Execute(ctxt, GetChain(ChainName(chainname)), tx)
if err != nil {
return nil, fmt.Errorf("Error deploying chaincode: %s", err)
}
//TODO - new ledger access will change this call to take a context
lgr.CommitTxBatch("1", []*pb.Transaction{tx}, nil, nil)
return b, err
}
......@@ -25,6 +25,7 @@ import (
"golang.org/x/net/context"
"github.com/hyperledger/fabric/core/ledger"
ledgernext "github.com/hyperledger/fabric/core/ledgernext"
"github.com/hyperledger/fabric/events/producer"
pb "github.com/hyperledger/fabric/protos"
)
......@@ -33,10 +34,16 @@ import (
func Execute(ctxt context.Context, chain *ChaincodeSupport, t *pb.Transaction) ([]byte, *pb.ChaincodeEvent, error) {
var err error
// get a handle to ledger to mark the begin/finish of a tx
ledger, ledgerErr := ledger.GetLedger()
if ledgerErr != nil {
return nil, nil, fmt.Errorf("Failed to get handle to ledger (%s)", ledgerErr)
//are we in V1 mode doing simulation ?
txsim, _ := ctxt.Value(TXSimulatorKey).(ledgernext.TxSimulator)
var lgr *ledger.Ledger
if txsim == nil {
// get a handle to ledger to mark the begin/finish of a tx
lgr, err = ledger.GetLedger()
if err != nil {
return nil, nil, fmt.Errorf("Failed to get handle to ledger (%s)", err)
}
}
if secHelper := chain.getSecHelper(); nil != secHelper {
......@@ -55,13 +62,13 @@ func Execute(ctxt context.Context, chain *ChaincodeSupport, t *pb.Transaction) (
}
//launch and wait for ready
markTxBegin(ledger, t)
markTxBegin(lgr, t)
_, _, err = chain.Launch(ctxt, t)
if err != nil {
markTxFinish(ledger, t, false)
markTxFinish(lgr, t, false)
return nil, nil, fmt.Errorf("%s", err)
}
markTxFinish(ledger, t, true)
markTxFinish(lgr, t, true)
} else if t.Type == pb.Transaction_CHAINCODE_INVOKE || t.Type == pb.Transaction_CHAINCODE_QUERY {
//will launch if necessary (and wait for ready)
cID, cMsg, err := chain.Launch(ctxt, t)
......@@ -97,15 +104,15 @@ func Execute(ctxt context.Context, chain *ChaincodeSupport, t *pb.Transaction) (
}
}
markTxBegin(ledger, t)
markTxBegin(lgr, t)
resp, err := chain.Execute(ctxt, chaincode, ccMsg, timeout, t)
if err != nil {
// Rollback transaction
markTxFinish(ledger, t, false)
markTxFinish(lgr, t, false)
return nil, nil, fmt.Errorf("Failed to execute transaction or query(%s)", err)
} else if resp == nil {
// Rollback transaction
markTxFinish(ledger, t, false)
markTxFinish(lgr, t, false)
return nil, nil, fmt.Errorf("Failed to receive a response for (%s)", t.Txid)
} else {
if resp.ChaincodeEvent != nil {
......@@ -115,14 +122,14 @@ func Execute(ctxt context.Context, chain *ChaincodeSupport, t *pb.Transaction) (
if resp.Type == pb.ChaincodeMessage_COMPLETED || resp.Type == pb.ChaincodeMessage_QUERY_COMPLETED {
// Success
markTxFinish(ledger, t, true)
markTxFinish(lgr, t, true)
return resp.Payload, resp.ChaincodeEvent, nil
} else if resp.Type == pb.ChaincodeMessage_ERROR || resp.Type == pb.ChaincodeMessage_QUERY_ERROR {
// Rollback transaction
markTxFinish(ledger, t, false)
markTxFinish(lgr, t, false)
return nil, resp.ChaincodeEvent, fmt.Errorf("Transaction or query returned with failure: %s", string(resp.Payload))
}
markTxFinish(ledger, t, false)
markTxFinish(lgr, t, false)
return resp.Payload, nil, fmt.Errorf("receive a response for (%s) but in invalid state(%d)", t.Txid, resp.Type)
}
......@@ -201,17 +208,23 @@ func getTimeout(cID *pb.ChaincodeID) (time.Duration, error) {
}
func markTxBegin(ledger *ledger.Ledger, t *pb.Transaction) {
if t.Type == pb.Transaction_CHAINCODE_QUERY {
return
//ledger would be nil if are in simulation mode
if ledger != nil {
if t.Type == pb.Transaction_CHAINCODE_QUERY {
return
}
ledger.TxBegin(t.Txid)
}
ledger.TxBegin(t.Txid)
}
func markTxFinish(ledger *ledger.Ledger, t *pb.Transaction, successful bool) {
if t.Type == pb.Transaction_CHAINCODE_QUERY {
return
//ledger would be nil if are in simulation mode
if ledger != nil {
if t.Type == pb.Transaction_CHAINCODE_QUERY {
return
}
ledger.TxFinished(t.Txid, successful)
}
ledger.TxFinished(t.Txid, successful)
}
func sendTxRejectedEvent(tx *pb.Transaction, errorMsg string) {
......
......@@ -26,6 +26,7 @@ import (
ccintf "github.com/hyperledger/fabric/core/container/ccintf"
"github.com/hyperledger/fabric/core/crypto"
"github.com/hyperledger/fabric/core/ledger/statemgmt"
ledgernext "github.com/hyperledger/fabric/core/ledgernext"
"github.com/hyperledger/fabric/core/util"
pb "github.com/hyperledger/fabric/protos"
"github.com/looplab/fsm"
......@@ -61,6 +62,8 @@ type transactionContext struct {
// tracks open iterators used for range queries
rangeQueryIteratorMap map[string]statemgmt.RangeScanIterator
txsimulator ledgernext.TxSimulator
}
type nextStateInfo struct {
......@@ -113,7 +116,7 @@ func (handler *Handler) serialSend(msg *pb.ChaincodeMessage) error {
return nil
}
func (handler *Handler) createTxContext(txid string, tx *pb.Transaction) (*transactionContext, error) {
func (handler *Handler) createTxContext(ctxt context.Context, txid string, tx *pb.Transaction) (*transactionContext, error) {
if handler.txCtxs == nil {
return nil, fmt.Errorf("cannot create notifier for txid:%s", txid)
}
......@@ -125,6 +128,10 @@ func (handler *Handler) createTxContext(txid string, tx *pb.Transaction) (*trans
txctx := &transactionContext{transactionSecContext: tx, responseNotifier: make(chan *pb.ChaincodeMessage, 1),
rangeQueryIteratorMap: make(map[string]statemgmt.RangeScanIterator)}
handler.txCtxs[txid] = txctx
if txsim, ok := ctxt.Value(TXSimulatorKey).(ledgernext.TxSimulator); ok {
txctx.txsimulator = txsim
}
return txctx, nil
}
......@@ -624,7 +631,16 @@ func (handler *Handler) handleGetState(msg *pb.ChaincodeMessage) {
chaincodeID := handler.ChaincodeID.Name
readCommittedState := !handler.getIsTransaction(msg.Txid)
res, err := ledgerObj.GetState(chaincodeID, key, readCommittedState)
var res []byte
var err error
txContext := handler.getTxContext(msg.Txid)
if txContext.txsimulator != nil {
res, err = txContext.txsimulator.GetState(chaincodeID, key)
} else {
res, err = ledgerObj.GetState(chaincodeID, key, readCommittedState)
}
if err != nil {
// Send error msg back to chaincode. GetState will not trigger event
payload := []byte(err.Error())
......@@ -1038,12 +1054,23 @@ func (handler *Handler) enterBusyState(e *fsm.Event, state string) {
// Encrypt the data if the confidential is enabled
if pVal, err = handler.encrypt(msg.Txid, putStateInfo.Value); err == nil {
// Invoke ledger to put state
err = ledgerObj.SetState(chaincodeID, putStateInfo.Key, pVal)
txContext := handler.getTxContext(msg.Txid)
if txContext.txsimulator != nil {
err = txContext.txsimulator.SetState(chaincodeID, putStateInfo.Key, pVal)
} else {
err = ledgerObj.SetState(chaincodeID, putStateInfo.Key, pVal)
}
}
} else if msg.Type.String() == pb.ChaincodeMessage_DEL_STATE.String() {
// Invoke ledger to delete state
key := string(msg.Payload)
err = ledgerObj.DeleteState(chaincodeID, key)
txContext := handler.getTxContext(msg.Txid)
if txContext.txsimulator != nil {
err = txContext.txsimulator.DeleteState(chaincodeID, key)
} else {
err = ledgerObj.DeleteState(chaincodeID, key)
}
} else if msg.Type.String() == pb.ChaincodeMessage_INVOKE_CHAINCODE.String() {
//check and prohibit C-call-C for CONFIDENTIAL txs
if triggerNextStateMsg = handler.canCallChaincode(msg.Txid); triggerNextStateMsg != nil {
......@@ -1256,11 +1283,11 @@ func (handler *Handler) setChaincodeSecurityContext(tx *pb.Transaction, msg *pb.
//if initArgs is set (should be for "deploy" only) move to Init
//else move to ready
func (handler *Handler) initOrReady(txid string, initArgs [][]byte, tx *pb.Transaction, depTx *pb.Transaction) (chan *pb.ChaincodeMessage, error) {
func (handler *Handler) initOrReady(ctxt context.Context, txid string, initArgs [][]byte, tx *pb.Transaction, depTx *pb.Transaction) (chan *pb.ChaincodeMessage, error) {
var ccMsg *pb.ChaincodeMessage
var send bool
txctx, funcErr := handler.createTxContext(txid, tx)
txctx, funcErr := handler.createTxContext(ctxt, txid, tx)
if funcErr != nil {
return nil, funcErr
}
......@@ -1451,8 +1478,8 @@ func filterError(errFromFSMEvent error) error {
return nil
}
func (handler *Handler) sendExecuteMessage(msg *pb.ChaincodeMessage, tx *pb.Transaction) (chan *pb.ChaincodeMessage, error) {
txctx, err := handler.createTxContext(msg.Txid, tx)
func (handler *Handler) sendExecuteMessage(ctxt context.Context, msg *pb.ChaincodeMessage, tx *pb.Transaction) (chan *pb.ChaincodeMessage, error) {
txctx, err := handler.createTxContext(ctxt, msg.Txid, tx)
if err != nil {
return nil, err
}
......
......@@ -17,11 +17,15 @@ limitations under the License.
package endorser
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/op/go-logging"
"golang.org/x/net/context"
"github.com/hyperledger/fabric/core/chaincode"
ledger "github.com/hyperledger/fabric/core/ledgernext"
"github.com/hyperledger/fabric/core/ledgernext/kvledger"
"github.com/hyperledger/fabric/core/peer"
"github.com/hyperledger/fabric/core/util"
pb "github.com/hyperledger/fabric/protos"
......@@ -64,16 +68,102 @@ func (*Endorser) checkEsccAndVscc(prop *pb.Proposal) error {
return nil
}
func (*Endorser) getTxSimulator(ledgername string) (ledger.TxSimulator, error) {
lgr := kvledger.GetLedger(ledgername)
return lgr.NewTxSimulator()
}
//getChaincodeDeploymentSpec returns a ChaincodeDeploymentSpec given args
func (e *Endorser) getChaincodeDeploymentSpec(code []byte) (*pb.ChaincodeDeploymentSpec, error) {
cds := &pb.ChaincodeDeploymentSpec{}
err := proto.Unmarshal(code, cds)
if err != nil {
return nil, err
}
return cds, nil
}
//deploy the chaincode after call to the system chaincode is successful
func (e *Endorser) deploy(ctxt context.Context, chainname string, cds *pb.ChaincodeDeploymentSpec) error {
//TODO : this needs to be converted to another data structure to be handled
// by the chaincode framework (which currently handles "Transaction")
t, err := pb.NewChaincodeDeployTransaction(cds, cds.ChaincodeSpec.ChaincodeID.Name)
if err != nil {
return err
}
//TODO - create chaincode support for chainname, for now use DefaultChain
chaincodeSupport := chaincode.GetChain(chaincode.ChainName(chainname))
_, err = chaincodeSupport.Deploy(ctxt, t)
if err != nil {
return fmt.Errorf("Failed to deploy chaincode spec(%s)", err)
}
//launch and wait for ready
_, _, err = chaincodeSupport.Launch(ctxt, t)
if err != nil {
return fmt.Errorf("%s", err)
}
//stop now that we are done
chaincodeSupport.Stop(ctxt, cds)
return nil
}
//call specified chaincode (system or user)
func (*Endorser) callChaincode(cis *pb.ChaincodeInvocationSpec) ([]byte, error) {
func (e *Endorser) callChaincode(ctxt context.Context, cis *pb.ChaincodeInvocationSpec) ([]byte, []byte, error) {
var txsim ledger.TxSimulator
var err error
var b []byte
//TODO - get chainname from cis when defined
chainName := string(chaincode.DefaultChain)
b, err := chaincode.ExecuteChaincode(pb.Transaction_CHAINCODE_INVOKE, chainName, cis.ChaincodeSpec.ChaincodeID.Name, cis.ChaincodeSpec.CtorMsg.Args)
return b, err
if txsim, err = e.getTxSimulator(chainName); err != nil {
return nil, nil, err
}
ctxt = context.WithValue(ctxt, chaincode.TXSimulatorKey, txsim)
b, err = chaincode.ExecuteChaincode(ctxt, pb.Transaction_CHAINCODE_INVOKE, chainName, cis.ChaincodeSpec.ChaincodeID.Name, cis.ChaincodeSpec.CtorMsg.Args)
if err != nil {
return nil, nil, err
}
//----- BEGIN - SECTION THAT MAY NEED TO BE DONE IN LCCC ------
//if this a call to deploy a chaincode, We need a mechanism
//to pass TxSimulator into LCCC. Till that is worked out this
//special code does the actual deploy, upgrade here so as to collect
//all state under one TxSimulator
//
//NOTE that if there's an error all simulation, including the chaincode
//table changes in lccc will be thrown away
if cis.ChaincodeSpec.ChaincodeID.Name == "lccc" && len(cis.ChaincodeSpec.CtorMsg.Args) == 3 && string(cis.ChaincodeSpec.CtorMsg.Args[0]) == "deploy" {
var cds *pb.ChaincodeDeploymentSpec
cds, err = e.getChaincodeDeploymentSpec(cis.ChaincodeSpec.CtorMsg.Args[2])
if err != nil {
return nil, nil, err
}
err = e.deploy(ctxt, chainName, cds)
if err != nil {
return nil, nil, err
}
}
//----- END -------
var txSimulationResults []byte
if txSimulationResults, err = txsim.GetTxSimulationResults(); err != nil {
return nil, nil, err
}
return txSimulationResults, b, err
}
//simulate the proposal by calling the chaincode
func (e *Endorser) simulateProposal(prop *pb.Proposal) ([]byte, []byte, error) {
func (e *Endorser) simulateProposal(ctx context.Context, prop *pb.Proposal) ([]byte, []byte, error) {
//we do expect the payload to be a ChaincodeInvocationSpec
//if we are supporting other payloads in future, this be glaringly point
//as something that should change
......@@ -91,17 +181,15 @@ func (e *Endorser) simulateProposal(prop *pb.Proposal) ([]byte, []byte, error) {
return nil, nil, err
}
//---3. execute the proposal
//---3. execute the proposal and get simulation results
var simResult []byte
var resp []byte
resp, err = e.callChaincode(cis)
simResult, resp, err = e.callChaincode(ctx, cis)
if err != nil {
return nil, nil, err
}
//---4. get simulation results
simulationResult := []byte("TODO: sim results")
return resp, simulationResult, nil
return resp, simResult, nil
}
//endorse the proposal by calling the ESCC
......@@ -127,7 +215,7 @@ func (e *Endorser) ProcessProposal(ctx context.Context, prop *pb.Proposal) (*pb.
//1 -- simulate
//TODO what do we do with response ? We need it for Invoke responses for sure
//Which field in PayloadResponse will carry return value ?
_, simulationResult, err := e.simulateProposal(prop)
_, simulationResult, err := e.simulateProposal(ctx, prop)
if err != nil {
return &pb.ProposalResponse{Response: &pb.Response2{Status: 500, Message: err.Error()}}, err
}
......
......@@ -30,6 +30,7 @@ import (
"github.com/hyperledger/fabric/core/container"
"github.com/hyperledger/fabric/core/crypto"
"github.com/hyperledger/fabric/core/db"
"github.com/hyperledger/fabric/core/ledgernext/kvledger"
"github.com/hyperledger/fabric/core/system_chaincode"
"github.com/hyperledger/fabric/core/system_chaincode/api"
u "github.com/hyperledger/fabric/core/util"
......@@ -57,12 +58,18 @@ func initPeer() (net.Listener, error) {
}
grpcServer := grpc.NewServer(opts...)
viper.Set("peer.fileSystemPath", filepath.Join(os.TempDir(), "hyperledger", "production"))
peerAddress := viper.GetString("peer.address")
lis, err := net.Listen("tcp", peerAddress)
if err != nil {
return nil, fmt.Errorf("Error starting peer listener %s", err)
}
//initialize ledger
lpath := viper.GetString("peer.fileSystemPath")
kvledger.Initialize(lpath)
getPeerEndpoint := func() (*pb.PeerEndpoint, error) {
return &pb.PeerEndpoint{ID: &pb.PeerID{Name: "testpeer"}, Address: peerAddress}, nil
}
......@@ -237,9 +244,9 @@ func TestRedeploy(t *testing.T) {
return
}
//second time should fail
//second time should not fail as we are just simulating
_, err = deploy(endorserServer, spec, nil)
if err == nil {
if err != nil {
t.Fail()
t.Logf("error in endorserServer.ProcessProposal %s", err)
chaincode.GetChain(chaincode.DefaultChain).Stop(context.Background(), &pb.ChaincodeDeploymentSpec{ChaincodeSpec: spec})
......
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kvledger
import (
"fmt"
"strings"
"sync"
)
//--!!!!IMPORTANT!!!--!!!IMPORTANT!!!---!!!IMPORTANT!!!-----------
//
//Three things about this code
//
// 1. This code is TEMPORARY - it can go away when (sub) ledgers
// management is implemented
// 2. This code is PLACEHOLDER - in lieu of proper (sub) ledgers
// management, we need a mechanism to hold onto ledger handles
// This merely does that
// 3. Do NOT add any function related to subledgers till it has
// been agreed upon
//-----------------------------------------------------------------
//-------- initialization --------
var lManager *ledgerManager
//Initialize kv ledgers
func Initialize(lpath string) {
if lpath == "" {
panic("DB path not specified")
}
if !strings.HasSuffix(lpath, "/") {
lpath = lpath + "/"
}
//TODO - when we don't need 0.5 DB, we can remove this
lpath = lpath + "ledgernext/"
lManager = &ledgerManager{ledgerPath: lpath, ledgers: make(map[string]*KVLedger)}
}
//--------- errors -----------
//LedgerNotInitializedErr exists error
type LedgerNotInitializedErr string
func (l LedgerNotInitializedErr) Error() string {
return fmt.Sprintf("ledger manager not inialized")
}
//LedgerExistsErr exists error
type LedgerExistsErr string
func (l LedgerExistsErr) Error() string {
return fmt.Sprintf("ledger exists %s", string(l))
}
//LedgerCreateErr exists error
type LedgerCreateErr string
func (l LedgerCreateErr) Error() string {
return fmt.Sprintf("ledger creation failed %s", string(l))
}
//--------- ledger manager ---------
// just a container for ledgers
type ledgerManager struct {
sync.RWMutex
ledgerPath string
ledgers map[string]*KVLedger
}
//create a ledger if one does not exist
func (lMgr *ledgerManager) create(name string) (*KVLedger, error) {
lMgr.Lock()
defer lMgr.Unlock()
lPath := lMgr.ledgerPath + name
lgr, _ := lMgr.ledgers[lPath]
if lgr != nil {
return lgr, LedgerExistsErr(name)
}
var err error
ledgerConf := NewConf(lPath, 0)
if lgr, err = NewKVLedger(ledgerConf); err != nil || lgr == nil {
return nil, LedgerCreateErr(name)
}
lMgr.ledgers[lPath] = lgr
return lgr, nil
}
//GetLedger returns a kvledger, creating one if necessary
//the call will panic if it cannot create a ledger
func GetLedger(name string) *KVLedger {
lgr, err := lManager.create(name)
if lgr == nil {
panic("Cannot get ledger " + name + "(" + err.Error() + ")")
}
return lgr
}
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/