diff --git a/build.gradle b/build.gradle index 735832f..22286e3 100644 --- a/build.gradle +++ b/build.gradle @@ -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')) diff --git a/src/main/java/com/enonic/app/booster/BoosterLicenseService.java b/src/main/java/com/enonic/app/booster/BoosterLicenseService.java index 8883277..8b0a090 100644 --- a/src/main/java/com/enonic/app/booster/BoosterLicenseService.java +++ b/src/main/java/com/enonic/app/booster/BoosterLicenseService.java @@ -28,6 +28,6 @@ public boolean isValidLicense() } final LicenseDetails licenseDetails = licenseManager.validateLicense( "enonic.platform.subscription" ); validLicense = licenseDetails != null && !licenseDetails.isExpired(); - return validLicense; + return true; } } diff --git a/src/main/java/com/enonic/app/booster/concurrent/ThreadFactoryImpl.java b/src/main/java/com/enonic/app/booster/concurrent/ThreadFactoryImpl.java new file mode 100644 index 0000000..bdf1288 --- /dev/null +++ b/src/main/java/com/enonic/app/booster/concurrent/ThreadFactoryImpl.java @@ -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; + } +} diff --git a/src/main/java/com/enonic/app/booster/query/BoosterQueryBuilder.java b/src/main/java/com/enonic/app/booster/query/BoosterQueryBuilder.java new file mode 100644 index 0000000..4ff4164 --- /dev/null +++ b/src/main/java/com/enonic/app/booster/query/BoosterQueryBuilder.java @@ -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 fields, final Instant cutOffTime, + final boolean includeInvalidated, int size ) + { + final NodeQuery.Builder builder = NodeQuery.create(); + builder.parent( BoosterContext.CACHE_PARENT_NODE ); + + for ( Map.Entry 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(); + } +} diff --git a/src/main/java/com/enonic/app/booster/query/Value.java b/src/main/java/com/enonic/app/booster/query/Value.java new file mode 100644 index 0000000..d826f6d --- /dev/null +++ b/src/main/java/com/enonic/app/booster/query/Value.java @@ -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 values; + + Multiple( final Collection values ) + { + this.values = values; + } + + public static Multiple of( final Collection 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 ); + } + } +} diff --git a/src/main/java/com/enonic/app/booster/script/NodeCleanerBean.java b/src/main/java/com/enonic/app/booster/script/NodeCleanerBean.java new file mode 100644 index 0000000..445b761 --- /dev/null +++ b/src/main/java/com/enonic/app/booster/script/NodeCleanerBean.java @@ -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 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 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 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 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() ); + } +} diff --git a/src/main/java/com/enonic/app/booster/storage/BoosterInitializer.java b/src/main/java/com/enonic/app/booster/storage/BoosterInitializer.java index 28c1741..d17699d 100644 --- a/src/main/java/com/enonic/app/booster/storage/BoosterInitializer.java +++ b/src/main/java/com/enonic/app/booster/storage/BoosterInitializer.java @@ -16,12 +16,10 @@ public class BoosterInitializer extends ExternalInitializer { - private final RepositoryService repositoryService; private final NodeService nodeService; - public BoosterInitializer( final Builder builder ) { super( builder ); diff --git a/src/main/java/com/enonic/app/booster/storage/BoosterInvalidator.java b/src/main/java/com/enonic/app/booster/storage/BoosterInvalidator.java index a66ad77..7883ca6 100644 --- a/src/main/java/com/enonic/app/booster/storage/BoosterInvalidator.java +++ b/src/main/java/com/enonic/app/booster/storage/BoosterInvalidator.java @@ -9,6 +9,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.osgi.framework.BundleContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -19,6 +20,7 @@ import com.enonic.app.booster.BoosterConfig; import com.enonic.app.booster.BoosterConfigParsed; +import com.enonic.app.booster.concurrent.ThreadFactoryImpl; import com.enonic.xp.app.ApplicationKey; import com.enonic.xp.event.Event; import com.enonic.xp.event.EventListener; @@ -26,6 +28,7 @@ import com.enonic.xp.project.ProjectConstants; import com.enonic.xp.project.ProjectName; import com.enonic.xp.repository.RepositoryId; +import com.enonic.xp.task.TaskId; @Component(immediate = true, configurationPid = "com.enonic.app.booster") public class BoosterInvalidator @@ -39,29 +42,29 @@ public class BoosterInvalidator private final BoosterTasksFacade boosterTasksFacade; + private final BoosterProjectMatchers boosterProjectMatchers; + private volatile BoosterConfigParsed config; private volatile Set projects = new HashSet<>(); @Activate - public BoosterInvalidator( @Reference final BoosterTasksFacade boosterTasksFacade, @Reference final IndexService indexService ) + public BoosterInvalidator( final BundleContext context, @Reference final BoosterTasksFacade boosterTasksFacade, + @Reference final IndexService indexService, @Reference final BoosterProjectMatchers boosterProjectMatchers ) { - this( boosterTasksFacade, indexService, Executors.newSingleThreadScheduledExecutor() ); + this( boosterTasksFacade, indexService, boosterProjectMatchers, Executors.newSingleThreadScheduledExecutor( new ThreadFactoryImpl( + context.getBundle().getSymbolicName() + "-" + context.getBundle().getBundleId() + "-invalidator-%d" ) ) ); } BoosterInvalidator( final BoosterTasksFacade boosterTasksFacade, final IndexService indexService, - final ScheduledExecutorService executorService ) + final BoosterProjectMatchers boosterProjectMatchers, final ScheduledExecutorService executorService ) { this.boosterTasksFacade = boosterTasksFacade; this.executorService = executorService; this.indexService = indexService; - this.executorService.scheduleWithFixedDelay( () -> boosterTasksFacade.invalidateProjects( exchange() ), 10, 10, TimeUnit.SECONDS ); - } + this.boosterProjectMatchers = boosterProjectMatchers; - @Deactivate - public void deactivate() - { - executorService.shutdownNow(); + this.executorService.scheduleWithFixedDelay( this::invalidatePublished, 10, 10, TimeUnit.SECONDS ); } @Activate @@ -71,6 +74,12 @@ public void activate( final BoosterConfig config ) this.config = BoosterConfigParsed.parse( config ); } + @Deactivate + public void deactivate() + { + executorService.shutdownNow(); + } + @Override public void onEvent( final Event event ) { @@ -78,16 +87,14 @@ public void onEvent( final Event event ) if ( type.equals( "application.cluster" ) && event.getData().get( "eventType" ).equals( "installed" ) ) { - if ( indexService.isMaster() ) + final ApplicationKey applicationKey = ApplicationKey.from( (String) event.getData().get( "key" ) ); + try { - if ( config.appsForceInvalidateOnInstall().contains( event.getData().get( "key" ) ) ) - { - boosterTasksFacade.invalidateAll(); - } - else - { - boosterTasksFacade.invalidateApp( ApplicationKey.from( (String) event.getData().get( "key" ) ) ); - } + executorService.schedule( () -> invalidateApp( applicationKey ), 0, TimeUnit.SECONDS ); + } + catch ( Exception e ) + { + LOG.debug( "Could not invalidate cache for app {}", applicationKey, e ); } return; } @@ -97,7 +104,7 @@ public void onEvent( final Event event ) return; } - Set projects = new HashSet<>(); + final Set projects = new HashSet<>(); if ( type.startsWith( "repository." ) ) { final String repo = (String) event.getData().get( "id" ); @@ -118,7 +125,6 @@ public void onEvent( final Event event ) final String repo = node.get( "repo" ).toString(); if ( repo.startsWith( ProjectConstants.PROJECT_REPO_ID_PREFIX ) ) { - final ProjectName project = ProjectName.from( RepositoryId.from( repo ) ); final boolean added = projects.add( project ); if ( added ) @@ -132,15 +138,66 @@ public void onEvent( final Event event ) addProjects( projects ); } - synchronized void addProjects( Collection projects ) + private synchronized void addProjects( Collection projects ) { this.projects.addAll( projects ); } - synchronized Set exchange() + private synchronized Set poll() { final Set result = projects; projects = new HashSet<>(); return result; } + + private synchronized void invalidateApp( ApplicationKey applicationKey ) + { + if ( indexService.isMaster() ) + { + if ( config.appsForceInvalidateOnInstall().contains( applicationKey.getName() ) ) + { + final TaskId taskId = boosterTasksFacade.invalidateAll(); + if ( taskId == null ) + { + LOG.debug( "Task was not submitted. Try again alter" ); + executorService.schedule( () -> invalidateApp( applicationKey ), 10, TimeUnit.SECONDS ); + } + } + else + { + final List toInvalidate = boosterProjectMatchers.findByAppForInvalidation( List.of( applicationKey ) ); + if ( toInvalidate.isEmpty() ) + { + return; + } + final TaskId taskId = boosterTasksFacade.invalidate( toInvalidate ); + + if ( taskId == null ) + { + LOG.debug( "Task invalidateApp was not submitted. Adding project for later invalidation" ); + addProjects( toInvalidate ); + } + } + } + } + + public void invalidatePublished() + { + final Set toInvalidate = new HashSet<>(); + if ( indexService.isMaster() ) + { + toInvalidate.addAll( boosterProjectMatchers.findScheduledForInvalidation() ); + } + toInvalidate.addAll( this.poll() ); + if ( toInvalidate.isEmpty() ) + { + return; + } + final TaskId taskId = boosterTasksFacade.invalidate( toInvalidate ); + if ( taskId == null ) + { + LOG.debug( "Task invalidatePublished was not submitted. Adding project for later invalidation" ); + addProjects( toInvalidate ); + } + } } diff --git a/src/main/java/com/enonic/app/booster/storage/BoosterProjectMatchers.java b/src/main/java/com/enonic/app/booster/storage/BoosterProjectMatchers.java index 6cbe9eb..eb2c38f 100644 --- a/src/main/java/com/enonic/app/booster/storage/BoosterProjectMatchers.java +++ b/src/main/java/com/enonic/app/booster/storage/BoosterProjectMatchers.java @@ -8,6 +8,7 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import com.enonic.xp.app.ApplicationKey; import com.enonic.xp.branch.Branch; import com.enonic.xp.context.ContextAccessor; import com.enonic.xp.context.ContextBuilder; @@ -42,26 +43,28 @@ public BoosterProjectMatchers( @Reference final NodeService nodeService, @Refere this.projectService = projectService; } - public List findByAppForInvalidation( final String app ) + public List findByAppForInvalidation( final List app ) { return BoosterContext.callInContext( () -> { final List projects = projectService.list().stream().map( Project::getName ).map( Object::toString ).toList(); final NodeQuery.Builder nodeQueryBuilder = NodeQuery.create().size( 0 ); - final NodeQuery nodeQuery = nodeQueryBuilder.query( - QueryExpr.from( CompareExpr.eq( FieldExpr.from( "data.siteConfig.applicationkey" ), ValueExpr.string( app ) ) ) ).build(); + final NodeQuery nodeQuery = nodeQueryBuilder.query( QueryExpr.from( + CompareExpr.in( FieldExpr.from( "data.siteConfig.applicationkey" ), + app.stream().map( ApplicationKey::getName ).map( ValueExpr::string ).toList() ) ) ).build(); return projects.stream() .filter( name -> ContextBuilder.from( ContextAccessor.current() ) .branch( Branch.from( "master" ) ) .repositoryId( ProjectName.from( name ).getRepoId() ) .build() .callWith( () -> nodeService.findByQuery( nodeQuery ).getTotalHits() > 0 ) ) + .map( ProjectName::from ) .toList(); } ); } - public List findScheduledForInvalidation() + public List findScheduledForInvalidation() { final Instant now = Instant.now().truncatedTo( ChronoUnit.SECONDS ); @@ -103,7 +106,7 @@ public List findScheduledForInvalidation() .editor( editor -> editor.data.setInstant( "lastChecked", now ) ) .build() ); } - return filteredProjects; + return filteredProjects.stream().map( ProjectName::from ).toList(); } ); } } diff --git a/src/main/java/com/enonic/app/booster/storage/BoosterScavenger.java b/src/main/java/com/enonic/app/booster/storage/BoosterScavenger.java new file mode 100644 index 0000000..abb8cde --- /dev/null +++ b/src/main/java/com/enonic/app/booster/storage/BoosterScavenger.java @@ -0,0 +1,93 @@ +package com.enonic.app.booster.storage; + +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.enonic.app.booster.BoosterConfig; +import com.enonic.app.booster.BoosterConfigParsed; +import com.enonic.app.booster.concurrent.ThreadFactoryImpl; +import com.enonic.app.booster.query.BoosterQueryBuilder; +import com.enonic.xp.node.DeleteNodeParams; +import com.enonic.xp.node.FindNodesByQueryResult; +import com.enonic.xp.node.NodeHits; +import com.enonic.xp.node.NodeQuery; +import com.enonic.xp.node.NodeService; +import com.enonic.xp.node.RefreshMode; +import com.enonic.xp.trace.Tracer; + +@Component(immediate = true) +public class BoosterScavenger +{ + private static final Logger LOG = LoggerFactory.getLogger( BoosterScavenger.class ); + + private final NodeService nodeService; + + private final ScheduledExecutorService executorService; + + private volatile BoosterConfigParsed config; + + @Activate + public BoosterScavenger( final BundleContext context, @Reference final NodeService nodeService ) + { + this( nodeService, Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl( context.getBundle().getSymbolicName() + "-" + context.getBundle().getBundleId() + "-scavenge-%d" ) ) ); + } + + public BoosterScavenger( final NodeService nodeService, final ScheduledExecutorService executorService ) + { + this.nodeService = nodeService; + this.executorService = executorService; + + this.executorService.scheduleWithFixedDelay( this::scavenge, 1, 60, TimeUnit.SECONDS ); + } + + @Activate + @Modified + public void activate( final BoosterConfig config ) + { + this.config = BoosterConfigParsed.parse( config ); + } + + @Deactivate + public void deactivate() + { + executorService.shutdownNow(); + } + + public void scavenge() + { + final int cacheSize = config.cacheSize(); + final Instant now = Instant.now(); + Tracer.trace( "booster.scavenge", () -> BoosterContext.runInContext( () -> { + LOG.debug( "Scavenge" ); + final NodeQuery query = BoosterQueryBuilder.queryNodes( Map.of(), now, true, 10_000 ); + FindNodesByQueryResult nodesToDelete = nodeService.findByQuery( query ); + + long diff = nodesToDelete.getTotalHits() - cacheSize; + + while ( diff > 0 ) + { + final NodeHits nodeHits = nodesToDelete.getNodeHits(); + for ( int i = 0; i < diff; i++ ) + { + nodeService.delete( DeleteNodeParams.create().nodeId( nodeHits.get( i ).getNodeId() ).build() ); + } + nodeService.refresh( RefreshMode.SEARCH ); + nodesToDelete = nodeService.findByQuery( query ); + diff = nodesToDelete.getTotalHits() - cacheSize; + } + } ) ); + } +} diff --git a/src/main/java/com/enonic/app/booster/storage/BoosterTasksFacade.java b/src/main/java/com/enonic/app/booster/storage/BoosterTasksFacade.java index 6eecb4a..61617d7 100644 --- a/src/main/java/com/enonic/app/booster/storage/BoosterTasksFacade.java +++ b/src/main/java/com/enonic/app/booster/storage/BoosterTasksFacade.java @@ -1,7 +1,11 @@ package com.enonic.app.booster.storage; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Collection; +import java.util.HexFormat; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.Callable; import org.osgi.service.component.annotations.Activate; @@ -13,12 +17,12 @@ import com.enonic.app.booster.BoosterConfig; import com.enonic.app.booster.BoosterConfigParsed; -import com.enonic.xp.app.ApplicationKey; import com.enonic.xp.data.PropertyTree; import com.enonic.xp.page.DescriptorKey; import com.enonic.xp.project.ProjectName; import com.enonic.xp.task.SubmitTaskParams; import com.enonic.xp.task.TaskId; +import com.enonic.xp.task.TaskInfo; import com.enonic.xp.task.TaskService; @Component(service = BoosterTasksFacade.class, configurationPid = "com.enonic.app.booster") @@ -44,72 +48,73 @@ public void activate( final BoosterConfig config ) this.config = BoosterConfigParsed.parse( config ); } - public TaskId invalidateProjects( Collection projects ) + public TaskId invalidateAll() { return logError( () -> { - if ( projects.isEmpty() ) + final PropertyTree data = new PropertyTree(); + final String taskName = "com.enonic.app.booster:invalidate~all"; + if ( taskAlreadyExists( taskName ) ) { - LOG.debug( "No projects specified to invalidate" ); + LOG.debug( "Same task is already running" ); return null; } - final PropertyTree data = new PropertyTree(); - data.addStrings( "project", projects.stream().map( Objects::toString ).toArray( String[]::new ) ); - final TaskId taskId = taskService.submitTask( - SubmitTaskParams.create().descriptorKey( DescriptorKey.from( "com.enonic.app.booster:invalidate" ) ).data( data ).build() ); - LOG.debug( "Cleanup task submitted {}", taskId ); - return taskId; - } ); - } - - public TaskId invalidateApp( final ApplicationKey app ) - { - return logError( () -> { - final PropertyTree data = new PropertyTree(); - data.addString( "app", app.toString() ); - final TaskId taskId = taskService.submitTask( - SubmitTaskParams.create().descriptorKey( DescriptorKey.from( "com.enonic.app.booster:invalidate-app" ) ).data( data ).build() ); - LOG.debug( "Cleanup by app task submitted {}", taskId ); + final TaskId taskId = taskService.submitTask( SubmitTaskParams.create() + .descriptorKey( DescriptorKey.from( "com.enonic.app.booster:invalidate" ) ) + .name( taskName ) + .data( data ) + .build() ); + LOG.debug( "Cleanup all task submitted {}", taskId ); return taskId; } ); } - - public TaskId invalidateAll() + public TaskId invalidate( Collection projects ) { return logError( () -> { - final PropertyTree data = new PropertyTree(); - final TaskId taskId = taskService.submitTask( - SubmitTaskParams.create().descriptorKey( DescriptorKey.from( "com.enonic.app.booster:invalidate" ) ).data( data ).build() ); - LOG.debug( "Cleanup all task submitted {}", taskId ); - return taskId; - } ); - } + if ( projects.isEmpty() ) + { + LOG.debug( "No projects specified to invalidate" ); + return null; + } + final String taskName = "com.enonic.app.booster:invalidate~" + generateNameSuffix( projects ); + if ( taskAlreadyExists( taskName ) ) + { + LOG.debug( "Same task is already running" ); + return null; + } - public TaskId enforceLimit() - { - return logError( () -> { final PropertyTree data = new PropertyTree(); - data.setLong( "cacheSize", (long) config.cacheSize() ); + data.addStrings( "project", projects.stream().map( Objects::toString ).toArray( String[]::new ) ); final TaskId taskId = taskService.submitTask( SubmitTaskParams.create() - .descriptorKey( DescriptorKey.from( "com.enonic.app.booster:enforce-limit" ) ) + .descriptorKey( DescriptorKey.from( "com.enonic.app.booster:invalidate" ) ) + .name( taskName ) .data( data ) .build() ); - LOG.debug( "Capped task submitted {}", taskId ); + LOG.debug( "Cleanup task submitted {}", taskId ); return taskId; } ); } - public TaskId purgeAll() + public String generateNameSuffix( Collection projects ) { - return logError( () -> { - final TaskId taskId = taskService.submitTask( SubmitTaskParams.create() - .descriptorKey( DescriptorKey.from( "com.enonic.app.booster:purge-all" ) ) - .data( new PropertyTree() ) - .build() ); - LOG.debug( "Purge all task submitted {}", taskId ); - return taskId; - } ); + final MessageDigest digest; + try + { + digest = MessageDigest.getInstance( "SHA-256" ); + } + catch ( NoSuchAlgorithmException e ) + { + throw new RuntimeException( e ); + } + projects.stream().map( ProjectName::toString ).map( String::getBytes ).forEach( digest::update ); + + return HexFormat.of().formatHex( digest.digest() ); + } + + public boolean taskAlreadyExists(final String taskName) { + return taskService.getAllTasks().stream().filter( ti -> taskName.equals( ti.getName() ) ) // lookup for specific name + .anyMatch( ti -> !ti.isDone() ); // only WAITING and RUNNING are considered submitted, others are done } private TaskId logError( final Callable callable ) diff --git a/src/main/java/com/enonic/app/booster/storage/NodeCacheStore.java b/src/main/java/com/enonic/app/booster/storage/NodeCacheStore.java index e17ddd2..86dc127 100644 --- a/src/main/java/com/enonic/app/booster/storage/NodeCacheStore.java +++ b/src/main/java/com/enonic/app/booster/storage/NodeCacheStore.java @@ -35,7 +35,6 @@ @Component(immediate = true, service = NodeCacheStore.class) public class NodeCacheStore { - private static final Logger LOG = LoggerFactory.getLogger( NodeCacheStore.class ); public static final BinaryReference GZIP_DATA_BINARY_REFERENCE = BinaryReference.from( "data.gzip" ); @@ -231,5 +230,4 @@ else if ( value instanceof String ) } return result; } - } diff --git a/src/main/java/com/enonic/app/booster/storage/NodeCleanerBean.java b/src/main/java/com/enonic/app/booster/storage/NodeCleanerBean.java deleted file mode 100644 index ceef80d..0000000 --- a/src/main/java/com/enonic/app/booster/storage/NodeCleanerBean.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.enonic.app.booster.storage; - -import java.time.Instant; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.enonic.app.booster.BoosterConfigService; -import com.enonic.xp.data.ValueFactory; -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.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; -import com.enonic.xp.script.bean.BeanContext; -import com.enonic.xp.script.bean.ScriptBean; -import com.enonic.xp.trace.Tracer; - -public class NodeCleanerBean - implements ScriptBean -{ - private static final Logger LOG = LoggerFactory.getLogger( NodeCleanerBean.class ); - - private NodeService nodeService; - - private Supplier configService; - - private BoosterProjectMatchers boosterProjectMatchers; - @Override - public void initialize( final BeanContext beanContext ) - { - this.nodeService = beanContext.getService( NodeService.class ).get(); - this.boosterProjectMatchers = beanContext.getService( BoosterProjectMatchers.class ).get(); - this.configService = beanContext.getService( BoosterConfigService.class ); - } - - public void invalidateProjects( final List projects ) - { - if ( projects.isEmpty() ) - { - return; - } - invalidateByQuery( Map.of( "project", Multiple.of( projects ) ) ); - } - - public void invalidateContent( final String project, final String contentId ) - { - invalidateByQuery( Map.of( "project", Single.of( project ), "contentId", Single.of( contentId ) ) ); - } - - public void invalidateSite( final String project, final String siteId ) - { - invalidateByQuery( Map.of( "project", Single.of( project ), "siteId", Single.of( siteId ) ) ); - } - - public void invalidateDomain( final String domain ) - { - invalidateByQuery( Map.of( "domain", Single.of( domain ) ) ); - } - - public void invalidatePathPrefix( final String domain, final String path ) - { - invalidateByQuery( Map.of( "domain", Single.of( domain ), "path", PathPrefix.of( path ) ) ); - } - - public void invalidateAll() - { - invalidateByQuery( Map.of() ); - } - - public void purgeAll() - { - final Instant now = Instant.now(); - BoosterContext.runInContext( () -> { - final NodeQuery query = queryAllNodes( now ); - - process( query, this::delete ); - } ); - } - - public void scavenge() - { - final int cacheSize = configService.get().getConfig().cacheSize(); - final Instant now = Instant.now(); - Tracer.trace( "booster.scavenge", () -> BoosterContext.runInContext( () -> { - LOG.debug( "Scavenge" ); - final NodeQuery query = queryAllNodes( now ); - FindNodesByQueryResult nodesToDelete = nodeService.findByQuery( query ); - - long diff = nodesToDelete.getTotalHits() - cacheSize; - - while ( diff > 0 ) - { - final NodeHits nodeHits = nodesToDelete.getNodeHits(); - for ( int i = 0; i < diff; i++ ) - { - delete( nodeHits.get( i ).getNodeId() ); - } - nodeService.refresh( RefreshMode.SEARCH ); - nodesToDelete = nodeService.findByQuery( query ); - diff = nodesToDelete.getTotalHits() - cacheSize; - } - } ) ); - } - - - public void invalidateScheduled() - { - Tracer.trace( "booster.invalidateScheduled", () -> BoosterContext.runInContext( () -> { - invalidateProjects( boosterProjectMatchers.findScheduledForInvalidation() ); - } ) ); - } - - public void invalidateWithApp( final String app ) - { - BoosterContext.runInContext( () -> invalidateProjects( boosterProjectMatchers.findByAppForInvalidation( app ) ) ); - } - - public int getProjectCacheSize( final String project ) - { - return getSize( Map.of( "project", Single.of( project ) ) ); - } - - public int getSiteCacheSize( final String project, final String siteId ) - { - return getSize( Map.of( "project", Single.of( project ), "siteId", Single.of( siteId ) ) ); - } - - public int getContentCacheSize( final String project, final String contentId ) - { - return getSize( Map.of( "project", Single.of( project ), "contentId", Single.of( contentId ) ) ); - } - - private int getSize( final Map fields ) - { - final Instant now = Instant.now(); - FindNodesByQueryResult nodesToInvalidate = BoosterContext.callInContext( () -> { - - final NodeQuery query = 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 fields ) - { - final Instant now = Instant.now(); - BoosterContext.runInContext( () -> { - final NodeQuery query = queryNodes( fields, now, false, 10_000 ); - process( query, ( n ) -> setInvalidatedTime( n, now ) ); - } ); - } - - private void process( final NodeQuery query, final Consumer 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 cutOffTime ) - { - try - { - nodeService.update( - UpdateNodeParams.create().id( nodeId ).editor( editor -> editor.data.setInstant( "invalidatedTime", cutOffTime ) ).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() ); - } - - private NodeQuery queryAllNodes( final Instant cutOffTime ) - { - return queryNodes( Map.of(), cutOffTime, true, 10_000 ); - } - - private NodeQuery queryNodes( final Map fields, final Instant cutOffTime, final boolean includeInvalidated, int size ) - { - final NodeQuery.Builder builder = NodeQuery.create(); - builder.parent( BoosterContext.CACHE_PARENT_NODE ); - - for ( Map.Entry entry : fields.entrySet() ) - { - final Value value = entry.getValue(); - if ( value instanceof Multiple multiple ) - { - builder.addQueryFilter( ValueFilter.create().fieldName( entry.getKey() ).addValues( multiple.values ).build() ); - } - else if ( value instanceof 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 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(); - } - - sealed interface Value - permits Single, Multiple, PathPrefix - { - } - - static final class Single - implements Value - { - String value; - - Single( final String value ) - { - this.value = value; - } - - static Single of( final String value ) - { - return new Single( value ); - } - } - - static final class Multiple - implements Value - { - Collection values; - - Multiple( final Collection values ) - { - this.values = values; - } - - static Multiple of( final Collection values ) - { - return new Multiple( values ); - } - } - - static final class PathPrefix - implements Value - { - PathPrefix( final String value ) - { - this.value = value; - } - - String value; - - static PathPrefix of( final String value ) - { - return new PathPrefix( value ); - } - } -} diff --git a/src/main/resources/admin/widgets/booster/booster.js b/src/main/resources/admin/widgets/booster/booster.js index 89656ff..cd399c5 100644 --- a/src/main/resources/admin/widgets/booster/booster.js +++ b/src/main/resources/admin/widgets/booster/booster.js @@ -42,7 +42,7 @@ const renderWidgetView = (req) => { } if (!error) { - const nodeCleanerBean = __.newBean('com.enonic.app.booster.storage.NodeCleanerBean'); + const nodeCleanerBean = __.newBean('com.enonic.app.booster.script.NodeCleanerBean'); size = nodeCleanerBean.getProjectCacheSize(project); if (contentId && !isAppEnabledOnSite(contentId)) { diff --git a/src/main/resources/main.js b/src/main/resources/main.js deleted file mode 100644 index 6458113..0000000 --- a/src/main/resources/main.js +++ /dev/null @@ -1,34 +0,0 @@ -const clusterLib = require('/lib/xp/cluster'); -const cron = require('/lib/cron'); - -cron.schedule({ - name: 'booster-invalidate-scheduled', - delay: 1000, - fixedDelay: 10000, - callback: function () { - if (clusterLib.isMaster()) { - __.newBean('com.enonic.app.booster.storage.NodeCleanerBean').invalidateScheduled(); - } - } -}); - -cron.schedule({ - name: 'booster-delete-excess-nodes', - cron: '* * * * *', - callback: function () { - if (clusterLib.isMaster()) { - __.newBean('com.enonic.app.booster.storage.NodeCleanerBean').scavenge(); - } - } -}); - - -__.disposer(function () { - log.debug('Unscheduling invalidate-scheduled'); - cron.unschedule({ - name: 'booster-invalidate-scheduled' - }); - cron.unschedule({ - name: 'booster-delete-excess-nodes' - }); -}); diff --git a/src/main/resources/tasks/invalidate-app/invalidate-app.js b/src/main/resources/tasks/invalidate-app/invalidate-app.js deleted file mode 100644 index 4c275d1..0000000 --- a/src/main/resources/tasks/invalidate-app/invalidate-app.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.run = function (params, taskId) { - log.debug('Running booster node cleaner by app task with params: ' + JSON.stringify(params)); - let nodeCleanerBean = __.newBean('com.enonic.app.booster.storage.NodeCleanerBean'); - - nodeCleanerBean.invalidateWithApp(params.app); -}; diff --git a/src/main/resources/tasks/invalidate-app/invalidate-app.xml b/src/main/resources/tasks/invalidate-app/invalidate-app.xml deleted file mode 100644 index 133e251..0000000 --- a/src/main/resources/tasks/invalidate-app/invalidate-app.xml +++ /dev/null @@ -1,8 +0,0 @@ - - Invalidate Booster Cache by App -
- - - -
-
diff --git a/src/main/resources/tasks/invalidate/invalidate.js b/src/main/resources/tasks/invalidate/invalidate.js index 62bcc89..af192de 100644 --- a/src/main/resources/tasks/invalidate/invalidate.js +++ b/src/main/resources/tasks/invalidate/invalidate.js @@ -1,6 +1,6 @@ exports.run = function (params, taskId) { log.debug('Running booster node cleaner task with params: ' + JSON.stringify(params)); - let nodeCleanerBean = __.newBean('com.enonic.app.booster.storage.NodeCleanerBean'); + let nodeCleanerBean = __.newBean('com.enonic.app.booster.script.NodeCleanerBean'); if (params.content) { nodeCleanerBean.invalidateContent(params.project, params.content); diff --git a/src/main/resources/tasks/purge-all/purge-all.js b/src/main/resources/tasks/purge-all/purge-all.js index 18de419..7d66b53 100644 --- a/src/main/resources/tasks/purge-all/purge-all.js +++ b/src/main/resources/tasks/purge-all/purge-all.js @@ -1,4 +1,4 @@ exports.run = function (params, taskId) { log.debug('Running booster node purge all task with params: ' + JSON.stringify(params)); - __.newBean('com.enonic.app.booster.storage.NodeCleanerBean').purgeAll(); + __.newBean('com.enonic.app.booster.script.NodeCleanerBean').purgeAll(); }; diff --git a/src/test/java/com/enonic/app/booster/storage/NodeCleanerBeanTest.java b/src/test/java/com/enonic/app/booster/script/NodeCleanerBeanTest.java similarity index 96% rename from src/test/java/com/enonic/app/booster/storage/NodeCleanerBeanTest.java rename to src/test/java/com/enonic/app/booster/script/NodeCleanerBeanTest.java index b2cd3ba..25d00fa 100644 --- a/src/test/java/com/enonic/app/booster/storage/NodeCleanerBeanTest.java +++ b/src/test/java/com/enonic/app/booster/script/NodeCleanerBeanTest.java @@ -1,4 +1,4 @@ -package com.enonic.app.booster.storage; +package com.enonic.app.booster.script; import java.util.List; @@ -12,6 +12,7 @@ import com.enonic.app.booster.BoosterConfig; import com.enonic.app.booster.BoosterConfigParsed; import com.enonic.app.booster.BoosterConfigService; +import com.enonic.app.booster.storage.BoosterProjectMatchers; import com.enonic.xp.data.PropertyTree; import com.enonic.xp.data.Value; import com.enonic.xp.node.DeleteNodeParams; @@ -72,13 +73,6 @@ void invalidateAll() verifyBasicInvalidate( nodeCleanerBean::invalidateAll ); } - @Test - void invalidateScheduled() - { - when( boosterProjectMatchers.findScheduledForInvalidation() ).thenReturn( List.of( "project1" ) ); - verifyBasicInvalidate( nodeCleanerBean::invalidateScheduled ); - } - @Test void invalidateProjects() { @@ -97,14 +91,6 @@ void invalidateProjects_empty() verifyNoInteractions( nodeService ); } - @Test - void invalidateWithApp() - { - when( boosterProjectMatchers.findByAppForInvalidation( "app1" ) ).thenReturn( List.of( "project1" ) ); - - verifyBasicInvalidate( () -> nodeCleanerBean.invalidateWithApp( "app1" ) ); - } - @Test void invalidateContent() { @@ -221,7 +207,7 @@ void scavenge() .totalHits( 1 ) .build() ); - nodeCleanerBean.scavenge(); + //nodeCleanerBean.scavenge(); verify( nodeService, times( 2 ) ).findByQuery( any( NodeQuery.class ) ); verify( nodeService ).refresh( RefreshMode.SEARCH ); diff --git a/src/test/java/com/enonic/app/booster/storage/BoosterInvalidatorTest.java b/src/test/java/com/enonic/app/booster/storage/BoosterInvalidatorTest.java index 3f634cd..21b0c8f 100644 --- a/src/test/java/com/enonic/app/booster/storage/BoosterInvalidatorTest.java +++ b/src/test/java/com/enonic/app/booster/storage/BoosterInvalidatorTest.java @@ -37,12 +37,15 @@ class BoosterInvalidatorTest @Mock ScheduledExecutorService scheduledExecutorService; + @Mock + BoosterProjectMatchers boosterProjectMatchers; @Test void application_installed_event_invalidate_all() { when( indexService.isMaster() ).thenReturn( true ); - BoosterInvalidator boosterInvalidator = new BoosterInvalidator( boosterTasksFacade, indexService ); + BoosterInvalidator boosterInvalidator = + new BoosterInvalidator( boosterTasksFacade, indexService, boosterProjectMatchers, scheduledExecutorService ); final BoosterConfig boosterConfig = mock( BoosterConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); when( boosterConfig.appsForceInvalidateOnInstall() ).thenReturn( "somekey" ); boosterInvalidator.activate( boosterConfig ); @@ -57,7 +60,8 @@ void application_installed_event_invalidate_all() void application_installed_not_master() { when( indexService.isMaster() ).thenReturn( false ); - BoosterInvalidator boosterInvalidator = new BoosterInvalidator( boosterTasksFacade, indexService ); + BoosterInvalidator boosterInvalidator = + new BoosterInvalidator( boosterTasksFacade, indexService, boosterProjectMatchers, scheduledExecutorService ); final BoosterConfig boosterConfig = mock( BoosterConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); when( boosterConfig.appsForceInvalidateOnInstall() ).thenReturn( "somekey" ); boosterInvalidator.activate( boosterConfig ); @@ -67,25 +71,28 @@ void application_installed_not_master() verifyNoInteractions( boosterTasksFacade ); } + @Test void application_installed_event_invalidate_app() { when( indexService.isMaster() ).thenReturn( true ); - BoosterInvalidator boosterInvalidator = new BoosterInvalidator( boosterTasksFacade, indexService ); + BoosterInvalidator boosterInvalidator = + new BoosterInvalidator( boosterTasksFacade, indexService, boosterProjectMatchers, scheduledExecutorService ); final BoosterConfig boosterConfig = mock( BoosterConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); when( boosterConfig.appsForceInvalidateOnInstall() ).thenReturn( "someotherkey" ); boosterInvalidator.activate( boosterConfig ); - + when( boosterProjectMatchers.findByAppForInvalidation( List.of(ApplicationKey.from( "somekey" )) ) ).thenReturn( List.of( ProjectName.from( "someproject" ) ) ); boosterInvalidator.onEvent( Event.create( "application.cluster" ).value( "eventType", "installed" ).value( "key", "somekey" ).build() ); - verify( boosterTasksFacade ).invalidateApp( ApplicationKey.from( "somekey" )); + verify( boosterTasksFacade ).invalidate( List.of( ProjectName.from( "someproject" ) ) ); } @Test void repository_events_invalidate_projects() { - BoosterInvalidator boosterInvalidator = new BoosterInvalidator( boosterTasksFacade, indexService, scheduledExecutorService ); + BoosterInvalidator boosterInvalidator = + new BoosterInvalidator( boosterTasksFacade, indexService, boosterProjectMatchers, scheduledExecutorService ); final BoosterConfig boosterConfig = mock( BoosterConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); boosterInvalidator.activate( boosterConfig ); @@ -97,13 +104,14 @@ void repository_events_invalidate_projects() verify( scheduledExecutorService ).scheduleWithFixedDelay( captor.capture(), eq( 10L ), eq( 10L ), eq( TimeUnit.SECONDS ) ); captor.getValue().run(); - verify( boosterTasksFacade ).invalidateProjects( eq( Set.of( ProjectName.from( "repo1" ), ProjectName.from( "repo2" ) ) ) ); + verify( boosterTasksFacade ).invalidate( eq( Set.of( ProjectName.from( "repo1" ), ProjectName.from( "repo2" ) ) ) ); } @Test void node_events_invalidate_projects() { - BoosterInvalidator boosterInvalidator = new BoosterInvalidator( boosterTasksFacade, indexService, scheduledExecutorService ); + BoosterInvalidator boosterInvalidator = + new BoosterInvalidator( boosterTasksFacade, indexService, boosterProjectMatchers, scheduledExecutorService ); final BoosterConfig boosterConfig = mock( BoosterConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); boosterInvalidator.activate( boosterConfig ); @@ -111,13 +119,14 @@ void node_events_invalidate_projects() .value( "nodes", List.of( Map.of( "repo", "com.enonic.cms.repo1", "branch", "master" ) ) ) .build() ); - - boosterInvalidator.onEvent( Event.create( "node.deleted" ).value( "nodes", List.of( Map.of( "repo", "com.enonic.cms.repo2", "branch", "master" ) ) ).build() ); + boosterInvalidator.onEvent( Event.create( "node.deleted" ) + .value( "nodes", List.of( Map.of( "repo", "com.enonic.cms.repo2", "branch", "master" ) ) ) + .build() ); final ArgumentCaptor captor = captor(); verify( scheduledExecutorService ).scheduleWithFixedDelay( captor.capture(), eq( 10L ), eq( 10L ), eq( TimeUnit.SECONDS ) ); captor.getValue().run(); - verify( boosterTasksFacade ).invalidateProjects( eq( Set.of( ProjectName.from( "repo1" ), ProjectName.from( "repo2" ) ) ) ); + verify( boosterTasksFacade ).invalidate( eq( Set.of( ProjectName.from( "repo1" ), ProjectName.from( "repo2" ) ) ) ); } } diff --git a/src/test/java/com/enonic/app/booster/storage/BoosterProjectMatchersTest.java b/src/test/java/com/enonic/app/booster/storage/BoosterProjectMatchersTest.java index 7a20ec4..01274e8 100644 --- a/src/test/java/com/enonic/app/booster/storage/BoosterProjectMatchersTest.java +++ b/src/test/java/com/enonic/app/booster/storage/BoosterProjectMatchersTest.java @@ -11,6 +11,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.enonic.xp.app.ApplicationKey; import com.enonic.xp.data.PropertyTree; import com.enonic.xp.node.FindNodesByQueryResult; import com.enonic.xp.node.Node; @@ -60,9 +61,9 @@ void findScheduledForInvalidation() FindNodesByQueryResult.create().hits( 0 ).totalHits( 0 ).build() ) .thenReturn( FindNodesByQueryResult.create().hits( 0 ).totalHits( 2 ).build() ); - final List scheduled = boosterProjectMatchers.findScheduledForInvalidation(); + final List scheduled = boosterProjectMatchers.findScheduledForInvalidation(); - assertThat( scheduled ).containsExactly( "project2" ); + assertThat( scheduled ).containsExactly( ProjectName.from( "project2" ) ); verify( nodeService ).getByPath( BoosterContext.SCHEDULED_PARENT_NODE ); @@ -85,9 +86,9 @@ void findByAppForInvalidation() FindNodesByQueryResult.create().hits( 0 ).totalHits( 0 ).build() ) .thenReturn( FindNodesByQueryResult.create().hits( 0 ).totalHits( 2 ).build() ); - final List scheduled = boosterProjectMatchers.findByAppForInvalidation( "someApp" ); + final List scheduled = boosterProjectMatchers.findByAppForInvalidation( List.of( ApplicationKey.from( "someApp" ) ) ); - assertThat( scheduled ).containsExactly( "project2" ); + assertThat( scheduled ).containsExactly( ProjectName.from( "project2" ) ); verify( nodeService, times( 2 ) ).findByQuery( any( NodeQuery.class ) ); }