This is an automated email from the ASF dual-hosted git repository. ahuber pushed a commit to branch v3 in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/v3 by this push: new a67d5deb3f7 CAUSEWAY-2297: ditch all the custom ObjectSpec caches a67d5deb3f7 is described below commit a67d5deb3f7b8ddf5a39e6d9501877a0cf421df4 Author: Andi Huber <ahu...@apache.org> AuthorDate: Tue Nov 12 19:13:47 2024 +0100 CAUSEWAY-2297: ditch all the custom ObjectSpec caches --- .../collections/snapshot/_VersionedList.java | 209 --------------------- .../collections/snapshot/VersionedListTest.java | 67 ------- .../metamodel/specloader/SpecificationCache.java | 108 ----------- .../specloader/SpecificationLoaderDefault.java | 22 ++- .../core/metamodel/specloader/_LogUtil.java | 11 +- .../specloader/SpecificationCacheDefaultTest.java | 109 ----------- 6 files changed, 20 insertions(+), 506 deletions(-) diff --git a/commons/src/main/java/org/apache/causeway/commons/internal/collections/snapshot/_VersionedList.java b/commons/src/main/java/org/apache/causeway/commons/internal/collections/snapshot/_VersionedList.java deleted file mode 100644 index 84444b7c444..00000000000 --- a/commons/src/main/java/org/apache/causeway/commons/internal/collections/snapshot/_VersionedList.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -package org.apache.causeway.commons.internal.collections.snapshot; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.NonNull; - -/** - * Thread-safe pseudo list, that increments its version each time a snapshot is requested. - * <p> - * This allows to easily keep track of any additions to the list that occurred in between - * snapshots. - * - * @since 2.0 - * @param <T> - */ -public final class _VersionedList<T> { - - private UUID uuid = UUID.randomUUID(); - private final List<List<T>> versions = new ArrayList<>(); - private List<T> currentVersion = new ArrayList<>(); - private int size; - - @AllArgsConstructor(access = AccessLevel.PRIVATE) - public static final class Snapshot<T> { - private UUID ownerUuid; - @SuppressWarnings("unused") - private final int fromIndex; //low endpoint (inclusive) of the copy - private final int toIndex; // high endpoint (exclusive) of the copy - private final List<List<T>> versions; - - public boolean isEmpty() { - return versions.isEmpty(); - } - - public Stream<T> stream() { - return versions.stream() - .flatMap(List::stream); - } - - public void forEach(final Consumer<T> action) { - for(var ver : versions) { - for(var element : ver) { - action.accept(element); - } - } - } - - public void forEachParallel(final Consumer<T> action) { - for(var ver : versions) { - if(ver.size()>8) { - ver.parallelStream().forEach(action); - } else { - for(var element : ver) { - action.accept(element); - } - } - } - } - - } - - public Snapshot<T> snapshot() { - synchronized(versions) { - commit(); - return new Snapshot<>(uuid, 0, versions.size(), defensiveCopy()); - } - } - - public Snapshot<T> deltaSince(final @NonNull Snapshot<T> snapshot) { - - if(snapshot.ownerUuid!=uuid) { - throw new IllegalArgumentException("Snapshot's UUID is different from the VersionedList's."); - } - - synchronized(versions) { - commit(); - var from = snapshot.toIndex; - var to = versions.size(); - return new Snapshot<>(uuid, from, to, defensiveCopy(from, to)); - } - } - - public int size() { - return size; - } - - public boolean isEmpty() { - return size==0; - } - - /** current implementation cannot handle concurrent additions that occur during traversal*/ - public Stream<T> stream() { - final List<List<T>> defensiveCopy; - synchronized(versions) { - commit(); - defensiveCopy = defensiveCopy(); - } - return defensiveCopy.stream() - .flatMap(List::stream); - } - - public boolean add(final T e) { - synchronized(versions) { - ++size; - return currentVersion.add(e); - } - } - - public boolean addAll(final Collection<? extends T> c) { - synchronized(versions) { - size+=c.size(); - return currentVersion.addAll(c); - } - } - - public void clear() { - synchronized(versions) { - uuid = UUID.randomUUID(); - size=0; - versions.clear(); - currentVersion.clear(); - } - } - - /** - * Also handles concurrent additions that occur during traversal. - * @param action - */ - public void forEach(final Consumer<T> action) { - var snapshot = snapshot(); - snapshot.forEach(action); - Snapshot<T> delta = deltaSince(snapshot); - while(!delta.isEmpty()) { - delta.forEach(action); - delta = deltaSince(delta); - } - } - - /** - * Also handles concurrent additions that occur during traversal. - * @param action - */ - public void forEachConcurrent(final Consumer<T> action) { - var snapshot = snapshot(); - snapshot.forEachParallel(action); - Snapshot<T> delta = deltaSince(snapshot); - while(!delta.isEmpty()) { - delta.forEachParallel(action); - delta = deltaSince(delta); - } - } - - // -- HELPER - - /** - * @implNote only call within synchronized block! - * @param fromIndex low endpoint (inclusive) of the copy - * @param toIndex high endpoint (exclusive) of the copy - */ - private List<List<T>> defensiveCopy(final int fromIndex, final int toIndex) { - if(fromIndex==toIndex) { - return Collections.emptyList(); - } - return new ArrayList<>(versions.subList(fromIndex, toIndex)); - } - - /** @implNote only call within synchronized block! */ - private List<List<T>> defensiveCopy() { - if(versions.isEmpty()) { - return Collections.emptyList(); - } - return new ArrayList<>(versions); - } - - /** @implNote only call within synchronized block! */ - private void commit() { - if(!currentVersion.isEmpty()) { - versions.add(currentVersion); - currentVersion = new ArrayList<>(); // create a new array for others to write to next - } - } - -} diff --git a/commons/src/test/java/org/apache/causeway/commons/internal/collections/snapshot/VersionedListTest.java b/commons/src/test/java/org/apache/causeway/commons/internal/collections/snapshot/VersionedListTest.java deleted file mode 100644 index 3e473b9a8a5..00000000000 --- a/commons/src/test/java/org/apache/causeway/commons/internal/collections/snapshot/VersionedListTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -package org.apache.causeway.commons.internal.collections.snapshot; - -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class VersionedListTest { - - @Test - void test() { - - var vList = new _VersionedList<String>(); - - assertEquals(0, vList.size()); - assertTrue(vList.isEmpty()); - - vList.add("foo"); - - assertEquals(1, vList.size()); - assertFalse(vList.isEmpty()); - - var snapshot1 = vList.snapshot(); - assertEquals(1, snapshot1.stream().count()); - - vList.add("bar"); - - var snapshot2 = vList.snapshot(); - assertEquals(2, snapshot2.stream().count()); - assertEquals("foo,bar", snapshot2.stream().collect(Collectors.joining(","))); - - var delta = vList.deltaSince(snapshot1); - assertEquals(1, delta.stream().count()); - assertEquals("bar", delta.stream().collect(Collectors.joining(","))); - - vList.add("gru"); - - var snapshot3 = vList.snapshot(); - assertEquals("foo,bar,gru", snapshot3.stream().collect(Collectors.joining(","))); - - var snapshot4 = vList.deltaSince(snapshot3); - assertTrue(snapshot4.isEmpty()); - - } - -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationCache.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationCache.java deleted file mode 100644 index 69ba7063a45..00000000000 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationCache.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -package org.apache.causeway.core.metamodel.specloader; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.springframework.lang.Nullable; - -import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.commons.internal.collections.snapshot._VersionedList; -import org.apache.causeway.core.metamodel.spec.ObjectSpecification; - -import lombok.NonNull; - -record SpecificationCache( - Map<Class<?>, ObjectSpecification> specByClass, - // optimization: specialized list to keep track of any additions to the cache fast - _VersionedList<ObjectSpecification> vList) { - - SpecificationCache() { - this(new HashMap<>(), new _VersionedList<>()); - } - - Optional<ObjectSpecification> lookup(final Class<?> cls) { - synchronized(this) { - return Optional.ofNullable(specByClass.get(cls)); - } - } - - ObjectSpecification computeIfAbsent( - final Class<?> cls, - final Function<Class<?>, ObjectSpecification> mappingFunction) { - synchronized(this) { - var spec = specByClass.get(cls); - if(spec==null) { - spec = mappingFunction.apply(cls); - internalPut(spec); - } - return spec; - } - } - - void clear() { - synchronized(this) { - specByClass.clear(); - vList.clear(); - } - } - - /** @returns thread-safe defensive copy */ - Can<ObjectSpecification> snapshotSpecs() { - synchronized(this) { - return Can.ofCollection(specByClass.values()); - } - } - - ObjectSpecification remove(@NonNull final Class<?> cls) { - synchronized(this) { - var removed = specByClass.remove(cls); - if(removed!=null) { - vList.clear(); // invalidate - vList.addAll(specByClass.values()); - } - return removed; - } - } - - void forEachConcurrent(final Consumer<ObjectSpecification> onSpec) { - vList.forEachConcurrent(onSpec); - } - - void forEach(final Consumer<ObjectSpecification> onSpec) { - vList.forEach(onSpec); - } - - // -- HELPER - - private void internalPut(@Nullable final ObjectSpecification spec) { - if(spec==null) return; - - var cls = spec.getCorrespondingClass(); - var existing = specByClass.put(cls, spec); - if(existing==null) { - vList.add(spec); // add to vList only if we don't have it already - } - } - -} diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java index 15acdf02974..7a44e5fa4be 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/SpecificationLoaderDefault.java @@ -21,8 +21,10 @@ package org.apache.causeway.core.metamodel.specloader; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -132,7 +134,7 @@ implements private FacetProcessor facetProcessor; - private final SpecificationCache cache = new SpecificationCache(); + private final Map<Class<?>, ObjectSpecification> cache = new ConcurrentHashMap<>(); private final LogicalTypeResolver logicalTypeResolver = new LogicalTypeResolver(); /** @@ -287,7 +289,7 @@ implements //XXX[CAUSEWAY-2382] when parallel introspecting, make sure we have the mixins before their holders // (observation by experiment, no real understanding as to why) - _LogUtil.logBefore(log, cache, knownSpecs); + _LogUtil.logBefore(log, this::snapshotSpecifications, knownSpecs); log.info(" - introspecting {} type hierarchies", knownSpecs.size()); introspect(Can.ofCollection(knownSpecs), IntrospectionState.TYPE_INTROSPECTED); @@ -311,10 +313,10 @@ implements introspect(Can.ofCollection(domainObjectSpecs), IntrospectionState.FULLY_INTROSPECTED); - _LogUtil.logAfter(log, cache, knownSpecs); + _LogUtil.logAfter(log, this::snapshotSpecifications, knownSpecs); if(isFullIntrospect()) { - var snapshot = cache.snapshotSpecs(); + var snapshot = snapshotSpecifications(); log.info(" - introspecting all {} types eagerly (FullIntrospect=true)", snapshot.size()); introspect(snapshot.filter(x->x.getBeanSort().isMixin()), IntrospectionState.FULLY_INTROSPECTED); introspect(snapshot.filter(x->!x.getBeanSort().isMixin()), IntrospectionState.FULLY_INTROSPECTED); @@ -452,18 +454,22 @@ implements @Override public Can<ObjectSpecification> snapshotSpecifications() { - return cache.snapshotSpecs(); + return Can.ofCollection(cache.values()); } @Override public void forEach(final Consumer<ObjectSpecification> onSpec) { var shouldRunConcurrent = causewayConfiguration.getCore().getMetaModel().getValidator().isParallelize(); + var snapshot = snapshotSpecifications(); if(shouldRunConcurrent) { - cache.forEachConcurrent(onSpec); + snapshot + .stream() + .parallel() + .forEach(onSpec); } else { - cache.forEach(onSpec); + snapshot + .forEach(onSpec); } - } @Override diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java index 041a2df6025..c521cc0e4de 100644 --- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java +++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/_LogUtil.java @@ -20,10 +20,12 @@ package org.apache.causeway.core.metamodel.specloader; import java.util.Collection; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.logging.log4j.Logger; +import org.apache.causeway.commons.collections.Can; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import lombok.experimental.UtilityClass; @@ -33,15 +35,14 @@ final class _LogUtil { void logBefore( final Logger log, - final SpecificationCache cache, + final Supplier<Can<ObjectSpecification>> snapshot, final List<? extends ObjectSpecification> scanned) { if(!log.isDebugEnabled()) { return; } - var cached = cache.snapshotSpecs(); - + var cached = snapshot.get(); log.debug(String.format( "scanned.size = %d ; cached.size = %d", scanned.size(), cached.size())); @@ -60,14 +61,14 @@ final class _LogUtil { void logAfter( final Logger log, - final SpecificationCache cache, + final Supplier<Can<ObjectSpecification>> snapshot, final Collection<? extends ObjectSpecification> scanned) { if(!log.isDebugEnabled()) { return; } - var cached = cache.snapshotSpecs(); + var cached = snapshot.get(); var cachedAfterNotBefore = cached.stream() .filter(spec -> !scanned.contains(spec)) .collect(Collectors.toList()); diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/SpecificationCacheDefaultTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/SpecificationCacheDefaultTest.java deleted file mode 100644 index f129b7aaf37..00000000000 --- a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/SpecificationCacheDefaultTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -package org.apache.causeway.core.metamodel.specloader; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; - -import org.apache.causeway.applib.id.LogicalType; -import org.apache.causeway.core.metamodel.spec.ObjectSpecification; - -class SpecificationCacheDefaultTest { - - private LogicalType cus = _LogicalTypeTestFactory.cus(); - private LogicalType ord = _LogicalTypeTestFactory.ord(); - - ObjectSpecification customerSpec; - ObjectSpecification orderSpec; - - private SpecificationCache specificationCache = new SpecificationCache(); - private LogicalTypeResolver logicalTypeResolver = new LogicalTypeResolver(); - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @BeforeEach - public void setUp() throws Exception { - - customerSpec = Mockito.mock(ObjectSpecification.class); - orderSpec = Mockito.mock(ObjectSpecification.class); - - Mockito.when(customerSpec.getCorrespondingClass()).thenReturn((Class)Customer.class); - Mockito.when(customerSpec.getLogicalType()).thenReturn(cus); - - Mockito.when(orderSpec.getCorrespondingClass()).thenReturn((Class)Order.class); - Mockito.when(orderSpec.getLogicalType()).thenReturn(ord); - } - - @AfterEach - public void tearDown() throws Exception { - specificationCache = null; - } - - static class Customer {} - static class Order {} - - @Test - public void get_whenNotCached() { - assertFalse(specificationCache.lookup(Customer.class).isPresent()); - } - - @Test - public void get_whenCached() { - - specificationCache.computeIfAbsent(Customer.class, __->customerSpec); - - final ObjectSpecification objectSpecification = specificationCache.lookup(Customer.class) - .orElse(null); - - assertSame(objectSpecification, customerSpec); - } - - @Test - public void allSpecs_whenCached() { - specificationCache.computeIfAbsent(Customer.class, __->customerSpec); - specificationCache.computeIfAbsent(Order.class, __->orderSpec); - - var allSpecs = specificationCache.snapshotSpecs(); - - assertThat(allSpecs.size(), is(2)); - } - - @Test - public void getByObjectType_whenNotSet() { - var type = logicalTypeResolver.lookup(cus.logicalName()); - assertFalse(type.isPresent()); - } - - @Test - public void getByObjectType_whenSet() { - - specificationCache.computeIfAbsent(Customer.class, __->customerSpec); - - var objectSpec = specificationCache.lookup(Customer.class).orElse(null); - - assertSame(objectSpec, customerSpec); - } - -}