/* * 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:

* * 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: *
    *
  1. Select currentID from appropriate db row. *
  2. Increment id returned from db. *
  3. 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 + '}'; } } }