/*
* Copyright 2015-2016 OpenCB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.opencb.opencga.storage.mongodb.utils;
import org.apache.commons.lang3.time.StopWatch;
import org.bson.conversions.Bson;
import org.opencb.commons.datastore.mongodb.MongoDBCollection;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeoutException;
import static com.mongodb.client.model.Filters.*;
import static com.mongodb.client.model.Updates.combine;
import static com.mongodb.client.model.Updates.set;
/**
* Concurrent lock using a MongoDB document.
*
* Created on 13/06/16
*
* see http://stackoverflow.com/questions/31064750/mongodb-implement-a-read-write-lock-mutex
*
* @author Jacobo Coll <jacobo167@gmail.com>
*/
public class MongoLock {
private static final String LOCK_FIELD = "lock";
private static final String WRITE_FIELD = "write";
private final String lockWriteField;
private final MongoDBCollection collection;
public MongoLock(MongoDBCollection collection) {
this(collection, LOCK_FIELD);
}
public MongoLock(MongoDBCollection collection, String lockField) {
this.collection = collection;
lockWriteField = lockField + "." + WRITE_FIELD;
}
/**
* Apply for the lock.
*
* @param id _id the document to lock
* @param lockDuration Duration un milliseconds of the token. After this time the token is expired.
* @param timeout Max time in milliseconds to wait for the lock
*
* @return Lock token
*
* @throws InterruptedException if any thread has interrupted the current thread.
* @throws TimeoutException if the operations takes more than the timeout value.
*/
public long lock(Object id, long lockDuration, long timeout)
throws InterruptedException, TimeoutException {
StopWatch watch = new StopWatch();
watch.start();
long modifiedCount;
Date date;
do {
date = new Date(Calendar.getInstance().getTimeInMillis() + lockDuration);
Date now = Calendar.getInstance().getTime();
Bson query = and(eq("_id", id), or(eq(lockWriteField, null), lt(lockWriteField, now)));
Bson update = combine(set(lockWriteField, date));
modifiedCount = collection.update(query, update, null).first().getModifiedCount();
if (modifiedCount != 1) {
Thread.sleep(100);
//Check if the lock is still valid
if (watch.getTime() > timeout) {
throw new TimeoutException("Unable to get the lock");
}
}
} while (modifiedCount == 0);
return date.getTime();
}
/**
* Releases the lock.
*
* @param id _id the document to lock
* @param lockToken Lock token
* @throws IllegalStateException if the lockToken does not match with the current lockToken
*/
public void unlock(Object id, long lockToken) {
Date date = new Date(lockToken);
Bson query = and(eq("_id", id), eq(lockWriteField, date));
Bson update = set(lockWriteField, null);
long matchedCount = collection.update(query, update, null).first().getMatchedCount();
if (matchedCount == 0) {
throw new IllegalStateException("Lock token " + lockToken + " not found!");
}
}
}