mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
Managed Resources Engine (#1194)
This commit is contained in:
parent
f00b187438
commit
2a6dbf0eda
108
docs/semantics/managed-resources.md
Normal file
108
docs/semantics/managed-resources.md
Normal 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.
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user