@@ -465,6 +465,7 @@ func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error)
465
465
func (dbclient * CouchDatabase ) SaveDoc (id string , rev string , couchDoc * CouchDoc ) (string , error ) {
466
466
467
467
logger .Debugf ("Entering SaveDoc() id=[%s]" , id )
468
+
468
469
if ! utf8 .ValidString (id ) {
469
470
return "" , fmt .Errorf ("doc id [%x] not a valid utf8 string" , id )
470
471
}
@@ -479,19 +480,6 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
479
480
// id can contain a '/', so encode separately
480
481
saveURL = & url.URL {Opaque : saveURL .String () + "/" + encodePathElement (id )}
481
482
482
- if rev == "" {
483
-
484
- //See if the document already exists, we need the rev for save
485
- _ , revdoc , err2 := dbclient .ReadDoc (id )
486
- if err2 != nil {
487
- //set the revision to indicate that the document was not found
488
- rev = ""
489
- } else {
490
- //set the revision to the rev returned from the document read
491
- rev = revdoc
492
- }
493
- }
494
-
495
483
logger .Debugf (" rev=%s" , rev )
496
484
497
485
//Set up a buffer for the data to be pushed to couchdb
@@ -540,9 +528,10 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
540
528
//get the number of retries
541
529
maxRetries := dbclient .CouchInstance .conf .MaxRetries
542
530
543
- //handle the request for saving the JSON or attachments
544
- resp , _ , err := dbclient .CouchInstance .handleRequest (http .MethodPut , saveURL .String (), data ,
545
- rev , defaultBoundary , maxRetries , keepConnectionOpen )
531
+ //handle the request for saving document with a retry if there is a revision conflict
532
+ resp , _ , err := dbclient .handleRequestWithRevisionRetry (id , http .MethodPut ,
533
+ * saveURL , data , rev , defaultBoundary , maxRetries , keepConnectionOpen )
534
+
546
535
if err != nil {
547
536
return "" , err
548
537
}
@@ -560,6 +549,20 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
560
549
561
550
}
562
551
552
+ //getDocumentRevision will return the revision if the document exists, otherwise it will return ""
553
+ func (dbclient * CouchDatabase ) getDocumentRevision (id string ) string {
554
+
555
+ var rev = ""
556
+
557
+ //See if the document already exists, we need the rev for saves and deletes
558
+ _ , revdoc , err := dbclient .ReadDoc (id )
559
+ if err == nil {
560
+ //set the revision to the rev returned from the document read
561
+ rev = revdoc
562
+ }
563
+ return rev
564
+ }
565
+
563
566
func createAttachmentPart (couchDoc * CouchDoc , defaultBoundary string ) (bytes.Buffer , string , error ) {
564
567
565
568
//Create a buffer for writing the result
@@ -646,7 +649,8 @@ func getRevisionHeader(resp *http.Response) (string, error) {
646
649
647
650
}
648
651
649
- //ReadDoc method provides function to retrieve a document from the database by id
652
+ //ReadDoc method provides function to retrieve a document and its revision
653
+ //from the database by id
650
654
func (dbclient * CouchDatabase ) ReadDoc (id string ) (* CouchDoc , string , error ) {
651
655
var couchDoc CouchDoc
652
656
attachments := []* Attachment {}
@@ -906,27 +910,14 @@ func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error {
906
910
// id can contain a '/', so encode separately
907
911
deleteURL = & url.URL {Opaque : deleteURL .String () + "/" + encodePathElement (id )}
908
912
909
- if rev == "" {
910
-
911
- //See if the document already exists, we need the rev for delete
912
- _ , revdoc , err2 := dbclient .ReadDoc (id )
913
- if err2 != nil {
914
- //set the revision to indicate that the document was not found
915
- rev = ""
916
- } else {
917
- //set the revision to the rev returned from the document read
918
- rev = revdoc
919
- }
920
- }
921
-
922
- logger .Debugf (" rev=%s" , rev )
923
-
924
913
//get the number of retries
925
914
maxRetries := dbclient .CouchInstance .conf .MaxRetries
926
915
927
- resp , couchDBReturn , err := dbclient .CouchInstance .handleRequest (http .MethodDelete , deleteURL .String (), nil , rev , "" , maxRetries , true )
916
+ //handle the request for saving document with a retry if there is a revision conflict
917
+ resp , couchDBReturn , err := dbclient .handleRequestWithRevisionRetry (id , http .MethodDelete ,
918
+ * deleteURL , nil , "" , "" , maxRetries , true )
919
+
928
920
if err != nil {
929
- fmt .Printf ("couchDBReturn=%v" , couchDBReturn )
930
921
if couchDBReturn != nil && couchDBReturn .StatusCode == 404 {
931
922
logger .Debug ("Document not found (404), returning nil value instead of 404 error" )
932
923
// non-existent document should return nil value instead of a 404 error
@@ -1173,9 +1164,52 @@ func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*B
1173
1164
1174
1165
}
1175
1166
1167
+ //handleRequestWithRevisionRetry method is a generic http request handler with
1168
+ //a retry for document revision conflict errors,
1169
+ //which may be detected during saves or deletes that timed out from client http perspective,
1170
+ //but which eventually succeeded in couchdb
1171
+ func (dbclient * CouchDatabase ) handleRequestWithRevisionRetry (id , method string , connectURL url.URL , data []byte , rev string ,
1172
+ multipartBoundary string , maxRetries int , keepConnectionOpen bool ) (* http.Response , * DBReturn , error ) {
1173
+
1174
+ //Initialize a flag for the revsion conflict
1175
+ revisionConflictDetected := false
1176
+ var resp * http.Response
1177
+ var couchDBReturn * DBReturn
1178
+ var errResp error
1179
+
1180
+ //attempt the http request for the max number of retries
1181
+ //In this case, the retry is to catch problems where a client timeout may miss a
1182
+ //successful CouchDB update and cause a document revision conflict on a retry in handleRequest
1183
+ for attempts := 0 ; attempts < maxRetries ; attempts ++ {
1184
+
1185
+ //if the revision was not passed in, or if a revision conflict is detected on prior attempt,
1186
+ //query CouchDB for the document revision
1187
+ if rev == "" || revisionConflictDetected {
1188
+ rev = dbclient .getDocumentRevision (id )
1189
+ }
1190
+
1191
+ //handle the request for saving/deleting the couchdb data
1192
+ resp , couchDBReturn , errResp = dbclient .CouchInstance .handleRequest (method , connectURL .String (),
1193
+ data , rev , multipartBoundary , maxRetries , keepConnectionOpen )
1194
+
1195
+ //If there was a 409 conflict error during the save/delete, log it and retry it.
1196
+ //Otherwise, break out of the retry loop
1197
+ if couchDBReturn != nil && couchDBReturn .StatusCode == 409 {
1198
+ logger .Warningf ("CouchDB document revision conflict detected, retrying. Attempt:%v" , attempts + 1 )
1199
+ revisionConflictDetected = true
1200
+ } else {
1201
+ break
1202
+ }
1203
+ }
1204
+
1205
+ // return the handleRequest results
1206
+ return resp , couchDBReturn , errResp
1207
+ }
1208
+
1176
1209
//handleRequest method is a generic http request handler.
1177
- // if it returns an error, it ensures that the response body is closed, else it is the
1178
- // callee's responsibility to close response correctly
1210
+ // If it returns an error, it ensures that the response body is closed, else it is the
1211
+ // callee's responsibility to close response correctly.
1212
+ // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned
1179
1213
func (couchInstance * CouchInstance ) handleRequest (method , connectURL string , data []byte , rev string ,
1180
1214
multipartBoundary string , maxRetries int , keepConnectionOpen bool ) (* http.Response , * DBReturn , error ) {
1181
1215
@@ -1251,20 +1285,20 @@ func (couchInstance *CouchInstance) handleRequest(method, connectURL string, dat
1251
1285
//Execute http request
1252
1286
resp , errResp = couchInstance .client .Do (req )
1253
1287
1254
- //if an error is not detected then drop out of the retry
1288
+ //if there is no golang http error and no CouchDB 500 error, then drop out of the retry
1255
1289
if errResp == nil && resp != nil && resp .StatusCode < 500 {
1256
1290
break
1257
1291
}
1258
1292
1259
- //if this is an error, record the retry error, else this is a 500 error
1293
+ //if this is an unexpected golang http error, log the error and retry
1260
1294
if errResp != nil {
1261
1295
1262
1296
//Log the error with the retry count and continue
1263
1297
logger .Warningf ("Retrying couchdb request in %s. Attempt:%v Error:%v" ,
1264
1298
waitDuration .String (), attempts + 1 , errResp .Error ())
1265
1299
1300
+ //otherwise this is an unexpected 500 error from CouchDB. Log the error and retry.
1266
1301
} else {
1267
-
1268
1302
//Read the response body and close it for next attempt
1269
1303
jsonError , err := ioutil .ReadAll (resp .Body )
1270
1304
closeResponseBody (resp )
@@ -1288,18 +1322,19 @@ func (couchInstance *CouchInstance) handleRequest(method, connectURL string, dat
1288
1322
//backoff, doubling the retry time for next attempt
1289
1323
waitDuration *= 2
1290
1324
1291
- }
1325
+ } // end retry loop
1292
1326
1293
- //if the error present, return the error
1327
+ //if a golang http error is still present after retries are exhausted , return the error
1294
1328
if errResp != nil {
1295
1329
return nil , nil , errResp
1296
1330
}
1297
1331
1298
1332
//set the return code for the couchDB request
1299
1333
couchDBReturn .StatusCode = resp .StatusCode
1300
1334
1301
- //check to see if the status code is 400 or higher
1302
- //response codes 4XX and 500 will be treated as errors
1335
+ //check to see if the status code from couchdb is 400 or higher
1336
+ //response codes 4XX and 500 will be treated as errors -
1337
+ //golang error will be created from the couchDBReturn contents and both will be returned
1303
1338
if resp .StatusCode >= 400 {
1304
1339
// close the response before returning error
1305
1340
defer closeResponseBody (resp )
@@ -1325,7 +1360,7 @@ func (couchInstance *CouchInstance) handleRequest(method, connectURL string, dat
1325
1360
1326
1361
logger .Debugf ("Exiting handleRequest()" )
1327
1362
1328
- //If no errors, then return the results
1363
+ //If no errors, then return the http response and the couchdb return object
1329
1364
return resp , couchDBReturn , nil
1330
1365
}
1331
1366
0 commit comments