/**
* Copyright (C) 2010-14 diirt developers. See COPYRIGHT.TXT
* All rights reserved. Use is subject to license terms. See LICENSE.TXT
*/
package org.diirt.datasource;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.diirt.datasource.expression.DesiredRateExpression;
/**
* Orchestrates the different elements of pvmanager to make a reader functional.
* <p>
* This class is responsible for the correct read operation, including:
* <ul>
* <li>Setting up the collector for notifications</li>
* <li>Setting up the collector for connection notification</li>
* <li>Building connection recipes and forwarding them to the datasource<li>
* <li>Managing the scanning task and notification for new values, connection status
* or errors</li>
* <li>Disconnecting the expressions from the datasources if the reader is closed
* or if it's garbage collected</li>
* </ul>
*
* @param <T> value type for the reader managed by this director
* @author carcassi
*/
public class PVDirector<T> {
private static final Logger log = Logger.getLogger(PVDirector.class.getName());
// Required for connection and exception notification
/** Executor used to notify of new values/connection/exception */
private final Executor notificationExecutor;
/** Executor used to scan the connection/exception queues */
private final ScheduledExecutorService scannerExecutor;
/** PVReader to update during the notification */
private final WeakReference<PVReaderImpl<T>> pvReaderRef;
/** Function for the new value */
private final ReadFunction<T> readFunction;
/** Creation for stack trace */
private final Exception creationStackTrace = new Exception("PV was never closed (stack trace for creation)");
/** Used to ignore duplicated errors */
private final AtomicReference<Exception> previousCalculationException = new AtomicReference<>();
// Required to connect/disconnect expressions
private final DataSource dataSource;
private final Object lock = new Object();
private final Map<DesiredRateExpression<?>, ReadRecipe> readRecipies =
new HashMap<>();
private SourceDesiredRateDecoupler scanStrategy;
// Required for multiple operations
/** Connection collector required to connect/disconnect expressions and for connection notification */
private final ConnectionCollector readConnCollector =
new ConnectionCollector();
/** Exception queue to be used to connect/disconnect expression and for exception notification */
private final QueueCollector<Exception> readExceptionCollector;
void setScanner(final SourceDesiredRateDecoupler scanStrategy) {
synchronized(lock) {
this.scanStrategy = scanStrategy;
}
readExceptionCollector.setChangeNotification(new Runnable() {
@Override
public void run() {
scanStrategy.newReadExceptionEvent();
}
});
readConnCollector.setChangeNotification(new Runnable() {
@Override
public void run() {
scanStrategy.newReadConnectionEvent();
}
});
}
public void registerCollector(Collector<?, ?> collector) {
collector.setChangeNotification(new Runnable() {
@Override
public void run() {
if (scanStrategy != null) {
scanStrategy.newValueEvent();
}
}
});
}
ReadRecipe getCurrentReadRecipe() {
ReadRecipeBuilder builder = new ReadRecipeBuilder();
for (Map.Entry<DesiredRateExpression<?>, ReadRecipe> entry : readRecipies.entrySet()) {
ReadRecipe readRecipe = entry.getValue();
for (ChannelReadRecipe channelReadRecipe : readRecipe.getChannelReadRecipes()) {
builder.addChannel(channelReadRecipe.getChannelName(), channelReadRecipe.getReadSubscription().getValueCache());
}
}
return builder.build(readExceptionCollector, readConnCollector);
}
/**
* Connects the given expression.
* <p>
* This can be used for dynamic expression to add and connect child expressions.
* The added expression will be automatically closed when the associated
* reader is closed, if it's not disconnected first.
*
* @param expression the expression to connect
*/
public void connectReadExpression(DesiredRateExpression<?> expression) {
ReadRecipeBuilder builder = new ReadRecipeBuilder();
expression.fillReadRecipe(this, builder);
ReadRecipe recipe = builder.build(readExceptionCollector, readConnCollector);
synchronized(lock) {
readRecipies.put(expression, recipe);
}
if (!recipe.getChannelReadRecipes().isEmpty()) {
try {
dataSource.connectRead(recipe);
} catch(Exception ex) {
recipe.getChannelReadRecipes().iterator().next().getReadSubscription().getExceptionWriteFunction().writeValue(ex);
}
}
}
/**
* Simulate a static connection in which the channel has one exception
* and the connection will never change.
* <p>
* This is a temporary method an will be subject to change in the future.
* The aim is to allow to connect expressions that are not channels
* but can influence exception and connection state. For example,
* to report problems encountered during expression creation as runtime
* problems through the normal exception/connection methods.
* <p>
* In the future, this should be generalized to allow fully fledged expressions
* that connect/disconnect and can report errors.
*
* @param ex the exception to queue
* @param connection the connection flag
* @param channelName the channel name
*/
public void connectStaticRead(Exception ex, boolean connection, String channelName) {
readExceptionCollector.writeValue(ex);
readConnCollector.addChannel(channelName).writeValue(connection);
}
/**
* Disconnects the given expression.
* <p>
* This can be used for dynamic expression, to remove and disconnects child
* expressions.
*
* @param expression the expression to disconnect
*/
public void disconnectReadExpression(DesiredRateExpression<?> expression) {
ReadRecipe recipe;
synchronized(lock) {
recipe = readRecipies.remove(expression);
}
if (recipe == null) {
log.log(Level.SEVERE, "Director was asked to disconnect expression '" + expression + "' which was not found.");
}
if (!recipe.getChannelReadRecipes().isEmpty()) {
try {
for (ChannelReadRecipe channelRecipe : recipe.getChannelReadRecipes()) {
readConnCollector.removeChannel(channelRecipe.getChannelName());
}
dataSource.disconnectRead(recipe);
} catch(Exception ex) {
recipe.getChannelReadRecipes().iterator().next().getReadSubscription().getExceptionWriteFunction().writeValue(ex);
}
}
}
private volatile boolean closed = false;
void close() {
closed = true;
disconnect();
}
/**
* Close and disconnects all the child expressions.
*/
private void disconnect() {
synchronized(lock) {
while (!readRecipies.isEmpty()) {
DesiredRateExpression<?> expression = readRecipies.keySet().iterator().next();
disconnectReadExpression(expression);
}
}
}
/**
* Creates a new notifier. The new notifier will notifier the given pv
* with new values calculated by the function, and will use onThread to
* perform the notifications.
* <p>
* After construction, one MUST set the pvRecipe, so that the
* dataSource is appropriately closed.
*
* @param pv the pv on which to notify
* @param function the function used to calculate new values
* @param notificationExecutor the thread switching mechanism
*/
PVDirector(PVReaderImpl<T> pv, ReadFunction<T> function, ScheduledExecutorService scannerExecutor,
Executor notificationExecutor, DataSource dataSource, ExceptionHandler exceptionHandler) {
this.pvReaderRef = new WeakReference<>(pv);
this.readFunction = function;
this.notificationExecutor = notificationExecutor;
this.scannerExecutor = scannerExecutor;
this.dataSource = dataSource;
if (exceptionHandler == null) {
readExceptionCollector = new QueueCollector<>(1);
} else {
readExceptionCollector = new LastExceptionCollector(1, exceptionHandler);
}
}
/**
* Determines whether the notifier is active or not.
* <p>
* The notifier becomes inactive if the PVReader is closed or is garbage collected.
* The first time this function determines that the notifier is inactive,
* it will ask the data source to close all channels relative to the
* pv.
*
* @return true if new notification should be performed
*/
private boolean isActive() {
// Making sure to get the reference once for thread safety
final PVReader<T> pv = pvReaderRef.get();
if (pv != null && !pv.isClosed()) {
return true;
} else if (pv == null && closed != true) {
log.log(Level.WARNING, "PVReader wasn't properly closed and it was garbage collected. Closing the associated connections...", creationStackTrace);
return false;
} else {
return false;
}
}
void pause() {
scanStrategy.pause();
}
void resume() {
scanStrategy.resume();
}
private volatile boolean notificationInFlight = false;
/**
* Notifies the PVReader of a new value.
*/
private void notifyPv() {
// Don't even calculate if notification is in flight.
// This makes pvManager automatically throttle back if the consumer
// is slower than the producer.
if (notificationInFlight)
return;
// Calculate new value
T newValue = null;
Exception calculationException = null;
boolean calculationSucceeded = false;
try {
// Tries to calculate the value
newValue = readFunction.readValue();
if (newValue != null) {
NotificationSupport.findNotificationSupportFor(newValue);
}
calculationSucceeded = true;
} catch (RuntimeException ex) {
// Calculation failed
Exception previousException = previousCalculationException.get();
if (previousException == null ||
!ex.getClass().equals(previousException.getClass()) ||
!ex.getMessage().equals(previousException.getMessage())) {
calculationException = ex;
previousCalculationException.set(ex);
}
} catch (Throwable ex) {
log.log(Level.SEVERE, "Unrecoverable error during scanning", ex);
}
// Calculate new connection
final boolean connected = readConnCollector.readValue();
List<Exception> exceptions = readExceptionCollector.readValue();
final Exception lastException;
if (calculationException != null) {
lastException = calculationException;
} else if (exceptions.isEmpty()) {
lastException = null;
} else {
lastException = exceptions.get(exceptions.size() - 1);
}
// TODO: if payload is immutable, the difference test should be done here
// and not in the runnable (to save SWT time)
// Prepare values to ship to the other thread.
// The data will be shipped as part of the task,
// which is properly synchronized by the executor
final T finalValue = newValue;
final boolean finalCalculationSucceeded = calculationSucceeded;
notificationInFlight = true;
notificationExecutor.execute(new Runnable() {
@Override
public void run() {
try {
PVReaderImpl<T> pv = pvReaderRef.get();
// Proceed with notification only if PVReader was not garbage
// collected
if (pv != null) {
// Atomicity guaranteed by:
// - all the modification on the PVReader
// are done here, on the same thread where the listeners will be called.
// This means the callbacks are guaranteed to run after all
// changes are done
// - notificationInFlight guarantees that no other notification
// will run while one notification is running. This means
// the next event is serialized after the end of this one.
pv.setConnected(connected);
if (lastException != null) {
if (lastException instanceof TimeoutException &&
(connected || finalValue != null)) {
// Skip TimeoutExceptions if we are connected and/or
// have a value
} else {
pv.setLastException(lastException);
}
}
// XXX Are we sure that we should skip notifications if values are null?
if (finalCalculationSucceeded && finalValue != null) {
Notification<T> notification =
NotificationSupport.notification(pv.getValue(), finalValue);
// Remember to notify anyway if an exception need to be notified
if (notification.isNotificationNeeded()) {
pv.setValue(notification.getNewValue());
} else if (pv.isLastExceptionToNotify() || pv.isReadConnectionToNotify()) {
pv.firePvValueChanged();
}
} else {
// Remember to notify anyway if an exception need to be notified
if (pv.isLastExceptionToNotify() || pv.isReadConnectionToNotify()) {
pv.firePvValueChanged();
}
}
}
} finally {
notificationInFlight = false;
scanStrategy.readyForNextEvent();
}
}
});
}
/**
* Posts a readTimeout exception in the exception queue.
*
* @param timeoutMessage the message for the readTimeout
*/
private void processReadTimeout(String timeoutMessage) {
PVReaderImpl<T> pv = pvReaderRef.get();
if (pv != null && !pv.isSentFirsEvent()) {
readExceptionCollector.writeValue(new TimeoutException(timeoutMessage));
}
}
void readTimeout(Duration timeout, final String timeoutMessage) {
scannerExecutor.schedule(new Runnable() {
@Override
public void run() {
processReadTimeout(timeoutMessage);
}
}, timeout.toNanos(), TimeUnit.NANOSECONDS);
}
private final DesiredRateEventListener desiredRateEventListener = new DesiredRateEventListener() {
@Override
public void desiredRateEvent(DesiredRateEvent event) {
if (isActive()) {
notifyPv();
} else {
close();
}
}
};
DesiredRateEventListener getDesiredRateEventListener() {
return desiredRateEventListener;
}
}