/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.ejb.plugins.cmp.jdbc2.schema; import org.jboss.system.ServiceMBeanSupport; import org.jboss.metadata.MetaData; import org.jboss.deployment.DeploymentException; import org.w3c.dom.Element; import javax.transaction.Transaction; import java.util.Map; import java.util.HashMap; /** * Simple LRU cache. Items are evicted when maxCapacity is exceeded. * * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a> * @version <tt>$Revision: 81030 $</tt> * @jmx:mbean extends="org.jboss.system.ServiceMBean" */ public class TableCache extends ServiceMBeanSupport implements Cache, TableCacheMBean { private Cache.Listener listener = Cache.Listener.NOOP; private final Map rowsById; private CachedRow head; private CachedRow tail; private int maxCapacity; private final int minCapacity; private boolean locked; private final int partitionIndex; public TableCache(int partitionIndex, int initialCapacity, int maxCapacity) { this.maxCapacity = maxCapacity; this.minCapacity = initialCapacity; rowsById = new HashMap(initialCapacity); this.partitionIndex = partitionIndex; } public TableCache(Element conf) throws DeploymentException { String str = MetaData.getOptionalChildContent(conf, "min-capacity"); minCapacity = (str == null ? 1000 : Integer.parseInt(str)); rowsById = new HashMap(minCapacity); str = MetaData.getOptionalChildContent(conf, "max-capacity"); maxCapacity = (str == null ? 10000 : Integer.parseInt(str)); this.partitionIndex = 0; } /** * @jmx.managed-operation */ public void registerListener(Cache.Listener listener) { if(log.isTraceEnabled() && getServiceName() != null) { log.trace("registered listener for " + getServiceName()); } this.listener = listener; } /** * @jmx.managed-operation */ public int size() { lock(); try { return rowsById.size(); } finally { unlock(); } } /** * @jmx.managed-attribute */ public int getMaxCapacity() { return maxCapacity; } /** * @jmx.managed-attribute */ public void setMaxCapacity(int maxCapacity) { this.maxCapacity = maxCapacity; } /** * @jmx.managed-attribute */ public int getMinCapacity() { return minCapacity; } public synchronized void lock() { if(locked) { long start = System.currentTimeMillis(); while(locked) { try { wait(); } catch(InterruptedException e) { } } listener.contention(partitionIndex, System.currentTimeMillis() - start); } locked = true; } public void lock(Object key) { lock(); } public synchronized void unlock() { if(!locked) { throw new IllegalStateException("The instance is not locked!"); } locked = false; notify(); } public void unlock(Object key) { unlock(); } public Object[] getFields(Object pk) { Object[] fields; CachedRow row = (CachedRow) rowsById.get(pk); if(row != null && row.locker == null) { promoteRow(row); fields = new Object[row.fields.length]; System.arraycopy(row.fields, 0, fields, 0, fields.length); listener.hit(partitionIndex); } else { fields = null; listener.miss(partitionIndex); } return fields; } public Object[] getRelations(Object pk) { Object[] relations; CachedRow row = (CachedRow) rowsById.get(pk); if(row != null && row.relations != null && row.locker == null) { promoteRow(row); relations = new Object[row.relations.length]; System.arraycopy(row.relations, 0, relations, 0, relations.length); } else { relations = null; } return relations; } public void put(Transaction tx, Object pk, Object[] fields, Object[] relations) { CachedRow row = (CachedRow) rowsById.get(pk); if(row == null) // the row is not cached { Object[] fieldsCopy = new Object[fields.length]; System.arraycopy(fields, 0, fieldsCopy, 0, fields.length); row = new CachedRow(pk, fieldsCopy); if(relations != null) { Object[] relationsCopy = new Object[relations.length]; System.arraycopy(relations, 0, relationsCopy, 0, relations.length); row.relations = relationsCopy; } rowsById.put(pk, row); if(head == null) { head = row; tail = row; } else { head.prev = row; row.next = head; head = row; } } else if(row.locker == null || row.locker.equals(tx)) // the row is cached { promoteRow(row); System.arraycopy(fields, 0, row.fields, 0, fields.length); if(relations != null) { if(row.relations == null) { row.relations = new Object[relations.length]; } System.arraycopy(relations, 0, row.relations, 0, relations.length); } row.lastUpdated = System.currentTimeMillis(); row.locker = null; } CachedRow victim = tail; while(rowsById.size() > maxCapacity && victim != null) { CachedRow nextVictim = victim.prev; if(victim.locker == null) { dereference(victim); rowsById.remove(victim.pk); listener.eviction(partitionIndex, row.pk, rowsById.size()); } victim = nextVictim; } } public void ageOut(long lastUpdated) { CachedRow victim = tail; while(victim != null && victim.lastUpdated < lastUpdated) { CachedRow nextVictim = victim.prev; if(victim.locker == null) { dereference(victim); rowsById.remove(victim.pk); listener.eviction(partitionIndex, victim.pk, rowsById.size()); } victim = nextVictim; } } public void remove(Transaction tx, Object pk) { CachedRow row = (CachedRow) rowsById.remove(pk); if(row == null || row.locker != null && !tx.equals(row.locker)) { String msg = "removal of " + pk + " rejected for " + tx + ": " + (row == null ? "the entry could not be found" : "the entry is locked for update by " + row.locker); throw new RemoveException(msg); } dereference(row); row.locker = null; } public boolean contains(Transaction tx, Object pk) { CachedRow row = (CachedRow) rowsById.get(pk); return row != null && (row.locker == null || tx.equals(row.locker)); } public void lockForUpdate(Transaction tx, Object pk) throws Exception { CachedRow row = (CachedRow) rowsById.get(pk); if(row != null) { if(row.locker != null && !tx.equals(row.locker)) { throw new Exception("lock acquisition rejected for " + tx + ", the entry is locked for update by " + row.locker + ", id=" + pk); } row.locker = tx; } // else?! } public void releaseLock(Transaction tx, Object pk) throws Exception { CachedRow row = (CachedRow) rowsById.get(pk); if(row != null) { if(!tx.equals(row.locker)) { throw new Exception("rejected to release lock for " + tx + ", the entry is locked for update by " + row.locker + ", id=" + pk); } row.locker = null; } // else?! } public void flush() { this.rowsById.clear(); this.head = null; this.tail = null; } public String toString() { StringBuffer buf = new StringBuffer(); buf.append('['); try { lock(); CachedRow cursor = head; while(cursor != null) { buf.append('(') .append(cursor.pk) .append('|'); for(int i = 0; i < cursor.fields.length; ++i) { if(i > 0) { buf.append(','); } buf.append(cursor.fields[i]); } buf.append(')'); cursor = cursor.next; } } finally { unlock(); } buf.append(']'); return buf.toString(); } // Private private void dereference(CachedRow row) { CachedRow next = row.next; CachedRow prev = row.prev; if(row == head) { head = next; } if(row == tail) { tail = prev; } if(next != null) { next.prev = prev; } if(prev != null) { prev.next = next; } row.next = null; row.prev = null; } private void promoteRow(CachedRow row) { if(head == null) // this is the first row in the cache { head = row; tail = row; } else if(row == head) // this is the head { } else if(row == tail) // this is the tail { tail = row.prev; tail.next = null; row.prev = null; row.next = head; head.prev = row; head = row; } else // somewhere in the middle { CachedRow next = row.next; CachedRow prev = row.prev; if(prev != null) { prev.next = next; } if(next != null) { next.prev = prev; } head.prev = row; row.next = head; row.prev = null; head = row; } } private class CachedRow { public final Object pk; public final Object[] fields; public Object[] relations; private Transaction locker; private CachedRow next; private CachedRow prev; public long lastUpdated = System.currentTimeMillis(); public CachedRow(Object pk, Object[] fields) { this.pk = pk; this.fields = fields; } } }