GP-1861 - Changed locking to prevent potential out-of-order events

This commit is contained in:
dragonmacher 2022-03-29 15:30:47 -04:00
parent 2d526352ee
commit 1ef3f71dd1
16 changed files with 192 additions and 197 deletions

View file

@ -28,15 +28,19 @@ public class DomainObjectEventQueues {
protected final DomainObject source;
protected final Lock lock;
protected final DomainObjectChangeSupport eventQueue;
protected final Map<EventQueueID, DomainObjectChangeSupport> privateEventQueues = CacheBuilder
.newBuilder().removalListener(this::privateQueueRemoved).weakKeys().build().asMap();
protected final Map<EventQueueID, DomainObjectChangeSupport> privateEventQueues =
CacheBuilder.newBuilder()
.removalListener(this::privateQueueRemoved)
.weakKeys()
.build()
.asMap();
protected volatile boolean eventsEnabled = true;
public DomainObjectEventQueues(DomainObject source, int timeInterval, int bufsize, Lock lock) {
public DomainObjectEventQueues(DomainObject source, int timeInterval, Lock lock) {
this.source = source;
this.lock = lock;
eventQueue = new DomainObjectChangeSupport(source, timeInterval, bufsize, lock);
eventQueue = new DomainObjectChangeSupport(source, timeInterval, lock);
}
private void privateQueueRemoved(
@ -51,18 +55,18 @@ public class DomainObjectEventQueues {
}
}
public synchronized void addListener(DomainObjectListener l) {
public void addListener(DomainObjectListener l) {
eventQueue.addListener(l);
}
public synchronized void removeListener(DomainObjectListener l) {
public void removeListener(DomainObjectListener l) {
eventQueue.removeListener(l);
}
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
EventQueueID id = new EventQueueID();
DomainObjectChangeSupport privateQueue =
new DomainObjectChangeSupport(source, maxDelay, 1000, lock);
new DomainObjectChangeSupport(source, maxDelay, lock);
privateQueue.addListener(listener);
privateEventQueues.put(id, privateQueue);
return id;

View file

@ -29,12 +29,12 @@ public class DBTraceUserData extends DomainObjectAdapterDB implements TraceUserD
}
protected DBTraceUserData(DBTrace trace) throws IOException {
super(new DBHandle(), getName(trace), 500, 1000, trace);
super(new DBHandle(), getName(trace), 500, trace);
// TODO: Create database and such
}
public DBTraceUserData(DBHandle dbh, DBTrace trace, TaskMonitor monitor) {
super(dbh, getName(trace), 500, 1000, trace);
super(dbh, getName(trace), 500, trace);
// TODO Auto-generated constructor stub
}

View file

@ -913,7 +913,7 @@ public class DBTraceProgramView implements TraceProgramView {
this.viewport.setSnap(snap);
this.eventQueues =
new DomainObjectEventQueues(this, TIME_INTERVAL, BUF_SIZE, trace.getLock());
new DomainObjectEventQueues(this, TIME_INTERVAL, trace.getLock());
this.regViewsByThread = new WeakValueHashMap<>();

View file

@ -61,7 +61,7 @@ public class DBTraceProgramViewRegisters implements TraceProgramView {
this.thread = codeSpace.getThread(); // TODO: Bleh, should be parameter
this.eventQueues = new DomainObjectEventQueues(this, DBTraceProgramView.TIME_INTERVAL,
DBTraceProgramView.BUF_SIZE, view.trace.getLock());
view.trace.getLock());
// TODO: Make these create code/memory spaces lazily, to allow null at construction
// NOTE: Use reference manager as example

View file

@ -38,7 +38,7 @@ public abstract class DBDomainObjectSupport extends DomainObjectAdapterDB {
protected DBDomainObjectSupport(DBHandle dbh, DBOpenMode openMode, TaskMonitor monitor,
String name, int timeInterval, int bufSize, Object consumer) {
super(dbh, name, timeInterval, bufSize, consumer);
super(dbh, name, timeInterval, consumer);
this.openMode = openMode;
this.monitor = monitor;
}

View file

@ -211,7 +211,7 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession, VTC
}
private VTSessionDB(DBHandle dbHandle, Object consumer) {
super(dbHandle, UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, EVENT_BUFFER_SIZE, consumer);
super(dbHandle, UNUSED_DEFAULT_NAME, EVENT_NOTIFICATION_DELAY, consumer);
propertyTable = dbHandle.getTable(PROPERTY_TABLE_NAME);
}

View file

@ -37,7 +37,7 @@ import org.apache.commons.collections4.IteratorUtils;
* that the structure cannot be mutated while it is being iterated. See
* {@link WeakDataStructureFactory#createSingleThreadAccessWeakSet()}.
*
* @param <T>
* @param <T> the type
* @see WeakSet
*/
class CopyOnWriteWeakSet<T> extends WeakSet<T> {

View file

@ -60,9 +60,8 @@ public abstract class DomainObjectAdapter implements DomainObject {
private ArrayList<Object> consumers;
protected Map<String, String> metadata = new LinkedHashMap<String, String>();
// a flag indicating whether the domain object has changed
// any methods of this domain object which cause its state to
// to change must set this flag to true
// A flag indicating whether the domain object has changed. Any methods of this domain object
// which cause its state to change must set this flag to true
protected boolean changed = false;
// a flag indicating that this object is temporary
@ -77,15 +76,14 @@ public abstract class DomainObjectAdapter implements DomainObject {
* @param name name of the object
* @param timeInterval the time (in milliseconds) to wait before the event queue is flushed. If
* a new event comes in before the time expires, the timer is reset.
* @param bufsize initial size of event buffer
* @param consumer the object that created this domain object
*/
protected DomainObjectAdapter(String name, int timeInterval, int bufsize, Object consumer) {
protected DomainObjectAdapter(String name, int timeInterval, Object consumer) {
if (consumer == null) {
throw new IllegalArgumentException("Consumer must not be null");
}
this.name = name;
docs = new DomainObjectChangeSupport(this, timeInterval, bufsize, lock);
docs = new DomainObjectChangeSupport(this, timeInterval, lock);
consumers = new ArrayList<Object>();
consumers.add(consumer);
if (!UserData.class.isAssignableFrom(getClass())) {
@ -94,9 +92,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
}
}
/**
* @see ghidra.framework.model.DomainObject#release(java.lang.Object)
*/
@Override
public void release(Object consumer) {
synchronized (consumers) {
@ -115,9 +110,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
return lock;
}
/**
* @see ghidra.framework.model.DomainObject#getDomainFile()
*/
@Override
public DomainFile getDomainFile() {
return domainFile;
@ -136,17 +128,11 @@ public abstract class DomainObjectAdapter implements DomainObject {
return null;
}
/**
* @see ghidra.framework.model.DomainObject#getName()
*/
@Override
public String getName() {
return name;
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String classname = getClass().getName();
@ -154,9 +140,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
return name + " - " + classname;
}
/**
* @see ghidra.framework.model.DomainObject#setName(java.lang.String)
*/
@Override
public void setName(String newName) {
synchronized (this) {
@ -180,25 +163,16 @@ public abstract class DomainObjectAdapter implements DomainObject {
}
}
/**
* @see ghidra.framework.model.DomainObject#isChanged()
*/
@Override
public boolean isChanged() {
return changed && !temporary;
}
/**
* @see ghidra.framework.model.DomainObject#setTemporary(boolean)
*/
@Override
public void setTemporary(boolean state) {
temporary = state;
}
/**
* @see ghidra.framework.model.DomainObject#isTemporary()
*/
@Override
public boolean isTemporary() {
return temporary;
@ -237,9 +211,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
closeListeners.clear();
}
/**
* @see ghidra.framework.model.DomainObject#flushEvents()
*/
@Override
public void flushEvents() {
docs.flush();
@ -257,17 +228,11 @@ public abstract class DomainObjectAdapter implements DomainObject {
return changed;
}
/**
* @see ghidra.framework.model.DomainObject#addListener(ghidra.framework.model.DomainObjectListener)
*/
@Override
public void addListener(DomainObjectListener l) {
docs.addListener(l);
}
/**
* @see ghidra.framework.model.DomainObject#removeListener(ghidra.framework.model.DomainObjectListener)
*/
@Override
public void removeListener(DomainObjectListener l) {
docs.removeListener(l);
@ -286,7 +251,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
@Override
public EventQueueID createPrivateEventQueue(DomainObjectListener listener, int maxDelay) {
EventQueueID eventQueueID = new EventQueueID();
DomainObjectChangeSupport queue = new DomainObjectChangeSupport(this, maxDelay, 1000, lock);
DomainObjectChangeSupport queue = new DomainObjectChangeSupport(this, maxDelay, lock);
queue.addListener(listener);
changeSupportMap.put(eventQueueID, queue);
return eventQueueID;
@ -310,9 +275,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
}
}
/**
* @see ghidra.framework.model.DomainObject#getDescription()
*/
@Override
public abstract String getDescription();
@ -331,9 +293,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
}
}
/**
* @see ghidra.framework.model.DomainObject#setEventsEnabled(boolean)
*/
@Override
public void setEventsEnabled(boolean v) {
if (eventsEnabled != v) {
@ -353,9 +312,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
return eventsEnabled;
}
/**
* @see ghidra.framework.model.DomainObject#hasExclusiveAccess()
*/
@Override
public boolean hasExclusiveAccess() {
return domainFile == null || !domainFile.isCheckedOut() ||
@ -372,9 +328,6 @@ public abstract class DomainObjectAdapter implements DomainObject {
changed = state;
}
/**
* @see ghidra.framework.model.DomainObject#addConsumer(java.lang.Object)
*/
@Override
public boolean addConsumer(Object consumer) {
if (consumer == null) {
@ -403,7 +356,9 @@ public abstract class DomainObjectAdapter implements DomainObject {
}
/**
* Returns true if the this file is used only by the given tool
* Returns true if the this file is used only by the given consumer
* @param consumer the consumer
* @return true if the this file is used only by the given consumer
*/
boolean isUsedExclusivelyBy(Object consumer) {
synchronized (consumers) {
@ -453,6 +408,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
*
* @param contentType domain object content type
* @return content handler
* @throws IOException if no content handler can be found
*/
static synchronized ContentHandler getContentHandler(String contentType) throws IOException {
checkContentHandlerMaps();
@ -468,6 +424,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
*
* @param dobj domain object
* @return content handler
* @throws IOException if no content handler can be found
*/
public static synchronized ContentHandler getContentHandler(DomainObject dobj)
throws IOException {

View file

@ -90,12 +90,10 @@ public abstract class DomainObjectAdapterDB extends DomainObjectAdapter
* @param timeInterval the time (in milliseconds) to wait before the
* event queue is flushed. If a new event comes in before the time expires,
* the timer is reset.
* @param bufSize initial size of event buffer
* @param consumer the object that created this domain object
*/
protected DomainObjectAdapterDB(DBHandle dbh, String name, int timeInterval, int bufSize,
Object consumer) {
super(name, timeInterval, bufSize, consumer);
protected DomainObjectAdapterDB(DBHandle dbh, String name, int timeInterval, Object consumer) {
super(name, timeInterval, consumer);
this.dbh = dbh;
options = new OptionsDB(this);
transactionMgr = new DomainObjectTransactionManager(this);

View file

@ -25,13 +25,24 @@ import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
/**
* A class to queue and send {@link DomainObjectChangeRecord} events.
* <p>
* For simplicity, this class requires all mutations to internal data structures to be locked using
* the internal write lock. Clients are not required to use any synchronization when using this
* class.
* <p>
* Internally, events are queued and will be fired on a timer.
*/
class DomainObjectChangeSupport {
private WeakSet<DomainObjectListener> listeners;
private DomainObject src;
private List<DomainObjectChangeRecord> changesQueue;
private WeakSet<DomainObjectListener> listeners =
WeakDataStructureFactory.createSingleThreadAccessWeakSet();
private List<EventNotification> notificationQueue = new ArrayList<>();
private List<DomainObjectChangeRecord> recordsQueue = new ArrayList<>();
private GhidraTimer timer;
private DomainObject src;
private Lock domainObjectLock;
private Lock writeLock = new Lock("DOCS Change Records Queue Lock");
@ -42,111 +53,92 @@ class DomainObjectChangeSupport {
*
* @param src The object to be put as the src for all events generated.
* @param timeInterval The time (in milliseconds) this object will wait before flushing its
* event buffer. If a new event comes in before the time expires, the timer is reset.
* event buffer. If a new event comes in before the time expires, the timer is reset.
* @param lock the lock used to verify that calls to {@link #flush()} are not performed while a
* lock is held; this is the lock to guard the DB
* lock is held; this is the lock to guard the DB
*/
DomainObjectChangeSupport(DomainObject src, int timeInterval, int bufsize, Lock lock) {
DomainObjectChangeSupport(DomainObject src, int timeInterval, Lock lock) {
this.src = src;
this.src = Objects.requireNonNull(src);
this.domainObjectLock = Objects.requireNonNull(lock);
changesQueue = new ArrayList<>(bufsize);
listeners = WeakDataStructureFactory.createCopyOnWriteWeakSet();
timer = GhidraTimerFactory.getGhidraTimer(timeInterval, timeInterval, () -> sendEventNow());
this.timer =
GhidraTimerFactory.getGhidraTimer(timeInterval, timeInterval, this::sendEventNow);
timer.setInitialDelay(25);
timer.setDelay(500);
timer.setRepeats(true);
}
void addListener(DomainObjectListener listener) {
// Capture the pending event to send to the existing listeners. This prevents the new
// listener from getting events registered before the listener was added.
DomainObjectChangedEvent pendingEvent;
List<DomainObjectListener> previousListeners;
synchronized (src) {
pendingEvent = convertEventQueueRecordsToEvent();
previousListeners = atomicAddListener(listener);
}
/*
* Do outside the synchronized block, so that we do not get this deadlock:
* Thread1 has Domain Lock -> wants AWT lock
* Swing has AWT lock -> wants Domain lock
*/
SystemUtilities.runIfSwingOrPostSwingLater(
() -> notifyEvent(previousListeners, pendingEvent));
}
void removeListener(DomainObjectListener listener) {
synchronized (src) {
listeners.remove(listener);
}
}
// Note: must be called on the Swing thread
private void sendEventNow() {
DomainObjectChangedEvent ev = convertEventQueueRecordsToEvent();
notifyEvent(listeners, ev);
}
List<EventNotification> notifications = withLock(() -> {
private DomainObjectChangedEvent convertEventQueueRecordsToEvent() {
DomainObjectChangedEvent event = lockQueue(() -> {
if (changesQueue.isEmpty()) {
timer.stop();
return null;
}
DomainObjectChangedEvent e = new DomainObjectChangedEvent(src, changesQueue);
changesQueue = new ArrayList<>();
return e;
DomainObjectChangedEvent e = createEventFromQueuedRecords();
notificationQueue.add(new EventNotification(e, new ArrayList<>(listeners.values())));
List<EventNotification> existingNotifications = new ArrayList<>(notificationQueue);
notificationQueue.clear();
return existingNotifications;
});
return event;
for (EventNotification notification : notifications) {
notification.doNotify();
}
}
// This version of notify takes in the listeners to notify so that we can send events to
// some listeners, but not all of them (like flushing when adding new listeners)
private void notifyEvent(Iterable<DomainObjectListener> listenersToNotify,
DomainObjectChangedEvent ev) {
// Note: must be called inside of withLock()
private DomainObjectChangedEvent createEventFromQueuedRecords() {
if (ev == null) {
return; // this implies there we no changes when the timer expired
if (recordsQueue.isEmpty()) {
timer.stop();
return null;
}
DomainObjectChangedEvent e = new DomainObjectChangedEvent(src, recordsQueue);
recordsQueue = new ArrayList<>();
return e;
}
void addListener(DomainObjectListener listener) {
if (isDisposed) {
return;
}
for (DomainObjectListener dol : listenersToNotify) {
try {
dol.domainObjectChanged(ev);
}
catch (Exception exc) {
Msg.showError(this, null, "Error", "Error in Domain Object listener", exc);
}
withLock(() -> {
// Capture the pending event to send to the existing listeners. This prevents the new
// listener from getting events registered before the listener was added.
DomainObjectChangedEvent pendingEvent = createEventFromQueuedRecords();
List<DomainObjectListener> previousListeners = new ArrayList<>(listeners.values());
listeners.add(listener);
notificationQueue.add(new EventNotification(pendingEvent, previousListeners));
timer.start();
});
}
void removeListener(DomainObjectListener listener) {
if (isDisposed) {
return;
}
withLock(() -> listeners.remove(listener));
}
void flush() {
Thread lockOwner = domainObjectLock.getOwner();
if (lockOwner == Thread.currentThread()) {
/*
* We have decided that flushing events with a lock can lead to deadlocks. There
* should be no reason to flush events while holding a lock. This is the
* potential deadlock:
* Thread1 has Domain Lock -> wants AWT lock
* Swing has AWT lock -> wants Domain lock
*/
//
// We have decided that flushing events with a lock can lead to deadlocks. There
// should be no reason to flush events while holding a lock. This is the potential
// deadlock:
// Thread1 has Domain Lock -> wants AWT lock
// Swing has AWT lock -> wants Domain lock
//
throw new IllegalStateException("Cannot call flush() with locks!");
}
SystemUtilities.runSwingNow(() -> sendEventNow());
Swing.runNow(this::sendEventNow);
}
void fireEvent(DomainObjectChangeRecord docr) {
@ -155,15 +147,20 @@ class DomainObjectChangeSupport {
return;
}
lockQueue(() -> {
changesQueue.add(docr);
withLock(() -> {
recordsQueue.add(docr);
timer.start();
});
}
void fatalErrorOccurred(final Throwable t) {
void fatalErrorOccurred(Throwable t) {
List<DomainObjectListener> listenersCopy = new ArrayList<>(listeners.values());
if (isDisposed) {
return;
}
List<DomainObjectListener> listenersCopy =
withLock(() -> new ArrayList<>(listeners.values()));
dispose();
@ -176,42 +173,35 @@ class DomainObjectChangeSupport {
l.domainObjectChanged(ev);
}
catch (Throwable t2) {
// I guess we don't care (probably because some other fatal error has
// already happened)
// We don't care (probably because some other fatal error has already happened)
}
}
};
SystemUtilities.runSwingLater(errorTask);
Swing.runLater(errorTask);
}
void dispose() {
lockQueue(() -> {
isDisposed = true;
timer.stop();
changesQueue.clear();
});
listeners.clear();
}
private List<DomainObjectListener> atomicAddListener(DomainObjectListener l) {
List<DomainObjectListener> previousListeners = new ArrayList<>();
for (DomainObjectListener listener : listeners) {
previousListeners.add(listener);
if (isDisposed) {
return;
}
listeners.add(l);
return previousListeners;
withLock(() -> {
isDisposed = true;
timer.stop();
recordsQueue.clear();
notificationQueue.clear();
listeners.clear();
});
}
//==================================================================================================
//=================================================================================================
// Lock Methods
//==================================================================================================
//=================================================================================================
private void lockQueue(Runnable r) {
// Note: all clients of lockQueue() must not call external APIs that could use locking
private void withLock(Runnable r) {
try {
writeLock.acquire();
@ -222,7 +212,8 @@ class DomainObjectChangeSupport {
}
}
private <T> T lockQueue(Callable<T> c) {
// Note: all clients of lockQueue() must not call external APIs that could use locking
private <T> T withLock(Callable<T> c) {
try {
writeLock.acquire();
@ -241,4 +232,49 @@ class DomainObjectChangeSupport {
writeLock.release();
}
}
//=================================================================================================
// Inner Classes
//=================================================================================================
/**
* This class allows us to bind the given event with the given listeners. This is used to
* send events to the correct listeners as listeners are added. In other words, new listeners
* will not receive pre-existing buffered events. Also, using this class allows us to ensure
* events are processed linearly by processing each of these notification objects linearly
* from a single queue.
*
* Note: this class shall perform no synchronization; that shall be handled by the client
*/
private class EventNotification {
private DomainObjectChangedEvent event;
private List<DomainObjectListener> receivers;
EventNotification(DomainObjectChangedEvent event, List<DomainObjectListener> recievers) {
this.event = event;
this.receivers = recievers;
}
// Note: must be called on the Swing thread; must be called outside of lockQueue()
void doNotify() {
if (isDisposed) {
return;
}
if (event == null) {
return; // this implies there were no changes when the timer expired
}
for (DomainObjectListener dol : receivers) {
try {
dol.domainObjectChanged(event);
}
catch (Exception exc) {
Msg.showError(this, null, "Error", "Error in Domain Object listener", exc);
}
}
}
}
}

View file

@ -1774,7 +1774,7 @@ public class GhidraFileData {
private class GenericDomainObjectDB extends DomainObjectAdapterDB {
protected GenericDomainObjectDB(DBHandle dbh) throws IOException {
super(dbh, "Generic", 500, 1000, GhidraFileData.this);
super(dbh, "Generic", 500, GhidraFileData.this);
loadMetadata();
}

View file

@ -29,7 +29,7 @@ public class DummyDomainObject extends DomainObjectAdapterDB {
}
public DummyDomainObject(String name, Object consumer) throws IOException {
super(new DBHandle(), name, 10, 1, consumer);
super(new DBHandle(), name, 10, consumer);
}
@Override

View file

@ -28,7 +28,7 @@ public class GenericDomainObjectDB extends DomainObjectAdapterDB {
List<String> transactionsList = new ArrayList<String>();
public GenericDomainObjectDB(Object consumer) throws IOException {
super(new DBHandle(), "Generic", 500, 1000, consumer);
super(new DBHandle(), "Generic", 500, consumer);
}
@Override

View file

@ -100,7 +100,7 @@ public class DataTypeArchiveDB extends DomainObjectAdapterDB
*/
public DataTypeArchiveDB(DomainFolder folder, String name, Object consumer)
throws IOException, DuplicateNameException, InvalidNameException {
super(new DBHandle(), name, 500, 1000, consumer);
super(new DBHandle(), name, 500, consumer);
this.name = name;
recordChanges = false;
@ -153,7 +153,7 @@ public class DataTypeArchiveDB extends DomainObjectAdapterDB
public DataTypeArchiveDB(DBHandle dbh, int openMode, TaskMonitor monitor, Object consumer)
throws IOException, VersionException, CancelledException {
super(dbh, "Untitled", 500, 1000, consumer);
super(dbh, "Untitled", 500, consumer);
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
}

View file

@ -218,7 +218,7 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM
*/
public ProgramDB(String name, Language language, CompilerSpec compilerSpec, Object consumer)
throws IOException {
super(new DBHandle(), name, 500, 1000, consumer);
super(new DBHandle(), name, 500, consumer);
if (!(compilerSpec instanceof BasicCompilerSpec)) {
throw new IllegalArgumentException(
@ -287,7 +287,7 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM
public ProgramDB(DBHandle dbh, int openMode, TaskMonitor monitor, Object consumer)
throws IOException, VersionException, LanguageNotFoundException, CancelledException {
super(dbh, "Untitled", 500, 1000, consumer);
super(dbh, "Untitled", 500, consumer);
if (monitor == null) {
monitor = TaskMonitor.DUMMY;

View file

@ -122,7 +122,7 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
}
public ProgramUserDataDB(ProgramDB program) throws IOException {
super(new DBHandle(), getName(program), 500, 1000, program);
super(new DBHandle(), getName(program), 500, program);
this.program = program;
this.language = program.getLanguage();
languageID = language.getLanguageID();
@ -162,7 +162,7 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
public ProgramUserDataDB(DBHandle dbh, ProgramDB program, TaskMonitor monitor)
throws IOException, VersionException, LanguageNotFoundException, CancelledException {
super(dbh, getName(program), 500, 1000, program);
super(dbh, getName(program), 500, program);
this.program = program;
if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR;