diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a60d9b109..97dd78dc80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ - [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] +- [The reload button clears the HTTP cache.][11673] [11235]: https://github.com/enso-org/enso/pull/11235 [11255]: https://github.com/enso-org/enso/pull/11255 @@ -116,6 +117,7 @@ [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 +[11673]: https://github.com/enso-org/enso/pull/11673 #### Enso Language & Runtime diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Reload_Detector.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Reload_Detector.enso new file mode 100644 index 0000000000..966975d0ec --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Reload_Detector.enso @@ -0,0 +1,31 @@ +import Standard.Base.Error.Error +import Standard.Base.Meta +import Standard.Base.Nothing.Nothing +import Standard.Base.Runtime.Managed_Resource.Managed_Resource +import Standard.Base.Runtime.Ref.Ref +from Standard.Base.Data.Boolean import Boolean, True, False + +## PRIVATE + This is used by ReloadDetector.java to create a `Managed_Resource` that is + garbage collected when the reload button is pressed. + + The managed resource contains a Ref containing a 0 (the value is + unimportant). When the reload button is pressed, the ref is removed and + attempting to access it using `with` throws an `Uninitialized_State`. When + the `Uninitialized_State` is detected, it indicates that the reload has been + initiated. +type Reload_Detector + private Value mr:Managed_Resource + + new -> Reload_Detector = + mr = Managed_Resource.register (Ref.new 1) (x-> Nothing) True + Reload_Detector.Value mr + + has_reload_occurred self = + self.mr.has_been_finalized + +## PRIVATE +simulate_reload_test_only reload_detector = reload_detector.mr.finalize + +## PRIVATE +create_reload_detector = Reload_Detector.new diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Managed_Resource.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Managed_Resource.enso index ed3bdbb237..33beacf92d 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Managed_Resource.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Managed_Resource.enso @@ -1,8 +1,10 @@ ## An API for manual resource management. import project.Any.Any +import project.Errors.Common.Uninitialized_State +import project.Meta import project.Nothing.Nothing -from project.Data.Boolean import Boolean, False +from project.Data.Boolean import Boolean, True, False ## Resource provides an API for manual management of computation resources. @@ -90,5 +92,15 @@ type Managed_Resource take : Any take self = @Builtin_Method "Managed_Resource.take" + ## PRIVATE + ADVANCED + + Returns true iff the resource has been collected by the engine, false + otherwise. If `with` throws any other error, it is propagated. + has_been_finalized : Boolean + has_been_finalized self -> Boolean = self.with x-> + if x.is_error.not then False else + if x.catch.is_a Uninitialized_State then True else x + register_builtin r fn sys:Boolean = @Builtin_Method "Managed_Resource.register_builtin" with_builtin r fn = @Builtin_Method "Managed_Resource.with_builtin" diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala index 43b6df2bab..1658288502 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/command/RecomputeContextCmd.scala @@ -11,6 +11,7 @@ import org.enso.interpreter.instrument.{ } import org.enso.interpreter.instrument.execution.RuntimeContext import org.enso.interpreter.instrument.job.{EnsureCompiledJob, ExecuteJob} +import org.enso.interpreter.runtime.EnsoContext import org.enso.polyglot.runtime.Runtime.Api import org.enso.polyglot.runtime.Runtime.Api.RequestId @@ -42,6 +43,10 @@ class RecomputeContextCmd( ec: ExecutionContext ): Future[Boolean] = { Future { + EnsoContext + .get(null) + .getResourceManager() + .scheduleFinalizationOfSystemReferences(); ctx.jobControlPlane.abortJobs( request.contextId, "recompute context", diff --git a/std-bits/base/src/main/java/org/enso/base/cache/LRUCache.java b/std-bits/base/src/main/java/org/enso/base/cache/LRUCache.java index 2cfc0d24fe..eba05b6c33 100644 --- a/std-bits/base/src/main/java/org/enso/base/cache/LRUCache.java +++ b/std-bits/base/src/main/java/org/enso/base/cache/LRUCache.java @@ -73,6 +73,9 @@ public class LRUCache { /** Used to get the current free disk space; mockable. */ private final DiskSpaceGetter diskSpaceGetter; + /** Used to clear the cache on reload. */ + private final ReloadDetector reloadDetector = new ReloadDetector(); + public LRUCache() { this(LRUCacheSettings.getDefault(), new NowGetter(), new DiskSpaceGetter()); } @@ -89,6 +92,8 @@ public class LRUCache { */ public CacheResult getResult(ItemBuilder itemBuilder) throws IOException, InterruptedException, ResponseTooLargeException { + clearOnReload(); + String cacheKey = itemBuilder.makeCacheKey(); try { @@ -221,6 +226,12 @@ public class LRUCache { removeCacheEntriesByPredicate(e -> true); } + private void clearOnReload() { + if (reloadDetector.hasReloadOccurred()) { + clear(); + } + } + /** Remove all cache entries (and their cache files) that match the predicate. */ private void removeCacheEntriesByPredicate(Predicate> predicate) { List>> toRemove = @@ -352,6 +363,11 @@ public class LRUCache { return settings; } + /** Public for testing. */ + public void simulateReloadTestOnly() { + reloadDetector.simulateReloadTestOnly(); + } + private record CacheEntry(File responseData, M metadata, long size, ZonedDateTime expiry) {} /** diff --git a/std-bits/base/src/main/java/org/enso/base/cache/ReloadDetector.java b/std-bits/base/src/main/java/org/enso/base/cache/ReloadDetector.java new file mode 100644 index 0000000000..5328c8003a --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/cache/ReloadDetector.java @@ -0,0 +1,39 @@ +package org.enso.base.cache; + +import org.enso.base.polyglot.EnsoMeta; +import org.graalvm.polyglot.Value; + +/** + * Detects that the reload button has been pressed. + * + *

.hasReloadOccurred() returns true if the reload button was pressed since the last call to + * .hasReloadOccurred(). + * + *

This uses a `Managed_Resource` (created in eval'd Enso code) that is cleared on reload. + */ +public class ReloadDetector { + private Value ensoReloadDetector; + + public ReloadDetector() { + resetEnsoReloadDetector(); + } + + public boolean hasReloadOccurred() { + var reloadHasOccurred = ensoReloadDetector.invokeMember("has_reload_occurred").asBoolean(); + if (reloadHasOccurred) { + resetEnsoReloadDetector(); + } + return reloadHasOccurred; + } + + private void resetEnsoReloadDetector() { + ensoReloadDetector = + EnsoMeta.callStaticModuleMethod( + "Standard.Base.Network.Reload_Detector", "create_reload_detector"); + } + + void simulateReloadTestOnly() { + EnsoMeta.callStaticModuleMethod( + "Standard.Base.Network.Reload_Detector", "simulate_reload_test_only", ensoReloadDetector); + } +} diff --git a/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java index ff6677f2ff..2e7b622e68 100644 --- a/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java +++ b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java @@ -19,12 +19,10 @@ import org.enso.base.cache.ResponseTooLargeException; import org.enso.base.net.URISchematic; import org.enso.base.net.URIWithSecrets; import org.graalvm.collections.Pair; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Value; /** Makes HTTP requests with secrets in either header or query string. */ public final class EnsoSecretHelper extends SecretValueResolver { - private static Value cache; + private static EnsoHTTPResponseCache cache; /** Gets a JDBC connection resolving EnsoKeyValuePair into the properties. */ public static Connection getJDBCConnection( @@ -179,43 +177,10 @@ public final class EnsoSecretHelper extends SecretValueResolver { } public static EnsoHTTPResponseCache getOrCreateCache() { - if (getCache() instanceof EnsoHTTPResponseCache httpCache) { - return httpCache; - } else { - var module = - Context.getCurrent() - .eval( - "enso", - """ - import Standard.Base.Runtime.Managed_Resource.Managed_Resource - import Standard.Base.Data.Boolean.Boolean - - type Cache - private Value ref:Managed_Resource - - new obj -> Cache = - on_finalize _ = 0 - ref = Managed_Resource.register obj on_finalize Boolean.True - Cache.Value ref - - get self = self.ref.with (r->r) - """); - var cacheNew = module.invokeMember("eval_expression", "Cache.new"); - var httpCache = new EnsoHTTPResponseCache(); - cache = cacheNew.execute(httpCache); - return httpCache; - } - } - - public static EnsoHTTPResponseCache getCache() { - var c = cache instanceof Value v ? v.invokeMember("get") : null; - if (c != null - && c.isHostObject() - && c.asHostObject() instanceof EnsoHTTPResponseCache httpCache) { - return httpCache; - } else { - return null; + if (cache == null) { + cache = new EnsoHTTPResponseCache(); } + return cache; } private static final Comparator> headerNameComparator = diff --git a/test/Base_Tests/src/Runtime/Managed_Resource_Spec.enso b/test/Base_Tests/src/Runtime/Managed_Resource_Spec.enso index 7e30fe2111..83cffffda0 100644 --- a/test/Base_Tests/src/Runtime/Managed_Resource_Spec.enso +++ b/test/Base_Tests/src/Runtime/Managed_Resource_Spec.enso @@ -95,6 +95,7 @@ add_specs suite_builder = suite_builder.group "Managed_Resource" group_builder-> # finalizes the resource mr.finalize + mr.has_been_finalized . should_be_true builder.append "Finalized:"+mr.to_text # operation on finalized resource diff --git a/test/Table_Tests/src/IO/Fetch_Spec.enso b/test/Table_Tests/src/IO/Fetch_Spec.enso index fa45ff5c48..b1c3344506 100644 --- a/test/Table_Tests/src/IO/Fetch_Spec.enso +++ b/test/Table_Tests/src/IO/Fetch_Spec.enso @@ -172,6 +172,9 @@ add_specs suite_builder = lru_cache = LRUCache.new with_lru_cache lru_cache action + fake_reload = + EnsoSecretHelper.getOrCreateCache.getLRUCache.simulateReloadTestOnly + url0 = base_url_with_slash+'test_download?max-age=16&length=10' url1 = base_url_with_slash+'test_download?max-age=16&length=20' url_post = base_url_with_slash + "post" @@ -541,6 +544,27 @@ add_specs suite_builder = Test_Environment.unsafe_with_environment_override "ENSO_LIB_HTTP_CACHE_MAX_TOTAL_CACHE_LIMIT" "101%" <| LRUCache.new . getSettings . getTotalCacheLimit . should_equal (TotalCacheLimit.Percentage.new 0.2) + group_builder.specify "Cache should be cleared when a reload is detected" <| + HTTP.fetch base_url_with_slash+'test_download?length=10' + HTTP.fetch base_url_with_slash+'test_download?length=11' + HTTP.fetch base_url_with_slash+'test_download?length=12' + get_num_response_cache_entries . should_equal 3 + + fake_reload + + get_num_response_cache_entries . should_equal 3 # Cleaning is not triggered until the next request + HTTP.fetch base_url_with_slash+'test_download?length=10' + get_num_response_cache_entries . should_equal 1 + HTTP.fetch base_url_with_slash+'test_download?length=14' + HTTP.fetch base_url_with_slash+'test_download?length=15' + get_num_response_cache_entries . should_equal 3 + + fake_reload + + get_num_response_cache_entries . should_equal 3 # Cleaning is not triggered until the next request + HTTP.fetch base_url_with_slash+'test_download?length=16' + get_num_response_cache_entries . should_equal 1 + group_builder.specify "Reissues the request if the cache file disappears" pending=pending_has_url <| Test.with_retries <| with_default_cache <| url = base_url_with_slash+'test_download?max-age=16&length=10'