diff --git a/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/Asset.java b/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/Asset.java index 4f063b09..bb31275b 100644 --- a/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/Asset.java +++ b/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/Asset.java @@ -1,27 +1,19 @@ -/* - * Copyright 2019 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.gestalt.assets; import com.google.common.base.Preconditions; - +import com.google.common.collect.Lists; import net.jcip.annotations.ThreadSafe; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.terasology.context.annotation.API; +import java.lang.ref.WeakReference; +import java.security.PrivilegedActionException; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.Optional; /** @@ -50,12 +42,17 @@ @API @ThreadSafe public abstract class Asset { + private static final Logger logger = LoggerFactory.getLogger(Asset.class); + + private final List>> instances; + private Asset parent; private final ResourceUrn urn; private final AssetType assetType; private final DisposalHook disposalHook = new DisposalHook(); private volatile boolean disposed; + /** * The constructor for an asset. It is suggested that implementing classes provide a constructor taking both the urn, and an initial AssetData to load. * @@ -65,19 +62,63 @@ public abstract class Asset { protected Asset(ResourceUrn urn, AssetType assetType) { Preconditions.checkNotNull(urn); Preconditions.checkNotNull(assetType); + instances = urn.isInstance() ? Collections.emptyList() : Lists.newArrayList(); this.urn = urn; this.assetType = assetType; assetType.registerAsset(this, disposalHook); } + private void appendInstance(Asset asset) { + if (parent == null) { + Preconditions.checkArgument(!getUrn().isInstance()); + asset.parent = this; + instances.add(new WeakReference<>(asset)); + } else { + asset.parent = parent; + parent.instances.add(new WeakReference<>(asset)); + } + } + + /** + * return all the instances that are associated with this type of asset. + * @return instances + */ + protected final Iterable>> instances() { + if (this.parent != null) { + return parent.instances(); + } + return instances; + } + + private void compactInstances() { + if (this.parent != null) { + parent.compactInstances(); + return; + } + Iterator>> iter = instances.iterator(); + while(iter.hasNext()) { + WeakReference> reference = iter.next(); + Asset inst = reference.get(); + if(inst == null || inst.isDisposed()) { + iter.remove(); + } + } + } + /** * set a resource handler so the disposable hook can clean up resources not managed by the JVM * @param resource A resource to close when disposing this class. The resource must not have a reference to this asset - * this would prevent it being garbage collected. It must be a static inner class, or not contained in the asset class * (or an anonymous class defined in a static context). A warning will be logged if this is not the case. */ - protected void setDisposableResource(DisposableResource resource) { - this.disposalHook.setDisposableResource(resource); + public Asset(ResourceUrn urn, AssetType assetType, DisposableResource resource) { + Preconditions.checkNotNull(urn); + Preconditions.checkNotNull(assetType); + instances = urn.isInstance() ? Collections.emptyList(): Lists.newArrayList(); + this.urn = urn; + this.assetType = assetType; + disposalHook.setDisposableResource(resource); + assetType.registerAsset(this, disposalHook); } /** @@ -113,12 +154,36 @@ public final synchronized void reload(T data) { @SuppressWarnings("unchecked") public final > Optional createInstance() { Preconditions.checkState(!disposed); - return (Optional) assetType.createInstance(this); + ResourceUrn instanceUrn = getUrn().getInstanceUrn(); + + Optional> assetCopy = doCreateCopy(instanceUrn, assetType); + if (!assetCopy.isPresent()) { + Optional assetData; + try { + assetData = assetType.fetchAssetData(instanceUrn); + } catch (PrivilegedActionException e) { + logger.error("Failed to load asset '" + urn + "'", e.getCause()); + return Optional.empty(); + } + if (assetData.isPresent()) { + assetCopy = Optional.ofNullable(assetType.loadAsset(instanceUrn, assetData.get())); + } + } + assetCopy.ifPresent(this::appendInstance); + return (Optional) assetCopy; } - final synchronized Optional> createCopy(ResourceUrn copyUrn) { - Preconditions.checkState(!disposed); - return doCreateCopy(copyUrn, assetType); + /** + * return the non-instanced version of this asset. if the asset is already the normal + * type then it returns itself. instanced assets are temporary copies from the normal loaded instances. + * + * @return non-instanced version of this Asset + */ + public final Asset getNormalAsset() { + if (parent == null) { + return this; + } + return parent; } /** @@ -126,9 +191,17 @@ final synchronized Optional> createCopy(ResourceUrn copyUrn) */ public final synchronized void dispose() { if (!disposed) { + compactInstances(); disposed = true; - assetType.onAssetDisposed(this); disposalHook.dispose(); + if(parent == null) { + for (WeakReference> inst : this.instances()) { + Asset current = inst.get(); + if (current != null) { + current.dispose(); + } + } + } } } diff --git a/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/AssetType.java b/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/AssetType.java index 074c1217..eea38773 100644 --- a/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/AssetType.java +++ b/gestalt-asset-core/src/main/java/org/terasology/gestalt/assets/AssetType.java @@ -1,37 +1,16 @@ -/* - * Copyright 2019 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.gestalt.assets; import android.support.annotation.Nullable; - import com.google.common.base.Function; import com.google.common.base.Preconditions; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; -import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; - import net.jcip.annotations.ThreadSafe; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.context.annotation.API; @@ -43,17 +22,21 @@ import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.lang.reflect.Type; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Semaphore; +import java.util.stream.Collectors; /** * AssetType manages all assets of a particular type/class. It provides the ability to resolve and load assets by Urn, and caches assets so that there is only @@ -75,8 +58,7 @@ public final class AssetType, U extends AssetData> implements private final Class assetDataClass; private final AssetFactory factory; private final List> producers = Lists.newCopyOnWriteArrayList(); - private final Map loadedAssets = new MapMaker().concurrencyLevel(4).makeMap(); - private final ListMultimap> instanceAssets = Multimaps.synchronizedListMultimap(ArrayListMultimap.>create()); + private final Map> loadedAssets = new MapMaker().concurrencyLevel(4).makeMap(); // Per-asset locks to deal with situations where multiple threads attempt to obtain or create the same unloaded asset concurrently private final Map locks = new MapMaker().concurrencyLevel(1).makeMap(); @@ -133,12 +115,16 @@ public synchronized void close() { */ @SuppressWarnings("unchecked") public void processDisposal() { - Reference> ref = disposalQueue.poll(); - while (ref != null) { + Set urns = new HashSet<>(); + Reference> ref = null; + while ((ref = disposalQueue.poll()) != null) { AssetReference> assetRef = (AssetReference>) ref; + urns.add(assetRef.parentUrn); assetRef.dispose(); references.remove(assetRef); - ref = disposalQueue.poll(); + } + for (ResourceUrn urn : urns) { + disposeAsset(urn); } } @@ -153,23 +139,14 @@ public synchronized boolean isClosed() { * Disposes all assets of this type. */ public synchronized void disposeAll() { - loadedAssets.values().forEach(T::dispose); - - for (WeakReference assetRef : ImmutableList.copyOf(instanceAssets.values())) { - T asset = assetRef.get(); + loadedAssets.values().forEach(k -> { + Asset asset = k.get(); if (asset != null) { asset.dispose(); } - } + }); + loadedAssets.clear(); processDisposal(); - if (!loadedAssets.isEmpty()) { - logger.error("Assets remained loaded after disposal - {}", loadedAssets.keySet()); - loadedAssets.clear(); - } - if (!instanceAssets.isEmpty()) { - logger.error("Asset instances remained loaded after disposal - {}", instanceAssets.keySet()); - instanceAssets.clear(); - } } /** @@ -181,12 +158,13 @@ public synchronized void disposeAll() { */ public void refresh() { if (!closed) { - for (T asset : loadedAssets.values()) { - if (!followRedirects(asset.getUrn()).equals(asset.getUrn()) || !reloadFromProducers(asset)) { + for (Reference target : loadedAssets.values()) { + T asset = target.get(); + if (asset != null && (!followRedirects(asset.getUrn()).equals(asset.getUrn()) || !reloadFromProducers(asset))) { asset.dispose(); - for (WeakReference instanceRef : ImmutableList.copyOf(instanceAssets.get(asset.getUrn().getInstanceUrn()))) { - T instance = instanceRef.get(); - if (instance != null) { + for(WeakReference> it :asset.instances()) { + Asset instance = it.get(); + if(instance != null) { instance.dispose(); } } @@ -266,19 +244,6 @@ public Optional getAsset(ResourceUrn urn) { } } - /** - * Notifies the asset type when an asset is disposed - * - * @param asset The asset that was disposed. - */ - void onAssetDisposed(Asset asset) { - if (asset.getUrn().isInstance()) { - instanceAssets.get(asset.getUrn()).remove(new WeakReference<>(assetClass.cast(asset))); - } else { - loadedAssets.remove(asset.getUrn()); - } - } - /** * Notifies the asset type when an asset is created * @@ -288,15 +253,40 @@ synchronized void registerAsset(Asset asset, DisposalHook disposer) { if (closed) { throw new IllegalStateException("Cannot create asset for disposed asset type: " + assetClass); } else { - if (asset.getUrn().isInstance()) { - instanceAssets.put(asset.getUrn(), new WeakReference<>(assetClass.cast(asset))); - } else { - loadedAssets.put(asset.getUrn(), assetClass.cast(asset)); + if (!asset.getUrn().isInstance()) { + loadedAssets.put(asset.getUrn(), new SoftReference(assetClass.cast(asset))); } references.add(new AssetReference<>(asset, disposalQueue, disposer)); } } + /** + * dispose asset and remove loaded asset from {@link #loadedAssets} + * @param target urn to free + */ + private void disposeAsset(ResourceUrn target) { + Preconditions.checkArgument(!target.isInstance()); + if (!loadedAssets.containsKey(target)) { + return; + } + + Reference reference = loadedAssets.get(target); + Asset current = reference.get(); + if (current == null) { + loadedAssets.remove(target); + } else if (current.isDisposed()) { + for (WeakReference> it : current.instances()) { + Asset instance = it.get(); + if (instance != null && !instance.isDisposed()) { + logger.warn("non instanced asset is disposed with instances. instances will become orphaned."); + break; + } + } + loadedAssets.remove(target); + } + } + + /** * Creates and returns an instance of an asset, if possible. The following methods are used to create the copy, in order, with the first technique to succeeed used: *
    @@ -306,12 +296,13 @@ synchronized void registerAsset(Asset asset, DisposalHook disposer) { * * @param urn The urn of the asset to create an instance of * @return An instance of the desired asset + * */ @SuppressWarnings("unchecked") public Optional getInstanceAsset(ResourceUrn urn) { Optional parentAsset = getAsset(urn.getParentUrn()); - if (parentAsset.isPresent()) { - return createInstance(parentAsset.get()); + if (parentAsset.isPresent() && !parentAsset.get().isDisposed()) { + return parentAsset.get().createInstance(); } else { return Optional.empty(); } @@ -324,26 +315,21 @@ public Optional getInstanceAsset(ResourceUrn urn) { * @return The new instance, or {@link Optional#empty} if it could not be created */ Optional createInstance(Asset asset) { - Preconditions.checkArgument(assetClass.isAssignableFrom(asset.getClass())); - Optional> result = asset.createCopy(asset.getUrn().getInstanceUrn()); - if (!result.isPresent()) { - try { - return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> { - for (AssetDataProducer producer : producers) { - Optional data = producer.getAssetData(asset.getUrn()); - if (data.isPresent()) { - return Optional.of(loadAsset(asset.getUrn().getInstanceUrn(), data.get())); - } - } - return Optional.ofNullable(assetClass.cast(result.get())); - }); - } catch (PrivilegedActionException e) { - logger.error("Failed to load asset '" + asset.getUrn().getInstanceUrn() + "'", e.getCause()); - } - } - return Optional.ofNullable(assetClass.cast(result.get())); + return asset.createInstance(); } + + public Optional fetchAssetData(ResourceUrn urn) throws PrivilegedActionException { + return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> { + for (AssetDataProducer producer : producers) { + Optional data = producer.getAssetData(urn); + if (data.isPresent()) { + return data; + } + } + return Optional.empty(); + }); + } /** * Forces a reload of an asset from a data producer, if possible. The resource urn must not be an instance urn (it doesn't make sense to reload an instance by urn). * If there is no available source for the asset (it has no producer) then it will not be reloaded. @@ -355,15 +341,12 @@ public Optional reload(ResourceUrn urn) { Preconditions.checkArgument(!urn.isInstance(), "Cannot reload an asset instance urn"); ResourceUrn redirectUrn = followRedirects(urn); try { - return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> { - for (AssetDataProducer producer : producers) { - Optional data = producer.getAssetData(redirectUrn); - if (data.isPresent()) { - return Optional.of(loadAsset(redirectUrn, data.get())); - } - } - return Optional.ofNullable(loadedAssets.get(redirectUrn)); - }); + Optional data = fetchAssetData(urn); + if (data.isPresent()) { + return Optional.of(loadAsset(redirectUrn, data.get())); + } + Reference reference = loadedAssets.get(redirectUrn); + return Optional.ofNullable(reference == null ? null : reference.get()); } catch (PrivilegedActionException e) { if (redirectUrn.equals(urn)) { logger.error("Failed to load asset '{}'", redirectUrn, e.getCause()); @@ -374,6 +357,51 @@ public Optional reload(ResourceUrn urn) { return Optional.empty(); } + /** + * Loads an asset with the given urn and data. If the asset already exists, it is reloaded with the data instead + * + * @param urn The urn of the asset + * @param data The data to load the asset with + * @return The loaded (or reloaded) asset + */ + public T loadAsset(ResourceUrn urn, U data) { + if (urn.isInstance()) { + return factory.build(urn, this, data); + } else { + Reference reference = loadedAssets.get(urn); + T asset = reference == null ? null : reference.get(); + if (asset != null) { + asset.reload(data); + } else { + ResourceLock lock; + synchronized (locks) { + lock = locks.computeIfAbsent(urn, k -> new ResourceLock(urn)); + } + try { + lock.lock(); + if (!closed) { + reference = loadedAssets.get(urn); + asset = reference == null ? null : reference.get(); + if (asset == null) { + asset = factory.build(urn, this, data); + } else { + asset.reload(data); + } + } + synchronized (locks) { + if (lock.unlock()) { + locks.remove(urn); + } + } + } catch (InterruptedException e) { + logger.error("Failed to load asset - interrupted awaiting lock on resource {}", urn); + } + } + + return asset; + } + } + /** * Obtains a non-instance asset * @@ -382,10 +410,14 @@ public Optional reload(ResourceUrn urn) { */ private Optional getNormalAsset(ResourceUrn urn) { ResourceUrn redirectUrn = followRedirects(urn); - T asset = loadedAssets.get(redirectUrn); + Reference reference = loadedAssets.get(redirectUrn); + T asset = reference == null ? null : reference.get(); if (asset == null) { return reload(redirectUrn); } + if (asset.isDisposed()) { + return Optional.empty(); + } return Optional.ofNullable(asset); } @@ -500,13 +532,12 @@ private boolean reloadFromProducers(Asset asset) { try { for (AssetDataProducer producer : producers) { Optional data = producer.getAssetData(asset.getUrn()); - if (data.isPresent()) { asset.reload(data.get()); - for (WeakReference assetInstanceRef : instanceAssets.get(asset.getUrn().getInstanceUrn())) { - T assetInstance = assetInstanceRef.get(); - if (assetInstance != null) { - assetInstance.reload(data.get()); + for(WeakReference> it :asset.instances()) { + Asset instance = it.get(); + if(instance != null) { + instance.reload(data.get()); } } return true; @@ -518,49 +549,6 @@ private boolean reloadFromProducers(Asset asset) { return false; } - /** - * Loads an asset with the given urn and data. If the asset already exists, it is reloaded with the data instead - * - * @param urn The urn of the asset - * @param data The data to load the asset with - * @return The loaded (or reloaded) asset - */ - public T loadAsset(ResourceUrn urn, U data) { - if (urn.isInstance()) { - return factory.build(urn, this, data); - } else { - T asset = loadedAssets.get(urn); - if (asset != null) { - asset.reload(data); - } else { - ResourceLock lock; - synchronized (locks) { - lock = locks.computeIfAbsent(urn, k -> new ResourceLock(urn)); - } - try { - lock.lock(); - if (!closed) { - asset = loadedAssets.get(urn); - if (asset == null) { - asset = factory.build(urn, this, data); - } else { - asset.reload(data); - } - } - synchronized (locks) { - if (lock.unlock()) { - locks.remove(urn); - } - } - } catch (InterruptedException e) { - logger.error("Failed to load asset - interrupted awaiting lock on resource {}", urn); - } - } - - return asset; - } - } - /** * @param urn The urn of the asset to check. Must not be an instance urn * @return Whether an asset is loaded with the given urn @@ -581,7 +569,7 @@ public Set getLoadedAssetUrns() { * @return A list of all the loaded assets. */ public Set getLoadedAssets() { - return ImmutableSet.copyOf(loadedAssets.values()); + return loadedAssets.values().stream().map(Reference::get).filter(Objects::nonNull).collect(Collectors.toSet()); } /** @@ -640,15 +628,17 @@ public boolean unlock() { public String toString() { return "lock(" + urn + ")"; } + } - private static final class AssetReference extends PhantomReference { + private static final class AssetReference> extends PhantomReference { private final DisposalHook disposalHook; - + public final ResourceUrn parentUrn; public AssetReference(T asset, ReferenceQueue queue, DisposalHook hook) { super(asset, queue); this.disposalHook = hook; + parentUrn = asset.getUrn().getParentUrn(); } public void dispose() { diff --git a/gestalt-asset-core/src/test/java/virtualModules/test/stubs/book/Book.java b/gestalt-asset-core/src/test/java/virtualModules/test/stubs/book/Book.java index d42d00c0..24e27272 100644 --- a/gestalt-asset-core/src/test/java/virtualModules/test/stubs/book/Book.java +++ b/gestalt-asset-core/src/test/java/virtualModules/test/stubs/book/Book.java @@ -35,8 +35,7 @@ public class Book extends Asset { private ImmutableList lines = ImmutableList.of(); public Book(ResourceUrn urn, BookData data, AssetType type) { - super(urn, type); - setDisposableResource(new DisposalAction(urn)); + super(urn, type, new DisposalAction(urn)); reload(data); }