Managed Resources Engine (#1194)

This commit is contained in:
Marcin Kostrzewa 2020-10-06 15:47:06 +02:00 committed by GitHub
parent f00b187438
commit 2a6dbf0eda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 800 additions and 79 deletions

View File

@ -0,0 +1,108 @@
---
layout: developer-doc
title: Managed Resources
category: semantics
tags: [resources, finalization, cleanup]
order: 9
---
# Managed Resources
Enso is a language targeting an audience with possibly low programming skills
and aims to be as user-friendly as possible. Therefore, it is crucial to provide
some mechanisms to automatically clean up unclosed resources (such as file
handles, sockets, machine pointers, etc.). The Managed Resources system solves
this problem, by allowing library authors to attach garbage collection hooks to
certain objects, such that a clean up action can be performed as soon as the
runtime discovers the resource will never be used again. This document outlines
the behavior of this system, as well as important notes regarding its use.
<!-- MarkdownTOC levels="2,3" autolink="true" -->
- [Basic Usage](#basic-usage)
- [Semantics and Guarantees](#semantics-and-guarantees)
- [Execution Guarantess](#execution-guarantees)
- [Multiple Managed Resources Wrapping The Same Underlying Resource](#multiple-managed-resources-wrapping-the-same-underlying-resource)
- [Thread Safety](#thread-safety)
<!-- /MarkdownTOC -->
## Basic Usage
A new managed resource is created by calling the
`Managed_Resource.register resource finalizer` method. The `resource` is the
object being finalized and `finalizer` is a one-argument function that will be
passed the `resource` upon finalization. this call returns an object wrapping
the original resource. The underlying resource will be finalized as soon as the
_returned wrapper_ is garbage collected. It is therefore crucial to stop using
`resource` right after the call, as it may be finalized at any point after this
call.
> #### Important
>
> Due to the limitations of current implementation of Enso, the `finalizer`
> passed to `Managed_Resource.register` must not be a lambda. This is because
> lambdas implicitly capture the whole lexical scope they are defined in, so in
> `res = Managed_Resource.register object (o -> o.close)`, the `finalizer`
> closes over the value of `res`, preventing it from being garbage collected.
> The same limitation concerns the underscore-lambda syntax, as `_.close` is
> equivalent to `o -> o.close`. The finalizer should be a (possibly curried)
> call to a function defined outside of the lexical scope of the
> `Managed_Resource.register` call.
To perform operations on the underlying resource, use the
`Managed_Resource.with resource action` method, where `resource` is the object
returned from the call to `Managed_Resource.register`, and `action` is a
function taking the underlying object as its only argument. It is important that
the object passed to `action` is not stored and is not used past the return of
`action`. This means in particular that it is unsafe to give another thread a
reference to that object, if the thread remains alive past the return of
`action`. If such an operation is necessary, the other thread should call `with`
itself, using a reference to the original manged resource.
A managed resource can be closed manually, using
`Managed_Resource.close resource`. The underlying object is then finalized
immediately.
The finalization of a resource can be aborted using
`Managed_Resource.take resource`. This call will abort any automatic
finalization actions and return the original underlying object. The return value
is no longer managed by the runtime and must either be finalized manually or
wrapped in a new managed resource using a call to `Managed_Resource.register`.
## Semantics and Guarantees
This section outlines the runtime semantics and guarantees provided by the
managed resources system.
### Execution Guarantees
The finalizer attached to a managed resource is guaranteed to be executed
at-most-once.
There are no guarantees that the finalizer will ever _be_ executed. It is
executed as soon as the runtime garbage-collects the managed resource, but this
is not to say "as soon as the managed resource becomes unreachable". The runtime
is free to run garbage collection at any point, including to not run it at all
over the course of program execution. A call to `Runtime.gc` serves as hint to
the runtime system to perform garbage collection, but does not guarantee that
garbage collection will actually run.
The finalizer may be run from any application thread, with no guarantees as to
which thread will perform the finalization.
### Multiple Managed Resources Wrapping The Same Underlying Resource
In case the same underlying resource is used in multiple managed resources, it
will be finalized as soon as the first managed resource is garbage collected.
Moreover, the finalizer will be called for each garbage collected managed
resource, possibly leading to multiple-finalization of the underlying object.
Therefore, using the same underlying resource with multiple managed resource
instances should be considered an error.
### Thread Safety
Operations on managed resources are thread safe. Therefore, the safety
guarantees of the underlying resources are the limitation if the underlying
resource is not thread-safe, calls to `Managed_Resource.with` will also not be
thread-safe.

View File

@ -86,11 +86,7 @@ public final class Language extends TruffleLanguage<Context> {
return Truffle.getRuntime().createCallTarget(root);
}
/**
* Returns the supported options descriptors, for use by Graal's engine.
*
* @return The supported options descriptors
*/
/** {@inheritDoc} */
@Override
protected OptionDescriptors getOptionDescriptors() {
return RuntimeOptions.OPTION_DESCRIPTORS;

View File

@ -0,0 +1,29 @@
package org.enso.interpreter.node.expression.builtin.managedResource;
import com.oracle.truffle.api.dsl.CachedContext;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.Language;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.Context;
import org.enso.interpreter.runtime.data.ManagedResource;
@BuiltinMethod(
type = "Managed_Resource",
name = "finalize",
description = "Finalizes a managed resource, even if it is still reachable.")
public abstract class FinalizeNode extends Node {
static FinalizeNode build() {
return FinalizeNodeGen.create();
}
abstract Object execute(Object _this, ManagedResource resource);
@Specialization
Object doClose(
Object _this, ManagedResource resource, @CachedContext(Language.class) Context context) {
context.getResourceManager().close(resource);
return context.getBuiltins().unit().newInstance();
}
}

View File

@ -0,0 +1,34 @@
package org.enso.interpreter.node.expression.builtin.managedResource;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.CachedContext;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.Language;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.Context;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.ManagedResource;
@BuiltinMethod(
type = "Managed_Resource",
name = "register",
description =
"Makes an object into a managed resource, automatically finalized when the returned object is garbage collected.")
public abstract class RegisterNode extends Node {
static RegisterNode build() {
return RegisterNodeGen.create();
}
abstract ManagedResource execute(Object _this, Object resource, Function finalizer);
@Specialization
@CompilerDirectives.TruffleBoundary
ManagedResource doRegister(
Object _this,
Object resource,
Function function,
@CachedContext(Language.class) Context context) {
return context.getResourceManager().register(resource, function);
}
}

View File

@ -0,0 +1,31 @@
package org.enso.interpreter.node.expression.builtin.managedResource;
import com.oracle.truffle.api.dsl.CachedContext;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.Language;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.Context;
import org.enso.interpreter.runtime.data.ManagedResource;
@BuiltinMethod(
type = "Managed_Resource",
name = "take",
description =
"Takes the value held by the managed resource and removes the finalization callbacks,"
+ " effectively making the underlying resource unmanaged again.")
public abstract class TakeNode extends Node {
static TakeNode build() {
return TakeNodeGen.create();
}
abstract Object execute(Object _this, ManagedResource resource);
@Specialization
Object doTake(
Object _this, ManagedResource resource, @CachedContext(Language.class) Context context) {
context.getResourceManager().take(resource);
return resource.getResource();
}
}

View File

@ -0,0 +1,56 @@
package org.enso.interpreter.node.expression.builtin.managedResource;
import com.oracle.truffle.api.dsl.CachedContext;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.Language;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.dsl.MonadicState;
import org.enso.interpreter.node.callable.InvokeCallableNode;
import org.enso.interpreter.runtime.Context;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.data.ManagedResource;
import org.enso.interpreter.runtime.state.Stateful;
@BuiltinMethod(
type = "Managed_Resource",
name = "with",
description =
"Applies the passed action to the underlying resource managed by the passed Managed_Resource object.")
public abstract class WithNode extends Node {
private @Child InvokeCallableNode invokeCallableNode =
InvokeCallableNode.build(
new CallArgumentInfo[] {new CallArgumentInfo()},
InvokeCallableNode.DefaultsExecutionMode.EXECUTE,
InvokeCallableNode.ArgumentsExecutionMode.PRE_EXECUTED);
static WithNode build() {
return WithNodeGen.create();
}
abstract Stateful execute(
@MonadicState Object state,
VirtualFrame frame,
Object _this,
ManagedResource resource,
Object action);
@Specialization
Stateful doWith(
Object state,
VirtualFrame frame,
Object _this,
ManagedResource resource,
Object action,
@CachedContext(Language.class) Context context) {
context.getResourceManager().park(resource);
try {
return invokeCallableNode.execute(
action, frame, state, new Object[] {resource.getResource()});
} finally {
context.getResourceManager().unpark(resource);
}
}
}

View File

@ -40,6 +40,7 @@ public class Context {
private final List<Package<TruffleFile>> packages;
private final TopLevelScope topScope;
private final ThreadManager threadManager;
private final ResourceManager resourceManager;
private final boolean isCachingDisabled;
/**
@ -56,6 +57,7 @@ public class Context {
this.in = environment.in();
this.inReader = new BufferedReader(new InputStreamReader(environment.in()));
this.threadManager = new ThreadManager();
this.resourceManager = new ResourceManager(this);
this.isCachingDisabled = environment.getOptions().get(RuntimeOptions.DISABLE_INLINE_CACHES_KEY);
TruffleFileSystem fs = new TruffleFileSystem();
@ -284,6 +286,11 @@ public class Context {
return threadManager;
}
/** @return the resource manager for this context */
public ResourceManager getResourceManager() {
return resourceManager;
}
/** @return whether inline caches should be disabled for this context. */
public boolean isCachingDisabled() {
return isCachingDisabled;

View File

@ -0,0 +1,268 @@
package org.enso.interpreter.runtime;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.interop.InteropLibrary;
import org.enso.interpreter.runtime.data.ManagedResource;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/** Allows the context to attach garbage collection hooks on the removal of certain objects. */
public class ResourceManager {
private final Context context;
private volatile boolean isClosed = false;
private volatile Thread workerThread;
private final Runner worker = new Runner();
private final ReferenceQueue<ManagedResource> referenceQueue = new ReferenceQueue<>();
private final ConcurrentMap<PhantomReference<ManagedResource>, Item> items =
new ConcurrentHashMap<>();
/**
* Creates a new instance of Resource Manager.
*
* @param context the language context owning the new instance
*/
public ResourceManager(Context context) {
this.context = context;
}
/**
* Puts the finalization of the resource on hold, until {@link #unpark(ManagedResource)} is
* called. The resource won't be finalized, even if it becomes unreachable between the calls.
*
* @param resource the resource to park.
*/
@CompilerDirectives.TruffleBoundary
public void park(ManagedResource resource) {
Item it = items.get(resource.getPhantomReference());
if (it == null) {
return;
}
it.getParkedCount().incrementAndGet();
}
/**
* Resumes finalization of the resource. If the resource has become unreachable and there are no
* other threads parking the resource, it will get finalized right away.
*
* @param resource the resource to unpark.
*/
@CompilerDirectives.TruffleBoundary
public void unpark(ManagedResource resource) {
Item it = items.get(resource.getPhantomReference());
if (it == null) {
return;
}
it.getParkedCount().decrementAndGet();
tryFinalize(it);
}
/**
* Manually and unconditionally finalizes the resource. Ignores the parking mechanism, assuming
* the user now has full control over the resource.
*
* @param resource the resource to finalize.
*/
@CompilerDirectives.TruffleBoundary
public void close(ManagedResource resource) {
Item it = items.remove(resource.getPhantomReference());
if (it == null) {
return;
}
// Unconditional finalization user controls the resource manually.
it.doFinalize(context);
}
/**
* Removes the resource from the system, cancelling any automatic finalization action attached to
* it.
*
* @param resource the resource to take away from this system.
*/
@CompilerDirectives.TruffleBoundary
public void take(ManagedResource resource) {
items.remove(resource.getPhantomReference());
}
private void tryFinalize(Item it) {
if (it.isFlaggedForFinalization().get()) {
if (it.getParkedCount().get() == 0) {
// We already know that isFlaggedForFinalization was true at some
// point and there are no other threads still parking the underlying
// value. Note that it is impossible for parked count to increase after
// the value is flagged for finalization, as parking the value requires
// a live reference. We need to check if another thread didn't reach
// here earlier to perform the finalization and reset the flag, so that
// no further attempts are made.
boolean continueFinalizing = it.isFlaggedForFinalization().compareAndSet(true, false);
if (continueFinalizing) {
it.doFinalize(context);
items.remove(it.reference);
}
}
}
}
/**
* Registers a new resource to the system. {@code function} will be called on {@code object} when
* the value returned by this method becomes unreachable.
*
* @param object the underlying resource
* @param function the finalizer action to call on the underlying resource
* @return a wrapper object, containing the resource and serving as a reachability probe
*/
public ManagedResource register(Object object, Object function) {
if (isClosed) {
throw new IllegalStateException(
"Can't register new resources after resource manager is closed.");
}
if (workerThread == null || !workerThread.isAlive()) {
worker.setKilled(false);
workerThread = context.createThread(worker);
workerThread.start();
}
ManagedResource resource = new ManagedResource(object);
PhantomReference<ManagedResource> ref = new PhantomReference<>(resource, referenceQueue);
resource.setPhantomReference(ref);
items.put(ref, new Item(object, function, ref));
return resource;
}
/**
* Stops this system, stops and joins any threads created by it. Unconditionally finalizes all the
* registered resources, ignoring their reachability status.
*
* <p>This is only useful when the underlying context is being finalized and no more user code
* will be run in it.
*/
public void shutdown() {
isClosed = true;
worker.setKilled(true);
if (workerThread != null) {
while (true) {
try {
workerThread.interrupt();
workerThread.join();
break;
} catch (InterruptedException ignored) {
}
}
}
for (PhantomReference<ManagedResource> key : items.keySet()) {
Item it = items.remove(key);
if (it != null) {
// Finalize unconditionally all other threads are dead by now.
it.doFinalize(context);
}
}
}
/**
* The worker action for the underlying logic of this module. At least one such thread must be
* spawned in order for this module to be operational.
*/
private class Runner implements Runnable {
private volatile boolean killed = false;
@Override
public void run() {
while (true) {
try {
Reference<? extends ManagedResource> ref = referenceQueue.remove();
if (!killed) {
Item it = items.get(ref);
if (it == null) {
continue;
}
it.isFlaggedForFinalization().set(true);
tryFinalize(it);
}
if (killed) {
return;
}
} catch (InterruptedException e) {
if (killed) {
return;
}
}
}
}
/**
* Sets the killed flag of this thread. This flag being set to {@code true} will force it to
* stop execution at the soonest possible safe point. Other than setting this flag, the thread
* should also be interrupted to read it, in case it is blocked on an interruptible operation.
*
* @param killed whether the thread should stop execution upon reading the flag.
*/
public void setKilled(boolean killed) {
this.killed = killed;
}
}
/** A storage representation of a finalizable object handled by this system. */
private static class Item {
private final Object underlying;
private final Object finalizer;
private final PhantomReference<ManagedResource> reference;
private final AtomicInteger parkedCount = new AtomicInteger();
private final AtomicBoolean flaggedForFinalization = new AtomicBoolean();
/**
* Creates a new finalizable item.
*
* @param underlying the underlying object that should be finalized
* @param finalizer the finalizer to run on the underlying object
* @param reference a phantom reference used for tracking the reachability status of the
* resource.
*/
public Item(Object underlying, Object finalizer, PhantomReference<ManagedResource> reference) {
this.underlying = underlying;
this.finalizer = finalizer;
this.reference = reference;
}
/**
* Unconditionally performs the finalization action of this resource.
*
* @param context current execution context
*/
public void doFinalize(Context context) {
context.getThreadManager().enter();
try {
InteropLibrary.getUncached(finalizer).execute(finalizer, underlying);
} catch (Exception e) {
context.getErr().println("Exception in finalizer: " + e.getMessage());
} finally {
context.getThreadManager().leave();
}
}
/**
* Returns the counter of actions parking this object. The object can be safely finalized only
* if it's unreachable {@link #isFlaggedForFinalization()} and this counter is zero.
*
* @return the parking actions counter
*/
public AtomicInteger getParkedCount() {
return parkedCount;
}
/**
* Returns the boolean representing finalization status of this object. The object should be
* removed by the first thread that observes this flag to be set to true and the {@link
* #getParkedCount()} to be zero. If a thread intends to perform the finalization, it should set
* this flag to {@code false}.
*
* @return the finalization flag
*/
public AtomicBoolean isFlaggedForFinalization() {
return flaggedForFinalization;
}
}
}

View File

@ -1,7 +1,5 @@
package org.enso.interpreter.runtime.builtin;
import com.oracle.truffle.api.RootCallTarget;
import com.oracle.truffle.api.Truffle;
import org.enso.interpreter.Language;
import org.enso.interpreter.node.expression.builtin.debug.DebugBreakpointMethodGen;
import org.enso.interpreter.node.expression.builtin.debug.DebugEvalMethodGen;
@ -11,11 +9,8 @@ import org.enso.interpreter.node.expression.builtin.error.ThrowErrorMethodGen;
import org.enso.interpreter.node.expression.builtin.error.ThrowPanicMethodGen;
import org.enso.interpreter.node.expression.builtin.function.ApplicationOperatorMethodGen;
import org.enso.interpreter.node.expression.builtin.function.ExplicitCallFunctionMethodGen;
import org.enso.interpreter.node.expression.builtin.interop.generic.*;
import org.enso.interpreter.node.expression.builtin.interop.java.AddToClassPathMethodGen;
import org.enso.interpreter.node.expression.builtin.interop.java.LookupClassMethodGen;
import org.enso.interpreter.node.expression.builtin.interop.syntax.ConstructorDispatchNode;
import org.enso.interpreter.node.expression.builtin.interop.syntax.MethodDispatchNode;
import org.enso.interpreter.node.expression.builtin.io.PrintErrMethodGen;
import org.enso.interpreter.node.expression.builtin.io.PrintlnMethodGen;
import org.enso.interpreter.node.expression.builtin.io.ReadlnMethodGen;
@ -30,12 +25,8 @@ import org.enso.interpreter.node.expression.builtin.thread.WithInterruptHandlerM
import org.enso.interpreter.node.expression.builtin.unsafe.SetAtomFieldMethodGen;
import org.enso.interpreter.runtime.Context;
import org.enso.interpreter.runtime.Module;
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.callable.function.FunctionSchema;
import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.pkg.QualifiedName;
@ -63,6 +54,7 @@ public class Builtins {
private final System system;
private final Array array;
private final Polyglot polyglot;
private final ManagedResource managedResource;
/**
* Creates an instance with builtin methods installed.
@ -85,6 +77,7 @@ public class Builtins {
system = new System(language, scope);
number = new Number(language, scope);
polyglot = new Polyglot(language, scope);
managedResource = new ManagedResource(language, scope);
AtomConstructor nil = new AtomConstructor("Nil", scope).initializeFields();
AtomConstructor cons =

View File

@ -0,0 +1,28 @@
package org.enso.interpreter.runtime.builtin;
import org.enso.interpreter.Language;
import org.enso.interpreter.node.expression.builtin.managedResource.FinalizeMethodGen;
import org.enso.interpreter.node.expression.builtin.managedResource.RegisterMethodGen;
import org.enso.interpreter.node.expression.builtin.managedResource.TakeMethodGen;
import org.enso.interpreter.node.expression.builtin.managedResource.WithMethodGen;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.scope.ModuleScope;
/** Container for builtin Managed_Resource types */
public class ManagedResource {
/**
* Creates and registers the relevant constructors.
*
* @param language the current language instance.
* @param scope the scope to register constructors in.
*/
public ManagedResource(Language language, ModuleScope scope) {
AtomConstructor resource = new AtomConstructor("Managed_Resource", scope).initializeFields();
scope.registerConstructor(resource);
scope.registerMethod(resource, "register", RegisterMethodGen.makeFunction(language));
scope.registerMethod(resource, "with", WithMethodGen.makeFunction(language));
scope.registerMethod(resource, "take", TakeMethodGen.makeFunction(language));
scope.registerMethod(resource, "finalize", FinalizeMethodGen.makeFunction(language));
}
}

View File

@ -0,0 +1,40 @@
package org.enso.interpreter.runtime.data;
import com.oracle.truffle.api.interop.TruffleObject;
import java.lang.ref.PhantomReference;
/** A runtime representation of a managed resource. */
public class ManagedResource implements TruffleObject {
private final Object resource;
private PhantomReference<ManagedResource> phantomReference;
/**
* Creates a new managed resource.
*
* @param resource the underlying resource
*/
public ManagedResource(Object resource) {
this.resource = resource;
this.phantomReference = null;
}
/** @return the underlying resource */
public Object getResource() {
return resource;
}
/** @return the phantom reference tracking this managed resource */
public PhantomReference<ManagedResource> getPhantomReference() {
return phantomReference;
}
/**
* Sets the value of the reference used to track reachability of this managed resource.
*
* @param phantomReference the phantom reference tracking this managed resource.
*/
public void setPhantomReference(PhantomReference<ManagedResource> phantomReference) {
this.phantomReference = phantomReference;
}
}

View File

@ -11,6 +11,7 @@ import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.Array;
import org.enso.interpreter.runtime.data.ManagedResource;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.error.RuntimeError;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@ -37,7 +38,8 @@ import org.enso.interpreter.runtime.number.EnsoBigInteger;
RuntimeError.class,
UnresolvedSymbol.class,
Array.class,
EnsoBigInteger.class
EnsoBigInteger.class,
ManagedResource.class
})
public class Types {

View File

@ -0,0 +1,193 @@
package org.enso.interpreter.test.semantic
import org.enso.interpreter.runtime.Context
import org.enso.interpreter.test.{InterpreterContext, InterpreterTest}
import org.enso.polyglot.{LanguageInfo, MethodNames}
import scala.ref.WeakReference
import scala.util.Try
class RuntimeManagementTest extends InterpreterTest {
override def subject: String = "Enso Code Execution"
override def specify(implicit
interpreterContext: InterpreterContext
): Unit = {
"Interrupt threads through Thread#interrupt()" in {
val langCtx = interpreterContext.ctx
.getBindings(LanguageInfo.ID)
.invokeMember(MethodNames.TopScope.LEAK_CONTEXT)
.asHostObject[Context]()
val code =
"""from Builtins import all
|
|foo x =
| if x == 0 then IO.println "Start." else Unit
| here.foo x+1
|
|main =
| Thread.with_interrupt_handler (here.foo 0) (IO.println "Interrupted.")
|""".stripMargin
val main = getMain(code)
val runnable: Runnable = { () =>
langCtx.getThreadManager.enter()
try {
Try(main.execute())
} finally {
langCtx.getThreadManager.leave()
}
}
def runTest(n: Int = 5): Unit = {
val threads = 0.until(n).map(_ => new Thread(runnable))
threads.foreach(_.start())
var reportedCount = 0
while (reportedCount < n) {
Thread.sleep(100)
reportedCount += consumeOut.length
}
val expectedOut = List.fill(n)("Interrupted.")
threads.foreach(_.interrupt())
langCtx.getThreadManager.checkInterrupts()
threads.foreach(_.join())
consumeOut shouldEqual expectedOut
threads.forall(!_.isAlive) shouldBe true
}
runTest()
runTest()
}
/**
* Don't use this in production code, ever.
*/
def forceGC(): Unit = {
var obj = new Object
val ref = new WeakReference[Object](obj)
obj = null
while (ref.get.isDefined) {
System.gc()
}
}
"Automatically free managed resources" in {
val code =
"""
|from Builtins import all
|
|type Mock_File i
|
|free_resource r = IO.println ("Freeing: " + r.to_text)
|
|create_resource i =
| c = Mock_File i
| r = Managed_Resource.register c here.free_resource
| Managed_Resource.with r f-> IO.println ("Accessing: " + f.to_text)
|
|main =
| here.create_resource 0
| here.create_resource 1
| here.create_resource 2
| here.create_resource 3
| here.create_resource 4
|""".stripMargin
eval(code)
var totalOut: List[String] = Nil
forceGC()
totalOut = consumeOut
while (totalOut.length < 10) {
Thread.sleep(100)
totalOut ++= consumeOut
}
def mkAccessStr(i: Int): String = s"Accessing: (Mock_File $i)"
def mkFreeStr(i: Int): String = s"Freeing: (Mock_File $i)"
def all = 0.to(4).map(mkAccessStr) ++ 0.to(4).map(mkFreeStr)
totalOut should contain theSameElementsAs all
}
"Automatically free managed resources amongst manual closure of other managed resources" in {
val code =
"""
|from Builtins import all
|
|type Mock_File i
|
|free_resource r = IO.println ("Freeing: " + r.to_text)
|
|create_resource i =
| c = Mock_File i
| r = Managed_Resource.register c here.free_resource
| Managed_Resource.with r f-> IO.println ("Accessing: " + f.to_text)
| if i % 2 == 0 then Managed_Resource.finalize r else Unit
|
|main =
| here.create_resource 0
| here.create_resource 1
| here.create_resource 2
| here.create_resource 3
| here.create_resource 4
|""".stripMargin
eval(code)
var totalOut: List[String] = Nil
forceGC()
totalOut = consumeOut
while (totalOut.length < 10) {
Thread.sleep(100)
totalOut ++= consumeOut
}
def mkAccessStr(i: Int): String = s"Accessing: (Mock_File $i)"
def mkFreeStr(i: Int): String = s"Freeing: (Mock_File $i)"
def all = 0.to(4).map(mkAccessStr) ++ 0.to(4).map(mkFreeStr)
totalOut should contain theSameElementsAs all
}
"Automatically free managed resources amongst manual takeover of other managed resources" in {
val code =
"""
|from Builtins import all
|
|type Mock_File i
|
|free_resource r = IO.println ("Freeing: " + r.to_text)
|
|create_resource i =
| c = Mock_File i
| r = Managed_Resource.register c here.free_resource
| Managed_Resource.with r f-> IO.println ("Accessing: " + f.to_text)
| if i % 2 == 0 then Managed_Resource.take r else Unit
|
|main =
| here.create_resource 0
| here.create_resource 1
| here.create_resource 2
| here.create_resource 3
| here.create_resource 4
|""".stripMargin
eval(code)
var totalOut: List[String] = Nil
forceGC()
totalOut = consumeOut
while (totalOut.length < 7) {
Thread.sleep(100)
totalOut ++= consumeOut
}
def mkAccessStr(i: Int): String = s"Accessing: (Mock_File $i)"
def mkFreeStr(i: Int): String = s"Freeing: (Mock_File $i)"
def all = 0.to(4).map(mkAccessStr) ++ List(1, 3).map(mkFreeStr)
totalOut should contain theSameElementsAs all
}
}
}

View File

@ -1,64 +0,0 @@
package org.enso.interpreter.test.semantic
import org.enso.interpreter.runtime.Context
import org.enso.interpreter.test.{InterpreterTest, InterpreterContext}
import org.enso.polyglot.{LanguageInfo, MethodNames}
import scala.util.Try
class ThreadInterruptionTest extends InterpreterTest {
override def subject: String = "Enso Code Execution"
override def specify(
implicit interpreterContext: InterpreterContext
): Unit = {
"be interruptible through Thread#interrupt()" in {
val langCtx = interpreterContext.ctx
.getBindings(LanguageInfo.ID)
.invokeMember(MethodNames.TopScope.LEAK_CONTEXT)
.asHostObject[Context]()
val code =
"""from Builtins import all
|
|foo x =
| if x == 0 then IO.println "Start." else Unit
| here.foo x+1
|
|main =
| Thread.with_interrupt_handler (here.foo 0) (IO.println "Interrupted.")
|""".stripMargin
val main = getMain(code)
val runnable: Runnable = { () =>
langCtx.getThreadManager.enter()
try {
Try(main.execute())
} finally {
langCtx.getThreadManager.leave()
}
}
def runTest(n: Int = 5): Unit = {
val threads = 0.until(n).map(_ => new Thread(runnable))
threads.foreach(_.start())
var reportedCount = 0
while (reportedCount < n) {
Thread.sleep(100)
reportedCount += consumeOut.length
}
val expectedOut = List.fill(n)("Interrupted.")
threads.foreach(_.interrupt())
langCtx.getThreadManager.checkInterrupts()
threads.foreach(_.join())
consumeOut shouldEqual expectedOut
threads.forall(!_.isAlive) shouldBe true
}
runTest()
runTest()
}
}
}