Skip to content

Commit 6db0e99

Browse files
committed
Merge pull request #1034 from couchbase/feature/issue_1033_Router_bulk_docs_hang
Fixed #1033 - Requests pending with POST _bulk_docs
2 parents 11712fd + 2839fc7 commit 6db0e99

2 files changed

Lines changed: 62 additions & 46 deletions

File tree

src/main/java/com/couchbase/lite/Database.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,9 @@ public void addChangeListener(ChangeListener listener) {
251251
*/
252252
@InterfaceAudience.Public
253253
public void compact() throws CouchbaseLiteException {
254-
store.compact();
254+
synchronized (store) {
255+
store.compact();
256+
}
255257
garbageCollectAttachments();
256258
}
257259

src/main/java/com/couchbase/lite/router/Router.java

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,9 +1014,7 @@ public Status do_DELETE_Database(Database _db, String _docID, String _attachment
10141014
if (getQuery("rev") != null) {
10151015
return new Status(Status.BAD_REQUEST); // CouchDB checks for this; probably meant to be a document deletion
10161016
}
1017-
synchronized (db) {
1018-
db.delete();
1019-
}
1017+
db.delete();
10201018
return new Status(Status.OK);
10211019
}
10221020

@@ -1176,6 +1174,8 @@ public Status do_POST_Document_bulk_docs(Database _db, String _docID, String _at
11761174

11771175
final Status status = new Status(Status.OK);
11781176
final List<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
1177+
// Transaction provide synchronized feature by SQLiteDatabase.
1178+
// In the transaction block, should not use `synchronized` block
11791179
boolean ret = db.getStore().runInTransaction(new TransactionalTask() {
11801180
@Override
11811181
public boolean run() {
@@ -1190,7 +1190,6 @@ public boolean run() {
11901190
if (noNewEdits) {
11911191
rev = new RevisionInternal(docBody);
11921192
if (rev.getRevID() == null || rev.getDocID() == null || !rev.getDocID().equals(docID)) {
1193-
//status = new Status(Status.BAD_REQUEST);
11941193
status.setCode(Status.BAD_REQUEST);
11951194
} else {
11961195
List<String> history = Database.parseCouchDBRevisionHistory(doc);
@@ -1210,7 +1209,6 @@ public boolean run() {
12101209
result.put("rev", rev.getRevID());
12111210
}
12121211
} else if (allOrNothing) {
1213-
//return status; // all_or_nothing backs out if there's any error
12141212
return false;
12151213
} else if (status.getCode() == Status.FORBIDDEN) {
12161214
result = new HashMap<String, Object>();
@@ -1264,6 +1262,7 @@ public Status do_POST_Document_revs_diff(Database _db, String _docID, String _at
12641262

12651263
// Look them up, removing the existing ones from revs:
12661264
try {
1265+
// query db only, not necessary to be syncrhonized
12671266
db.findMissingRevisions(revs);
12681267
} catch (SQLException e) {
12691268
Log.e(Log.TAG_ROUTER, "Exception", e);
@@ -1301,6 +1300,7 @@ public Status do_POST_Document_revs_diff(Database _db, String _docID, String _at
13011300
public Status do_POST_Document_compact(Database _db, String _docID, String _attachmentName) {
13021301
Status status = new Status(Status.OK);
13031302
try {
1303+
// Make Database.compact() thread-safe
13041304
_db.compact();
13051305
} catch (CouchbaseLiteException e) {
13061306
status = e.getCBLStatus();
@@ -1341,6 +1341,7 @@ public Status do_POST_Document_purge(Database _db, String ignored1, String ignor
13411341
Future future = db.runAsync(new AsyncTask() {
13421342
@Override
13431343
public void run(Database database) {
1344+
// purgeRevisions uses transaction internally.
13441345
Map<String, Object> purgedRevisions = db.purgeRevisions(docsToRevs);
13451346
asyncApiCallResponse.add(purgedRevisions);
13461347
}
@@ -1563,6 +1564,7 @@ public Status do_GET_Document_changes(Database _db, String docID, String _attach
15631564
Log.v(Log.TAG_ROUTER, "Filter params=" + changesFilterParams);
15641565
}
15651566

1567+
// changesSince() is query only. not required synchronized
15661568
RevisionList changes = db.changesSince(since, options, changesFilter, changesFilterParams);
15671569

15681570
if (changes == null) {
@@ -1623,27 +1625,14 @@ public Status do_GET_Document(Database _db, String docID, String _attachmentName
16231625
String revID = getQuery("rev"); // often null
16241626
RevisionInternal rev = null;
16251627
if (isLocalDoc) {
1628+
// query only -> not required synchronized
16261629
rev = db.getLocalDocument(docID, revID);
16271630
} else {
1628-
/*
1629-
rev = db.getDocument(docID, revID, options);
1630-
// Handle ?atts_since query by stubbing out older attachments:
1631-
//?atts_since parameter - value is a (URL-encoded) JSON array of one or more revision IDs.
1632-
// The response will include the content of only those attachments that changed since the given revision(s).
1633-
//(You can ask for this either in the default JSON or as multipart/related, as previously described.)
1634-
List<String> attsSince = (List<String>) getJSONQuery("atts_since");
1635-
if (attsSince != null) {
1636-
String ancestorId = db.findCommonAncestorOf(rev, attsSince);
1637-
if (ancestorId != null) {
1638-
int generation = RevisionInternal.generationFromRevID(ancestorId);
1639-
db.stubOutAttachmentsIn(rev, generation + 1);
1640-
}
1641-
}
1642-
*/
16431631
boolean includeAttachments = options.contains(TDContentOptions.TDIncludeAttachments);
16441632
if (includeAttachments) {
16451633
options.remove(TDContentOptions.TDIncludeAttachments);
16461634
}
1635+
// query only -> not required synchronized
16471636
rev = db.getDocument(docID, revID, true);
16481637
if (rev != null) {
16491638
rev = applyOptionsToRevision(options, rev);
@@ -1658,8 +1647,6 @@ public Status do_GET_Document(Database _db, String docID, String _attachmentName
16581647
if (cacheWithEtag(rev.getRevID()))
16591648
return new Status(Status.NOT_MODIFIED); // set ETag and check conditional GET
16601649

1661-
// TODO
1662-
16631650
connection.setResponseBody(rev.getBody());
16641651
} else {
16651652
List<Map<String, Object>> result = null;
@@ -1670,6 +1657,7 @@ public Status do_GET_Document(Database _db, String docID, String _attachmentName
16701657
for (RevisionInternal rev : allRevs) {
16711658

16721659
try {
1660+
// loadRevisionBody is synchronized with store instance.
16731661
db.loadRevisionBody(rev);
16741662
} catch (CouchbaseLiteException e) {
16751663
if (e.getCBLStatus().getCode() != Status.INTERNAL_SERVER_ERROR) {
@@ -1807,14 +1795,14 @@ public Status do_GET_Attachment(Database _db, String docID, String _attachmentNa
18071795
return new Status(Status.NOT_MODIFIED); // set ETag and check conditional GET
18081796
}
18091797

1810-
String type = null;
18111798
String acceptEncoding = connection.getRequestProperty("accept-encoding");
1799+
// getAttachment is safe. this could be static method??
18121800
AttachmentInternal attachment = db.getAttachment(rev, _attachmentName);
18131801
if (attachment == null) {
18141802
return new Status(Status.NOT_FOUND);
18151803
}
18161804

1817-
type = attachment.getContentType();
1805+
String type = attachment.getContentType();
18181806
if (type != null) {
18191807
connection.getResHeader().add("Content-Type", type);
18201808
}
@@ -1850,7 +1838,7 @@ private RevisionInternal update(Database _db, String docID, Body body, boolean d
18501838
}
18511839

18521840
boolean isLocalDoc = docID != null && docID.startsWith(("_local"));
1853-
String prevRevID = null;
1841+
String prevRevID;
18541842

18551843
if (!deleting) {
18561844
Boolean deletingBoolean = (Boolean) body.getPropertyForKey("_deleted");
@@ -1887,19 +1875,46 @@ private RevisionInternal update(Database _db, String docID, Body body, boolean d
18871875

18881876
RevisionInternal result = null;
18891877
try {
1890-
synchronized (_db) {
1891-
if (isLocalDoc) {
1892-
result = _db.getStore().putLocalRevision(rev, prevRevID, true);
1893-
} else {
1894-
result = _db.putRevision(rev, prevRevID, allowConflict);
1878+
if (isLocalDoc) {
1879+
// NOTE: putLocalRevision() does not use transaction internally with obeyMVCC=true
1880+
1881+
final Database fDb = _db;
1882+
final RevisionInternal _rev = rev;
1883+
final String _prevRevID = prevRevID;
1884+
final List<RevisionInternal> _revs = new ArrayList<RevisionInternal>();
1885+
try {
1886+
fDb.getStore().runInTransaction(new TransactionalTask() {
1887+
@Override
1888+
public boolean run() {
1889+
try {
1890+
RevisionInternal r = fDb.getStore().putLocalRevision(_rev, _prevRevID, true);
1891+
_revs.add(r);
1892+
return true;
1893+
} catch (CouchbaseLiteException e) {
1894+
throw new RuntimeException(e);
1895+
}
1896+
}
1897+
});
1898+
// success
1899+
if (_revs.size() > 0)
1900+
result = _revs.get(0);
1901+
} catch (RuntimeException ex) {
1902+
if (ex.getCause() != null &&
1903+
ex.getCause().getCause() != null &&
1904+
ex.getCause().getCause() instanceof CouchbaseLiteException)
1905+
throw (CouchbaseLiteException) ex.getCause().getCause();
1906+
else
1907+
throw new CouchbaseLiteException(ex, Status.INTERNAL_SERVER_ERROR);
18951908
}
1909+
} else {
1910+
// putRevision() uses transaction internally.
1911+
result = _db.putRevision(rev, prevRevID, allowConflict);
18961912
}
18971913
if (deleting) {
18981914
outStatus.setCode(Status.OK);
18991915
} else {
19001916
outStatus.setCode(Status.CREATED);
19011917
}
1902-
19031918
} catch (CouchbaseLiteException e) {
19041919
if (e.getCBLStatus() != null && e.getCBLStatus().getCode() == Status.CONFLICT) {
19051920
// conflict is not critical error for replicators, not print stack trace
@@ -1984,6 +1999,7 @@ public Status do_PUT_Document(Database _db, String docID, String _attachmentName
19841999
throw new CouchbaseLiteException(Status.BAD_REQUEST);
19852000
}
19862001
List<String> history = Database.parseCouchDBRevisionHistory(body.getProperties());
2002+
// forceInsert uses transaction internally, not necessary to apply synchronized
19872003
db.forceInsert(rev, history, source);
19882004
}
19892005
return status;
@@ -2014,17 +2030,15 @@ private Status updateAttachment(String attachment,
20142030
throw new CouchbaseLiteException(e.getCause(), Status.BAD_ATTACHMENT);
20152031
}
20162032

2017-
RevisionInternal rev;
2018-
synchronized (db) {
2019-
rev = db.updateAttachment(
2020-
attachment,
2021-
body,
2022-
connection.getRequestProperty("content-type"),
2023-
AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone,
2024-
docID,
2025-
revID,
2026-
null);
2027-
}
2033+
// updateAttachment uses transaction internally, not necessary to be synchronized
2034+
RevisionInternal rev = db.updateAttachment(
2035+
attachment,
2036+
body,
2037+
connection.getRequestProperty("content-type"),
2038+
AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone,
2039+
docID,
2040+
revID,
2041+
null);
20282042
Map<String, Object> resultDict = new HashMap<String, Object>();
20292043
resultDict.put("ok", true);
20302044
resultDict.put("id", rev.getDocID());
@@ -2085,6 +2099,7 @@ private View compileView(String viewName, Map<String, Object> viewProps) {
20852099

20862100
private Status queryDesignDoc(String designDoc, String viewName, List<Object> keys) throws CouchbaseLiteException {
20872101
String tdViewName = String.format("%s/%s", designDoc, viewName);
2102+
// getExistingView is not thread-safe, but not access to db. In the database, it should protect instance variable
20882103
View view = db.getExistingView(tdViewName);
20892104
if (view == null || view.getMap() == null) {
20902105
// No TouchDB view is defined, or it hasn't had a map block assigned;
@@ -2119,9 +2134,8 @@ private Status queryDesignDoc(String designDoc, String viewName, List<Object> ke
21192134
options.setKeys(keys);
21202135
}
21212136

2122-
synchronized (db) {
2123-
view.updateIndex();
2124-
}
2137+
// updateIndex() uses transaction internally, not necessary to apply syncrhonized.
2138+
view.updateIndex();
21252139

21262140
long lastSequenceIndexed = view.getLastSequenceIndexed();
21272141

0 commit comments

Comments
 (0)