org.tentackle.dbms.TableSerialExpirationBacklog Maven / Gradle / Ivy
/*
* Tentackle - https://tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.dbms;
import org.tentackle.common.TentackleRuntimeException;
import org.tentackle.misc.IdSerialTuple;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Keeps a backlog of expiration sets.
* Used in RMI-servers to reduce database roundtrips for clients
* requesting the expiration info for their local caches.
*
* @author harald
*/
public class TableSerialExpirationBacklog {
private static final int DEFAULT_SIZE = 64; // should be enough for the 99% case
// holds a set of expiration info
private static class ExpirationSet implements Cloneable {
private long minSerial; // > minSerial
private long maxSerial; // <= maxSerial
private final List idSerTuples; // pairs of id/tableserial
private ExpirationSet(long minSerial, long maxSerial, List idSerTuples) {
this.minSerial = minSerial;
this.maxSerial = maxSerial;
this.idSerTuples = idSerTuples;
}
@Override
public ExpirationSet clone() {
try {
return (ExpirationSet) super.clone();
}
catch (CloneNotSupportedException ex) {
throw new TentackleRuntimeException(ex); // this shouldn't happen, since we are Cloneable
}
}
}
// the backlog
private final ExpirationSet[] expirations; // array of expirations
private int nextNdx; // round-robin index
// synchronization lock
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* Creates a backlog.
*
* @param size the size of the backlog
*/
public TableSerialExpirationBacklog(int size) {
expirations = new ExpirationSet[size]; // should be enough for the 99% case
for (int i=0; i < size; i++) {
expirations[i] = null; // null = unused yet
}
}
/**
* Creates a backlog with a default size.
*/
public TableSerialExpirationBacklog() {
this(DEFAULT_SIZE);
}
/**
* Adds an expiration set to the backlog.
*
* @param minSerial the lower serial bound of the query (minSerial < tableSerial)
* @param maxSerial the upper serial bound of the query (tableSerial ≤ maxSerial)
* @param idSerTuples the expiration info as pairs of id/tableserial.
*/
public void addExpiration(long minSerial, long maxSerial, List idSerTuples) {
if (idSerTuples != null && !idSerTuples.isEmpty()) {
lock.writeLock().lock();
try {
expirations[nextNdx++] = new ExpirationSet(minSerial, maxSerial, idSerTuples);
if (nextNdx >= expirations.length) {
nextNdx = 0;
}
}
finally {
lock.writeLock().unlock();
}
}
}
/**
* Gets the expiration backlog for a given range of table-serials.
*
* @param minSerial the lower serial bound of the query (minSerial < tableSerial)
* @param maxSerial the upper serial bound of the query (tableSerial ≤ maxSerial)
* @return the expiration info as pairs of id/tableserial, null if given range was not found in the backlog
*/
public List getExpiration(long minSerial, long maxSerial) {
// extract matching sets
ExpirationSet[] matchedExps = new ExpirationSet[expirations.length];
int matchCount = 0;
lock.readLock().lock();
try {
// we start at current nextNdx: this is the oldest entry
int ndx = nextNdx;
for (int i=0; i < expirations.length; i++) {
if (ndx >= expirations.length) {
ndx = 0;
}
ExpirationSet exp = expirations[ndx++];
if (exp != null && exp.maxSerial >= minSerial && exp.minSerial <= maxSerial) {
// add to list of matched sets
matchedExps[matchCount++] = exp;
}
}
}
finally {
lock.readLock().unlock();
}
while (matchCount > 0) {
// merge the sets. note that a merge creates a clone, so there's no need for synchronization
matchedExps = merge(matchedExps, matchCount);
if (matchedExps.length == matchCount) {
// cannot be merged any further
for (int m=0; m < matchCount; m++) {
ExpirationSet exp = matchedExps[m];
if (exp.minSerial <= minSerial && exp.maxSerial >= maxSerial) {
// create the result of unique id/tableserial pairs, sorted by id + tableserial
Set setById = new TreeSet<>((IdSerialTuple o1, IdSerialTuple o2) -> {
// order by id + tableserial descending
int rv = Long.compare(o2.getId(), o1.getId());
if (rv == 0) {
rv = Long.compare(o2.getSerial(), o1.getSerial());
}
return rv;
});
for (IdSerialTuple idSer: exp.idSerTuples) {
if (idSer.getSerial() > minSerial && idSer.getSerial() <= maxSerial) {
setById.add(idSer);
}
}
// only take the id/tableserial with the highest tableserial for duplicate ids
List expireList = new ArrayList<>();
IdSerialTuple lastIdSer = null;
for (IdSerialTuple idSer: setById) {
if (lastIdSer != null && lastIdSer.getId() != idSer.getId()) {
expireList.add(lastIdSer);
}
lastIdSer = idSer;
}
if (lastIdSer != null) {
expireList.add(lastIdSer);
}
return expireList;
}
}
// no usable range found
break;
}
matchCount = matchedExps.length;
// continue merge...
}
return null; // no such range in backlog: ask the database!
}
/**
* Merge expiration sets.
*
* @param expSets the expiration sets to merge
* @return the merged sets
*/
private ExpirationSet[] merge(ExpirationSet[] expSets, int count) {
ExpirationSet[] mergedSets = new ExpirationSet[count];
mergedSets[0] = expSets[0].clone();
int mergeCount = 1;
for (int i=1; i < count; i++) {
ExpirationSet exp = expSets[i];
boolean merged = false;
for (int j=0; j < mergeCount; j++) {
ExpirationSet mexp = mergedSets[j];
if (exp.maxSerial >= mexp.minSerial && exp.minSerial <= mexp.minSerial) {
mexp.minSerial = exp.minSerial;
if (exp.maxSerial > mexp.maxSerial) {
mexp.maxSerial = exp.maxSerial;
}
merged = true;
}
if (exp.minSerial <= mexp.maxSerial && exp.maxSerial >= mexp.maxSerial) {
mexp.maxSerial = exp.maxSerial;
if (exp.minSerial < mexp.minSerial) {
mexp.minSerial = exp.minSerial;
}
merged = true;
}
if (merged) {
mexp.idSerTuples.addAll(exp.idSerTuples);
break;
}
}
if (!merged) {
// not merged: add a clone to merged sets
mergedSets[mergeCount++] = exp.clone();
}
}
// cut result array in size
ExpirationSet[] nMergedSets = new ExpirationSet[mergeCount];
System.arraycopy(mergedSets, 0, nMergedSets, 0, mergeCount);
return nMergedSets;
}
}