Skip to content

Commit 0183483

Browse files
committed
FAB-1291: Couch support for doing a savepoint.
Added two functions in couchdb_txmgr.go, recordSavepoint is unexported and called during commit go record a savepoint into couch. GetBlockNumFromSavepoint should be used during recovery to get the block number associated with the savepoint Added similar methods to goleveldb txmgr implementation Added unit test function to xxx_txmgmt_test.go Change-Id: Id2860232632d29cbe753b2840a625c34e541f2d9 Signed-off-by: Balaji Viswanathan <[email protected]>
1 parent ede30a4 commit 0183483

File tree

5 files changed

+189
-3
lines changed

5 files changed

+189
-3
lines changed

core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_txmgmt_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,33 @@ func TestDatabaseAutoCreate(t *testing.T) {
106106
}
107107

108108
}
109+
110+
//TestSavepoint tests the recordSavepoint and GetBlockNumfromSavepoint methods for recording and reading a savepoint document
111+
func TestSavepoint(t *testing.T) {
112+
113+
//Only run the tests if CouchDB is explitily enabled in the code,
114+
//otherwise CouchDB may not be installed and all the tests would fail
115+
//TODO replace this with external config property rather than config within the code
116+
if ledgerconfig.IsCouchDBEnabled() == true {
117+
118+
env := newTestEnv(t)
119+
120+
txMgr := NewCouchDBTxMgr(env.conf,
121+
env.couchDBAddress, //couchDB Address
122+
env.couchDatabaseName, //couchDB db name
123+
env.couchUsername, //enter couchDB id
124+
env.couchPassword) //enter couchDB pw
125+
126+
// record savepoint
127+
txMgr.blockNum = 5
128+
err := txMgr.recordSavepoint()
129+
testutil.AssertNoError(t, err, fmt.Sprintf("Error when saving recordpoint data"))
130+
131+
// read the savepoint
132+
blockNum, err := txMgr.GetBlockNumFromSavepoint()
133+
testutil.AssertNoError(t, err, fmt.Sprintf("Error when saving recordpoint data"))
134+
testutil.AssertEquals(t, txMgr.blockNum, blockNum)
135+
136+
txMgr.Shutdown()
137+
}
138+
}

core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_txmgr.go

+88
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package couchdbtxmgmt
1818

1919
import (
20+
"encoding/json"
21+
"errors"
2022
"sync"
2123

2224
"github.com/golang/protobuf/proto"
@@ -48,6 +50,15 @@ type updateSet struct {
4850
m map[string]*versionedValue
4951
}
5052

53+
// Savepoint docid (key) for couchdb
54+
const savepointDocID = "statedb_savepoint"
55+
56+
// Savepoint data for couchdb
57+
type couchSavepointData struct {
58+
BlockNum uint64 `json:"BlockNum"`
59+
UpdateSeq string `json:"UpdateSeq"`
60+
}
61+
5162
func newUpdateSet() *updateSet {
5263
return &updateSet{make(map[string]*versionedValue)}
5364
}
@@ -72,6 +83,7 @@ type CouchDBTxMgr struct {
7283
updateSet *updateSet
7384
commitRWLock sync.RWMutex
7485
couchDB *couchdb.CouchDBConnectionDef // COUCHDB new properties for CouchDB
86+
blockNum uint64 // block number corresponding to updateSet
7587
}
7688

7789
// CouchConnection provides connection info for CouchDB
@@ -119,6 +131,7 @@ func (txmgr *CouchDBTxMgr) ValidateAndPrepare(block *common.Block) (*common.Bloc
119131
invalidTxs := []*pb.InvalidTransaction{}
120132
var valid bool
121133
txmgr.updateSet = newUpdateSet()
134+
txmgr.blockNum = block.Header.Number
122135
logger.Debugf("Validating a block with [%d] transactions", len(block.Data.Data))
123136
for txIndex, envBytes := range block.Data.Data {
124137
// extract actions from the envelope message
@@ -159,6 +172,7 @@ func (txmgr *CouchDBTxMgr) ValidateAndPrepare(block *common.Block) (*common.Bloc
159172
Transaction: &pb.Transaction{ /* FIXME */ }, Cause: pb.InvalidTransaction_RWConflictDuringCommit})
160173
}
161174
}
175+
162176
logger.Debugf("===COUCHDB=== Exiting CouchDBTxMgr.ValidateAndPrepare()")
163177
return block, invalidTxs, nil
164178
}
@@ -258,13 +272,87 @@ func (txmgr *CouchDBTxMgr) Commit() error {
258272

259273
}
260274

275+
// Record a savepoint
276+
err := txmgr.recordSavepoint()
277+
if err != nil {
278+
logger.Errorf("===COUCHDB=== Error during recordSavepoint: %s\n", err.Error())
279+
return err
280+
}
281+
261282
logger.Debugf("===COUCHDB=== Exiting CouchDBTxMgr.Commit()")
262283
return nil
263284
}
264285

286+
// recordSavepoint Record a savepoint in statedb.
287+
// Couch parallelizes writes in cluster or sharded setup and ordering is not guaranteed.
288+
// Hence we need to fence the savepoint with sync. So ensure_full_commit is called before AND after writing savepoint document
289+
// TODO: Optimization - merge 2nd ensure_full_commit with savepoint by using X-Couch-Full-Commit header
290+
func (txmgr *CouchDBTxMgr) recordSavepoint() error {
291+
var err error
292+
var savepointDoc couchSavepointData
293+
// ensure full commit to flush all changes until now to disk
294+
dbResponse, err := txmgr.couchDB.EnsureFullCommit()
295+
if err != nil || dbResponse.Ok != true {
296+
logger.Errorf("====COUCHDB==== Failed to perform full commit\n")
297+
return errors.New("Failed to perform full commit")
298+
}
299+
300+
// construct savepoint document
301+
// UpdateSeq would be useful if we want to get all db changes since a logical savepoint
302+
dbInfo, _, err := txmgr.couchDB.GetDatabaseInfo()
303+
if err != nil {
304+
logger.Errorf("====COUCHDB==== Failed to get DB info %s\n", err.Error())
305+
return err
306+
}
307+
savepointDoc.BlockNum = txmgr.blockNum
308+
savepointDoc.UpdateSeq = dbInfo.UpdateSeq
309+
310+
savepointDocJSON, err := json.Marshal(savepointDoc)
311+
if err != nil {
312+
logger.Errorf("====COUCHDB==== Failed to create savepoint data %s\n", err.Error())
313+
return err
314+
}
315+
316+
// SaveDoc using couchdb client and use JSON format
317+
_, err = txmgr.couchDB.SaveDoc(savepointDocID, "", savepointDocJSON, nil)
318+
if err != nil {
319+
logger.Errorf("====CouchDB==== Failed to save the savepoint to DB %s\n", err.Error())
320+
}
321+
322+
// ensure full commit to flush savepoint to disk
323+
dbResponse, err = txmgr.couchDB.EnsureFullCommit()
324+
if err != nil || dbResponse.Ok != true {
325+
logger.Errorf("====COUCHDB==== Failed to perform full commit\n")
326+
return errors.New("Failed to perform full commit")
327+
}
328+
return nil
329+
}
330+
331+
// GetBlockNumFromSavepoint Reads the savepoint from database and returns the corresponding block number.
332+
// If no savepoint is found, it returns 0
333+
func (txmgr *CouchDBTxMgr) GetBlockNumFromSavepoint() (uint64, error) {
334+
var err error
335+
savepointJSON, _, err := txmgr.couchDB.ReadDoc(savepointDocID)
336+
if err != nil {
337+
// TODO: differentiate between 404 and some other error code
338+
logger.Errorf("====COUCHDB==== Failed to read savepoint data %s\n", err.Error())
339+
return 0, err
340+
}
341+
342+
savepointDoc := &couchSavepointData{}
343+
err = json.Unmarshal(savepointJSON, &savepointDoc)
344+
if err != nil {
345+
logger.Errorf("====COUCHDB==== Failed to read savepoint data %s\n", err.Error())
346+
return 0, err
347+
}
348+
349+
return savepointDoc.BlockNum, nil
350+
}
351+
265352
// Rollback implements method in interface `txmgmt.TxMgr`
266353
func (txmgr *CouchDBTxMgr) Rollback() {
267354
txmgr.updateSet = nil
355+
txmgr.blockNum = 0
268356
}
269357

270358
func (txmgr *CouchDBTxMgr) getCommitedVersion(ns string, key string) (*version.Height, error) {

core/ledger/kvledger/txmgmt/lockbasedtxmgmt/lockbased_txmgmt_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package lockbasedtxmgmt
1818

1919
import (
2020
"fmt"
21+
"math"
2122
"testing"
2223

2324
"github.com/hyperledger/fabric/core/ledger"
@@ -67,9 +68,13 @@ func TestTxSimulatorWithExistingData(t *testing.T) {
6768
testutil.AssertNoError(t, err, fmt.Sprintf("Error in validateTx(): %s", err))
6869
testutil.AssertSame(t, isValid, true)
6970
txMgr.addWriteSetToBatch(txRWSet, version.NewHeight(1, 1))
71+
txMgr.blockNum = math.MaxUint64
7072
err = txMgr.Commit()
7173
testutil.AssertNoError(t, err, fmt.Sprintf("Error while calling commit(): %s", err))
7274

75+
blockNum, err := txMgr.GetBlockNumFromSavepoint()
76+
testutil.AssertEquals(t, blockNum, txMgr.blockNum)
77+
7378
// simulate tx2 that make changes to existing data
7479
s2, _ := txMgr.NewTxSimulator()
7580
value, _ := s2.GetState("ns1", "key1")
@@ -258,7 +263,7 @@ func testIterator(t *testing.T, numKeys int, startKeyNum int, endKeyNum int) {
258263
keyNum := begin + count
259264
k := kv.(*ledger.KV).Key
260265
v := kv.(*ledger.KV).Value
261-
t.Logf("Retrieved k=%s, v=%s", k, v)
266+
t.Logf("Retrieved k=%s, v=%s at count=%d start=%s end=%s", k, v, count, startKey, endKey)
262267
testutil.AssertEquals(t, k, createTestKey(keyNum))
263268
testutil.AssertEquals(t, v, createTestValue(keyNum))
264269
count++

core/ledger/kvledger/txmgmt/lockbasedtxmgmt/lockbased_txmgr.go

+31
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package lockbasedtxmgmt
1818

1919
import (
2020
"bytes"
21+
"encoding/binary"
22+
"reflect"
2123
"sync"
2224

2325
"github.com/hyperledger/fabric/core/ledger"
@@ -50,6 +52,9 @@ type updateSet struct {
5052
m map[string]*versionedValue
5153
}
5254

55+
// savepoint key
56+
const savepointKey = "savepoint"
57+
5358
func newUpdateSet() *updateSet {
5459
return &updateSet{make(map[string]*versionedValue)}
5560
}
@@ -73,6 +78,7 @@ type LockBasedTxMgr struct {
7378
db *db.DB
7479
updateSet *updateSet
7580
commitRWLock sync.RWMutex
81+
blockNum uint64
7682
}
7783

7884
// NewLockBasedTxMgr constructs a `LockBasedTxMgr`
@@ -102,6 +108,7 @@ func (txmgr *LockBasedTxMgr) ValidateAndPrepare(block *common.Block) (*common.Bl
102108
invalidTxs := []*pb.InvalidTransaction{}
103109
var valid bool
104110
txmgr.updateSet = newUpdateSet()
111+
txmgr.blockNum = block.Header.Number
105112
logger.Debugf("Validating a block with [%d] transactions", len(block.Data.Data))
106113
for txIndex, envBytes := range block.Data.Data {
107114
// extract actions from the envelope message
@@ -203,6 +210,15 @@ func (txmgr *LockBasedTxMgr) Commit() error {
203210
batch.Put([]byte(k), encodeValue(v.value, v.version))
204211
}
205212
}
213+
214+
// record the savepoint along with batch
215+
if txmgr.blockNum != 0 {
216+
savepointValue := make([]byte, reflect.TypeOf(txmgr.blockNum).Size())
217+
binary.LittleEndian.PutUint64(savepointValue, txmgr.blockNum)
218+
// Need a composite key for iterator to function correctly - use separator itself as special/hidden namespace
219+
batch.Put(constructCompositeKey(string(compositeKeySep), savepointKey), savepointValue)
220+
}
221+
206222
txmgr.commitRWLock.Lock()
207223
defer txmgr.commitRWLock.Unlock()
208224
defer func() { txmgr.updateSet = nil }()
@@ -212,9 +228,24 @@ func (txmgr *LockBasedTxMgr) Commit() error {
212228
return nil
213229
}
214230

231+
// GetBlockNumFromSavepoint returns the block num recorded in savepoint,
232+
// returns 0 if NO savepoint is found
233+
func (txmgr *LockBasedTxMgr) GetBlockNumFromSavepoint() (uint64, error) {
234+
var blockNum uint64
235+
savepointValue, err := txmgr.db.Get(constructCompositeKey(string(compositeKeySep), savepointKey))
236+
if err != nil {
237+
return 0, err
238+
}
239+
240+
// savepointValue is not encoded with version
241+
blockNum = binary.LittleEndian.Uint64(savepointValue)
242+
return blockNum, nil
243+
}
244+
215245
// Rollback implements method in interface `txmgmt.TxMgr`
216246
func (txmgr *LockBasedTxMgr) Rollback() {
217247
txmgr.updateSet = nil
248+
txmgr.blockNum = 0
218249
}
219250

220251
func (txmgr *LockBasedTxMgr) getCommitedVersion(ns string, key string) (*version.Height, error) {

core/ledger/util/couchdb/couchdb.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,38 @@ func (dbclient *CouchDBConnectionDef) DropDatabase() (*DBOperationResponse, erro
230230

231231
}
232232

233+
// EnsureFullCommit calls _ensure_full_commit for explicit fsync
234+
func (dbclient *CouchDBConnectionDef) EnsureFullCommit() (*DBOperationResponse, error) {
235+
236+
logger.Debugf("===COUCHDB=== Entering EnsureFullCommit()")
237+
238+
url := fmt.Sprintf("%s/%s/_ensure_full_commit", dbclient.URL, dbclient.Database)
239+
240+
resp, _, err := dbclient.handleRequest(http.MethodPost, url, nil, "", "")
241+
if err != nil {
242+
logger.Errorf("====COUCHDB==== Failed to invoke _ensure_full_commit Error: %s\n", err.Error())
243+
return nil, err
244+
}
245+
defer resp.Body.Close()
246+
247+
dbResponse := &DBOperationResponse{}
248+
json.NewDecoder(resp.Body).Decode(&dbResponse)
249+
250+
if dbResponse.Ok == true {
251+
logger.Debugf("===COUCHDB=== _ensure_full_commit database %s ", dbclient.Database)
252+
}
253+
254+
logger.Debugf("===COUCHDB=== Exiting EnsureFullCommit()")
255+
256+
if dbResponse.Ok == true {
257+
258+
return dbResponse, nil
259+
260+
}
261+
262+
return dbResponse, fmt.Errorf("Error syncing database")
263+
}
264+
233265
//SaveDoc method provides a function to save a document, id and byte array
234266
func (dbclient *CouchDBConnectionDef) SaveDoc(id string, rev string, bytesDoc []byte, attachments []Attachment) (string, error) {
235267

@@ -497,7 +529,7 @@ func (dbclient *CouchDBConnectionDef) handleRequest(method, url string, data io.
497529
}
498530

499531
//add content header for PUT
500-
if method == http.MethodPut {
532+
if method == http.MethodPut || method == http.MethodPost {
501533

502534
//If the multipartBoundary is not set, then this is a JSON and content-type should be set
503535
//to application/json. Else, this is contains an attachment and needs to be multipart
@@ -514,7 +546,7 @@ func (dbclient *CouchDBConnectionDef) handleRequest(method, url string, data io.
514546
}
515547

516548
//add content header for PUT
517-
if method == http.MethodPut {
549+
if method == http.MethodPut || method == http.MethodPost {
518550
req.Header.Set("Accept", "application/json")
519551
}
520552

0 commit comments

Comments
 (0)