Java tutorial
/* Copyright 2013 Red Hat, Inc. and/or its affiliates. This file is part of lightblue. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.redhat.lightblue.mongo.crud; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.HashMap; import java.util.Date; import com.mongodb.ReadPreference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.DBCursor; import com.mongodb.WriteConcern; import com.mongodb.BasicDBObject; import com.mongodb.BulkWriteOperation; import com.mongodb.BulkWriteResult; import com.mongodb.BulkWriteException; import com.mongodb.BulkWriteError; import org.bson.types.ObjectId; import com.redhat.lightblue.util.Error; /** * This class implements a safe update protocol. Mongo updates are * read-update in memory-write algorithms, so during an update, * another thread can come and modify the doc. We would like to * prevent our updates overwriting the other thread's updates. * * To implement this, we add a 'docver' hidden field to all the * documents. The idea is to read the documents, perform the update in * memory, and replace the db document with the memory copy using _id * and only id dover is still the same. When done using bulk apis, * this scheme has a problem, becase there are two windows in which * another thread can modify the document, and we secure only one of * those windows. For example: * <pre> * thread1: thread2: * read: {_id:1,docver:a}, * {_id:2,docver:a} * * read: {_id:1,docver:a} * write: {_id:1,docver:a} update docver:b * * write: {_id:1,docver:a} update docver:c, * {_id:2,docver:a} update docver:c * -> only 1 doc written, no doc with {_id:1,docver:a} * * read: {_id:2,docver:c} * write: {_id:2,docver:c} update docver:d * * read: {_id:1,docver:b} * {_id:2,docver:d} * -> two modified documents, none with docver:c, but _id:1 has only thread2 modifications * applied, but _id:2 has thread1 modifications applied, and then thread2. * only _id:1 must be reported as update error * * </pre> * * If the document is modified by another thread, that update will not * appear as an update in the result. But, we do not know which * documents were failed to update this way, only the number of * documents actually updated. So, we read the database again to see * which docvers have been changed. Another thread may come an modify * the documents before this step but after the modifications are * applied, meaning we may find out more documents have been modified * than initially known. Here the question is which documents have our * modifications, and which don't. We would like to filter out the * documents that don't have our modifications, and mark them as * concurrent update failures, because any update performed after our * update is valid. * * This is important for incremental updates. Example: * <pre> * concurrent update failure: * thread1: thread2: * read doc, ver=1, field=1 * read doc, ver=1, field=1 * set field=2 * submit update with ver=2 * set field=2 * submit update with ver=2 * perform db update * update fails, ver!=1 * </pre> * * In the above scenario, the increment operation for thread1 should * fail, because thread2 modified the document after thread1 read * it. This is returned as one missing document in the write result of * thread 1. At this point, thread1 doesn't know which document * failed, so it reads the db to find out: * <pre> * thread1: thread2: * modify another doc, ver=2, set ver=3 * read all docs with ver=2 * * </pre> * * At this point, thread1 sees two documents missing, instead of * one. The first document doesn't have thread1 updates, the second * document has thread1 updates, but thread2 applied its own updates * before thread1 can find out which document was modified. Returning * the second document as failed might result in the client retrying * the update, applying the changes twice, * * To prevent this scenario, we will keep the document versions in an * array, so we'll know which updates are applied to the document. To * prevent unbounded array growth, we will truncate the array using * the docver timestamp. * */ public abstract class MongoSafeUpdateProtocol implements BatchUpdate { private static final Logger LOGGER = LoggerFactory.getLogger(MongoSafeUpdateProtocol.class); private static final class BatchDoc { Object id; public BatchDoc(DBObject doc) { id = doc.get("_id"); } } private final DBCollection collection; private final WriteConcern writeConcern; private ObjectId docVer; private BulkWriteOperation bwo; private final DBObject query; private List<BatchDoc> batch; private final ConcurrentModificationDetectionCfg cfg; /** * @param collection The DB collection * @param writeConcern Write concern to use. Can be null to use the default * @param cfg Concurrent modification detection options */ public MongoSafeUpdateProtocol(DBCollection collection, WriteConcern writeConcern, ConcurrentModificationDetectionCfg cfg) { this(collection, writeConcern, null, cfg); } /** * @param collection The DB collection * @param writeConcern Write concern to use. Can be null to use the default * @param query The update query, if this is an update call * @param cfg Concurrent modification detection options */ public MongoSafeUpdateProtocol(DBCollection collection, WriteConcern writeConcern, DBObject query, ConcurrentModificationDetectionCfg cfg) { this.collection = collection; this.writeConcern = writeConcern; this.cfg = cfg == null ? new ConcurrentModificationDetectionCfg(null) : cfg; this.query = query; reset(); } public ConcurrentModificationDetectionCfg getCfg() { return cfg; } /** * Override this method to define how to deal with retries * * This is called when a concurrent modification is detected, and * the failed document is reloaded. The implementation should * reapply the changes to the new version of the document. */ protected abstract DBObject reapplyChanges(int docIndex, DBObject doc); /** * Adds a document to the current batch. The document should * contain the original docver as read from the db */ @Override public void addDoc(DBObject doc) { DBObject q = writeReplaceQuery(doc); DocVerUtil.cleanupOldDocVer(doc, docVer); DocVerUtil.setDocVer(doc, docVer); LOGGER.debug("replaceQuery={}", q); bwo.find(q).replaceOne(doc); batch.add(new BatchDoc(doc)); } /** * Returns the number of queued requests */ @Override public int getSize() { return batch.size(); } /** * Commits the current batch, and prepares for the next * iteration. If any update operations fail, this call will detect * errors and associate them with the documents using the document * index. */ @Override public CommitInfo commit() { CommitInfo ci = new CommitInfo(); if (!batch.isEmpty()) { if (!BatchUpdate.batchUpdate(bwo, writeConcern, batch.size(), ci.errors, LOGGER)) findConcurrentModifications(ci.errors); } retryConcurrentUpdateErrorsIfNeeded(ci); reset(); return ci; } public void retryConcurrentUpdateErrorsIfNeeded(CommitInfo ci) { int nRetries = cfg.getFailureRetryCount(); while (nRetries-- > 0) { // Get the documents with concurrent modification errors List<Integer> failedDocs = getFailedDocIndexes(ci); if (!failedDocs.isEmpty()) { failedDocs = retryFailedDocs(failedDocs, ci); } else { break; } if (nRetries == 0 && !failedDocs.isEmpty()) { // retried failureRetryCount and still not able to update, the error will reach the client LOGGER.error("Retried docs.id in {} {} times, all times failed", failedDocs, cfg.getFailureRetryCount()); } } } private List<Integer> retryFailedDocs(List<Integer> failedDocs, CommitInfo ci) { List<Integer> newFailedDocs = new ArrayList<>(failedDocs.size()); for (Integer index : failedDocs) { BatchDoc doc = batch.get(index); // Read the doc DBObject findQuery = new BasicDBObject("_id", doc.id); if (cfg.isReevaluateQueryForRetry()) { if (query != null) { List<DBObject> list = new ArrayList<>(2); list.add(findQuery); list.add(query); findQuery = new BasicDBObject("$and", list); } } DBObject updatedDoc = collection.findOne(findQuery); if (updatedDoc != null) { // if updatedDoc is null, doc is lost. Error remains DBObject newDoc = reapplyChanges(index, updatedDoc); // Make sure reapplyChanges does not insert references // of objects from the old document into the // updatedDoc. That updates both copies of // documents. Use deepCopy if (newDoc != null) { DBObject replaceQuery = writeReplaceQuery(updatedDoc); // Update the doc ver to our doc ver. This doc is here // because its docVer is not set to our docver, so // this is ok DocVerUtil.setDocVer(newDoc, docVer); // Using bulkwrite here with one doc to use the // findAndReplace API, which is lacking in // DBCollection BulkWriteOperation nestedBwo = collection.initializeUnorderedBulkOperation(); nestedBwo.find(replaceQuery).replaceOne(newDoc); try { if (nestedBwo.execute().getMatchedCount() == 1) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Successfully retried to update a doc: replaceQuery={} newDoc={}", replaceQuery, newDoc); } // Successful update ci.errors.remove(index); } } catch (Exception e) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Failed retrying to update a doc: replaceQuery={} newDoc={} error={}", replaceQuery, newDoc, e.toString()); } newFailedDocs.add(index); } } else { // reapllyChanges removed the doc from the resultset ci.errors.remove(index); } } else { // Doc no longer exists if (LOGGER.isDebugEnabled()) { LOGGER.debug("Removing doc id={} from retry queue, because it does not exist or match anymore", index); } ci.errors.remove(index); ci.lostDocs.add(index); } } return newFailedDocs; } /** * Returns a list of indexes into the current batch containing the docs that failed because of concurrent update errors */ private List<Integer> getFailedDocIndexes(CommitInfo ci) { List<Integer> ret = new ArrayList<>(ci.errors.size()); for (Map.Entry<Integer, Error> entry : ci.errors.entrySet()) { if (entry.getValue().getErrorCode().equals(MongoCrudConstants.ERR_CONCURRENT_UPDATE)) { ret.add(entry.getKey()); } } return ret; } /** * This executes a query to find out documents with concurrent modification errors * * Returns true if there are concurrent modification errors */ protected boolean findConcurrentModifications(Map<Integer, Error> results) { boolean ret = false; if (!cfg.isDetect()) return ret; List<Object> updatedIds = new ArrayList<>(batch.size()); // Collect all ids without errors int index = 0; for (BatchDoc doc : batch) { if (!results.containsKey(index)) updatedIds.add(doc.id); index++; } LOGGER.debug("checking for concurrent modifications:{}", updatedIds); if (!updatedIds.isEmpty()) { Set<Object> failedIds = BatchUpdate.getFailedUpdates(collection, docVer, updatedIds); if (!failedIds.isEmpty()) { index = 0; for (BatchDoc doc : batch) { if (!results.containsKey(index)) { // No other errors for this id if (failedIds.contains(doc.id)) { // concurrency errors for this id results.put(index, Error.get("update", MongoCrudConstants.ERR_CONCURRENT_UPDATE, doc.id.toString())); ret = true; } } index++; } } } return ret; } /** * Writes a replace query, which is: * <pre> * {_id:<docId>, "@mongoHidden.docver.0":ObjectId(<originalDocver>)} * </pre> */ private DBObject writeReplaceQuery(DBObject doc) { DBObject hidden = DocVerUtil.getHidden(doc, false); ObjectId top = null; if (hidden != null) { List<ObjectId> list = (List<ObjectId>) hidden.get(DocVerUtil.DOCVER); if (list != null && !list.isEmpty()) top = list.get(0); } BasicDBObject query = new BasicDBObject("_id", doc.get("_id")); if (cfg.isDetect()) query.append(DOCVER_FLD0, top); return query; } private void reset() { docVer = new ObjectId(); bwo = collection.initializeUnorderedBulkOperation(); batch = new ArrayList<>(128); } }