blob: 71ce0c3173cb6db209ca5f7a133e95783b739de7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2018 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.osgi.storage.bundlefile;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.osgi.framework.eventmgr.*;
/**
* A simple/quick/small implementation of an MRU (Most Recently Used) list to keep
* track of open BundleFiles. The MRU will use the file limit specified by the property
* "osgi.bundlefile.limit" by default unless the MRU is constructed with a specific
* file limit.
*/
public class MRUBundleFileList implements EventDispatcher<Object, Object, BundleFile> {
private static final int MIN = 10;
private static final ThreadLocal<BundleFile> closingBundleFile = new ThreadLocal<>();
// list of open bundle files
final private BundleFile[] bundleFileList;
// list of open bundle files use stamps
final private long[] useStampList;
// the limit of open files to allow before least used bundle file is closed
final private int fileLimit; // value < MIN will disable MRU
final private int delayLimit;
private EventManager bundleFileCloserManager = null;
final private Map<Object, Object> bundleFileCloser;
// the current number of open bundle files
private int numOpen = 0;
// the current use stamp
private long curUseStamp = 0;
// used to work around bug 275166
private boolean firstDispatch = true;
private AtomicInteger pending = new AtomicInteger();
public MRUBundleFileList(int fileLimit) {
// only enable the MRU if the initFileLimit is > MIN
this.fileLimit = fileLimit;
// If the filelimit is > 5000 then use it as the delayLimit also;
// Otherwise use the max between fileLimit * 2 and 500
this.delayLimit = Math.max(fileLimit > 5000 ? fileLimit : fileLimit * 2, 500);
if (fileLimit >= MIN) {
this.bundleFileList = new BundleFile[fileLimit];
this.useStampList = new long[fileLimit];
this.bundleFileCloser = Collections.<Object, Object> singletonMap(this, this);
} else {
this.bundleFileList = null;
this.useStampList = null;
this.bundleFileCloser = null;
}
}
/**
* Adds a BundleFile which is about to be opened to the MRU list. If
* the number of open BundleFiles == the fileLimit then the least
* recently used BundleFile is closed.
* @param bundleFile the bundle file about to be opened.
*/
public void add(BundleFile bundleFile) {
if (fileLimit < MIN)
return; // MRU is disabled
BundleFile toRemove = null;
EventManager manager = null;
synchronized (this) {
if (bundleFile.getMruIndex() >= 0)
return; // do nothing; someone is trying add a bundleFile that is already in an MRU list
int index = 0; // default to the first slot
if (numOpen < fileLimit) {
// numOpen does not exceed the fileLimit
// find the first null slot to use in the MRU
for (int i = 0; i < fileLimit; i++)
if (bundleFileList[i] == null) {
index = i;
break;
}
} else {
// numOpen has reached the fileLimit
// find the least recently used bundleFile and close it
// and use it slot for the new bundleFile to be opened.
index = 0;
for (int i = 1; i < fileLimit; i++)
if (useStampList[i] < useStampList[index])
index = i;
toRemove = bundleFileList[index];
if (toRemove.getMruIndex() != index)
throw new IllegalStateException("The BundleFile has the incorrect mru index: " + index + " != " + toRemove.getMruIndex()); //$NON-NLS-1$//$NON-NLS-2$
removeInternal(toRemove);
}
// found an index to place to bundleFile to be opened
bundleFileList[index] = bundleFile;
bundleFile.setMruIndex(index);
incUseStamp(index);
numOpen++;
if (toRemove != null) {
if (bundleFileCloserManager == null)
bundleFileCloserManager = new EventManager("Bundle File Closer"); //$NON-NLS-1$
manager = bundleFileCloserManager;
}
}
// must not close the toRemove bundle file while holding the lock of another bundle file (bug 161976)
// This queues the bundle file for close asynchronously.
closeBundleFile(toRemove, manager);
}
/**
* Removes a bundle file which is about to be closed
* @param bundleFile the bundle file about to be closed
* @return true if the bundleFile existed in the MRU; false otherwise
*/
public boolean remove(BundleFile bundleFile) {
if (fileLimit < MIN)
return false; // MRU is disabled
synchronized (this) {
int index = bundleFile.getMruIndex();
if ((index >= 0 && index < fileLimit) && bundleFileList[index] == bundleFile) {
removeInternal(bundleFile);
return true;
}
}
return false;
}
// must be called while synchronizing "this"
private void removeInternal(BundleFile bundleFile) {
int index = bundleFile.getMruIndex();
bundleFile.setMruIndex(-1);
bundleFileList[index] = null;
useStampList[index] = -1;
numOpen--;
}
/**
* Increments the use stamp of a bundle file
* @param bundleFile the bundle file to increment the use stamp for
*/
public void use(BundleFile bundleFile) {
if (fileLimit < MIN)
return; // MRU is disabled
synchronized (this) {
int index = bundleFile.getMruIndex();
if ((index >= 0 && index < fileLimit) && bundleFileList[index] == bundleFile)
incUseStamp(index);
}
}
// must be called while synchronizing "this"
private void incUseStamp(int index) {
if (curUseStamp == Long.MAX_VALUE) {
// we hit the curUseStamp max better reset all the stamps
for (int i = 0; i < fileLimit; i++)
useStampList[i] = 0;
curUseStamp = 0;
}
useStampList[index] = ++curUseStamp;
}
public final void dispatchEvent(Object eventListener, Object listenerObject, int eventAction, BundleFile eventObject) {
if (firstDispatch) {
// used to work around bug 275166; we don't want to leak the TCCL in this thread.
Thread.currentThread().setContextClassLoader(null);
firstDispatch = false;
}
try {
closingBundleFile.set(eventObject);
eventObject.close();
} catch (IOException e) {
// TODO should log ??
} finally {
closingBundleFile.set(null);
pending.decrementAndGet();
}
}
private void closeBundleFile(BundleFile toRemove, EventManager manager) {
if (toRemove == null)
return;
int pendingNum = pending.incrementAndGet();
if (pendingNum > delayLimit) {
// delay to allow the closer to catchup
try {
Thread.sleep(Math.min(500, pendingNum));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
try {
/* queue to hold set of listeners */
ListenerQueue<Object, Object, BundleFile> queue = new ListenerQueue<>(manager);
/* add bundle file closer to the queue */
queue.queueListeners(bundleFileCloser.entrySet(), this);
/* dispatch event to set of listeners */
queue.dispatchEventAsynchronous(0, toRemove);
} catch (Throwable t) {
// we cannot propagate exceptions out of this method
// failing to queue a bundle close should not cause an error (bug 283797)
// TODO should consider logging
}
}
/**
* Closes the bundle file closer thread for the MRU list
*/
public void shutdown() {
synchronized (this) {
if (bundleFileCloserManager != null)
bundleFileCloserManager.close();
bundleFileCloserManager = null;
}
}
/**
* Returns true if this MRUBundleFileList is currently closing the specified bundle file on the current thread.
* @param bundleFile the bundle file
* @return true if the bundle file is being closed on the current thread
*/
public boolean isClosing(BundleFile bundleFile) {
if (fileLimit < MIN)
return false; // MRU is disabled
// check the thread local variable
return closingBundleFile.get() == bundleFile;
}
public boolean isEnabled() {
return fileLimit >= MIN;
}
}