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
@@ -0,0 +1,44 @@
package com.dotcms.rest.api.v1.system.cache;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

import java.util.List;
import java.util.Map;

/**
* Per-provider cache statistics view. Each provider reports its own set of
* stat columns, so individual stat rows are represented as {@code Map<String, String>}
* keyed by column name.
*
* @author hassandotcms
*/
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = CacheProviderStatsView.class)
@JsonDeserialize(as = CacheProviderStatsView.class)
@Schema(description = "Cache statistics for a single cache provider")
public interface AbstractCacheProviderStatsView {

@Schema(
description = "Cache provider display name",
example = "Guava Memory Cache",
requiredMode = Schema.RequiredMode.REQUIRED
)
String providerName();

@Schema(
description = "Ordered list of stat column names reported by this provider",
example = "[\"Region\", \"Configured Size\", \"Memory Used\", \"Is Default\", \"Hit Count\", \"Miss Count\"]",
requiredMode = Schema.RequiredMode.REQUIRED
)
List<String> columns();

@Schema(
description = "Per-region statistics, each row keyed by column name",
requiredMode = Schema.RequiredMode.REQUIRED
)
List<Map<String, String>> stats();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.dotcms.rest.api.v1.system.cache;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

import java.util.List;

/**
* Top-level cache statistics view combining JVM memory info
* with per-provider cache statistics.
*
* @author hassandotcms
*/
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = CacheStatsView.class)
@JsonDeserialize(as = CacheStatsView.class)
@Schema(description = "Cache statistics including cluster/server identity, JVM memory, and per-provider region stats")
public interface AbstractCacheStatsView {

@Schema(
description = "Cluster identifier (shared across all nodes in the cluster)",
example = "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
requiredMode = Schema.RequiredMode.REQUIRED
)
String clusterId();

@Schema(
description = "Server identifier (unique per node — identifies which node reported these stats)",
example = "a9f3c1d2-e456-7890-bcde-f12345678901",
requiredMode = Schema.RequiredMode.REQUIRED
)
String serverId();

@Schema(
description = "Server hostname",
example = "dotcms-node-1.example.com",
requiredMode = Schema.RequiredMode.REQUIRED
)
String serverName();

@Schema(
description = "JVM memory statistics",
requiredMode = Schema.RequiredMode.REQUIRED
)
JvmMemoryView memory();

@Schema(
description = "Per-provider cache statistics",
requiredMode = Schema.RequiredMode.REQUIRED
)
List<CacheProviderStatsView> providers();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.dotcms.rest.api.v1.system.cache;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import org.immutables.value.Value;

/**
* JVM memory statistics view.
*
* @author hassandotcms
*/
@Value.Style(typeImmutable = "*", typeAbstract = "Abstract*")
@Value.Immutable
@JsonSerialize(as = JvmMemoryView.class)
@JsonDeserialize(as = JvmMemoryView.class)
@Schema(description = "JVM memory statistics in bytes")
public interface AbstractJvmMemoryView {

@Schema(
description = "Maximum memory the JVM will attempt to use (Xmx)",
example = "8589934592",
requiredMode = Schema.RequiredMode.REQUIRED
)
long maxMemory();

@Schema(
description = "Total memory currently allocated by the JVM",
example = "4294967296",
requiredMode = Schema.RequiredMode.REQUIRED
)
long allocatedMemory();

@Schema(
description = "Memory currently in use (allocated minus free)",
example = "2147483648",
requiredMode = Schema.RequiredMode.REQUIRED
)
long usedMemory();

@Schema(
description = "Available memory (max minus used)",
example = "6442450944",
requiredMode = Schema.RequiredMode.REQUIRED
)
long freeMemory();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.dotcms.rest.api.v1.system.cache;

import com.dotcms.config.DotInitializer;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.CacheLocator;
import com.dotmarketing.business.PermissionAPI;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.MaintenanceUtil;
import com.google.common.annotations.VisibleForTesting;

import com.dotcms.rest.exception.BadRequestException;
import java.util.stream.Stream;

/**
* Encapsulates cache flush business logic and post-flush side effects
* (permission reference reset and PushPublishing filter reload).
* Shared by both the flush-region and flush-all REST endpoints.
*
* @author hassandotcms
*/
public class CacheMaintenanceHelper {

private final PermissionAPI permissionAPI;

public CacheMaintenanceHelper() {
this(APILocator.getPermissionAPI());
}

@VisibleForTesting
public CacheMaintenanceHelper(final PermissionAPI permissionAPI) {
this.permissionAPI = permissionAPI;
}

/**
* Flushes a specific cache region by name, then performs post-flush side effects.
* Region name lookup is case-insensitive; the canonical name from {@code CacheIndex} is used.
*
* @param regionName the cache region name (case-insensitive)
* @return the canonical region name that was flushed
* @throws BadRequestException if the region name is not recognized
*/
public String flushRegion(final String regionName) {

final String canonical = resolveRegionName(regionName);
if (canonical == null) {
throw new BadRequestException("Unknown cache region: " + regionName);
}

CacheLocator.getCache(canonical).clearCache();

performPostFlushActions(canonical);

return canonical;
}

/**
* Flushes all caches via {@link MaintenanceUtil#flushCache()},
* then performs post-flush side effects including PushPublishing reload.
*/
public void flushAllCaches() {

MaintenanceUtil.flushCache();

try {
permissionAPI.resetAllPermissionReferences();
} catch (DotDataException e) {
Logger.error(this, "Error resetting permission references after flushing all caches", e);
}

reloadPublishingFilters();
}

/**
* Resolves a region name case-insensitively to its canonical {@code CacheIndex} value.
*
* @return the canonical region name, or {@code null} if not found
*/
private String resolveRegionName(final String regionName) {

final Object[] caches = CacheLocator.getCacheIndexes();
return Stream.of(caches)
.map(Object::toString)
.filter(name -> name.equalsIgnoreCase(regionName))
.findFirst()
.orElse(null);
}

private void performPostFlushActions(final String regionName) {

try {
permissionAPI.resetAllPermissionReferences();
} catch (DotDataException e) {
Logger.error(this, "Error resetting permission references after flushing " + regionName, e);
}

if ("system".equalsIgnoreCase(regionName)) {
reloadPublishingFilters();
}
}

private void reloadPublishingFilters() {

try {
DotInitializer.class.cast(APILocator.getPublisherAPI()).init();
} catch (Exception e) {
Logger.error(this, "Error reloading PushPublishing filters", e);
}
}

}
Loading
Loading