Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

package com.dotcms.enterprise.priv;

import com.dotcms.cost.RequestCost;
import com.dotcms.cost.RequestPrices.Price;
import com.dotcms.content.elasticsearch.business.ESContentFactoryImpl;
import com.dotcms.content.elasticsearch.business.ESSearchResults;
import com.dotcms.content.elasticsearch.business.IndiciesInfo;
Expand Down Expand Up @@ -204,6 +206,7 @@ public SearchResponse esSearchRelated(final Contentlet contentlet,
* @throws DotDataException
* An error occurred when retrieving the data.
*/
@RequestCost(Price.ES_QUERY)
private SearchResponse esSearchRaw(JSONObject jsonObject, boolean live, User user,
boolean respectFrontendRoles, int limit, int offset, String sortBy)
throws DotSecurityException, DotDataException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,40 @@ public static void incrementCost(final Price price, final Method method,
"Error in RequestCostHandler.incrementCost(): " + t.getMessage(), t);
}
}

/**
* Increments the request cost by {@code price * times} from a call site that charges
* directly rather than through the {@code @RequestCost} annotation.
* <p>
* Use this rather than calling {@code APILocator.getRequestCostAPI().incrementCost(..)}
* inline. The annotation path is protected — {@code RequestCostAdvice.enter} is declared
* {@code @Advice.OnMethodEnter(suppress = Throwable.class)} — so a failure in the cost API
* can never break the method being metered. A direct call has no such protection, and
* these charge points sit on page rendering and content loading: metering must never be
* able to take down serving.
*
* @param price the unit price
* @param clazz calling class
* @param method calling method
* @param args arguments, for the HTML accounting report
* @param times how many units of work were done
*/
public static void incrementCost(final Price price, final Class clazz, final String method,
final Object[] args, final int times) {
try {
APILocator.getRequestCostAPI().incrementCost(price, clazz, method, args, times);
} catch (Throwable t) {
Logger.warnAndDebug(RequestCostHandler.class,
"Error in RequestCostHandler.incrementCost(): " + t.getMessage(), t);
}
}

/**
* Convenience overload charging a single unit. See
* {@link #incrementCost(Price, Class, String, Object[], int)}.
*/
public static void incrementCost(final Price price, final Class clazz, final String method,
final Object[] args) {
incrementCost(price, clazz, method, args, 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dotcms.content.elasticsearch.business;

import com.dotcms.cost.RequestCost;
import com.dotcms.cost.RequestPrices.Price;
import static com.dotcms.content.index.IndexConfigHelper.haltMigration;
import static com.dotcms.content.index.IndexConfigHelper.isMigrationComplete;
import static com.dotcms.content.index.IndexConfigHelper.isMigrationNotStarted;
Expand Down Expand Up @@ -2321,6 +2323,7 @@ public void stopFullReindexation() throws DotDataException {
}
}

@RequestCost(Price.CONTENT_INDEX)
@Override
public void addContentToIndex(final List<Contentlet> contentToIndex) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
import com.dotcms.variant.model.Variant;
import com.dotmarketing.beans.Host;
import com.dotmarketing.beans.Identifier;
import com.dotcms.business.interceptor.RequestCostHandler;
import com.dotcms.cost.RequestCost;
import com.dotcms.cost.RequestPrices.Price;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.CacheLocator;
import com.dotmarketing.business.DotStateException;
Expand Down Expand Up @@ -115,7 +118,6 @@
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;

/**
Expand Down Expand Up @@ -755,6 +757,11 @@ public Optional<Contentlet> findInDb(final String inode) {
* @param ignoreStoryBlock if it is true then the StoryBlock are not hydrated
* @return
*/
// The cache-miss surcharge for the single-contentlet path. Reaching this method IS the
// miss - find() has already charged the CONTENT_FROM_CACHE base fee, so a warm find
// costs 1 and a cold one costs 11. Annotations are not inherited, so the @RequestCost on
// ContentletFactory's findInDb default does nothing here; this needs its own.
@RequestCost(Price.CONTENT_FROM_DB)
public Optional<Contentlet> findInDb(final String inode, final boolean ignoreStoryBlock) {
try {
if (inode != null) {
Expand Down Expand Up @@ -1274,17 +1281,35 @@ public Contentlet findContentletForLanguage(long languageId, Identifier identifi
@Override
public List<Contentlet> findContentlets(final List<String> inodes) throws DotDataException {

// Single pass: the cache lookup already knows which inodes missed, so collect them here
// rather than re-deriving the difference afterwards. A hit only counts when the cached
// inode matches the requested one: the cache stores the CACHE_404_CONTENTLET sentinel
// under the requested key after a failed single-item lookup, and treating it as a hit
// would silently drop that inode from the result instead of falling through to the DB
// (the old CollectionUtils.subtract over conMap's keys had the same inode-equality
// semantics, since the sentinel's inode never matches a requested inode).
final HashMap<String, Contentlet> conMap = new HashMap<>();
for (String i : inodes) {
final List<String> missingCons = new ArrayList<>();
for (final String i : inodes) {
final Contentlet contentlet = contentletCache.get(i);
if (contentlet != null && InodeUtils.isSet(contentlet.getInode())) {
conMap.put(contentlet.getInode(), processCachedContentlet(contentlet));
if (contentlet != null && i.equals(contentlet.getInode())) {
conMap.put(i, processCachedContentlet(contentlet));
} else {
missingCons.add(i);
}
}

if (conMap.size() != inodes.size()) {
final List<String> missingCons = new ArrayList<>(
CollectionUtils.subtract(inodes, conMap.keySet()));
// This is the bulk loader behind every search result (GraphQL, /api/content,
// /api/es/search, page render) and it bypasses ESContentletAPIImpl.find(), so nothing
// else meters it. Base fee per contentlet asked for; the cache misses pay a surcharge
// below. Note the surcharge is per missed ROW, not per SQL statement - the 200-row
// batching is our implementation detail and is deliberately not priced.
RequestCostHandler.incrementCost(Price.CONTENT_FROM_CACHE,
ESContentFactoryImpl.class, "findContentlets", new Object[]{}, inodes.size());

if (!missingCons.isEmpty()) {
RequestCostHandler.incrementCost(Price.CONTENT_FROM_DB,
ESContentFactoryImpl.class, "findContentlets", new Object[]{}, missingCons.size());

final String contentletBase =
"select contentlet.*, contentlet_1_.owner from contentlet join inode contentlet_1_ "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ public Contentlet find(final String inode, final User user, final boolean respec
* @throws DotDataException
* @throws DotSecurityException
*/
// Base fee for asking for one contentlet. If the factory misses cache and falls through
// to findInDb, that adds the CONTENT_FROM_DB surcharge - so warm is 1, cold is 11.
@RequestCost(Price.CONTENT_FROM_CACHE)
@CloseDBIfOpened
@Override
Expand Down Expand Up @@ -575,6 +577,13 @@ private Folder createFolder(final String folderPath, final Contentlet contentlet
}

@WrapInTransaction
// Deliberately NOT a terminal, and the stacking is intended: move() calls
// indexAPI.addContentToIndex(..) below, which reaches the annotated
// addContentToIndex(List) and adds CONTENT_INDEX on top of this CONTENT_MOVE. A move
// really does reindex, so it really should cost both. This is not the "annotating two
// methods in one chain double-charges" trap - that is about one operation being counted
// twice, this is two distinct operations each counted once. Do not remove either.
@RequestCost(Price.CONTENT_MOVE)
@Override
public Contentlet move(final Contentlet contentlet, final User incomingUser, final Host host,
final Folder folder,
Expand Down Expand Up @@ -5718,6 +5727,7 @@ private boolean isWorkflowInProgress(final Contentlet contentlet) {
return contentlet.isWorkflowInProgress();
}

@RequestCost(Price.CONTENT_CHECKIN)
private Contentlet internalCheckin(Contentlet contentlet,
ContentletRelationships contentRelationships, List<Category> categories,
final User incomingUser,
Expand Down Expand Up @@ -7100,6 +7110,7 @@ public List<Contentlet> checkout(String luceneQuery, User user, boolean respectF
}

@WrapInTransaction
@RequestCost(Price.CONTENT_CHECKOUT)
@Override
public Contentlet checkout(final String contentletInode, final User user,
final boolean respectFrontendRoles)
Expand Down Expand Up @@ -9464,6 +9475,7 @@ public Contentlet copyContentlet(final Contentlet sourceContentlet, final Host h
}

@WrapInTransaction
@RequestCost(Price.CONTENT_COPY)
@Override
@SuppressWarnings("unchecked")
public Contentlet copyContentlet(final Contentlet sourceContentlet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ public class LeakyTokenBucketImpl implements LeakyTokenBucket {
LeakyTokenBucketImpl() {
this(
Config.getBooleanProperty("RATE_LIMIT_ENABLED", false),
Config.getLongProperty("RATE_LIMIT_REFILL_PER_SECOND", 500),
Config.getLongProperty("RATE_LIMIT_MAX_BUCKET_SIZE", 10000)
Config.getLongProperty("RATE_LIMIT_REFILL_PER_SECOND", 5000),
Config.getLongProperty("RATE_LIMIT_MAX_BUCKET_SIZE", 100000)
);
}

Expand Down
14 changes: 14 additions & 0 deletions dotCMS/src/main/java/com/dotcms/cost/RequestCostApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ public static Accounting fromString(String text) {
*/
void incrementCost(Price price, @NotNull Class clazz, @NotNull String method, @NotNull Object[] args);

/**
* Increments the cost for the current request by {@code price * times}. Used where the work
* scales with the size of a result set (e.g. loading N contentlets) and charging once would
* make a 1-row and a 1000-row response cost the same.
*
* @param price the unit price
* @param clazz calling class
* @param method calling method
* @param args arguments, for the HTML accounting report
* @param times how many units of work were done; values &lt;= 0 are a no-op
*/
void incrementCost(Price price, @NotNull Class clazz, @NotNull String method,
@NotNull Object[] args, int times);

/**
* Returns the current cost for the current request.
*
Expand Down
Loading
Loading