-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
582 additions
and
483 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/main/java/com/enonic/app/booster/concurrent/ThreadFactoryImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
src/main/java/com/enonic/app/booster/query/BoosterQueryBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
161
src/main/java/com/enonic/app/booster/script/NodeCleanerBean.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.