Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not run more than one cleanup simultaneously #83 #90

Merged
merged 1 commit into from
Jan 20, 2025
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
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ dependencies {
include "com.enonic.xp:lib-project:${xpVersion}"
include "com.enonic.xp:lib-task:${xpVersion}"
include "com.enonic.lib:lib-mustache:2.1.1"
include 'com.enonic.lib:lib-cron:1.1.2'
include 'com.enonic.lib:lib-license:3.1.0'

testImplementation(platform('org.junit:junit-bom:5.11.4'))
Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Booster will automatically invalidate cache when one of the following events occ

. All cache gets deleted when:

* Applications listed in the configuration `appsInvalidateCacheOnStart` starts
* Applications listed in the configuration `appsForceInvalidateOnInstall` installed

NOTE: When an item is invalidated, it is simply marked as invalid.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.enonic.app.booster.concurrent;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;

public final class ThreadFactoryImpl
implements ThreadFactory
{
private final AtomicLong count = new AtomicLong( 1 );

private final String namePattern;

public ThreadFactoryImpl( final String namePattern )
{
this.namePattern = namePattern;
}

@Override
public Thread newThread( final Runnable r )
{
final Thread thread = Executors.defaultThreadFactory().newThread( r );

thread.setName( String.format( namePattern, count.getAndIncrement() ) );

return thread;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.enonic.app.booster.query;

import java.time.Instant;
import java.util.Map;

import com.enonic.app.booster.storage.BoosterContext;
import com.enonic.xp.data.ValueFactory;
import com.enonic.xp.node.NodeQuery;
import com.enonic.xp.query.expr.CompareExpr;
import com.enonic.xp.query.expr.FieldExpr;
import com.enonic.xp.query.expr.FieldOrderExpr;
import com.enonic.xp.query.expr.LogicalExpr;
import com.enonic.xp.query.expr.OrderExpr;
import com.enonic.xp.query.expr.QueryExpr;
import com.enonic.xp.query.expr.ValueExpr;
import com.enonic.xp.query.filter.BooleanFilter;
import com.enonic.xp.query.filter.ExistsFilter;
import com.enonic.xp.query.filter.RangeFilter;
import com.enonic.xp.query.filter.ValueFilter;

public class BoosterQueryBuilder
{
private BoosterQueryBuilder()
{
}

public static NodeQuery queryNodes( final Map<String, Value> fields, final Instant cutOffTime,
final boolean includeInvalidated, int size )
{
final NodeQuery.Builder builder = NodeQuery.create();
builder.parent( BoosterContext.CACHE_PARENT_NODE );

for ( Map.Entry<String, Value> entry : fields.entrySet() )
{
final Value value = entry.getValue();
if ( value instanceof Value.Multiple multiple )
{
builder.addQueryFilter( ValueFilter.create().fieldName( entry.getKey() ).addValues( multiple.values ).build() );
}
else if ( value instanceof Value.PathPrefix pathPrefix )
{
final QueryExpr queryExpr = QueryExpr.from(
LogicalExpr.or( CompareExpr.eq( FieldExpr.from( entry.getKey() ), ValueExpr.string( pathPrefix.value ) ),
CompareExpr.like( FieldExpr.from( entry.getKey() ), ValueExpr.string( pathPrefix.value + "/*" ) ) ) );
builder.query( queryExpr );
}
else if ( value instanceof Value.Single single )
{
builder.addQueryFilter(
ValueFilter.create().fieldName( entry.getKey() ).addValue( ValueFactory.newString( single.value ) ).build() );
}
else
{
throw new IllegalArgumentException( "Unknown value type: " + value );
}
}

if ( !includeInvalidated )
{
builder.addQueryFilter(
BooleanFilter.create().mustNot( ExistsFilter.create().fieldName( "invalidatedTime" ).build() ).build() );
}
else
{
builder.addOrderBy( FieldOrderExpr.create( "invalidatedTime", OrderExpr.Direction.ASC ) );
}

if ( cutOffTime != null )
{
builder.addQueryFilter( RangeFilter.create().fieldName( "cachedTime" ).lt( ValueFactory.newDateTime( cutOffTime ) ).build() );
}
builder.addOrderBy( FieldOrderExpr.create( "cachedTime", OrderExpr.Direction.ASC ) ).size( size );

return builder.build();
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/enonic/app/booster/query/Value.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.enonic.app.booster.query;

import java.util.Collection;

public sealed interface Value
permits Value.Single, Value.Multiple, Value.PathPrefix
{
final class Multiple
implements Value
{
Collection<String> values;

Multiple( final Collection<String> values )
{
this.values = values;
}

public static Multiple of( final Collection<String> values )
{
return new Multiple( values );
}
}

final class Single
implements Value
{
String value;

Single( final String value )
{
this.value = value;
}

public static Single of( final String value )
{
return new Single( value );
}
}

final class PathPrefix
implements Value
{
PathPrefix( final String value )
{
this.value = value;
}

String value;

public static PathPrefix of( final String value )
{
return new PathPrefix( value );
}
}
}
161 changes: 161 additions & 0 deletions src/main/java/com/enonic/app/booster/script/NodeCleanerBean.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package com.enonic.app.booster.script;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.enonic.app.booster.query.BoosterQueryBuilder;
import com.enonic.app.booster.query.Value;
import com.enonic.app.booster.storage.BoosterContext;
import com.enonic.xp.node.DeleteNodeParams;
import com.enonic.xp.node.FindNodesByQueryResult;
import com.enonic.xp.node.NodeHit;
import com.enonic.xp.node.NodeHits;
import com.enonic.xp.node.NodeId;
import com.enonic.xp.node.NodeNotFoundException;
import com.enonic.xp.node.NodeQuery;
import com.enonic.xp.node.NodeService;
import com.enonic.xp.node.RefreshMode;
import com.enonic.xp.node.UpdateNodeParams;
import com.enonic.xp.script.bean.BeanContext;
import com.enonic.xp.script.bean.ScriptBean;

public class NodeCleanerBean
implements ScriptBean
{
private static final Logger LOG = LoggerFactory.getLogger( NodeCleanerBean.class );

private NodeService nodeService;

@Override
public void initialize( final BeanContext beanContext )
{
this.nodeService = beanContext.getService( NodeService.class ).get();
}

public void invalidateProjects( final List<String> projects )
{
if ( projects.isEmpty() )
{
return;
}
invalidateByQuery( Map.of( "project", Value.Multiple.of( projects ) ) );
}

public void invalidateContent( final String project, final String contentId )
{
invalidateByQuery( Map.of( "project", Value.Single.of( project ), "contentId", Value.Single.of( contentId ) ) );
}

public void invalidateSite( final String project, final String siteId )
{
invalidateByQuery( Map.of( "project", Value.Single.of( project ), "siteId", Value.Single.of( siteId ) ) );
}

public void invalidateDomain( final String domain )
{
invalidateByQuery( Map.of( "domain", Value.Single.of( domain ) ) );
}

public void invalidatePathPrefix( final String domain, final String path )
{
invalidateByQuery( Map.of( "domain", Value.Single.of( domain ), "path", Value.PathPrefix.of( path ) ) );
}

public void invalidateAll()
{
invalidateByQuery( Map.of() );
}

public void purgeAll()
{
final Instant now = Instant.now();
BoosterContext.runInContext( () -> {
final NodeQuery query = BoosterQueryBuilder.queryNodes( Map.of(), now, true, 10_000 );

process( query, this::delete );
} );
}

public int getProjectCacheSize( final String project )
{
return getSize( Map.of( "project", Value.Single.of( project ) ) );
}

public int getSiteCacheSize( final String project, final String siteId )
{
return getSize( Map.of( "project", Value.Single.of( project ), "siteId", Value.Single.of( siteId ) ) );
}

public int getContentCacheSize( final String project, final String contentId )
{
return getSize( Map.of( "project", Value.Single.of( project ), "contentId", Value.Single.of( contentId ) ) );
}

private int getSize( final Map<String, Value> fields )
{
final Instant now = Instant.now();
FindNodesByQueryResult nodesToInvalidate = BoosterContext.callInContext( () -> {

final NodeQuery query = BoosterQueryBuilder.queryNodes( fields, now, false, 0 );
return nodeService.findByQuery( query );
} );
return (int) Math.max( 0, Math.min( nodesToInvalidate.getTotalHits(), Integer.MAX_VALUE ) );
}

private void invalidateByQuery( final Map<String, Value> fields )
{
final Instant now = Instant.now();
BoosterContext.runInContext( () -> {
final NodeQuery query = BoosterQueryBuilder.queryNodes( fields, now, false, 10_000 );
process( query, ( n ) -> setInvalidatedTime( n, now ) );
} );
}

private void process( final NodeQuery query, final Consumer<NodeId> op )
{
FindNodesByQueryResult nodesToInvalidate = nodeService.findByQuery( query );

long hits = nodesToInvalidate.getHits();
LOG.debug( "Found {} nodes total to be processed", nodesToInvalidate.getTotalHits() );

while ( hits > 0 )
{
final NodeHits nodeHits = nodesToInvalidate.getNodeHits();
for ( NodeHit nodeHit : nodeHits )
{
op.accept( nodeHit.getNodeId() );
}
LOG.debug( "Processed nodes {}", nodeHits.getSize() );

nodeService.refresh( RefreshMode.SEARCH );
nodesToInvalidate = nodeService.findByQuery( query );

hits = nodesToInvalidate.getHits();
}
}

private void setInvalidatedTime( final NodeId nodeId, final Instant invalidatedTime )
{
try
{
nodeService.update( UpdateNodeParams.create()
.id( nodeId )
.editor( editor -> editor.data.setInstant( "invalidatedTime", invalidatedTime ) )
.build() );
}
catch ( NodeNotFoundException e )
{
LOG.debug( "Node for invalidate was already deleted", e );
}
}

private void delete( final NodeId nodeId )
{
nodeService.delete( DeleteNodeParams.create().nodeId( nodeId ).build() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@
public class BoosterInitializer
extends ExternalInitializer
{

private final RepositoryService repositoryService;

private final NodeService nodeService;


public BoosterInitializer( final Builder builder )
{
super( builder );
Expand Down
Loading
Loading