/*
*
* * Copyright 2014 Orient Technologies LTD (info(at)orientechnologies.com)
* *
* * 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.
* *
* * For more information: http://www.orientechnologies.com
*
*/
package com.orientechnologies.orient.core.db;
import com.orientechnologies.common.concur.lock.OInterruptedException;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.OOrientListenerAbstract;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OStorageExistsException;
import com.orientechnologies.orient.core.metadata.security.OToken;
import com.orientechnologies.orient.core.storage.OStorage;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* <p>
* Database pool which has good multicore scalability characteristics because of creation of several partitions for each logical
* thread group which drastically decrease thread contention when we acquire new connection to database.
* </p>
* <p>
* To acquire connection from the pool call {@link #acquire()} method but to release connection you just need to call
* {@link com.orientechnologies.orient.core.db.document.ODatabaseDocument#close()} method.
* </p>
* <p>
* In case of remote storage database pool will keep connections to the remote storage till you close pool. So in case of remote
* storage you should close pool at the end of it's usage, it also may be closed on application shutdown but you should not rely on
* this behaviour.
* </p>
* <p>
* This pool has one noticeable difference from other pools. If you perform several subsequent acquire calls in the same thread the
* <b>same</b> instance of database will be returned, but amount of calls to close method should match to amount of acquire calls to
* release database back in the pool. It will allow you to use such feature as transaction propagation when you perform call of one
* service from another one.
* </p>
* <p>
* Given pool has two parameters now, amount of maximum connections for single partition and total amount of connections
* which may be hold by pool. When you start to use pool it will automatically split by several partitions, each partition is
* independent from other which gives us very good multicore scalability.
* Amount of partitions will be close to amount of cores but it is not mandatory and depends how much application is
* loaded. Amount of connections which may be hold by single partition is defined by user but we suggest to use default parameters
* if your application load is not extremely high.
* <p>
* If total amount of connections which allowed to be hold by this pool is reached thread will wait till free connection will be
* available. If total amount of connection is set to value 0 or less it means that there is no connection limit.
* </p>
*
* @author Andrey Lomakin (a.lomakin-at-orientechnologies.com)
* @since 06/11/14
*/
public class OPartitionedDatabasePool extends OOrientListenerAbstract {
private static final int HASH_INCREMENT = 0x61c88647;
private static final int MIN_POOL_SIZE = 2;
private static final AtomicInteger nextHashCode = new AtomicInteger();
protected final Map<String, Object> properties = new HashMap<String, Object>();
private final String url;
private final String userName;
private final String password;
private final int maxPartitonSize;
private final AtomicBoolean poolBusy = new AtomicBoolean();
private int maxPartitions = Runtime.getRuntime().availableProcessors() ;
private final Semaphore connectionsCounter;
private volatile ThreadLocal<PoolData> poolData = new ThreadPoolData();
private volatile PoolPartition[] partitions;
private volatile boolean closed = false;
private boolean autoCreate = false;
public OPartitionedDatabasePool(String url, String userName, String password) {
this(url, userName, password, Runtime.getRuntime().availableProcessors(), -1);
}
public OPartitionedDatabasePool(String url, String userName, String password, int maxPartitionSize, int maxPoolSize) {
this.url = url;
this.userName = userName;
this.password = password;
if (maxPoolSize > 0) {
connectionsCounter = new Semaphore(maxPoolSize);
this.maxPartitions = 1;
this.maxPartitonSize = maxPoolSize;
} else {
this.maxPartitonSize = maxPartitionSize;
connectionsCounter = null;
}
final PoolPartition[] pts = new PoolPartition[maxPartitions];
for (int i = 0; i < pts.length; i++) {
final PoolPartition partition = new PoolPartition();
pts[i] = partition;
initQueue(url, partition);
}
partitions = pts;
Orient.instance().registerWeakOrientStartupListener(this);
Orient.instance().registerWeakOrientShutdownListener(this);
}
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public String getUrl() {
return url;
}
public String getUserName() {
return userName;
}
public int getMaxPartitonSize() {
return maxPartitonSize;
}
public int getAvailableConnections() {
checkForClose();
int result = 0;
for (PoolPartition partition : partitions) {
if (partition != null) {
result += partition.currentSize.get() - partition.acquiredConnections.get();
}
}
if (result < 0)
return 0;
return result;
}
public int getCreatedInstances() {
checkForClose();
int result = 0;
for (PoolPartition partition : partitions) {
if (partition != null) {
result += partition.currentSize.get();
}
}
if (result < 0)
return 0;
return result;
}
public ODatabaseDocumentTx acquire() {
checkForClose();
final PoolData data = poolData.get();
if (data.acquireCount > 0) {
data.acquireCount++;
assert data.acquiredDatabase != null;
final ODatabaseDocumentTx db = data.acquiredDatabase;
db.activateOnCurrentThread();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
db.setProperty(entry.getKey(), entry.getValue());
}
return db;
}
try {
if (connectionsCounter != null)
connectionsCounter.acquire();
} catch (InterruptedException ie) {
throw OException.wrapException(new OInterruptedException("Acquiring of new connection was interrupted"), ie);
}
boolean acquired = false;
try {
while (true) {
final PoolPartition[] pts = partitions;
final int index = (pts.length - 1) & data.hashCode;
PoolPartition partition = pts[index];
if (partition == null) {
if (!poolBusy.get() && poolBusy.compareAndSet(false, true)) {
if (pts == partitions) {
partition = pts[index];
if (partition == null) {
partition = new PoolPartition();
initQueue(url, partition);
pts[index] = partition;
}
}
poolBusy.set(false);
}
continue;
} else {
final DatabaseDocumentTxPooled db = partition.queue.poll();
if (db == null) {
if (pts.length < maxPartitions) {
if (!poolBusy.get() && poolBusy.compareAndSet(false, true)) {
if (pts == partitions) {
final PoolPartition[] newPartitions = new PoolPartition[partitions.length << 1];
System.arraycopy(partitions, 0, newPartitions, 0, partitions.length);
partitions = newPartitions;
}
poolBusy.set(false);
}
continue;
} else {
if (partition.currentSize.get() >= maxPartitonSize)
throw new IllegalStateException("You have reached maximum pool size for given partition");
final DatabaseDocumentTxPooled newDb = new DatabaseDocumentTxPooled(url);
for (Map.Entry<String, Object> entry : properties.entrySet()) {
newDb.setProperty(entry.getKey(), entry.getValue());
}
openDatabase(newDb);
newDb.partition = partition;
data.acquireCount = 1;
data.acquiredDatabase = newDb;
partition.acquiredConnections.incrementAndGet();
partition.currentSize.incrementAndGet();
acquired = true;
return newDb;
}
} else {
for (Map.Entry<String, Object> entry : properties.entrySet()) {
db.setProperty(entry.getKey(), entry.getValue());
}
openDatabase(db);
db.partition = partition;
partition.acquiredConnections.incrementAndGet();
data.acquireCount = 1;
data.acquiredDatabase = db;
acquired = true;
return db;
}
}
}
} finally {
if (!acquired && connectionsCounter != null)
connectionsCounter.release();
}
}
public boolean isAutoCreate() {
return autoCreate;
}
public OPartitionedDatabasePool setAutoCreate(final boolean autoCreate) {
this.autoCreate = autoCreate;
return this;
}
public boolean isClosed() {
return closed;
}
protected void openDatabase(final DatabaseDocumentTxPooled db) {
if (autoCreate) {
if (!db.getURL().startsWith("remote:") && !db.exists()) {
try {
db.create();
} catch (OStorageExistsException ex) {
OLogManager.instance().debug(this, "Can not create storage " + db.getStorage() + " because it already exists.");
db.internalOpen();
}
} else {
db.internalOpen();
}
} else {
db.internalOpen();
}
}
@Override
public void onShutdown() {
close();
}
@Override
public void onStartup() {
if (poolData == null)
poolData = new ThreadPoolData();
}
public void close() {
if (closed)
return;
closed = true;
for (PoolPartition partition : partitions) {
if (partition == null)
continue;
final Queue<DatabaseDocumentTxPooled> queue = partition.queue;
while (!queue.isEmpty()) {
DatabaseDocumentTxPooled db = queue.poll();
db.activateOnCurrentThread();
OStorage storage = db.getStorage();
storage.close();
ODatabaseRecordThreadLocal.INSTANCE.remove();
}
}
partitions = null;
poolData = null;
}
private void initQueue(String url, PoolPartition partition) {
ConcurrentLinkedQueue<DatabaseDocumentTxPooled> queue = partition.queue;
for (int n = 0; n < MIN_POOL_SIZE; n++) {
final DatabaseDocumentTxPooled db = new DatabaseDocumentTxPooled(url);
for (Map.Entry<String, Object> entry : properties.entrySet()) {
db.setProperty(entry.getKey(), entry.getValue());
}
queue.add(db);
}
partition.currentSize.addAndGet(MIN_POOL_SIZE);
}
private void checkForClose() {
if (closed)
throw new IllegalStateException("Pool is closed");
}
/**
* Sets a property value
*
* @param iName Property name
* @param iValue new value to set
* @return The previous value if any, otherwise null
*/
public Object setProperty(final String iName, final Object iValue) {
if (iValue != null) {
return properties.put(iName.toLowerCase(), iValue);
} else {
return properties.remove(iName.toLowerCase());
}
}
/**
* Gets the property value.
*
* @param iName Property name
* @return The previous value if any, otherwise null
*/
public Object getProperty(final String iName) {
return properties.get(iName.toLowerCase());
}
private static final class PoolData {
private final int hashCode;
private int acquireCount;
private DatabaseDocumentTxPooled acquiredDatabase;
private PoolData() {
hashCode = nextHashCode();
}
}
private static class ThreadPoolData extends ThreadLocal<PoolData> {
@Override
protected PoolData initialValue() {
return new PoolData();
}
}
private static final class PoolPartition {
private final AtomicInteger currentSize = new AtomicInteger();
private final AtomicInteger acquiredConnections = new AtomicInteger();
private final ConcurrentLinkedQueue<DatabaseDocumentTxPooled> queue = new ConcurrentLinkedQueue<DatabaseDocumentTxPooled>();
}
private final class DatabaseDocumentTxPooled extends ODatabaseDocumentTx {
private PoolPartition partition;
private DatabaseDocumentTxPooled(String iURL) {
super(iURL, true);
}
@Override
public <DB extends ODatabase> DB open(OToken iToken) {
throw new ODatabaseException("Impossible to open a database managed by a pool ");
}
@Override
public <DB extends ODatabase> DB open(String iUserName, String iUserPassword) {
throw new ODatabaseException("Impossible to open a database managed by a pool ");
}
/**
* @return <code>true</code> if database is obtained from the pool and <code>false</code> otherwise.
*/
@Override
public boolean isPooled() {
return true;
}
protected void internalOpen() {
super.open(userName, password);
}
@Override
public void close() {
if (poolData != null) {
final PoolData data = poolData.get();
if (data.acquireCount == 0)
return;
data.acquireCount--;
if (data.acquireCount > 0)
return;
PoolPartition p = partition;
partition = null;
final OStorage storage = getStorage();
if (storage == null)
return;
//if connection is lost and storage is closed as result we should not put closed connection back to the pool
if (!storage.isClosed()) {
activateOnCurrentThread();
super.close();
data.acquiredDatabase = null;
p.queue.offer(this);
} else {
//close database instance but be ready that it will throw exception because of storage is closed
try {
super.close();
} catch (Exception e) {
OLogManager.instance().error(this, "Error during closing of database % when storage %s was already closed", e, getUrl(),
storage.getName());
}
data.acquiredDatabase = null;
//we create new connection instead of old one
final DatabaseDocumentTxPooled db = new DatabaseDocumentTxPooled(url);
p.queue.offer(db);
}
if (connectionsCounter != null)
connectionsCounter.release();
p.acquiredConnections.decrementAndGet();
} else {
super.close();
}
}
}
}