/*******************************************************************************
* Copyright (c) 2015 Bruno Medeiros and other Contributors.
* 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:
* Bruno Medeiros - initial API and implementation
*******************************************************************************/
package melnorme.lang.utils.concurrency;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import melnorme.lang.tooling.common.ops.IOperationMonitor;
import melnorme.utilbox.concurrency.AsyncSupplier;
import melnorme.utilbox.concurrency.CancellableTask;
import melnorme.utilbox.concurrency.OperationCancellation;
import melnorme.utilbox.core.fntypes.CallableX;
import melnorme.utilbox.fields.ListenerListHelper;
/**
* Container for a data object that is created/updated concurrently by an update task.
*
* This container tracks when a new data update is requested,
* marking the current data object as stale until the update task finishes.
*
* The responsibility for actually executing the update task lies externally, not here.
*
* @param SELF must be a subtype of the parameterized class.
*/
public class ConcurrentlyDerivedData<DATA, SELF> {
protected final ListenerListHelper<IDataChangedListener<SELF>> connectedListeners =
new ListenerListHelper<>();
protected final ListenerListHelper<IDataUpdateRequestedListener<SELF>> updateRequestedListeners =
new ListenerListHelper<>();
private DATA data = null;
private DataUpdateTask<DATA> latestUpdateTask = null;
private CountDownLatch latch = new CountDownLatch(0);
public ConcurrentlyDerivedData() {
internalSetData(null);
}
protected void internalSetData(DATA newData) {
this.data = newData;
}
public synchronized DATA getStoredData() {
return data;
}
@SuppressWarnings("unchecked")
protected SELF getSelf() {
return (SELF) this;
}
public synchronized boolean isStale() {
return latestUpdateTask != null;
}
public synchronized boolean isStale(DATA data) {
return isStale() || getStoredData() != data;
}
public synchronized void runSynchronized(Runnable runnable) {
runnable.run();
}
public synchronized <R, E extends Exception> R callSynchronized(CallableX<R, E> callable) throws E {
return callable.call();
}
public synchronized CountDownLatch getLatchForUpdateTask() {
return latch;
}
public synchronized void setUpdateTask(DataUpdateTask<DATA> newUpdateTask) {
assertNotNull(newUpdateTask);
assertTrue(!newUpdateTask.hasExecuted());
assertTrue(latestUpdateTask != newUpdateTask);
if(latestUpdateTask == null) {
assertTrue(latch.getCount() == 0);
latch = new CountDownLatch(1);
} else {
assertTrue(latch.getCount() == 1);
latestUpdateTask.cancel();
}
latestUpdateTask = newUpdateTask;
doHandleDataUpdateRequested();
}
protected void doHandleDataUpdateRequested() {
for(IDataUpdateRequestedListener<SELF> listener : updateRequestedListeners.getListeners()) {
listener.dataUpdateRequested(getSelf());
}
}
public synchronized void setNewData(DATA newData, DataUpdateTask<DATA> updateTask) {
if(latestUpdateTask != updateTask) {
// Ignore, means this update task was cancelled
assertTrue(updateTask.isCancelled());
} else {
try {
internalSetData(newData);
doHandleDataChanged();
} finally {
latestUpdateTask = null;
latch.countDown();
}
}
}
public synchronized void cancelUpdateTask(DataUpdateTask<DATA> updateTask) {
if(latestUpdateTask == updateTask) {
latestUpdateTask = null;
latch.countDown();
}
}
protected void doHandleDataChanged() {
notifyStructureChanged(getSelf(), connectedListeners);
}
/* ----------------- ----------------- */
public static abstract class DataUpdateTask<DATA> extends CancellableTask {
protected final ConcurrentlyDerivedData<DATA, ?> derivedData;
protected final String taskDisplayName;
public DataUpdateTask(ConcurrentlyDerivedData<DATA, ?> derivedData, String taskDisplayName) {
this.taskDisplayName = taskDisplayName;
this.derivedData = derivedData;
}
@Override
public String getTaskDisplayName() {
return taskDisplayName;
}
@Override
protected final void doRun() {
try {
DATA newData = createNewData();
derivedData.setNewData(newData, this);
} catch(OperationCancellation e) {
derivedData.cancelUpdateTask(this);
} catch(RuntimeException e) {
derivedData.setNewData(null, this);
handleRuntimeException(e);
}
}
protected abstract void handleRuntimeException(RuntimeException e);
protected abstract DATA createNewData() throws OperationCancellation;
}
/* ----------------- ----------------- */
/**
* @return a {@link AsyncSupplier} that can be used to wait for the first non-stale data that becomes available.
*/
public DataUpdateFuture getSupplierForNextUpdate() {
return new DataUpdateFuture();
}
public DATA awaitUpdatedData() throws InterruptedException {
return getSupplierForNextUpdate().awaitResult();
}
public DATA awaitUpdatedData(IOperationMonitor om) throws OperationCancellation {
return getSupplierForNextUpdate().awaitResult(om);
}
public class DataUpdateFuture implements AsyncSupplier<DATA> {
public void awaitTermination() throws InterruptedException {
getLatchForUpdateTask().await();
}
public void awaitTermination(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
boolean success = getLatchForUpdateTask().await(timeout, unit);
if(!success) {
throw new TimeoutException();
}
}
@Override
public DATA awaitResult() throws InterruptedException {
awaitTermination();
return data;
}
@Override
public DATA awaitResult(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
awaitTermination(timeout, unit);
return data;
}
}
/* ----------------- ----------------- */
public static interface IDataUpdateRequestedListener<DERIVED_DATA> {
/**
* Indicates that an asynchronous request to change the underlying derived data has been made.
*
* This method runs under a {@link ConcurrentlyDerivedData} lock, so listeners should finish quickly.
*/
void dataUpdateRequested(DERIVED_DATA lockedDerivedData);
}
public static interface IDataChangedListener<DERIVED_DATA> {
/**
* Indicates that underlying derived data has changed.
*
* This method runs under a {@link ConcurrentlyDerivedData} lock, so listeners should finish quickly.
*/
void dataChanged(DERIVED_DATA lockedDerivedData);
}
protected static <DATA> void notifyStructureChanged(final DATA lockedDerivedData,
ListenerListHelper<? extends IDataChangedListener<DATA>> listeners) {
for(IDataChangedListener<DATA> listener : listeners.getListeners()) {
listener.dataChanged(lockedDerivedData);
}
}
}