il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine.BProgram Maven / Gradle / Ivy
package il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine;
import il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine.tasks.ResumeBThread;
import il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine.tasks.StartBThread;
import il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine.exceptions.BProgramException;
import il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine.jsproxy.BProgramJsProxy;
import il.ac.bgu.cs.bp.bpjs.events.BEvent;
import java.util.*;
import java.util.concurrent.*;
import il.ac.bgu.cs.bp.bpjs.bprogram.runtimeengine.listeners.BProgramListener;
import il.ac.bgu.cs.bp.bpjs.eventselection.EventSelectionResult;
import il.ac.bgu.cs.bp.bpjs.eventselection.EventSelectionStrategy;
import il.ac.bgu.cs.bp.bpjs.eventselection.SimpleEventSelectionStrategy;
import static il.ac.bgu.cs.bp.bpjs.eventsets.Events.all;
import static il.ac.bgu.cs.bp.bpjs.eventsets.Events.emptySet;
import il.ac.bgu.cs.bp.bpjs.exceptions.BPjsCodeEvaluationException;
import il.ac.bgu.cs.bp.bpjs.exceptions.BPjsException;
import il.ac.bgu.cs.bp.bpjs.exceptions.BPjsRuntimeException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import static java.nio.file.Files.readAllBytes;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.util.stream.Collectors.toList;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Scriptable;
import static java.util.stream.Collectors.toSet;
import static java.nio.file.Paths.get;
import static java.util.Collections.reverseOrder;
import java.util.concurrent.atomic.AtomicInteger;
import org.mozilla.javascript.ContinuationPending;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.WrappedException;
/**
* Base class for BPrograms. Contains the logic for managing {@link BThreadSyncSnapshot}s
* and the main event loop. Concrete BProgram extend this class by implementing
* the {@link #setupProgramScope(org.mozilla.javascript.Scriptable)} method.
*
*
* For creating a BProgram that uses a single Javascript file available in the
* classpath, see {@link SingleResourceBProgram}.
*
* @author michael
*/
public abstract class BProgram {
// ------------- Static Members ---------------
/**
* "Poison pill" to insert to the external event queue. Used only to turn the
* daemon mode off.
*/
private static final BEvent NO_MORE_DAEMON = new BEvent("NO_MORE_DAEMON");
/** Counter for giving anonymous instances some semantic name. */
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
// ------------- Instance Members ---------------
/**
* Snapshots of participating BThreads, when in bsync point.
*/
protected Set bthreads;
private Set nextRoundBthreads;
private String name;
/**
* When {@code true}, the BProgram waits for an external event when no
* internal ones are available.
*/
private boolean daemonMode;
private final ExecutorService executor = new ForkJoinPool();
private EventSelectionStrategy eventSelectionStrategy;
/**
* Events are enqueued here by external threads
*/
private final BlockingQueue recentlyEnqueuedExternalEvents = new LinkedBlockingQueue<>();
/**
* At the BProgram's leisure, the external event are moved here, where they
* can be managed.
*/
private final List enqueuedExternalEvents = new LinkedList<>();
/**
* BThreads added between bsyncs are added here.
*/
private final BlockingQueue recentlyRegisteredBthreads = new LinkedBlockingDeque<>();
private final List listeners = new ArrayList<>();
private volatile boolean started = false;
protected Scriptable programScope;
public BProgram() {
this("BProgram-" + INSTANCE_COUNTER.incrementAndGet());
}
public BProgram(String aName) {
this(aName, new SimpleEventSelectionStrategy());
}
public BProgram(String aName, EventSelectionStrategy anEventSelectionStrategy) {
name = aName;
bthreads = new HashSet<>();
eventSelectionStrategy = anEventSelectionStrategy;
}
public void start() throws InterruptedException {
try {
setup();
listeners.forEach(l -> l.started(this));
started = true;
addToNextRound(executor.invokeAll(bthreads.stream()
.map(bt -> new StartBThread(bt))
.collect(toList())));
addToNextRound(startRecentlyRegisteredBThreads());
finalizeRound();
if (bthreads.isEmpty()) {
// super corner case, where no bsyncs were called.
listeners.forEach(l -> l.ended(this));
} else {
do {
mainEventLoop();
} while (isDaemonMode() && waitForExternalEvent());
listeners.forEach(l -> l.ended(this));
}
} catch ( WrappedException we ) {
throw new BProgramException("Failed to start program.", we.getCause());
}
}
/**
* Advances the BProgram a single super-step, that is until there are no
* more internal events that can be selected.
*
* @throws InterruptedException If this thread is interrupted during the BProgram's execution.
*/
protected void mainEventLoop() throws InterruptedException {
boolean go = true;
while (go) {
// 1. Possibly select an event
recentlyEnqueuedExternalEvents.drainTo(enqueuedExternalEvents);
if (enqueuedExternalEvents.remove(NO_MORE_DAEMON)) {
daemonMode = false;
}
Set availableEvents = eventSelectionStrategy.selectableEvents(currentStatements(), enqueuedExternalEvents);
if ( availableEvents.isEmpty() ) {
go = false;
} else {
Optional res = eventSelectionStrategy.select(currentStatements(), enqueuedExternalEvents, availableEvents);
// 2.Trigger the event
if ( res.isPresent() ) {
EventSelectionResult esr = res.get();
try {
esr.getIndicesToRemove().stream().sorted(reverseOrder())
.forEach( idxObj -> enqueuedExternalEvents.remove(idxObj.intValue()) );
triggerEvent(esr.getEvent());
finalizeRound();
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
if ( bthreads.isEmpty() ) {
go = false; // no more BThreads left
}
} else {
go = false;
}
}
}
listeners.forEach(l -> l.superstepDone(this));
}
/**
* Awakens BThreads that waited for/requested this event in their last
* bsync, and waits for them to terminate.
*
* @param selectedEvent The event to trigger. Cannot be {@code null}.
* @throws InterruptedException If this thread is interrupted during the BProgram's execution
*/
protected void triggerEvent( BEvent selectedEvent) throws InterruptedException {
if (selectedEvent == null) {
throw new IllegalArgumentException("Cannot trigger a null event.");
}
listeners.forEach(l -> l.eventSelected(this, selectedEvent));
bthreads.forEach(bt -> {
if (bt.getBSyncStatement() == null) {
System.err.println("SEVERE: " + bt.getName() + " Has null stmt");
}
});
// We are about to execute Javascript code ////////////////
Context ctxt = Context.enter();
Set brokenUpon = bthreads.stream()
.filter(bt -> bt.getBSyncStatement().getInterrupt().contains(selectedEvent))
.collect(toSet());
// Handle breakUpons
if (!brokenUpon.isEmpty()) {
bthreads.removeAll(brokenUpon);
brokenUpon.forEach(bt -> {
listeners.forEach(l -> l.bthreadRemoved(this, bt));
bt.getInterrupt()
.ifPresent( func -> {
final Scriptable scope = bt.getScope();
scope.delete("bsync"); // can't call bsync from a break handler.
try {
ctxt.callFunctionWithContinuations(func, scope, new Object[]{selectedEvent});
} catch ( ContinuationPending ise ) {
throw new BProgramException("Cannot call bsync from a break-upon handler. Consider pushing an external event.");
}
});
});
}
// See who wakes up for the selected event and how skips this round.
Set resumingThisRound = new HashSet<>(bthreads.size());
Set sleepingThisRound = new HashSet<>(bthreads.size());
bthreads.forEach( snapshot -> {
(snapshot.getBSyncStatement().shouldWakeFor(selectedEvent) ? resumingThisRound : sleepingThisRound).add(snapshot);
});
Context.exit();
// Javascript code done ///////////////////////////////////
// add the run results of all those who advance this stage
addToNextRound(executor.invokeAll(resumingThisRound.stream()
.map(bt -> new ResumeBThread(bt, selectedEvent))
.collect(toList())));
// if any new bthreads are added, run and add them
addToNextRound(startRecentlyRegisteredBThreads());
// carry over BThreads that did not advance this round to next round.
nextRoundBthreads.addAll(sleepingThisRound);
}
private List> startRecentlyRegisteredBThreads() throws InterruptedException {
// Setup the new BThread's scopes.
Set newThreads = new HashSet<>(recentlyRegisteredBthreads);
recentlyRegisteredBthreads.clear();
try {
Context cx = ContextFactory.getGlobal().enterContext();
cx.setOptimizationLevel(-1); // must use interpreter mode
newThreads.forEach(this::setupAddedBThread);
} finally {
Context.exit();
}
// run the new BThreads.
final List> result = executor.invokeAll(newThreads.stream()
.map(bt -> new StartBThread(bt))
.filter(Objects::nonNull)
.collect(toList()));
return result;
}
/**
* Loads a Javascript resource (a file that's included in the .jar).
*
* @param pathInJar path of the resource, relative to the class.
*/
public void evaluateResource(String pathInJar) {
try {
final URL resource = Thread.currentThread().getContextClassLoader().getResource(pathInJar);
if (resource == null) {
throw new RuntimeException("Resource '" + pathInJar + "' not found.");
}
evaluateCodeAt(resource.toURI());
} catch (URISyntaxException ex) {
Logger.getLogger(BProgram.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* Reads and evaluates the code pointed by the URI. The resource has to be
* local (either in the file system or in the classpath.
* @param path path to the code to evaluate
* @return Result of code evaluation.
*/
protected Object evaluateCodeAt(URI path) {
Path pathObject = get(path);
try {
String script = new String(readAllBytes(pathObject), StandardCharsets.UTF_8);
return evaluate(script, pathObject.toString());
} catch (IOException e) {
throw new RuntimeException("Error while reading code at '" + path + "': " + e.getMessage(), e);
}
}
/**
* Reads and evaluates the code at the passed input stream. The stream is
* read to its end, but is not closed.
*
* @param inStrm Input stream for reading the script to be evaluated.
* @param scriptName for error reporting purposes.
* @return Result of evaluating the code at {@code inStrm}.
*/
protected Object evaluate(InputStream inStrm, String scriptName) {
InputStreamReader streamReader = new InputStreamReader(inStrm, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(streamReader);
StringBuilder sb = new StringBuilder();
String line;
try {
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
throw new RuntimeException("error while reading javascript from stream", e);
}
String script = sb.toString();
return evaluate(script, scriptName);
}
/**
* Runs the passed code in the passed scope.
* @param script Code to evaluate
* @param scriptName For error reporting purposes.
* @return Result of code evaluation.
*/
protected Object evaluate(String script, String scriptName) {
try {
return Context.getCurrentContext().evaluateString(programScope, script, scriptName, 1, null);
} catch (EcmaError rerr) {
if ( rerr.getErrorMessage().trim().equals("\"bsync\" is not defined.") ) {
throw new BPjsCodeEvaluationException("'bsync' is only defined in BThreads. Did you forget to call 'bp.registerBThread()'?", rerr);
}
throw new BPjsCodeEvaluationException(rerr);
} catch (WrappedException wrapped) {
if ( wrapped.getCause() instanceof BPjsException ) {
throw (BPjsException)wrapped.getCause();
} else {
throw wrapped;
}
} catch (EvaluatorException evalExp) {
throw new BPjsCodeEvaluationException(evalExp);
}
}
/**
* Creates a snapshot of the program, which includes the status of its BThreads,
* and the enqueued external events.
*
*
* This method will produce unexpected results when called while the program
* is not in BSync.
*
* @return A snapshot of the program.
*/
public BProgramSyncSnapshot getSnapshot() {
return new BProgramSyncSnapshot(bthreads, enqueuedExternalEvents);
}
/**
* Registers a BThread into the program. If the program started, the BThread
* will take part in the current bstep.
*
* @param bt the BThread to be registered.
*/
public void registerBThread(BThreadSyncSnapshot bt) {
listeners.forEach(l -> l.bthreadAdded(this, bt));
if (started) {
recentlyRegisteredBthreads.add(bt);
} else {
bthreads.add(bt);
}
}
/**
* Creates a set with the current {@link BSyncStatement}s of the current
* BThreads.
*
* @return Set of current BSyncStatements.
*/
public Set currentStatements() {
return bthreads.stream()
.map(BThreadSyncSnapshot::getBSyncStatement)
.collect(toSet());
}
/**
* Adds an event to {@code this}' external event queue.
*
* @param e The event to add.
*/
public void enqueueExternalEvent(BEvent e) {
recentlyEnqueuedExternalEvents.add(e);
}
/**
* Sets up internal data structures for running.
*/
protected void setup() {
try {
Context cx = ContextFactory.getGlobal().enterContext();
cx.setOptimizationLevel(-1); // must use interpreter mode
setupGlobalScope(cx);
setupBThreadScopes();
} finally {
Context.exit();
}
}
protected void setupAddedBThread(BThreadSyncSnapshot bt) {
bt.setupScope(programScope);
}
protected void setupBThreadScopes() {
bthreads.forEach(bt -> bt.setupScope(programScope));
}
private void setupGlobalScope(Context cx) {
// load and execute globalScopeInit.js
ImporterTopLevel importer = new ImporterTopLevel(cx);
programScope = cx.initStandardObjects(importer);
BProgramJsProxy proxy = new BProgramJsProxy(this);
programScope.put("bp", programScope,
Context.javaToJS(proxy, programScope));
programScope.put("emptySet", programScope,
Context.javaToJS(emptySet, programScope));
programScope.put("all", programScope,
Context.javaToJS(all, programScope));
evaluateResource("globalScopeInit.js");
setupProgramScope(programScope);
}
/**
* The BProgram should set up its scope here. Normally, this amount to
* loading the script with the BThreads.
*
* @param scope the scope to set up.
*/
protected abstract void setupProgramScope(Scriptable scope);
private boolean waitForExternalEvent() throws InterruptedException {
BEvent next = recentlyEnqueuedExternalEvents.take();
if (next == NO_MORE_DAEMON) {
daemonMode = false;
return false;
} else {
enqueuedExternalEvents.add(next);
return true;
}
}
/**
* Adds all the non-empty {@link BThreadSyncSnapshot} from {@code runResults}
* to the next event loop round.
*
* All futures must be done when this method is called. This is the normal
* case for when calling {@link ExecutorService#invokeAll}, so normally not a big requirement.
*
* @param runResults
*/
private void addToNextRound( List> runResults ) {
if ( nextRoundBthreads == null ) {
nextRoundBthreads = new HashSet<>(runResults.size());
}
nextRoundBthreads.addAll(
runResults.stream().map( f -> {
try {
return f.get();
} catch ( InterruptedException | ExecutionException ie ) {
System.out.println("**** Got an excetpion " + ie);
System.out.println("**** Message " + ie.getMessage());
ie.printStackTrace(System.out);
return null;
}})
.filter( Objects::nonNull )
.collect(toList())
);
}
/**
* Prepares {@code this} for the next iteration of the event loop.
*/
void finalizeRound() {
bthreads = nextRoundBthreads;
nextRoundBthreads = null;
}
/**
* Adds a listener to the BProgram.
* @param Actual type of listener.
* @param aListener the listener to add.
* @return The added listener, to allow call chaining.
*/
public R addListener(R aListener) {
listeners.add(aListener);
return aListener;
}
/**
* Removes the listener from the program. If the listener is not registered,
* this call is ignored. In other words, this call is idempotent.
* @param aListener the listener to remove.
*/
public void removeListener(BProgramListener aListener) {
listeners.remove(aListener);
}
/**
* Sets whether this program is a daemon or not. When daemon, program will
* wait for external events even when there are no selectable internal events.
*
* In normal mode ({@code daemon==false}), when no events are available for
* selection, the program terminates.
*
* @param newDaemonMode {@code true} to make the program a daemon,
* {@code false} otherwise.
*/
public void setDaemonMode(boolean newDaemonMode) {
if (daemonMode && !newDaemonMode) {
daemonMode = false;
recentlyEnqueuedExternalEvents.add(NO_MORE_DAEMON);
} else {
daemonMode = newDaemonMode;
}
}
/**
* Returns {@code true} iff the program is in daemon mode. When in this mode,
* the program will not terminate when it has no event available for selection.
* Rather, it will wait for an external event to be enqueued into its external
* event queue.
*
* @return {@code true} if this BProgram is in daemon mode,
* {@code false} otherwise.
* @see #enqueueExternalEvent(il.ac.bgu.cs.bp.bpjs.events.BEvent)
*/
public boolean isDaemonMode() {
return daemonMode;
}
/**
* Returns the program's global scope.
* @return the global scope of the program.
*/
public Scriptable getGlobalScope() {
return programScope;
}
/**
* Returns the snapshots of all current BThreads. This method will only yield
* meaningful results when the program is at BSync state.
* @return snapshots of the current BThreads.
*/
public Set getBThreadSnapshots() {
return bthreads;
}
/**
* Sets the name of the program
* @param name the new program's name
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the program's name
*/
public String getName() {
return name;
}
@Override
public String toString() {
return "[BProgram " + getName() + "]";
}
}