Skip to content

Commit

Permalink
Enhance Managed_Resource to allow implementation of in-memory caches (
Browse files Browse the repository at this point in the history
#11577)

(cherry picked from commit d687365)
  • Loading branch information
JaroslavTulach authored and jdunkerley committed Dec 3, 2024
1 parent d407a1e commit 17f85dd
Show file tree
Hide file tree
Showing 23 changed files with 498 additions and 156 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@
operation.][11490]
- [Added `Table.input` allowing creation of typed tables from vectors of data,
including auto parsing text columns.][11562]
- [Enhance Managed_Resource to allow implementation of in-memory caches][11577]

[11235]: https://github.com/enso-org/enso/pull/11235
[11255]: https://github.com/enso-org/enso/pull/11255
[11371]: https://github.com/enso-org/enso/pull/11371
[11373]: https://github.com/enso-org/enso/pull/11373
[11490]: https://github.com/enso-org/enso/pull/11490
[11562]: https://github.com/enso-org/enso/pull/11562
[11577]: https://github.com/enso-org/enso/pull/11577

#### Enso Language & Runtime

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import project.Any.Any
import project.Nothing.Nothing
from project.Data.Boolean import Boolean, False

## Resource provides an API for manual management of computation resources.

Expand Down Expand Up @@ -34,17 +35,25 @@ type Managed_Resource
ADVANCED

Registers a resource with the resource manager to be cleaned up using
function once it is no longer in use.
function once it is no longer in use. The optional `system_finalization_allowed`
flag allow the system to explicitly call `finalize` on the resource
when _"needed"_. The definition is intentionally vague, but
currently the IDE performs such a call when user requests a _"reload"_ -
e.g. using `Managed_Resource.register cache cleanup_fn True` is useful
for creating user managed caches.

Arguments:
- resource: The resource to register.
- function: The action to be executed on resource to clean it up when
it is no longer in use.
- system_finalization_allowed: is the system allowed to call `finalize`
on the resource when "needed"

Returns:
A `Managed_Resource` object that can be used to access the resource.
register : Any -> (Any -> Nothing) -> Managed_Resource
register resource function = @Builtin_Method "Managed_Resource.register"
register : Any -> (Any -> Nothing) -> Boolean -> Managed_Resource
register resource function system_finalization_allowed=False =
@Tail_Call register_builtin resource function system_finalization_allowed

## PRIVATE
ADVANCED
Expand All @@ -58,13 +67,19 @@ type Managed_Resource
ADVANCED

Executes the provided action on the resource managed by the managed
resource object.
resource object. The action is invoked with the managed resource only if
it has not yet been finalized. If the resource has already been finalized
then `Error` with `Uninitialized_State` payload is passed into the
action instead of the resource.

Arguments:
- action: The action that will be applied to the resource managed by
resource.
with : (Any -> Any) -> Any
with self ~action = @Builtin_Method "Managed_Resource.with"
the `Managed_Resource` (or to `Uninitialized_State` error).
Returns:
Value returned from the `action`

with : (Any -> Any) -> Any -> Any
with self ~action = @Tail_Call with_builtin self action

## PRIVATE
ADVANCED
Expand All @@ -74,3 +89,6 @@ type Managed_Resource
managed resources system.
take : Any
take self = @Builtin_Method "Managed_Resource.take"

register_builtin r fn sys:Boolean = @Builtin_Method "Managed_Resource.register_builtin"
with_builtin r fn = @Builtin_Method "Managed_Resource.with_builtin"
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import project.Data.Numbers.Integer
import project.Data.Text.Encoding.Encoding
import project.Data.Vector.Vector
import project.Error.Error
import project.Panic.Panic
import project.Errors.File_Error.File_Error
import project.Errors.Illegal_State.Illegal_State
import project.Errors.Problem_Behavior.Problem_Behavior
from project.Errors.Common import Uninitialized_State
import project.Nothing.Nothing
import project.Runtime.Managed_Resource.Managed_Resource
import project.System.Advanced.Restartable_Input_Stream.Restartable_Input_Stream
Expand All @@ -17,6 +19,7 @@ import project.System.File.Generic.Writable_File.Writable_File
import project.System.Internal.Reporting_Stream_Decoder_Helper
from project.Data.Boolean import Boolean, False, True

polyglot java import java.io.IOException
polyglot java import java.io.BufferedInputStream
polyglot java import java.io.ByteArrayInputStream
polyglot java import java.io.InputStream as Java_Input_Stream
Expand Down Expand Up @@ -114,9 +117,12 @@ type Input_Stream
Arguments:
- f: Applies a function over the internal java stream.
with_java_stream : (Java_Input_Stream -> Any) -> Any
with_java_stream self f = self.stream_resource . with java_like_stream->
java_stream = Stream_Utils.asInputStream java_like_stream
self.error_handler <| f java_stream
with_java_stream self f =
self.stream_resource . with java_like_stream->
java_like_stream.catch Uninitialized_State _->
Panic.throw <| IOException.new "Stream closed"
java_stream = Stream_Utils.asInputStream java_like_stream
self.error_handler <| f java_stream

## PRIVATE
Runs an action with a `ReportingStreamDecoder` decoding data from the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public void testSelfAssignment() throws Exception {
var error = run.execute(-1);
assertTrue("We get an error value back", error.isException());
assertTrue("The error value also represents null", error.isNull());
assertEquals("(Error: Uninitialized value)", error.toString());
assertEquals("(Error: 'Uninitialized value')", error.toString());
}

@Test
Expand All @@ -195,7 +195,7 @@ public void testRecursiveDefinition() throws Exception {
var error = run.execute("Nope: ");
assertTrue("We get an error value back", error.isException());
assertTrue("The error value also represents null", error.isNull());
assertEquals("(Error: Uninitialized value)", error.toString());
assertEquals("(Error: 'Uninitialized value')", error.toString());
}

@Ignore("Explicitly-default arguments will be implemented in #8480")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.enso.interpreter.runtime;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import org.enso.common.MethodNames;
import org.enso.test.utils.ContextUtils;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.hamcrest.Matchers;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class ManagedResourceTest {
private static Context ctx;
private static EnsoContext ensoCtx;
private static Value newResource;
private static Value createResource;
private static Value getResource;

@BeforeClass
public static void initCtx() throws Exception {
ctx = ContextUtils.createDefaultContext();
ensoCtx = ContextUtils.leakContext(ctx);
var code =
"""
import Standard.Base.Runtime.Managed_Resource.Managed_Resource
make_new obj =
Managed_Resource.register obj (_->0)
create_new obj system_resource =
Managed_Resource.register obj (_->0) system_resource
get_res ref = ref.with it->
it
""";
var src = Source.newBuilder("enso", code, "gc.enso").build();
var gcEnso = ctx.eval(src);
newResource = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "make_new");
createResource = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "create_new");
getResource = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "get_res");
}

@AfterClass
public static void closeCtx() throws Exception {
ctx.close();
ctx = null;
}

@Test
public void regularReference() throws Exception {
var obj = new Object();
var ref = newResource.execute(obj);

assertFalse("Value returned", ref.isNull());
assertEquals(
"Standard.Base.Runtime.Managed_Resource.Managed_Resource",
ref.getMetaObject().getMetaQualifiedName());

var weakRef = new WeakReference<>(obj);
obj = null;

assertEquals("We get the object", weakRef.get(), getResource.execute(ref).asHostObject());

assertGC("Weak wasn't released", false, weakRef);
assertFalse("Value was not GCed", getResource.execute(ref).isNull());
assertEquals("We get the object", weakRef.get(), getResource.execute(ref).asHostObject());

ensoCtx.getResourceManager().scheduleFinalizationOfSystemReferences();
assertEquals(
"scheduleFinalization has no effect on regular reference",
weakRef.get(),
getResource.execute(ref).asHostObject());
}

@Test
public void explicitlyReclaimableReference() throws Exception {
var obj = new Object();
var ref = createResource.execute(obj, true);

assertFalse("Value returned", ref.isNull());
assertEquals(
"Standard.Base.Runtime.Managed_Resource.Managed_Resource",
ref.getMetaObject().getMetaQualifiedName());
assertEquals("We get the object", obj, getResource.execute(ref).asHostObject());

ensoCtx.getResourceManager().scheduleFinalizationOfSystemReferences();

var none = getResource.execute(ref);
assertTrue("Value was GCed", none.isException());
assertEquals(
"It is an error", "Standard.Base.Error.Error", none.getMetaObject().getMetaQualifiedName());
assertThat(
"Contains Uninitialized_State as payload",
none.toString(),
Matchers.allOf(
Matchers.containsString("Uninitialized_State"),
Matchers.containsString("Error"),
Matchers.containsString("Managed_Resource")));
}

private static void assertGC(String msg, boolean expectGC, Reference<?> ref) {
for (var i = 1; i < Integer.MAX_VALUE / 2; i *= 2) {
if (ref.get() == null) {
break;
}
System.gc();
}
var obj = ref.get();
if (expectGC) {
assertNull(msg + " ref still alive", obj);
} else {
assertNotNull(msg + " ref has been cleaned", obj);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.enso.interpreter.runtime;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import org.enso.common.MethodNames;
import org.enso.test.utils.ContextUtils;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class RefTest {
private static Context ctx;
private static EnsoContext ensoCtx;
private static Value newRef;
private static Value createRef;
private static Value getRef;

@BeforeClass
public static void initCtx() throws Exception {
ctx = ContextUtils.createDefaultContext();
ensoCtx = ContextUtils.leakContext(ctx);
var code =
"""
import Standard.Base.Runtime.Ref.Ref
new_ref obj =
Ref.new obj
create_ref obj allow_gc =
Ref.new obj allow_gc
get_ref ref = ref.get
""";
var src = Source.newBuilder("enso", code, "gc.enso").build();
var gcEnso = ctx.eval(src);
newRef = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "new_ref");
createRef = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "create_ref");
getRef = gcEnso.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "get_ref");
}

@AfterClass
public static void closeCtx() throws Exception {
ctx.close();
ctx = null;
}

@Test
public void regularReference() throws Exception {
var obj = new Object();
var ref = newRef.execute(obj);

assertFalse("Value returned", ref.isNull());
assertEquals("Standard.Base.Runtime.Ref.Ref", ref.getMetaObject().getMetaQualifiedName());

var weakRef = new WeakReference<>(obj);
obj = null;

assertEquals("We get the object", weakRef.get(), getRef.execute(ref).asHostObject());

assertGC("Weak wasn't released", false, weakRef);
assertFalse("Value was not GCed", getRef.execute(ref).isNull());
assertEquals("We get the object", weakRef.get(), getRef.execute(ref).asHostObject());

// ensoCtx.getReferencesManager().releaseAll();
assertEquals(
"releaseAll has no effect on regular reference",
weakRef.get(),
getRef.execute(ref).asHostObject());
}

private static void assertGC(String msg, boolean expectGC, Reference<?> ref) {
for (var i = 1; i < Integer.MAX_VALUE / 2; i *= 2) {
if (ref.get() == null) {
break;
}
System.gc();
}
var obj = ref.get();
if (expectGC) {
assertNull(msg + " ref still alive", obj);
} else {
assertNotNull(msg + " ref has been cleaned", obj);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public void sendNotANumberChange() {
result.head().payload() instanceof Runtime$Api$ExecutionComplete);
Assert.assertEquals(
"Error is printed as a result",
List.newBuilder().addOne("(Error: Uninitialized value)"),
List.newBuilder().addOne("(Error: 'Uninitialized value')"),
context.consumeOut());
}

Expand Down
Loading

0 comments on commit 17f85dd

Please sign in to comment.