diff --git a/src/main/java/com/laytonsmith/PureUtilities/Quadruplet.java b/src/main/java/com/laytonsmith/PureUtilities/Quadruplet.java new file mode 100644 index 000000000..fa81d6014 --- /dev/null +++ b/src/main/java/com/laytonsmith/PureUtilities/Quadruplet.java @@ -0,0 +1,94 @@ +package com.laytonsmith.PureUtilities; + +import java.util.Objects; + +/** + * Creates an object quadruplet. The hashcode and equals functions have been overridden to use the underlying object's hash + * code and equals combined. The underlying objects may be null. + * + * @param The first object's type. + * @param The second object's type. + * @param The third object's type. + * @param The fourth object's type. + */ +public class Quadruplet { + + private final A fst; + private final B snd; + private final C trd; + private final D frth; + + /** + * Creates a new Quadruplet with the specified values. + * + * @param a + * @param b + * @param c + * @param d + */ + public Quadruplet(A a, B b, C c, D d) { + fst = a; + snd = b; + trd = c; + frth = d; + } + + @Override + public String toString() { + return "<" + + Objects.toString(fst) + ", " + + Objects.toString(snd) + ", " + + Objects.toString(trd) + ", " + + Objects.toString(frth) + ">"; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 47 * hash + Objects.hashCode(this.fst); + hash = 47 * hash + Objects.hashCode(this.snd); + hash = 47 * hash + Objects.hashCode(this.trd); + hash = 47 * hash + Objects.hashCode(this.frth); + return hash; + } + + @Override + public boolean equals(Object obj) { + if(obj == null) { + return false; + } + if(getClass() != obj.getClass()) { + return false; + } + final Quadruplet other = (Quadruplet) obj; + if(!Objects.equals(this.fst, other.fst)) { + return false; + } + if(!Objects.equals(this.snd, other.snd)) { + return false; + } + if(!Objects.equals(this.trd, other.trd)) { + return false; + } + if(!Objects.equals(this.frth, other.frth)) { + return false; + } + return true; + } + + public A getFirst() { + return fst; + } + + public B getSecond() { + return snd; + } + + public C getThird() { + return trd; + } + + public D getFourth() { + return frth; + } +} diff --git a/src/main/java/com/laytonsmith/abstraction/AbstractConvertor.java b/src/main/java/com/laytonsmith/abstraction/AbstractConvertor.java index be3b6520e..24fd05dba 100644 --- a/src/main/java/com/laytonsmith/abstraction/AbstractConvertor.java +++ b/src/main/java/com/laytonsmith/abstraction/AbstractConvertor.java @@ -13,16 +13,23 @@ import java.util.TimerTask; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; public abstract class AbstractConvertor implements Convertor { private final List shutdownHooks = new ArrayList<>(); + private final List persistentShutdownHooks = new ArrayList<>(); @Override public void addShutdownHook(Runnable r) { shutdownHooks.add(r); } + @Override + public void addPersistentShutdownHook(Runnable r) { + persistentShutdownHooks.add(r); + } + @Override public void runShutdownHooks() { // Fire off the shutdown event, before we shut down all the internal hooks @@ -35,9 +42,21 @@ public Object _GetObject() { }); Iterator iter = shutdownHooks.iterator(); while(iter.hasNext()) { - iter.next().run(); + try { + iter.next().run(); + } catch(Error e) { + Logger.getLogger(AbstractConvertor.class.getName()).severe(e.getMessage()); + } iter.remove(); } + + for(Runnable r : persistentShutdownHooks) { + try { + r.run(); + } catch(Error e) { + Logger.getLogger(AbstractConvertor.class.getName()).severe(e.getMessage()); + } + } } /** diff --git a/src/main/java/com/laytonsmith/abstraction/Convertor.java b/src/main/java/com/laytonsmith/abstraction/Convertor.java index bfac3e234..e3cda3041 100644 --- a/src/main/java/com/laytonsmith/abstraction/Convertor.java +++ b/src/main/java/com/laytonsmith/abstraction/Convertor.java @@ -131,14 +131,26 @@ public interface Convertor { MCInventoryHolder CreateInventoryHolder(String id, String title); /** - * Run whenever the server is shutting down (or restarting). There is no guarantee provided as to what thread the + * Runs whenever the server is shutting down (or reloading). There is no guarantee provided as to what thread the * runnables actually run on, so you should ensure that the runnable executes it's actions on the appropriate thread - * yourself. + * yourself. Note that this shutdown hook will only run once, so if multiple reload events occur, this will not + * be registered for the second run, unless you specifically add it yourself. If you need a shutdown hook to run + * every time, and only want to register it once, see {@link #addPersistentShutdownHook}. * * @param r */ void addShutdownHook(Runnable r); + /** + * Runs whenever the server is shutting down (or reloading). There is no guarantee provided as to what thread the + * runnables actually run on, so you should ensure that the runnable executes it's actions on the appropriate thread + * yourself. Note that this shutdown hook will never dequeue, so if multiple reload events occur, this will run + * every time, and so you should only call this once (i.e. from a static context). If you need a shutdown hook to + * dequeue after run, see {@link #addShutdownHook}. + * @param r + */ + void addPersistentShutdownHook(Runnable r); + /** * Runs all the registered shutdown hooks. This should only be called by the shutdown mechanism. After running, each * Runnable will be removed from the queue. @@ -294,4 +306,13 @@ public interface Convertor { * @return */ public MCTransformation GetTransformation(Quaternionf leftRotation, Quaternionf rightRotation, Vector3f scale, Vector3f translation); + + /** + * Returns true if this is the main thread of the application. This is only applicable in some managed environments, + * in other environments where this doesn't matter, this will always return false (i.e. all threads are considered + * equally important/unimportant). If this returns true, this means that the current thread is for instance the + * UI thread, and thus should not be blocked on. + * @return + */ + public boolean IsMainThread(); } diff --git a/src/main/java/com/laytonsmith/abstraction/StaticLayer.java b/src/main/java/com/laytonsmith/abstraction/StaticLayer.java index 9ed07c5ec..616f9d358 100644 --- a/src/main/java/com/laytonsmith/abstraction/StaticLayer.java +++ b/src/main/java/com/laytonsmith/abstraction/StaticLayer.java @@ -166,6 +166,10 @@ public static MCPlugin GetPlugin() { return convertor.GetPlugin(); } + public static boolean IsMainThread() { + return convertor.IsMainThread(); + } + public static synchronized Convertor GetConvertor() { return convertor; } diff --git a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitConvertor.java b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitConvertor.java index a9f5058a7..608099ebc 100644 --- a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitConvertor.java +++ b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitConvertor.java @@ -910,4 +910,10 @@ public MCNamespacedKey GetNamespacedKey(String key) { public MCTransformation GetTransformation(Quaternionf leftRotation, Quaternionf rightRotation, Vector3f scale, Vector3f translation) { return new BukkitMCTransformation(new Transformation(translation, leftRotation, scale, rightRotation)); } + + @Override + public boolean IsMainThread() { + return Bukkit.isPrimaryThread(); + } + } diff --git a/src/main/java/com/laytonsmith/core/functions/ResourceManager.java b/src/main/java/com/laytonsmith/core/functions/ResourceManager.java index 56e47f6d2..0b74eec87 100644 --- a/src/main/java/com/laytonsmith/core/functions/ResourceManager.java +++ b/src/main/java/com/laytonsmith/core/functions/ResourceManager.java @@ -62,7 +62,7 @@ public static ResourceTypes getResourceByType(Class type) { private static final Map> RESOURCES = new HashMap<>(); static { - StaticLayer.GetConvertor().addShutdownHook(new Runnable() { + StaticLayer.GetConvertor().addPersistentShutdownHook(new Runnable() { @Override public void run() { diff --git a/src/main/java/com/laytonsmith/core/functions/Threading.java b/src/main/java/com/laytonsmith/core/functions/Threading.java index afa5ab177..3186c36d7 100644 --- a/src/main/java/com/laytonsmith/core/functions/Threading.java +++ b/src/main/java/com/laytonsmith/core/functions/Threading.java @@ -1,6 +1,7 @@ package com.laytonsmith.core.functions; import com.laytonsmith.PureUtilities.DaemonManager; +import com.laytonsmith.PureUtilities.Triplet; import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.abstraction.Implementation; import com.laytonsmith.abstraction.StaticLayer; @@ -15,7 +16,8 @@ import com.laytonsmith.core.compiler.BranchStatement; import com.laytonsmith.core.compiler.SelfStatement; import com.laytonsmith.core.compiler.VariableScope; -import com.laytonsmith.core.constructs.CArray; +import com.laytonsmith.core.compiler.signature.FunctionSignatures; +import com.laytonsmith.core.compiler.signature.SignatureBuilder; import com.laytonsmith.core.constructs.CBoolean; import com.laytonsmith.core.constructs.CNull; import com.laytonsmith.core.constructs.CString; @@ -24,6 +26,7 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.StaticRuntimeEnv; import com.laytonsmith.core.exceptions.CRE.CRECastException; +import com.laytonsmith.core.exceptions.CRE.CREIllegalArgumentException; import com.laytonsmith.core.exceptions.CRE.CREInterruptedException; import com.laytonsmith.core.exceptions.CRE.CRENullPointerException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; @@ -33,13 +36,17 @@ import com.laytonsmith.core.exceptions.LoopManipulationException; import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; import com.laytonsmith.core.natives.interfaces.Mixed; - -import java.util.List; +import com.laytonsmith.core.natives.interfaces.ValueType; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import java.util.Map.Entry; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Random; import java.util.concurrent.Callable; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** * @@ -348,13 +355,105 @@ public Version since() { } + /** + * Contains the queue of callables that are waiting on the lock. + */ + private static final Map>> SYNC_OBJECT_QUEUE = new HashMap<>(); + /** + * Contains a mapping from Lock object, to references to the lock. For internal synchronization purposes, + * this value should always be used for synchronization. + */ + private static final Map SYNC_OBJECT_MAP = new HashMap<>(); + /** + * Contains a mapping from code object to lock. + */ + private static final Map SYNC_OBJECT_LOCKS = new HashMap<>(); + + private static Lock getSyncObject(Mixed cSyncObject, Function f, Target t) { + + if(cSyncObject instanceof CNull || cSyncObject == null) { + throw new CRENullPointerException("Synchronization object may not be null in " + f.getName() + "().", t); + } + Object syncObject = cSyncObject; + if(cSyncObject.isInstanceOf(ValueType.TYPE)) { + if(!(cSyncObject instanceof CString)) { + throw new CREIllegalArgumentException("Only strings and non-value types can be used for synchronization.", t); + } + syncObject = cSyncObject.val(); + } + + // Add String sync objects to the map to be able to synchronize by value. + synchronized(SYNC_OBJECT_MAP) { + Lock lock = SYNC_OBJECT_LOCKS.computeIfAbsent(syncObject, (x) -> { + return new ReentrantLock(); + }); + int currentCount = SYNC_OBJECT_MAP.computeIfAbsent(lock, x -> 0); + SYNC_OBJECT_MAP.put(lock, currentCount + 1); + return lock; + } + } + + private static void cleanupSync(Lock syncObject) { + // Remove 1 from the call count or remove the sync object from the map if it was a sync-by-value. + synchronized(SYNC_OBJECT_MAP) { + int count = SYNC_OBJECT_MAP.get(syncObject); // This should never return null. + if(count <= 1) { + SYNC_OBJECT_MAP.remove(syncObject); + SYNC_OBJECT_LOCKS.remove(syncObject); + SYNC_OBJECT_QUEUE.remove(syncObject); + } else { + SYNC_OBJECT_MAP.put(syncObject, count - 1); + } + + } + } + + static { + StaticLayer.GetConvertor().addPersistentShutdownHook(() -> { + SYNC_OBJECT_QUEUE.clear(); + }); + } + + private static void PumpQueue(Lock syncObject, DaemonManager dm) { + dm.activateThread(Thread.currentThread()); + try { + if(syncObject.tryLock()) { + try { + Triplet triplet; + synchronized(SYNC_OBJECT_MAP) { + triplet = SYNC_OBJECT_QUEUE.get(syncObject).poll(); + } + triplet.getFirst().executeCallable(triplet.getSecond(), triplet.getThird()); + synchronized(SYNC_OBJECT_MAP) { + if(!SYNC_OBJECT_QUEUE.get(syncObject).isEmpty()) { + StaticLayer.SetFutureRunnable(dm, 0, () -> PumpQueue(syncObject, dm)); + } + } + } finally { + cleanupSync(syncObject); + syncObject.unlock(); + } + } else { + int backoff = java.lang.Math.abs(new Random().nextInt()) % 100; + StaticLayer.SetFutureRunnable(dm, + backoff, () -> { + PumpQueue(syncObject, dm); + }); + } + } finally { + dm.deactivateThread(Thread.currentThread()); + } + } + @api @noboilerplate - @seealso({x_new_thread.class}) + @seealso({x_new_thread.class, x_get_lock.class}) @SelfStatement public static class _synchronized extends AbstractFunction implements VariableScope, BranchStatement { - private static final Map SYNC_OBJECT_MAP = new HashMap<>(); + @Override public Class[] thrown() { @@ -390,59 +489,15 @@ public Mixed execs(Target t, Environment env, Script parent, ParseTree... nodes) // Get the sync object (CArray or String value of the Mixed). Mixed cSyncObject = parent.seval(syncObjectTree, env); - if(cSyncObject instanceof CNull || cSyncObject == null) { - throw new CRENullPointerException("Synchronization object may not be null in " + getName() + "().", t); - } - Object syncObject; - if(cSyncObject.isInstanceOf(CArray.TYPE)) { - syncObject = cSyncObject; - } else { - syncObject = cSyncObject.val(); - } - - // Add String sync objects to the map to be able to synchronize by value. - if(syncObject instanceof String) { - synchronized(SYNC_OBJECT_MAP) { - searchLabel: - { - for(Entry entry : SYNC_OBJECT_MAP.entrySet()) { - Object key = entry.getKey(); - if(key instanceof String && key.equals(syncObject)) { - syncObject = key; // Get reference, value of this assign is the same. - entry.setValue(entry.getValue() + 1); - break searchLabel; - } - } - SYNC_OBJECT_MAP.put(syncObject, 1); - } - } - } + Lock syncObject = getSyncObject(cSyncObject, this, t); // Evaluate the code, synchronized by the passed sync object. try { - synchronized(syncObject) { - parent.eval(code, env); - } - } catch (RuntimeException e) { - throw e; + syncObject.lock(); + parent.eval(code, env); } finally { - - // Remove 1 from the call count or remove the sync object from the map if it was a sync-by-value. - if(syncObject instanceof String) { - synchronized(SYNC_OBJECT_MAP) { - int count = SYNC_OBJECT_MAP.get(syncObject); // This should never return null. - if(count <= 1) { - SYNC_OBJECT_MAP.remove(syncObject); - } else { - for(Entry entry : SYNC_OBJECT_MAP.entrySet()) { - if(entry.getKey() == syncObject) { // Equals by reference. - entry.setValue(count - 1); - break; - } - } - } - } - } + syncObject.unlock(); + cleanupSync(syncObject); } return CVoid.VOID; } @@ -469,7 +524,12 @@ public String docs() { + " This means that if two threads will call " + getName() + "('example', <code>), the second" + " call will hang the thread until the passed code of the first call has finished executing." + " If you call this function from within this function on the same thread using the same" - + " syncObject, the code will simply be executed." + + " syncObject, the code will simply be executed. Note that this uses the same pool of lock" + + " objects as x_get_lock, except this can be used off the main thread, whereas x_get_lock is" + + " preferred on the main thread. Generally speaking, it is almost always incorrect to use this" + + " on the main thread, though there is no technical restriction to doing so. Other than strings," + + " ValueTypes cannot be used as the lock object, and reference types such as an array or other" + + " object is required." + " For more information about synchronization, see:" + " https://en.wikipedia.org/wiki/Synchronization_(computer_science)"; } @@ -541,6 +601,96 @@ public List isBranch(List children) { } } + @api + @noboilerplate + @seealso({_synchronized.class, x_get_lock.class}) + public static class x_get_lock extends AbstractFunction { + + @Override + public Class[] thrown() { + return new Class[]{CRENullPointerException.class}; + } + + @Override + public boolean isRestricted() { + return true; + } + + @Override + public Boolean runAsync() { + return false; + } + + @Override + public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { + // Get the sync object tree and the code to synchronize. + Mixed cSyncObject = args[0]; + com.laytonsmith.core.natives.interfaces.Callable callable + = ArgumentValidation.getObject(args[1], t, com.laytonsmith.core.natives.interfaces.Callable.class); + + // Get the sync object (CArray or String value of the Mixed). + Lock syncObject = getSyncObject(cSyncObject, this, t); + + // Evaluate the code, synchronized by the passed sync object. + DaemonManager dm = env.getEnv(StaticRuntimeEnv.class).GetDaemonManager(); + Triplet triplet + = new Triplet<>(callable, env, t); + synchronized(SYNC_OBJECT_MAP) { + SYNC_OBJECT_QUEUE.computeIfAbsent(syncObject, k -> new LinkedList<>()) + .add(triplet); + } + StaticLayer.SetFutureRunnable(dm, 0, () -> PumpQueue(syncObject, dm)); + return CVoid.VOID; + } + + + + @Override + public String getName() { + return "x_get_lock"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "void {mixed lock, Callable action} Runs the specified action on the main thread (or a timer thread in cmdline) once the lock is obtained. Note that" + + " this lock is the same object as used in synchronized(). ---- " + + " The primary difference being that this" + + " function always returns immediately, scheduling the task for later (as soon as possible, but" + + " with a small, random backoff), whereas synchronized blocks until the lock is obtained. This" + + " is an appropriate call to use when running on the main thread, though it can also be used" + + " off the main thread as well, though note that regardless of what thread this is started" + + " on, it always runs the Callable on the main thread. The lock is re-entrant, but as the" + + " function always runs the Callable at some future point, this only matters when used in" + + " conjunction with synchronized().\n\n" + + "Note that in general, the queue of actions is unbounded, but will perform operations in a FIFO" + + " pattern. This is prone to overflowing though, and should not be used for large amounts of" + + " inputs. A good example of use for this function is when there is a synchronized block that runs" + + " on an external thread, but some sort of relatively infrequent player input has critical sections" + + " that must be locked against the same lock. Other than strings," + + " ValueTypes cannot be used as the lock object, and reference types such as an array or other" + + " object is required."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + @Override + public FunctionSignatures getSignatures() { + return new SignatureBuilder(CVoid.TYPE) + .param(Mixed.TYPE, "syncObject", "The object to sync on. This uses the same object pool as synchronized().") + .param(com.laytonsmith.core.natives.interfaces.Callable.TYPE, "action", "The action to run once the lock is obtained.") + .build(); + } + + } + @api public static class x_thread_join extends AbstractFunction { diff --git a/src/main/java/com/laytonsmith/core/functions/XGUI.java b/src/main/java/com/laytonsmith/core/functions/XGUI.java index 9cd595efc..326cb1113 100644 --- a/src/main/java/com/laytonsmith/core/functions/XGUI.java +++ b/src/main/java/com/laytonsmith/core/functions/XGUI.java @@ -45,7 +45,7 @@ public static String docs() { private static final AtomicInteger WINDOW_IDS = new AtomicInteger(0); static { - StaticLayer.GetConvertor().addShutdownHook(new Runnable() { + StaticLayer.GetConvertor().addPersistentShutdownHook(new Runnable() { @Override public void run() { diff --git a/src/main/java/com/laytonsmith/tools/Interpreter.java b/src/main/java/com/laytonsmith/tools/Interpreter.java index e1d7eca21..63a3c5d6e 100644 --- a/src/main/java/com/laytonsmith/tools/Interpreter.java +++ b/src/main/java/com/laytonsmith/tools/Interpreter.java @@ -1306,6 +1306,12 @@ public MCNamespacedKey GetNamespacedKey(String key) { public MCTransformation GetTransformation(Quaternionf leftRotation, Quaternionf rightRotation, Vector3f scale, Vector3f translation) { throw new UnsupportedOperationException("This method is not supported from a shell."); } + + @Override + public boolean IsMainThread() { + return false; + } + } } diff --git a/src/test/java/com/laytonsmith/testing/StaticTest.java b/src/test/java/com/laytonsmith/testing/StaticTest.java index d2d138483..df4e1842b 100644 --- a/src/test/java/com/laytonsmith/testing/StaticTest.java +++ b/src/test/java/com/laytonsmith/testing/StaticTest.java @@ -902,6 +902,12 @@ public MCNamespacedKey GetNamespacedKey(String key) { public MCTransformation GetTransformation(Quaternionf leftRotation, Quaternionf rightRotation, Vector3f scale, Vector3f translation) { throw new UnsupportedOperationException("Not supported yet."); } + + @Override + public boolean IsMainThread() { + return false; + } + } public static class FakeServerMixin implements EventMixinInterface {