Skip to content

Commit 46f52de

Browse files
author
Chris Elder
committed
[FAB-3368] Zero length CouchDB attachment delays
CouchDB attachments with zero length causes CouchDB to return successfully but the connection is not available for future connections. This is a bug identified in CouchDB. Zero length attachments are valid. When an empty byte array is saved, CouchDB stores the value as a zero length attachment. The next subsequent client request over the same connection times out reading headers from server. This is the CouchDB JIRA item for tracking purposes. https://issues.apache.org/jira/browse/COUCHDB-3394 The fix in this change checks to see if the attachment in SaveDoc is zero length. If the attachment is zero length, then request close value is set to true. This setting will not allow the http connection to be reused and will prevent connection timeouts. Change-Id: I1a1a33c44a3b96dfd2bffaf1cec5fe9ece084817 Signed-off-by: Chris Elder <[email protected]>
1 parent 0fe5cb2 commit 46f52de

File tree

2 files changed

+159
-19
lines changed

2 files changed

+159
-19
lines changed

core/ledger/util/couchdb/couchdb.go

+31-13
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() (*DBOperationResponse,
268268
maxRetries := dbclient.CouchInstance.conf.MaxRetries
269269

270270
//process the URL with a PUT, creates the database
271-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "", maxRetries)
271+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "", maxRetries, true)
272272
if err != nil {
273273
return nil, err
274274
}
@@ -309,7 +309,7 @@ func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) {
309309
//get the number of retries
310310
maxRetries := dbclient.CouchInstance.conf.MaxRetries
311311

312-
resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "", maxRetries)
312+
resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "", maxRetries, true)
313313
if err != nil {
314314
return nil, couchDBReturn, err
315315
}
@@ -347,7 +347,7 @@ func (couchInstance *CouchInstance) VerifyCouchConfig() (*ConnectionInfo, *DBRet
347347
maxRetriesOnStartup := couchInstance.conf.MaxRetriesOnStartup
348348

349349
resp, couchDBReturn, err := couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil,
350-
couchInstance.conf.Username, couchInstance.conf.Password, maxRetriesOnStartup)
350+
couchInstance.conf.Username, couchInstance.conf.Password, maxRetriesOnStartup, true)
351351

352352
if err != nil {
353353
return nil, couchDBReturn, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port: %s", err.Error())
@@ -396,7 +396,7 @@ func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) {
396396
//get the number of retries
397397
maxRetries := dbclient.CouchInstance.conf.MaxRetries
398398

399-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "", maxRetries)
399+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "", maxRetries, true)
400400
if err != nil {
401401
return nil, err
402402
}
@@ -436,7 +436,7 @@ func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error)
436436
//get the number of retries
437437
maxRetries := dbclient.CouchInstance.conf.MaxRetries
438438

439-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "", maxRetries)
439+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "", maxRetries, true)
440440
if err != nil {
441441
logger.Errorf("Failed to invoke _ensure_full_commit Error: %s\n", err.Error())
442442
return nil, err
@@ -500,6 +500,9 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
500500
//Set up a default boundary for use by multipart if sending attachments
501501
defaultBoundary := ""
502502

503+
//Create a flag for shared connections. This is set to false for zero length attachments
504+
keepConnectionOpen := true
505+
503506
//check to see if attachments is nil, if so, then this is a JSON only
504507
if couchDoc.Attachments == nil {
505508

@@ -519,6 +522,13 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
519522
return "", err3
520523
}
521524

525+
//If there is a zero length attachment, do not keep the connection open
526+
for _, attach := range couchDoc.Attachments {
527+
if attach.Length < 1 {
528+
keepConnectionOpen = false
529+
}
530+
}
531+
522532
//Set the data buffer to the data from the create multi-part data
523533
data = multipartData.Bytes()
524534

@@ -531,7 +541,8 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
531541
maxRetries := dbclient.CouchInstance.conf.MaxRetries
532542

533543
//handle the request for saving the JSON or attachments
534-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary, maxRetries)
544+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, saveURL.String(), data,
545+
rev, defaultBoundary, maxRetries, keepConnectionOpen)
535546
if err != nil {
536547
return "", err
537548
}
@@ -662,7 +673,7 @@ func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) {
662673
//get the number of retries
663674
maxRetries := dbclient.CouchInstance.conf.MaxRetries
664675

665-
resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "", maxRetries)
676+
resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "", maxRetries, true)
666677
if err != nil {
667678
if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
668679
logger.Debug("Document not found (404), returning nil value instead of 404 error")
@@ -815,7 +826,7 @@ func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip
815826
//get the number of retries
816827
maxRetries := dbclient.CouchInstance.conf.MaxRetries
817828

818-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "", maxRetries)
829+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "", maxRetries, true)
819830
if err != nil {
820831
return nil, err
821832
}
@@ -913,7 +924,7 @@ func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error {
913924
//get the number of retries
914925
maxRetries := dbclient.CouchInstance.conf.MaxRetries
915926

916-
resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "", maxRetries)
927+
resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "", maxRetries, true)
917928
if err != nil {
918929
fmt.Printf("couchDBReturn=%v", couchDBReturn)
919930
if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
@@ -950,7 +961,7 @@ func (dbclient *CouchDatabase) QueryDocuments(query string) (*[]QueryResult, err
950961
//get the number of retries
951962
maxRetries := dbclient.CouchInstance.conf.MaxRetries
952963

953-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, queryURL.String(), []byte(query), "", "", maxRetries)
964+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, queryURL.String(), []byte(query), "", "", maxRetries, true)
954965
if err != nil {
955966
return nil, err
956967
}
@@ -1036,7 +1047,7 @@ func (dbclient *CouchDatabase) BatchRetrieveIDRevision(keys []string) ([]*DocMet
10361047
//get the number of retries
10371048
maxRetries := dbclient.CouchInstance.conf.MaxRetries
10381049

1039-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries)
1050+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries, true)
10401051
if err != nil {
10411052
return nil, err
10421053
}
@@ -1131,7 +1142,7 @@ func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*B
11311142
//get the number of retries
11321143
maxRetries := dbclient.CouchInstance.conf.MaxRetries
11331144

1134-
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries)
1145+
resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries, true)
11351146
if err != nil {
11361147
return nil, err
11371148
}
@@ -1166,7 +1177,7 @@ func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*B
11661177
// if it returns an error, it ensures that the response body is closed, else it is the
11671178
// callee's responsibility to close response correctly
11681179
func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data []byte, rev string,
1169-
multipartBoundary string, maxRetries int) (*http.Response, *DBReturn, error) {
1180+
multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) {
11701181

11711182
logger.Debugf("Entering handleRequest() method=%s url=%v", method, connectURL)
11721183

@@ -1192,6 +1203,13 @@ func (couchInstance *CouchInstance) handleRequest(method, connectURL string, dat
11921203
return nil, nil, err
11931204
}
11941205

1206+
//set the request to close on completion if shared connections are not allowSharedConnection
1207+
//Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused.
1208+
//Apache JIRA item for CouchDB https://issues.apache.org/jira/browse/COUCHDB-3394
1209+
if !keepConnectionOpen {
1210+
req.Close = true
1211+
}
1212+
11951213
//add content header for PUT
11961214
if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete {
11971215

core/ledger/util/couchdb/couchdb_test.go

+128-6
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,119 @@ func TestDBCreateDatabaseAndPersist(t *testing.T) {
376376
//Assert that the update was saved and retrieved
377377
testutil.AssertEquals(t, assetResp.Owner, "bob")
378378

379-
//Drop the database
380-
_, errdbdrop := db.DropDatabase()
381-
testutil.AssertNoError(t, errdbdrop, fmt.Sprintf("Error dropping database"))
379+
testBytes2 := []byte(`test attachment 2`)
382380

383-
//Retrieve the info for the new database and make sure the name matches
384-
_, _, errdbinfo := db.GetDatabaseInfo()
385-
testutil.AssertError(t, errdbinfo, fmt.Sprintf("Error should have been thrown for missing database"))
381+
attachment2 := &Attachment{}
382+
attachment2.AttachmentBytes = testBytes2
383+
attachment2.ContentType = "application/octet-stream"
384+
attachment2.Name = "data"
385+
attachments2 := []*Attachment{}
386+
attachments2 = append(attachments2, attachment2)
387+
388+
//Save the test document with an attachment
389+
_, saveerr = db.SaveDoc("2", "", &CouchDoc{JSONValue: nil, Attachments: attachments2})
390+
testutil.AssertNoError(t, saveerr, fmt.Sprintf("Error when trying to save a document"))
391+
392+
//Retrieve the test document with attachments
393+
dbGetResp, _, geterr = db.ReadDoc("2")
394+
testutil.AssertNoError(t, geterr, fmt.Sprintf("Error when trying to retrieve a document"))
395+
396+
//verify the text from the attachment is correct
397+
testattach := dbGetResp.Attachments[0].AttachmentBytes
398+
testutil.AssertEquals(t, testattach, testBytes2)
399+
400+
testBytes3 := []byte{}
401+
402+
attachment3 := &Attachment{}
403+
attachment3.AttachmentBytes = testBytes3
404+
attachment3.ContentType = "application/octet-stream"
405+
attachment3.Name = "data"
406+
attachments3 := []*Attachment{}
407+
attachments3 = append(attachments3, attachment3)
408+
409+
//Save the test document with a zero length attachment
410+
_, saveerr = db.SaveDoc("3", "", &CouchDoc{JSONValue: nil, Attachments: attachments3})
411+
testutil.AssertNoError(t, saveerr, fmt.Sprintf("Error when trying to save a document"))
412+
413+
//Retrieve the test document with attachments
414+
dbGetResp, _, geterr = db.ReadDoc("3")
415+
testutil.AssertNoError(t, geterr, fmt.Sprintf("Error when trying to retrieve a document"))
416+
417+
//verify the text from the attachment is correct, zero bytes
418+
testattach = dbGetResp.Attachments[0].AttachmentBytes
419+
testutil.AssertEquals(t, testattach, testBytes3)
420+
421+
testBytes4a := []byte(`test attachment 4a`)
422+
attachment4a := &Attachment{}
423+
attachment4a.AttachmentBytes = testBytes4a
424+
attachment4a.ContentType = "application/octet-stream"
425+
attachment4a.Name = "data1"
426+
427+
testBytes4b := []byte(`test attachment 4b`)
428+
attachment4b := &Attachment{}
429+
attachment4b.AttachmentBytes = testBytes4b
430+
attachment4b.ContentType = "application/octet-stream"
431+
attachment4b.Name = "data2"
432+
433+
attachments4 := []*Attachment{}
434+
attachments4 = append(attachments4, attachment4a)
435+
attachments4 = append(attachments4, attachment4b)
436+
437+
//Save the updated test document with multiple attachments
438+
_, saveerr = db.SaveDoc("4", "", &CouchDoc{JSONValue: assetJSON, Attachments: attachments4})
439+
testutil.AssertNoError(t, saveerr, fmt.Sprintf("Error when trying to save the updated document"))
440+
441+
//Retrieve the test document with attachments
442+
dbGetResp, _, geterr = db.ReadDoc("4")
443+
testutil.AssertNoError(t, geterr, fmt.Sprintf("Error when trying to retrieve a document"))
444+
445+
for _, attach4 := range dbGetResp.Attachments {
446+
447+
currentName := attach4.Name
448+
if currentName == "data1" {
449+
testutil.AssertEquals(t, attach4.AttachmentBytes, testBytes4a)
450+
}
451+
if currentName == "data2" {
452+
testutil.AssertEquals(t, attach4.AttachmentBytes, testBytes4b)
453+
}
454+
455+
}
456+
457+
testBytes5a := []byte(`test attachment 5a`)
458+
attachment5a := &Attachment{}
459+
attachment5a.AttachmentBytes = testBytes5a
460+
attachment5a.ContentType = "application/octet-stream"
461+
attachment5a.Name = "data1"
462+
463+
testBytes5b := []byte{}
464+
attachment5b := &Attachment{}
465+
attachment5b.AttachmentBytes = testBytes5b
466+
attachment5b.ContentType = "application/octet-stream"
467+
attachment5b.Name = "data2"
468+
469+
attachments5 := []*Attachment{}
470+
attachments5 = append(attachments5, attachment5a)
471+
attachments5 = append(attachments5, attachment5b)
472+
473+
//Save the updated test document with multiple attachments and zero length attachments
474+
_, saveerr = db.SaveDoc("5", "", &CouchDoc{JSONValue: assetJSON, Attachments: attachments5})
475+
testutil.AssertNoError(t, saveerr, fmt.Sprintf("Error when trying to save the updated document"))
476+
477+
//Retrieve the test document with attachments
478+
dbGetResp, _, geterr = db.ReadDoc("5")
479+
testutil.AssertNoError(t, geterr, fmt.Sprintf("Error when trying to retrieve a document"))
480+
481+
for _, attach5 := range dbGetResp.Attachments {
482+
483+
currentName := attach5.Name
484+
if currentName == "data1" {
485+
testutil.AssertEquals(t, attach5.AttachmentBytes, testBytes5a)
486+
}
487+
if currentName == "data2" {
488+
testutil.AssertEquals(t, attach5.AttachmentBytes, testBytes5b)
489+
}
490+
491+
}
386492

387493
//Attempt to save the document with an invalid id
388494
_, saveerr = db.SaveDoc(string([]byte{0xff, 0xfe, 0xfd}), "", &CouchDoc{JSONValue: assetJSON, Attachments: nil})
@@ -392,6 +498,22 @@ func TestDBCreateDatabaseAndPersist(t *testing.T) {
392498
_, _, readerr := db.ReadDoc(string([]byte{0xff, 0xfe, 0xfd}))
393499
testutil.AssertError(t, readerr, fmt.Sprintf("Error should have been thrown when reading a document with an invalid ID"))
394500

501+
//Drop the database
502+
_, errdbdrop := db.DropDatabase()
503+
testutil.AssertNoError(t, errdbdrop, fmt.Sprintf("Error dropping database"))
504+
505+
//Make sure an error is thrown for getting info for a missing database
506+
_, _, errdbinfo := db.GetDatabaseInfo()
507+
testutil.AssertError(t, errdbinfo, fmt.Sprintf("Error should have been thrown for missing database"))
508+
509+
//Attempt to save a document to a deleted database
510+
_, saveerr = db.SaveDoc("6", "", &CouchDoc{JSONValue: assetJSON, Attachments: nil})
511+
testutil.AssertError(t, saveerr, fmt.Sprintf("Error should have been thrown while attempting to save to a deleted database"))
512+
513+
//Attempt to read from a deleted database
514+
_, _, geterr = db.ReadDoc("6")
515+
testutil.AssertNoError(t, geterr, fmt.Sprintf("Error should not have been thrown for a missing database, nil value is returned"))
516+
395517
}
396518
}
397519

0 commit comments

Comments
 (0)