Add SoftSchemaSourceCache 24/99424/2
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 26 Jan 2022 10:04:39 +0000 (11:04 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 26 Jan 2022 13:28:04 +0000 (14:28 +0100)
GuavaSchemaSourceCache has a rather ugly design, which cannot be easily
scaled. Introduce a simpler SoftSchemaSourceCache as its replacement.

JIRA: YANGTOOLS-1391
Change-Id: I0de9092885ede3efff9d76d48ec14b85a69818b2
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 0cd2c12e31e8bdf3463b8282a6c6cc2e6995c140)

yang/yang-repo-spi/src/main/java/org/opendaylight/yangtools/yang/model/repo/spi/GuavaSchemaSourceCache.java
yang/yang-repo-spi/src/main/java/org/opendaylight/yangtools/yang/model/repo/spi/SoftSchemaSourceCache.java [new file with mode: 0644]
yang/yang-repo-spi/src/test/java/org/opendaylight/yangtools/yang/model/repo/spi/GuavaSchemaSourceCacheTest.java
yang/yang-repo-spi/src/test/java/org/opendaylight/yangtools/yang/model/repo/spi/SoftSchemaSourceCacheTest.java [new file with mode: 0644]

index ce337c829789afb40168314d2a1142c4e1f922bb..4720e37519dc2dafa2833b550c15e2278b25cd76 100644 (file)
@@ -28,8 +28,10 @@ import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
  * A simple {@link AbstractSchemaSourceCache} based on {@link Cache Guava Cache}.
  *
  * @param <T> {@link SchemaSourceRepresentation} type stored in this cache
+ * @deprecated This class has a rather complicated and ugly design. Use {@link SoftSchemaSourceCache} instead.
  */
 @Beta
+@Deprecated(since = "7.0.13", forRemoval = true)
 public final class GuavaSchemaSourceCache<T extends SchemaSourceRepresentation> extends AbstractSchemaSourceCache<T>
         implements AutoCloseable {
     // FIXME: 7.0.0: use a java.util.Cleaner?
diff --git a/yang/yang-repo-spi/src/main/java/org/opendaylight/yangtools/yang/model/repo/spi/SoftSchemaSourceCache.java b/yang/yang-repo-spi/src/main/java/org/opendaylight/yangtools/yang/model/repo/spi/SoftSchemaSourceCache.java
new file mode 100644 (file)
index 0000000..cdbc4b6
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.model.repo.spi;
+
+import com.google.common.annotations.Beta;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.lang.ref.Cleaner;
+import java.lang.ref.Cleaner.Cleanable;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.opendaylight.yangtools.concepts.Registration;
+import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource.Costs;
+
+/**
+ * A simple {@link AbstractSchemaSourceCache} maintaining soft references.
+ *
+ * @param <T> {@link SchemaSourceRepresentation} type stored in this cache
+ */
+@Beta
+public final class SoftSchemaSourceCache<T extends SchemaSourceRepresentation> extends AbstractSchemaSourceCache<T>
+        implements AutoCloseable {
+    private static final Cleaner CLEANER = Cleaner.create();
+
+    private final ConcurrentMap<SourceIdentifier, SoftReference<T>> references = new ConcurrentHashMap<>();
+    private final ConcurrentMap<Registration, Cleanable> cleanables = new ConcurrentHashMap<>();
+
+    private boolean closed;
+
+    public SoftSchemaSourceCache(final SchemaSourceRegistry consumer, final Class<T> representation) {
+        super(consumer, representation, Costs.IMMEDIATE);
+    }
+
+    @Override
+    public ListenableFuture<? extends T> getSource(final SourceIdentifier sourceIdentifier) {
+        final var ref = references.get(sourceIdentifier);
+        if (ref != null) {
+            final var src = ref.get();
+            if (src != null) {
+                // We have a hit
+                return Futures.immediateFuture(src);
+            }
+
+            // Expired entry: remove it
+            references.remove(sourceIdentifier, ref);
+        }
+
+        return Futures.immediateFailedFuture(new MissingSchemaSourceException("Source not found", sourceIdentifier));
+    }
+
+    @Override
+    public synchronized void close() {
+        if (!closed) {
+            closed = true;
+            while (!cleanables.isEmpty()) {
+                cleanables.values().forEach(Cleanable::clean);
+            }
+        }
+    }
+
+    @Override
+    protected synchronized void offer(final T source) {
+        if (closed) {
+            return;
+        }
+
+        final var id = source.getIdentifier();
+        final var ref = new SoftReference<>(source);
+
+        while (true) {
+            final var prev = references.putIfAbsent(id, ref);
+            if (prev == null) {
+                // We have performed a fresh insert and need to add a cleanup
+                break;
+            }
+
+            if (prev.get() != null) {
+                // We still have a source for this identifier, no further action is needed
+                return;
+            }
+
+            // Existing reference is dead, remove it and retry
+            references.remove(id, prev);
+        }
+
+        // We have populated a cache entry, register the source and a cleanup action
+        final var reg = register(id);
+        cleanables.put(reg, CLEANER.register(source, () -> {
+            cleanables.remove(reg);
+            reg.close();
+            references.remove(id, ref);
+        }));
+
+        // Ensure 'source' is still reachable here. This is needed to ensure the cleanable action does not fire before
+        // we have had a chance to insert it into the map.
+        Reference.reachabilityFence(source);
+    }
+}
index b8e3494773f3ba5e02823fda09669f57f250857f..3e2c637c2bff8522576c2626c7a2042f9300c750 100644 (file)
@@ -31,6 +31,7 @@ import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
 import org.opendaylight.yangtools.yang.model.repo.api.YangSchemaSourceRepresentation;
 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
 
+@Deprecated
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
 public class GuavaSchemaSourceCacheTest {
     public static final Class<YangSchemaSourceRepresentation> REPRESENTATION = YangSchemaSourceRepresentation.class;
diff --git a/yang/yang-repo-spi/src/test/java/org/opendaylight/yangtools/yang/model/repo/spi/SoftSchemaSourceCacheTest.java b/yang/yang-repo-spi/src/test/java/org/opendaylight/yangtools/yang/model/repo/spi/SoftSchemaSourceCacheTest.java
new file mode 100644 (file)
index 0000000..e699da4
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.model.repo.spi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.base.MoreObjects.ToStringHelper;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangSchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class SoftSchemaSourceCacheTest {
+    public static final Class<YangSchemaSourceRepresentation> REPRESENTATION = YangSchemaSourceRepresentation.class;
+    public static final long LIFETIME = 1000L;
+    public static final TimeUnit UNITS = TimeUnit.MILLISECONDS;
+
+    @Mock
+    public SchemaSourceRegistry registry;
+    @Mock
+    public SchemaSourceRegistration<?> registration;
+
+    @Before
+    public void setUp() {
+        doNothing().when(registration).close();
+        doReturn(registration).when(registry).registerSchemaSource(any(SchemaSourceProvider.class),
+            any(PotentialSchemaSource.class));
+    }
+
+    @Test
+    public void inMemorySchemaSourceCacheTest() {
+        try (var cache = new SoftSchemaSourceCache<>(registry, REPRESENTATION)) {
+            assertNotNull(cache);
+        }
+    }
+
+    @Test
+    public void inMemorySchemaSourceCacheOfferAndGetSourcestest() throws Exception {
+        try (var cache = new SoftSchemaSourceCache<>(registry, REPRESENTATION)) {
+            final String content = "content";
+            final YangTextSchemaSource source = new TestingYangSource("test", "2012-12-12", content);
+            cache.offer(source);
+            final var sourceIdentifier = RevisionSourceIdentifier.create("test", Revision.of("2012-12-12"));
+            final var checkedSource = cache .getSource(sourceIdentifier);
+            assertNotNull(checkedSource);
+            final var yangSchemaSourceRepresentation = checkedSource.get();
+            assertNotNull(yangSchemaSourceRepresentation);
+            assertEquals(sourceIdentifier, yangSchemaSourceRepresentation.getIdentifier());
+        }
+    }
+
+    @Test
+    public void inMemorySchemaSourceCacheNullGetSourcestest() throws Exception {
+        try (var cache = new SoftSchemaSourceCache<>(registry, REPRESENTATION)) {
+            final var sourceIdentifier = RevisionSourceIdentifier.create("test", Revision.of("2012-12-12"));
+            final var checkedSource = cache.getSource(sourceIdentifier);
+            assertNotNull(checkedSource);
+            assertThrows(ExecutionException.class, () -> checkedSource.get());
+        }
+    }
+
+    @Test
+    public void inMemorySchemaSourceCache3test() throws InterruptedException, ExecutionException {
+        try (var cache1 = new SoftSchemaSourceCache<>(registry, REPRESENTATION)) {
+            try (var cache2 = new SoftSchemaSourceCache<>(registry, REPRESENTATION)) {
+                final String content = "content";
+                final YangTextSchemaSource source = new TestingYangSource("test", "2012-12-12", content);
+                cache1.offer(source);
+                cache2.offer(source);
+
+                final var sourceIdentifier = RevisionSourceIdentifier.create("test", Revision.of("2012-12-12"));
+                final var checkedSource = cache1.getSource(sourceIdentifier);
+                final var checkedSource2 = cache2.getSource(sourceIdentifier);
+                assertNotNull(checkedSource);
+                assertNotNull(checkedSource2);
+
+                assertEquals(checkedSource.get(), checkedSource2.get());
+            }
+        }
+    }
+
+    private static class TestingYangSource extends YangTextSchemaSource {
+        private final String content;
+
+        TestingYangSource(final String name, final String revision, final String content) {
+            super(RevisionSourceIdentifier.create(name, Revision.ofNullable(revision)));
+            this.content = content;
+        }
+
+        @Override
+        public InputStream openStream() {
+            return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+        }
+
+        @Override
+        public Optional<String> getSymbolicName() {
+            return Optional.empty();
+        }
+
+        @Override
+        protected ToStringHelper addToStringAttributes(final ToStringHelper toStringHelper) {
+            return toStringHelper;
+        }
+    }
+}