Skip to content

Commit

Permalink
Disable DataNucleus L2 cache globally
Browse files Browse the repository at this point in the history
Currently, DataNucleus will put all objects into the L2 cache. Given the volume of objects being processed by DT, this behavior quickly adds up to enormous cache sizes. Users continue to be bamboozled by DT's memory requirements, which for a large part are driven by the wasteful L2 caching.

While working on DependencyTrack#4305, it became obvious that the hit rates of the cache are absolutely dwarfed by the high rate of misses. Storing such large volumes of objects in RAM is simply not justified if hit rates are that low.

Disabling the L2 cache solves a lot of recurring issues we and users are facing. If we want to introduce caching again in the future, we should do it in targeted areas, and preferably not directly in the persistence layer.

We disabled the L2 cache in Hyades a long time ago, and it has worked out very well for us. It was a precondition to make the API server horizontally scalable. Some more context:

* DependencyTrack/hyades#375 (comment)
* DependencyTrack/hyades#576

Supersedes DependencyTrack#4305

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Oct 25, 2024
1 parent e2934b7 commit d5befb0
Show file tree
Hide file tree
Showing 12 changed files with 7 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void parse(final File file) {
final String cveId = values.get(0);
final BigDecimal epssScore = new BigDecimal(values.get(1));
final BigDecimal percentile = new BigDecimal(values.get(2));
try (final QueryManager qm = new QueryManager().withL2CacheDisabled()) {
try (final QueryManager qm = new QueryManager()) {
final Vulnerability vuln = qm.getVulnerabilityByVulnId(Vulnerability.Source.NVD, cveId);
if (vuln != null) {
vuln.setEpssScore(epssScore);
Expand Down
17 changes: 0 additions & 17 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import com.github.packageurl.PackageURL;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.ClassUtils;
import org.datanucleus.PropertyNames;
import org.datanucleus.api.jdo.JDOQuery;
import org.dependencytrack.event.IndexEvent;
import org.dependencytrack.model.AffectedVersionAttribution;
Expand Down Expand Up @@ -342,22 +341,6 @@ private CacheQueryManager getCacheQueryManager() {
return cacheQueryManager;
}

/**
* Disables the second level cache for this {@link QueryManager} instance.
* <p>
* Disabling the L2 cache is useful in situations where large amounts of objects
* are created or updated in close succession, and it's unlikely that they'll be
* accessed again anytime soon. Keeping those objects in cache would unnecessarily
* blow up heap usage.
*
* @return This {@link QueryManager} instance
* @see <a href="https://www.datanucleus.org/products/accessplatform_6_0/jdo/persistence.html#cache_level2">L2 Cache docs</a>
*/
public QueryManager withL2CacheDisabled() {
pm.setProperty(PropertyNames.PROPERTY_CACHE_L2_TYPE, "none");
return this;
}

/**
* Get the IDs of the {@link Team}s a given {@link Principal} is a member of.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
import org.dependencytrack.notification.vo.BomProcessingFailed;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.persistence.listener.IndexingInstanceLifecycleListener;
import org.dependencytrack.persistence.listener.L2CacheEvictingInstanceLifecycleListener;
import org.dependencytrack.util.InternalComponentIdentifier;
import org.json.JSONArray;
import org.slf4j.MDC;
Expand Down Expand Up @@ -233,7 +232,7 @@ private void processBom(final Context ctx, final org.cyclonedx.model.Bom cdxBom)
dispatchBomConsumedNotification(ctx);

final var processedComponents = new ArrayList<Component>(components.size());
try (final var qm = new QueryManager().withL2CacheDisabled()) {
try (final var qm = new QueryManager()) {
// Disable reachability checks on commit.
// See https://www.datanucleus.org/products/accessplatform_4_1/jdo/performance_tuning.html
//
Expand Down Expand Up @@ -266,8 +265,6 @@ private void processBom(final Context ctx, final org.cyclonedx.model.Bom cdxBom)

qm.getPersistenceManager().addInstanceLifecycleListener(new IndexingInstanceLifecycleListener(eventsToDispatch::add),
Component.class, Project.class, ProjectMetadata.class, ServiceComponent.class);
qm.getPersistenceManager().addInstanceLifecycleListener(new L2CacheEvictingInstanceLifecycleListener(qm),
Bom.class, Component.class, Project.class, ProjectMetadata.class, ServiceComponent.class);

final List<Component> finalComponents = components;
final List<ServiceComponent> finalServices = services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ private void retrieveAdvisories(final String advisoriesEndCursor) throws IOExcep
*/
void updateDatasource(final List<GitHubSecurityAdvisory> advisories) {
LOGGER.debug("Updating datasource with GitHub advisories");
try (QueryManager qm = new QueryManager().withL2CacheDisabled()) {
try (QueryManager qm = new QueryManager()) {
for (final GitHubSecurityAdvisory advisory : advisories) {
LOGGER.debug("Synchronizing GitHub advisory: " + advisory.getGhsaId());
final Vulnerability mappedVulnerability = mapAdvisoryToVulnerability(qm, advisory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import alpine.event.framework.Subscriber;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.datanucleus.PropertyNames;
import org.dependencytrack.event.InternalComponentIdentificationEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.persistence.QueryManager;
Expand Down Expand Up @@ -64,11 +63,6 @@ private void analyze() throws Exception {
try (final var qm = new QueryManager()) {
final PersistenceManager pm = qm.getPersistenceManager();

// Disable the DataNucleus L2 cache for this persistence manager.
// The cache will hold references to the queried objects, preventing them
// from being garbage collected. This is not required the case of this task.
pm.setProperty(PropertyNames.PROPERTY_CACHE_L2_TYPE, "none");

final var internalComponentIdentifier = new InternalComponentIdentifier();
List<Component> components = fetchNextComponentsPage(pm, null);
while (!components.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,10 @@
import org.dependencytrack.event.EpssMirrorEvent;
import org.dependencytrack.event.IndexEvent;
import org.dependencytrack.event.NistApiMirrorEvent;
import org.dependencytrack.model.AffectedVersionAttribution;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerableSoftware;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.persistence.listener.IndexingInstanceLifecycleListener;
import org.dependencytrack.persistence.listener.L2CacheEvictingInstanceLifecycleListener;
import org.dependencytrack.util.DebugDataEncryption;

import java.time.Duration;
Expand Down Expand Up @@ -163,13 +161,11 @@ public void inform(final Event e) {
final List<VulnerableSoftware> vsList = convertConfigurations(cveItem.getId(), cveItem.getConfigurations());

executor.submit(() -> {
try (final var qm = new QueryManager().withL2CacheDisabled()) {
try (final var qm = new QueryManager()) {
qm.getPersistenceManager().setProperty(PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT, "false");
qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true");
qm.getPersistenceManager().addInstanceLifecycleListener(new IndexingInstanceLifecycleListener(Event::dispatch),
Vulnerability.class, VulnerableSoftware.class);
qm.getPersistenceManager().addInstanceLifecycleListener(new L2CacheEvictingInstanceLifecycleListener(qm),
AffectedVersionAttribution.class, Vulnerability.class, VulnerableSoftware.class);

final Vulnerability persistentVuln = synchronizeVulnerability(qm, vuln);
qm.synchronizeVulnerableSoftware(persistentVuln, vsList, Vulnerability.Source.NVD);
Expand Down
6 changes: 1 addition & 5 deletions src/main/java/org/dependencytrack/tasks/NistMirrorTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
import org.dependencytrack.event.IndexEvent;
import org.dependencytrack.event.NistApiMirrorEvent;
import org.dependencytrack.event.NistMirrorEvent;
import org.dependencytrack.model.AffectedVersionAttribution;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerableSoftware;
import org.dependencytrack.notification.NotificationConstants;
Expand All @@ -53,7 +52,6 @@
import org.dependencytrack.parser.nvd.NvdParser;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.persistence.listener.IndexingInstanceLifecycleListener;
import org.dependencytrack.persistence.listener.L2CacheEvictingInstanceLifecycleListener;

import java.io.BufferedReader;
import java.io.Closeable;
Expand Down Expand Up @@ -399,13 +397,11 @@ private void uncompress(final File file, final ResourceType resourceType) {
}

private void processVulnerability(final Vulnerability vuln, final List<VulnerableSoftware> vsList) {
try (final var qm = new QueryManager().withL2CacheDisabled()) {
try (final var qm = new QueryManager()) {
qm.getPersistenceManager().setProperty(PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT, "false");
qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true");
qm.getPersistenceManager().addInstanceLifecycleListener(new IndexingInstanceLifecycleListener(Event::dispatch),
Vulnerability.class, VulnerableSoftware.class);
qm.getPersistenceManager().addInstanceLifecycleListener(new L2CacheEvictingInstanceLifecycleListener(qm),
AffectedVersionAttribution.class, Vulnerability.class, VulnerableSoftware.class);

final Vulnerability persistentVuln = synchronizeVulnerability(qm, vuln);
qm.synchronizeVulnerableSoftware(persistentVuln, vsList, Vulnerability.Source.NVD);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ private void unzipFolder(ZipInputStream zipIn) throws IOException {

public void updateDatasource(final OsvAdvisory advisory) {

try (QueryManager qm = new QueryManager().withL2CacheDisabled()) {
try (QueryManager qm = new QueryManager()) {

LOGGER.debug("Synchronizing Google OSV advisory: " + advisory.getId());
final Vulnerability vulnerability = mapAdvisoryToVulnerability(qm, advisory);
Expand Down
59 changes: 0 additions & 59 deletions src/main/java/org/dependencytrack/util/PersistenceUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,11 @@
import com.mysql.cj.exceptions.MysqlErrorNumbers;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.datanucleus.enhancement.Persistable;
import org.dependencytrack.persistence.QueryManager;
import org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException;
import org.postgresql.util.PSQLState;

import javax.jdo.JDOHelper;
import javax.jdo.ObjectState;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -242,61 +238,6 @@ public static boolean isPersistent(final Object object) {
|| objectState == HOLLOW_PERSISTENT_NONTRANSACTIONAL;
}

/**
* Evict a given object from the JDO L2 cache.
*
* @param qm The {@link QueryManager} to use
* @param object The object to evict from the cache
* @since 4.11.0
*/
public static void evictFromL2Cache(final QueryManager qm, final Object object) {
final PersistenceManagerFactory pmf = qm.getPersistenceManager().getPersistenceManagerFactory();
pmf.getDataStoreCache().evict(getDataNucleusJdoObjectId(object));
}

/**
* Evict a given {@link Collection} of objects from the JDO L2 cache.
*
* @param qm The {@link QueryManager} to use
* @param objects The objects to evict from the cache
* @since 4.11.0
*/
public static void evictFromL2Cache(final QueryManager qm, final Collection<?> objects) {
final PersistenceManagerFactory pmf = qm.getPersistenceManager().getPersistenceManagerFactory();
pmf.getDataStoreCache().evictAll(getDataNucleusJdoObjectIds(objects));
}

private static Collection<?> getDataNucleusJdoObjectIds(final Collection<?> objects) {
return objects.stream().map(PersistenceUtil::getDataNucleusJdoObjectId).toList();
}

/**
* {@link JDOHelper#getObjectId(Object)} and {@link PersistenceManager#getObjectId(Object)}
* return instances of {@link javax.jdo.identity.LongIdentity}, but the DataNucleus L2 cache is maintained
* with DataNucleus-specific {@link org.datanucleus.identity.LongId}s instead.
* <p>
* Calling {@link javax.jdo.datastore.DataStoreCache#evict(Object)} with {@link javax.jdo.identity.LongIdentity}
* is pretty much a no-op. The mismatch is undetectable because {@code evict} doesn't throw when a wrong identity
* type is passed either.
* <p>
* (╯°□°)╯︵ ┻━┻
*
* @param object The object to get the JDO object ID for
* @return A JDO object ID
*/
private static Object getDataNucleusJdoObjectId(final Object object) {
if (!(object instanceof final Persistable persistable)) {
throw new IllegalArgumentException("Can't get JDO object ID from non-Persistable objects");
}

final Object objectId = persistable.dnGetObjectId();
if (objectId == null) {
throw new IllegalStateException("Object does not have a JDO object ID");
}

return objectId;
}

public static boolean isUniqueConstraintViolation(final Throwable throwable) {
// NB: DataNucleus doesn't map constraint violation exceptions,
// so we have to depend on underlying JDBC driver's exception to
Expand Down
8 changes: 1 addition & 7 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,9 @@ alpine.database.pool.max.lifetime=600000
# Optional
# Controls the 2nd level cache type used by DataNucleus, the Object Relational Mapper (ORM).
# See https://www.datanucleus.org/products/accessplatform_6_0/jdo/persistence.html#cache_level2
# Values supported by Dependency-Track are "soft" (default), "weak", and "none".
#
# Setting this property to "none" may help in reducing the memory footprint of Dependency-Track,
# but has the potential to slow down database operations.
# Size of the cache may be monitored through the "datanucleus_cache_second_level_entries" metric,
# refer to https://docs.dependencytrack.org/getting-started/monitoring/#metrics for details.
#
# DO NOT CHANGE UNLESS THERE IS A GOOD REASON TO.
# alpine.datanucleus.cache.level2.type=
alpine.datanucleus.cache.level2.type=none

# Required
# Controls the maximum number of ExecutionContext objects that are pooled by DataNucleus.
Expand Down
27 changes: 0 additions & 27 deletions src/test/java/org/dependencytrack/util/PersistenceUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,13 @@
*/
package org.dependencytrack.util;

import org.datanucleus.api.jdo.JDOPersistenceManagerFactory;
import org.datanucleus.cache.Level2Cache;
import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.model.Project;
import org.dependencytrack.util.PersistenceUtil.Diff;
import org.dependencytrack.util.PersistenceUtil.Differ;
import org.junit.Before;
import org.junit.Test;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;
import java.util.Map;
Expand All @@ -37,7 +34,6 @@
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.dependencytrack.util.PersistenceUtil.assertNonPersistent;
import static org.dependencytrack.util.PersistenceUtil.assertPersistent;
import static org.dependencytrack.util.PersistenceUtil.evictFromL2Cache;

public class PersistenceUtilTest extends PersistenceCapableTest {

Expand Down Expand Up @@ -180,27 +176,4 @@ public void testDifferWithoutChanges() {
assertThat(differ.getDiffs()).isEmpty();
}

@Test
public void testEvictFromL2Cache() {
final var project = new Project();
project.setName("acme-app");
qm.persist(project);

final PersistenceManager pm = qm.getPersistenceManager();
final var pmf = (JDOPersistenceManagerFactory) pm.getPersistenceManagerFactory();
final Level2Cache l2Cache = pmf.getNucleusContext().getLevel2Cache();
assertThat(l2Cache.getSize()).isEqualTo(1);

// Try to evict using ID obtained from JDOHelper...
pmf.getDataStoreCache().evict(JDOHelper.getObjectId(project));
assertThat(l2Cache.getSize()).isEqualTo(1);

// Try to evict using ID obtained from PersistenceManager...
pmf.getDataStoreCache().evict(qm.getPersistenceManager().getObjectId(project));
assertThat(l2Cache.getSize()).isEqualTo(1);

evictFromL2Cache(qm, project);
assertThat(l2Cache.getSize()).isEqualTo(0);
}

}

0 comments on commit d5befb0

Please sign in to comment.