/*
* Copyright (C) 2004-2008 Jive Software, 2017-2020 Ignite Realtime Foundation. All rights reserved.
*
* 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.jivesoftware.database;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages sequences of unique ID's that get stored in the database. Database support for sequences
* varies widely; some don't use them at all. Instead, we handle unique ID generation with a
* combination VM/database solution.
*
* A special table in the database doles out blocks of unique ID's to each
* virtual machine that interacts with Jive. This has the following consequences:
*
* - There is no need to go to the database every time we want a new unique id.
*
- Multiple app servers can interact with the same db without id collision.
*
- The order of unique id's may not correspond to the creation date of objects.
*
- There can be gaps in ID's after server restarts since blocks will get "lost" if the block
* size is greater than 1.
*
* Each sequence type that this class manages has a different block size value. Objects that aren't
* created often have a block size of 1, while frequently created objects such as entries and
* comments have larger block sizes.
*
* @author Matt Tucker
* @author Bruce Ritchie
*/
public class SequenceManager {
private static final Logger Log = LoggerFactory.getLogger(SequenceManager.class);
private static final String CREATE_ID =
"INSERT INTO ofID (id, idType) VALUES (1, ?)";
private static final String LOAD_ID =
"SELECT id FROM ofID WHERE idType=?";
private static final String UPDATE_ID =
"UPDATE ofID SET id=? WHERE idType=? AND id=?";
// Statically startup a sequence manager for each of the sequence counters.
private static final Map managers = new ConcurrentHashMap<>();
static {
new SequenceManager(JiveConstants.ROSTER, 5);
new SequenceManager(JiveConstants.OFFLINE, 5);
new SequenceManager(JiveConstants.MUC_ROOM, 5);
new SequenceManager(JiveConstants.MUC_MESSAGE_ID, 50);
}
private static final Cache sequenceBlocks = CacheFactory.createCache("Sequences");
/**
* Returns the next ID of the specified type.
*
* @param type the type of unique ID.
* @return the next unique ID of the specified type.
*/
public static long nextID(int type) {
if (managers.containsKey(type)) {
return managers.get(type).nextUniqueID();
}
else {
// Verify type is valid from the db, if so create an instance for the type
// And return the next unique id
SequenceManager manager = new SequenceManager(type, 1);
return manager.nextUniqueID();
}
}
/**
* Returns the next id for an object that has defined the annotation {@link JiveID}.
* The JiveID annotation value is the synonymous for the type integer.
*
* The annotation JiveID should contain the id type for the object (the same number you would
* use to call nextID(int type)). Example class definition:
*
* \@JiveID(10)
* public class MyClass {
*
* }
*
*
* @param o object that has annotation JiveID.
* @return the next unique ID.
* @throws IllegalArgumentException If the object passed in does not defined {@link JiveID}
*/
public static long nextID(Object o) {
JiveID id = o.getClass().getAnnotation(JiveID.class);
if (id == null) {
Log.error("Annotation JiveID must be defined in the class " + o.getClass());
throw new IllegalArgumentException(
"Annotation JiveID must be defined in the class " + o.getClass());
}
return nextID(id.value());
}
/**
* Used to set the blocksize of a given SequenceManager. If no SequenceManager has
* been registered for the type, the type is verified as valid and then a new
* sequence manager is created.
*
* @param type the type of unique id.
* @param blockSize how many blocks of ids we should.
*/
public static void setBlockSize(int type, int blockSize) {
if (managers.containsKey(type)) {
managers.get(type).blockSize = blockSize;
}
else {
new SequenceManager(type, blockSize);
}
}
private final int type;
private int blockSize;
/**
* Creates a new DbSequenceManager.
*
* @param seqType the type of sequence.
* @param size the number of id's to "checkout" at a time.
*/
public SequenceManager(int seqType, int size) {
managers.put(seqType, this);
this.type = seqType;
this.blockSize = size;
}
/**
* Returns the next available unique ID. Essentially this provides for the functionality of an
* auto-increment database field.
* @return the next sequence number
*/
public long nextUniqueID() {
final Lock lock = sequenceBlocks.getLock(type);
lock.lock();
try {
Data data = sequenceBlocks.get(type);
if (data == null || !(data.getCurrentID() < data.getMaxID())) {
data = getNextBlock();
}
final long id = data.getCurrentID();
data.setCurrentID(id + 1);
sequenceBlocks.put(type, data);
return id;
} finally {
lock.unlock();
}
}
/**
* Performs a lookup to get the next available ID block. The algorithm is as follows:
*
* - Select currentID from appropriate db row.
*
- Increment id returned from db.
*
- Update db row with new id where id=old_id.
*
*
* Calls to this method should only occur while a lock has already been acquired.
*
* @return the next available ID block
*/
private Data getNextBlock() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
// Get the current ID from the database.
pstmt = con.prepareStatement(LOAD_ID);
pstmt.setInt(1, type);
rs = pstmt.executeQuery();
long currentID = 1;
if (rs.next()) {
currentID = rs.getLong(1);
}
else {
createNewID(con, type);
}
DbConnectionManager.fastcloseStmt(rs, pstmt);
// Increment the id to define our block.
long newID = currentID + blockSize;
// The WHERE clause includes the last value of the id. This ensures
// that an update will occur only if nobody else has performed an
// update first.
pstmt = con.prepareStatement(UPDATE_ID);
pstmt.setLong(1, newID);
pstmt.setInt(2, type);
pstmt.setLong(3, currentID);
// Check to see if the row was affected. If not, some other process
// already changed the original id that we read. This code should be
// called under a lock, so this is not supposed to happen. Throw a
// very verbose error if it does.
if (pstmt.executeUpdate() == 1) {
final Data result = new Data(currentID, newID);
sequenceBlocks.put(type, result);
return result;
} else {
throw new IllegalStateException("Failed at attempt to obtain an ID, aborting...");
}
}
catch (SQLException e) {
Log.error("An exception occurred while trying to obtain new sequence values from the database for type {}", type, e);
throw new IllegalStateException("Failed at attempt to obtain an ID, aborting...", e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private void createNewID(Connection con, int type) throws SQLException {
Log.warn("Autocreating jiveID row for type '" + type + "'");
// create new ID row
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(CREATE_ID);
pstmt.setInt(1, type);
pstmt.execute();
}
finally {
DbConnectionManager.closeStatement(pstmt);
}
}
static class Data implements Serializable {
private long currentID;
private long maxID;
Data() { // for serialization.
}
public Data(long currentID, long maxID) {
this.currentID = currentID;
this.maxID = maxID;
}
public long getCurrentID() {
return currentID;
}
public void setCurrentID(long currentID) {
this.currentID = currentID;
}
public long getMaxID() {
return maxID;
}
public void setMaxID(long maxID) {
this.maxID = maxID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Data data = (Data) o;
return currentID == data.currentID && maxID == data.maxID;
}
@Override
public int hashCode() {
return Objects.hash(currentID, maxID);
}
@Override
public String toString() {
return "Data{" +
"currentID=" + currentID +
", maxID=" + maxID +
'}';
}
}
}