Merge pull request #2517 from apache/pre-merge
Pre merge (3788)
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/metrics/MetricsService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/metrics/MetricsService.java
index 64ec4f7..94ce3cc 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/metrics/MetricsService.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/metrics/MetricsService.java
@@ -18,7 +18,11 @@
*/
package org.apache.causeway.applib.services.metrics;
+import java.util.Collections;
+import java.util.Set;
+
import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.iactn.InteractionProvider;
import org.apache.causeway.schema.ixn.v2.MemberExecutionDto;
@@ -27,33 +31,61 @@
* numbers of object loaded, dirtied etc.
*
* <p>
- * Only entities with {@link DomainObject#entityChangePublishing()} enabled
+ * Only entities with {@link DomainObject#entityChangePublishing()} enabled (in other words, auditing)
* are counted.
* </p>
*
+ * @implNote Implementation depends upon ORM callbacks.
* @since 1.x {@index}
*/
public interface MetricsService {
/**
- * The number of entities that have, so far in this request, been loaded from the database.
- * <p>
- * Corresponds to the number of times that <code>javax.jdo.listener.LoadLifecycleListener#postLoad(InstanceLifecycleEvent)</code> (or equivalent) is fired.
+ * The number of entities that were loaded (but not necessarily modified) from the database.
+ *
* <p>
* Is captured within {@link MemberExecutionDto#getMetrics()} (accessible from {@link InteractionProvider#currentInteraction()}).
+ *
+ * @see #entitiesLoaded()
+ * @see #numberEntitiesDirtied()
*/
int numberEntitiesLoaded();
/**
- * The number of objects that have, so far in this request, been dirtied/will need updating in the database); a
- * good measure of the footprint of the interaction.
+ * Returns the bookmarks of the entities loaded within the transaction.
+ *
* <p>
- * Corresponds to the number of times that <code>javax.jdo.listener.DirtyLifecycleListener#preDirty(InstanceLifecycleEvent)</code> (or equivalent) callback is fired.
+ * Requires detailed metrics to be enabled using config param.
+ *
+ * @see #numberEntitiesLoaded()
+ * @see #entitiesDirtied()
+ */
+ default Set<Bookmark> entitiesLoaded() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * The number of entities that were modified within the transaction.
+ *
* <p>
* Is captured within {@link MemberExecutionDto#getMetrics()} (accessible from {@link InteractionProvider#currentInteraction()}.
+ *
+ * @see #numberEntitiesLoaded()
+ * @see #entitiesDirtied()
*/
int numberEntitiesDirtied();
+ /**
+ * Returns the bookmarks of the entities that were modified within the transaction.
+ *
+ * <p>
+ * Requires detailed metrics to be enabled using config param.
+ *
+ * @see #numberEntitiesDirtied()
+ * @see #entitiesLoaded()
+ */
+ default Set<Bookmark> entitiesDirtied() {
+ return Collections.emptySet();
+ }
}
-
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/PageRenderLogger.java b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/PageRenderLogger.java
index dd379d4..8cbc181 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/PageRenderLogger.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/publishing/log/PageRenderLogger.java
@@ -24,8 +24,11 @@
import java.util.stream.Collectors;
import javax.annotation.Priority;
+import javax.inject.Inject;
import javax.inject.Named;
+import org.apache.causeway.applib.services.metrics.MetricsService;
+
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
@@ -36,6 +39,7 @@
import org.apache.causeway.applib.services.publishing.spi.PageRenderSubscriber;
import org.apache.causeway.commons.internal.base._NullSafe;
+import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
/**
@@ -48,9 +52,12 @@
@Named(PageRenderLogger.LOGICAL_TYPE_NAME)
@Priority(PriorityPrecedence.LATE)
@Qualifier("Logging")
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
@Log4j2
public class PageRenderLogger implements PageRenderSubscriber {
+ private final MetricsService metricsService;
+
static final String LOGICAL_TYPE_NAME = CausewayModuleApplib.NAMESPACE + ".PageRenderLogger";
@Override
@@ -60,7 +67,9 @@
@Override
public void onRenderingDomainObject(final Bookmark bookmark) {
- log.debug("rendering object: [ {} ]", doubleQuoted(bookmark.stringify()));
+ if(log.isDebugEnabled()) {
+ log.debug("rendering object: [ {} ]", doubleQuoted(bookmark.stringify()));
+ }
}
@Override
@@ -68,18 +77,26 @@
final var bookmarksStringified = bookmarksStringified(bookmarkSupplier);
- log.debug("rendering collection: [ {} ]", bookmarksStringified);
+ if (log.isDebugEnabled()) {
+ log.debug("rendering collection: [ {} ]", bookmarksStringified);
+ }
}
@Override
public void onRenderingValue(final Object value) {
- log.debug("rendering value: [ {} ]", doubleQuoted(value));
+ if(log.isDebugEnabled()) {
+ log.debug("rendering value: [ {} ]", doubleQuoted(value));
+ }
}
@Override
public void onRenderedDomainObject(final Bookmark bookmark) {
- log.debug("rendered object: [ {} ]", doubleQuoted(bookmark.stringify()));
+ if(log.isDebugEnabled()) {
+ // until @ActionLayout#redirectPolicy is reintroduced (if it ever is), there's no point in querying for the numberEntitiesDirtied,
+ // because (for Wicket viewer at least), the rendering is in a separate request to any modifying action.
+ log.debug("rendered object: [ {} ] numEntitiesLoaded: {}", doubleQuoted(bookmark.stringify()), metricsService.numberEntitiesLoaded());
+ }
}
@Override
@@ -87,13 +104,17 @@
final var bookmarksStringified = bookmarksStringified(bookmarkSupplier);
- log.debug("rendered collection: [ {} ]", bookmarksStringified);
+ if (log.isDebugEnabled()) {
+ log.debug("rendered collection: [ {} ]", bookmarksStringified);
+ }
}
@Override
public void onRenderedValue(final Object value) {
- log.debug("rendered value: [ {} ]", doubleQuoted(value));
+ if(log.isDebugEnabled()) {
+ log.debug("rendered value: [ {} ]", doubleQuoted(value));
+ }
}
// -- HELPER
@@ -109,5 +130,4 @@
private static String doubleQuoted(final @Nullable Object obj) {
return "\"" + obj + "\"";
}
-
}
diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
index 02459ce..c8191e4 100644
--- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
+++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java
@@ -1547,6 +1547,28 @@
}
}
+
+ private final Service service = new Service();
+ @Data
+ public static class Service {
+
+ private final MetricsService metricsService = new MetricsService();
+ @Data
+ public static class MetricsService {
+ public enum Level {
+ COUNTERS_ONLY,
+ COUNTERS_AND_DETAIL;
+
+ public boolean isCountersOnly() { return this == COUNTERS_ONLY; }
+ public boolean isCountersAndDetail() { return this == COUNTERS_AND_DETAIL; }
+ }
+
+ /**
+ * What level of detail the MetricsService should capture.
+ */
+ private Level level = Level.COUNTERS_ONLY;
+ }
+ }
}
private final Core core = new Core();
diff --git a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
index 154ba3d..4aec4ea 100644
--- a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
+++ b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
@@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Map;
@@ -87,6 +88,7 @@
import org.apache.causeway.core.transaction.changetracking.HasEnlistedEntityChanges;
import org.apache.causeway.persistence.commons.CausewayModulePersistenceCommons;
+import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.val;
@@ -171,30 +173,60 @@
enlistedPropertyChangeRecordsById.computeIfAbsent(pcrId, func);
}
- /**
- * Contains pre- and post- values of every property of every object that actually changed. A lazy snapshot.
- */
- private final _Lazy<Set<PropertyChangeRecord>> entityPropertyChangeRecordsForPublishing
- = _Lazy.of(() -> {
- Set<PropertyChangeRecord> records;
- try {
- records = changedRecords(enlistedPropertyChangeRecordsById.values());
- } catch(ConcurrentModificationException ex) {
- log.warn(
- "A concurrent modification exception, one of these properties seemed to change as we looked at it :\n" +
- enlistedPropertyChangeRecordsById.keySet()
+ private Changes evaluateChanges() {
+ val changedProperties = evaluateChangedProperties();
+
+ val isCountersAndDetail = causewayConfiguration.getApplib().getService().getMetricsService().getLevel().isCountersAndDetail();
+ Set<Bookmark> loadedBookmarks =
+ isCountersAndDetail
+ ? enlistedPropertyChangeRecordsById.keySet()
.stream()
- .map(PropertyChangeRecordId::toString)
- .collect(Collectors.joining("\n"))
- );
- // instead, we take a copy
- records = changedRecords(new ArrayList<PropertyChangeRecord>(enlistedPropertyChangeRecordsById.values()));
- }
+ .map(PropertyChangeRecordId::getBookmark)
+ .collect(Collectors.<Bookmark>toSet())
+ : Collections.emptySet();
+
+ Set<Bookmark> dirtiedBookmarks =
+ isCountersAndDetail
+ ? changedProperties
+ .stream()
+ .map(PropertyChangeRecord::getBookmark)
+ .collect(Collectors.<Bookmark>toSet())
+ : Collections.emptySet();
enlistedPropertyChangeRecordsById.clear();
- return records;
- });
+ return new Changes(changedProperties, loadedBookmarks, dirtiedBookmarks);
+ }
+
+ private Set<PropertyChangeRecord> evaluateChangedProperties() {
+ Set<PropertyChangeRecord> dirtiedProperties;
+ try {
+ dirtiedProperties = changedRecords(enlistedPropertyChangeRecordsById.values());
+ } catch (ConcurrentModificationException ex) {
+ log.warn(
+ "A concurrent modification exception, one of these properties seemed to change as we looked at it :\n" +
+ enlistedPropertyChangeRecordsById.keySet()
+ .stream()
+ .map(PropertyChangeRecordId::toString)
+ .collect(Collectors.joining("\n"))
+ );
+ // instead, we take a copy
+ dirtiedProperties = changedRecords(new ArrayList<>(enlistedPropertyChangeRecordsById.values()));
+ }
+ return dirtiedProperties;
+ }
+
+ @RequiredArgsConstructor
+ static class Changes {
+ @Getter private final Set<PropertyChangeRecord> dirtiedProperties;
+ @Getter private final Set<Bookmark> loadedBookmarks;
+ @Getter private final Set<Bookmark> dirtiedBookmarks;
+ }
+
+ /**
+ * Contains pre- and post- values of every property of every object that actually changed. A lazy snapshot.
+ */
+ private final _Lazy<Changes> changes = _Lazy.of(this::evaluateChanges);
/**
* when iterating over the original value set, if any of the properties causes the entity to change state as it is
@@ -215,12 +247,11 @@
.collect(_Sets.toUnmodifiable());
}
- private Set<PropertyChangeRecord> memoizePropertyChangeRecordsIfRequired() {
- return entityPropertyChangeRecordsForPublishing.get();
+ private Changes memoizeChangesIfRequired() {
+ return changes.get();
}
-
/**
* @implNote access to this {@link Map} must be thread-safe (insertion order preservation is not required)
*/
@@ -232,10 +263,10 @@
@Override
- public void destroy() throws Exception {
+ public void destroy() {
if(log.isDebugEnabled()) {
- val interactionId = interactionProviderProvider.get().currentInteraction().map(Interaction::getInteractionId).orElseGet(null);
+ val interactionId = interactionProviderProvider.get().currentInteraction().map(Interaction::getInteractionId).orElse(null);
log.debug("EntityChangeTrackerDefault.destroy xactn={} interactionId={} thread={}", transactionCounter.get(), interactionId, Thread.currentThread().getName());
}
@@ -281,7 +312,7 @@
// guard against transient
if(ManagedObjects.bookmark(entity).isEmpty()) return true;
- if(entityPropertyChangeRecordsForPublishing.isMemoized()) {
+ if(changes.isMemoized()) {
throw _Exceptions.illegalState("Cannot enlist additional changes for auditing, "
+ "since changedObjectPropertiesRef was already prepared (memoized) for auditing.");
}
@@ -297,7 +328,7 @@
log.debug("about to publish entity changes");
// we memoize the property changes to (hopefully) avoid ConcurrentModificationExceptions with ourselves later
- memoizePropertyChangeRecordsIfRequired();
+ memoizeChangesIfRequired();
entityPropertyChangePublisher.publishChangedProperties();
entityChangesPublisher.publishChangingEntities(this);
@@ -311,7 +342,7 @@
private void resetState(LongAdder entityChangeEventCount, LongAdder numberEntitiesLoaded) {
enlistedPropertyChangeRecordsById.clear();
- entityPropertyChangeRecordsForPublishing.clear();
+ changes.clear();
changeKindByEnlistedAdapter.clear();
entityChangeEventCount.reset();
@@ -323,7 +354,8 @@
private void enableCommandPublishing() {
val alreadySet = persistentChangesEncountered.getAndSet(true);
if(!alreadySet) {
- currentInteraction().getCommand(); //TODO does this call have side-effects? if so explain, else remove
+ // has side effects
+ val command = currentInteraction().getCommand();
}
}
@@ -343,7 +375,7 @@
// this code path has side-effects, it locks the result for this transaction,
// such that cannot enlist on top of it
- final int numberEntityPropertiesModified = memoizePropertyChangeRecordsIfRequired().size();
+ final int numberEntityPropertiesModified = memoizeChangesIfRequired().dirtiedProperties.size();
val interactionId = interaction.getInteractionId();
final int nextEventSequence = ((InteractionInternal) interaction).getThenIncrementTransactionSequence();
@@ -423,7 +455,7 @@
// this code path has side-effects, it locks the result for this transaction,
// such that cannot enlist on top of it
- Set<PropertyChangeRecord> propertyChangeRecords = memoizePropertyChangeRecordsIfRequired();
+ Set<PropertyChangeRecord> propertyChangeRecords = memoizeChangesIfRequired().getDirtiedProperties();
return propertyChangeRecords.stream()
.map(propertyChangeRecord -> propertyChangeRecord.toEntityPropertyChange(timestamp, userName, txId))
@@ -590,6 +622,16 @@
return changeKindByEnlistedAdapter.size();
}
+ @Override
+ public Set<Bookmark> entitiesLoaded() {
+ return memoizeChangesIfRequired().getLoadedBookmarks();
+ }
+
+ @Override
+ public Set<Bookmark> entitiesDirtied() {
+ return memoizeChangesIfRequired().getDirtiedBookmarks();
+ }
+
// -- HELPER
/**
@@ -602,7 +644,6 @@
boolean isEnabled();
}
- @Inject private Configuration configuration;
@Component
@Priority(PriorityPrecedence.LATE)
@@ -621,4 +662,8 @@
return causewayConfiguration.getPersistence().getCommons().getEntityChangeTracker().isEnabled();
}
}
+
+ @Inject private Configuration configuration;
+ @Inject private CausewayConfiguration causewayConfiguration;
+
}
\ No newline at end of file