blob: 22305d107e1f82d4f92a424eb9c4ac49872a9e2b [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.ofbiz.entity.util;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.transaction.Transaction;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.datasource.GenericHelperInfo;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.entity.model.ModelField;
import org.apache.ofbiz.entity.transaction.GenericTransactionException;
import org.apache.ofbiz.entity.transaction.TransactionFactoryLoader;
import org.apache.ofbiz.entity.transaction.TransactionUtil;
/**
* Sequence Utility to get unique sequences from named sequence banks
*/
public class SequenceUtil {
public static final String module = SequenceUtil.class.getName();
private final ConcurrentMap<String, SequenceBank> sequences = new ConcurrentHashMap<String, SequenceBank>();
private final GenericHelperInfo helperInfo;
private final String tableName;
private final String nameColName;
private final String idColName;
public SequenceUtil(GenericHelperInfo helperInfo, ModelEntity seqEntity, String nameFieldName, String idFieldName) {
this.helperInfo = helperInfo;
if (seqEntity == null) {
throw new IllegalArgumentException("The sequence model entity was null but is required.");
}
this.tableName = seqEntity.getTableName(helperInfo.getHelperBaseName());
ModelField nameField = seqEntity.getField(nameFieldName);
if (nameField == null) {
throw new IllegalArgumentException("Could not find the field definition for the sequence name field " + nameFieldName);
}
this.nameColName = nameField.getColName();
ModelField idField = seqEntity.getField(idFieldName);
if (idField == null) {
throw new IllegalArgumentException("Could not find the field definition for the sequence id field " + idFieldName);
}
this.idColName = idField.getColName();
}
public Long getNextSeqId(String seqName, long staggerMax, ModelEntity seqModelEntity) {
SequenceBank bank = this.getBank(seqName, seqModelEntity);
return bank.getNextSeqId(staggerMax);
}
public void forceBankRefresh(String seqName, long staggerMax) {
// don't use the get method because we don't want to create if it fails
SequenceBank bank = sequences.get(seqName);
if (bank == null) {
return;
}
bank.refresh(staggerMax);
}
private SequenceBank getBank(String seqName, ModelEntity seqModelEntity) {
SequenceBank bank = sequences.get(seqName);
if (bank == null) {
long bankSize = SequenceBank.defaultBankSize;
if (seqModelEntity != null && seqModelEntity.getSequenceBankSize() != null) {
bankSize = seqModelEntity.getSequenceBankSize().longValue();
if (bankSize > SequenceBank.maxBankSize) bankSize = SequenceBank.maxBankSize;
}
bank = new SequenceBank(seqName, bankSize);
SequenceBank bankFromCache = sequences.putIfAbsent(seqName, bank);
bank = bankFromCache != null ? bankFromCache : bank;
}
return bank;
}
private class SequenceBank {
public static final long defaultBankSize = 10;
public static final long maxBankSize = 5000;
public static final long startSeqId = 10000;
private final String seqName;
private final long bankSize;
private final String updateForLockStatement;
private final String selectSequenceStatement;
private long curSeqId;
private long maxSeqId;
private SequenceBank(String seqName, long bankSize) {
this.seqName = seqName;
curSeqId = 0;
maxSeqId = 0;
this.bankSize = bankSize;
updateForLockStatement = "UPDATE " + SequenceUtil.this.tableName + " SET " + SequenceUtil.this.idColName + "=" + SequenceUtil.this.idColName + " WHERE " + SequenceUtil.this.nameColName + "='" + this.seqName + "'";
selectSequenceStatement = "SELECT " + SequenceUtil.this.idColName + " FROM " + SequenceUtil.this.tableName + " WHERE " + SequenceUtil.this.nameColName + "='" + this.seqName + "'";
}
private Long getNextSeqId(long staggerMax) {
long stagger = 1;
if (staggerMax > 1) {
stagger = (long)Math.ceil(Math.random() * staggerMax);
if (stagger == 0) stagger = 1;
}
synchronized (this) {
if ((curSeqId + stagger) <= maxSeqId) {
long retSeqId = curSeqId;
curSeqId += stagger;
return retSeqId;
} else {
fillBank(stagger);
if ((curSeqId + stagger) <= maxSeqId) {
long retSeqId = curSeqId;
curSeqId += stagger;
return retSeqId;
} else {
Debug.logError("Fill bank failed, returning null", module);
return null;
}
}
}
}
private synchronized void refresh(long staggerMax) {
this.curSeqId = this.maxSeqId;
this.fillBank(staggerMax);
}
/*
The algorithm to get the new sequence id in a thread safe way is the following:
1 - run an update with no changes to get a lock on the record
1bis - if no record is found, try to create and update it to get the lock
2 - select the record (now locked) to get the curSeqId
3 - increment the sequence
The three steps are executed in one dedicated database transaction.
*/
private void fillBank(long stagger) {
// no need to get a new bank, SeqIds available
if ((curSeqId + stagger) <= maxSeqId) {
return;
}
long bankSize = this.bankSize;
if (stagger > 1) {
// NOTE: could use staggerMax for this, but if that is done it would be easier to guess a valid next id without a brute force attack
bankSize = stagger * defaultBankSize;
}
if (bankSize > maxBankSize) {
bankSize = maxBankSize;
}
Transaction suspendedTransaction = null;
try {
suspendedTransaction = TransactionUtil.suspend();
boolean beganTransaction = false;
try {
beganTransaction = TransactionUtil.begin();
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
try {
connection = TransactionFactoryLoader.getInstance().getConnection(SequenceUtil.this.helperInfo);
} catch (SQLException sqle) {
Debug.logWarning("Unable to establish a connection with the database. Error was:" + sqle.toString(), module);
throw sqle;
} catch (GenericEntityException e) {
Debug.logWarning("Unable to establish a connection with the database. Error was: " + e.toString(), module);
throw e;
}
if (connection == null) {
throw new GenericEntityException("Unable to establish a connection with the database, connection was null...");
}
try {
stmt = connection.createStatement();
String sql = null;
// 1 - run an update with no changes to get a lock on the record
if (stmt.executeUpdate(updateForLockStatement) <= 0) {
Debug.logWarning("Lock failed; no sequence row was found, will try to add a new one for sequence: " + seqName, module);
sql = "INSERT INTO " + SequenceUtil.this.tableName + " (" + SequenceUtil.this.nameColName + ", " + SequenceUtil.this.idColName + ") VALUES ('" + this.seqName + "', " + startSeqId + ")";
try {
stmt.executeUpdate(sql);
} catch (SQLException sqle) {
// insert failed: this means that another thread inserted the record; then retry to run an update with no changes to get a lock on the record
if (stmt.executeUpdate(updateForLockStatement) <= 0) {
// This should never happen
throw new GenericEntityException("No rows changed when trying insert new sequence: " + seqName);
}
}
}
// 2 - select the record (now locked) to get the curSeqId
rs = stmt.executeQuery(selectSequenceStatement);
boolean sequenceFound = rs.next();
if (sequenceFound) {
curSeqId = rs.getLong(SequenceUtil.this.idColName);
}
rs.close();
if (!sequenceFound) {
throw new GenericEntityException("Failed to find the sequence record for sequence: " + seqName);
}
// 3 - increment the sequence
sql = "UPDATE " + SequenceUtil.this.tableName + " SET " + SequenceUtil.this.idColName + "=" + SequenceUtil.this.idColName + "+" + bankSize + " WHERE " + SequenceUtil.this.nameColName + "='" + this.seqName + "'";
if (stmt.executeUpdate(sql) <= 0) {
throw new GenericEntityException("Update failed, no rows changes for seqName: " + seqName);
}
TransactionUtil.commit(beganTransaction);
} catch (SQLException sqle) {
Debug.logWarning(sqle, "SQL Exception:" + sqle.getMessage(), module);
throw sqle;
} finally {
try {
if (stmt != null) stmt.close();
} catch (SQLException sqle) {
Debug.logWarning(sqle, "Error closing statement in sequence util", module);
}
try {
if (connection != null) connection.close();
} catch (SQLException sqle) {
Debug.logWarning(sqle, "Error closing connection in sequence util", module);
}
}
} catch (Exception e) {
// reset the sequence fields and return (note: it would be better to throw an exception)
curSeqId = 0;
maxSeqId = 0;
String errMsg = "General error in getting a sequenced ID";
Debug.logError(e, errMsg, module);
try {
TransactionUtil.rollback(beganTransaction, errMsg, e);
} catch (GenericTransactionException gte2) {
Debug.logError(gte2, "Unable to rollback transaction", module);
}
return;
}
} catch (GenericTransactionException e) {
Debug.logError(e, "System Error suspending transaction in sequence util", module);
// reset the sequence fields and return (note: it would be better to throw an exception)
curSeqId = 0;
maxSeqId = 0;
return;
} finally {
if (suspendedTransaction != null) {
try {
TransactionUtil.resume(suspendedTransaction);
} catch (GenericTransactionException e) {
Debug.logError(e, "Error resuming suspended transaction in sequence util", module);
// reset the sequence fields and return (note: it would be better to throw an exception)
curSeqId = 0;
maxSeqId = 0;
return;
}
}
}
maxSeqId = curSeqId + bankSize;
if (Debug.infoOn()) Debug.logInfo("Got bank of sequenced IDs for [" + this.seqName + "]; curSeqId=" + curSeqId + ", maxSeqId=" + maxSeqId + ", bankSize=" + bankSize, module);
}
}
}