Skip to content

Commit

Permalink
HSEARCH-4316 Test automatic indexing with multi-tenancy enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Nov 30, 2021
1 parent 4460036 commit 47296de
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 1 deletion.
Expand Up @@ -38,7 +38,7 @@ public class OutboxPollingAutomaticIndexingWhileMassIndexingIT {
.coordinationStrategy( CoordinationStrategyExpectations.outboxPollingAndMassIndexing() );

@Test
public void test() throws InterruptedException {
public void singleTenant() throws InterruptedException {
backendMock.expectSchema( IndexedEntity.NAME, b -> b
.field( "text", String.class, f -> f.analyzerName( AnalyzerNames.DEFAULT ) ) );

Expand Down Expand Up @@ -104,6 +104,100 @@ public void test() throws InterruptedException {
backendMock.verifyExpectationsMet();
}

@Test
public void multiTenant() throws InterruptedException {
String tenant1Id = "tenant1";
String tenant2Id = "tenant2";

backendMock.expectSchema( IndexedEntity.NAME, b -> b
.field( "text", String.class, f -> f.analyzerName( AnalyzerNames.DEFAULT ) ) );

SessionFactory sessionFactory = setupHelper.start()
.tenants( tenant1Id, tenant2Id )
.setup( IndexedEntity.class );

with( sessionFactory, tenant1Id ).runInTransaction( session -> {
IndexedEntity entity = new IndexedEntity( 1, "initial value for tenant 1" );
session.save( entity );

backendMock.expectWorks( IndexedEntity.NAME, tenant1Id )
.addOrUpdate( String.valueOf( 1 ), b -> b
.field( "text", "initial value for tenant 1" ) );
} );

with( sessionFactory, tenant2Id ).runInTransaction( session -> {
IndexedEntity entity = new IndexedEntity( 1, "initial value for tenant 2" );
session.save( entity );

backendMock.expectWorks( IndexedEntity.NAME, tenant2Id )
.addOrUpdate( String.valueOf( 1 ), b -> b
.field( "text", "initial value for tenant 2" ) );
} );

// Wait for the initial indexing to be over.
backendMock.verifyExpectationsMet();

// Before loading entities, we expect mass indexing to perform a few operations
backendMock.expectIndexScaleWorks( IndexedEntity.NAME, tenant1Id )
.purge()
.mergeSegments();

// Upon loading the entity for mass indexing:
IndexedEntity.getTextConcurrentOperation.set( () -> {
// 1. We make sure this operation doesn't get executed multiple times.
IndexedEntity.getTextConcurrentOperation.set( () -> { } );
// 2. We simulate a concurrent transaction that updates another entity in a different tenant.
with( sessionFactory, tenant2Id ).runInTransaction( session -> {
IndexedEntity entity = session.get( IndexedEntity.class, 1 );
entity.setText( "updated value for tenant 2" );
backendMock.expectWorks( IndexedEntity.NAME, tenant2Id )
.addOrUpdate( String.valueOf( 1 ), b -> b
.field( "text", "updated value for tenant 2" ) );
} );
// 3. We expect the resulting event to be processed concurrently,
// because mass indexing is NOT in progress for this tenant.
backendMock.verifyExpectationsMet();
// 4. We simulate a concurrent transaction that updates the entity being mass-indexed, in the same tenant.
with( sessionFactory, tenant1Id ).runInTransaction( session -> {
IndexedEntity entity = session.get( IndexedEntity.class, 1 );
entity.setText( "updated value for tenant 1" );
} );
// 5. We give the event processor some time to process the change
// (it shouldn't process it, since mass indexing is in progress).
try {
Thread.sleep( 1000 );
}
catch (InterruptedException e) {
throw new RuntimeException( e );
}
// 6. We expect nothing more to be indexed at this point,
// because the event processor for tenant 1 should be suspended.
backendMock.verifyExpectationsMet();

// Later, we expect the mass indexer to reindex the entity it is currently loading, with the initial value...
backendMock.expectWorks( IndexedEntity.NAME, tenant1Id, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE )
.add( String.valueOf( 1 ), b -> b
.field( "text", "initial value for tenant 1" ) );

// Then we expect the mass indexer to perform finishing operations...
backendMock.expectIndexScaleWorks( IndexedEntity.NAME, tenant1Id )
.flush()
.refresh();

// ... and ONLY AFTER THAT,
// we expect the event processor to reindex the same entity with the updated value.
backendMock.expectWorks( IndexedEntity.NAME, tenant1Id )
.addOrUpdate( String.valueOf( 1 ), b -> b
.field( "text", "updated value for tenant 1" ) );
} );

Search.mapping( sessionFactory )
.scope( Object.class ).massIndexer( tenant1Id )
.startAndWait();

backendMock.verifyExpectationsMet();
}

@Entity(name = IndexedEntity.NAME)
@Indexed
public static class IndexedEntity {
Expand Down
@@ -0,0 +1,111 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.integrationtest.mapper.orm.automaticindexing;

import static org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils.with;

import java.util.concurrent.atomic.AtomicReference;
import javax.persistence.Entity;
import javax.persistence.Id;

import org.hibernate.SessionFactory;
import org.hibernate.search.engine.backend.analysis.AnalyzerNames;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.util.impl.integrationtest.common.rule.BackendMock;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmSetupHelper;
import org.hibernate.search.util.impl.test.annotation.TestForIssue;

import org.junit.Rule;
import org.junit.Test;

/**
* Simple test to check that automatic indexing works correctly when multi-tenancy is enabled.
* <p>
* This is especially relevant with coordination strategies that involve performing indexing
* from a background thread, since they will have to remember the tenant ID somehow.
*/
@TestForIssue(jiraKey = "HSEARCH-4316")
public class AutomaticIndexingMultiTenancyIT {

private static final String TENANT_1_ID = "tenant1";
private static final String TENANT_2_ID = "tenant2";

@Rule
public BackendMock backendMock = new BackendMock();

@Rule
public OrmSetupHelper setupHelper = OrmSetupHelper.withBackendMock( backendMock );

@Test
public void test() throws InterruptedException {
backendMock.expectSchema( IndexedEntity.NAME, b -> b
.field( "text", String.class, f -> f.analyzerName( AnalyzerNames.DEFAULT ) ) );

SessionFactory sessionFactory = setupHelper.start()
.tenants( TENANT_1_ID, TENANT_2_ID )
.setup( IndexedEntity.class );

with( sessionFactory, TENANT_1_ID ).runInTransaction( session -> {
IndexedEntity entity = new IndexedEntity( 1, "value for tenant 1" );
session.save( entity );

backendMock.expectWorks( IndexedEntity.NAME, TENANT_1_ID )
.add( String.valueOf( 1 ), b -> b.field( "text", "value for tenant 1" ) );
} );
backendMock.verifyExpectationsMet();

with( sessionFactory, TENANT_2_ID ).runInTransaction( session -> {
IndexedEntity entity = new IndexedEntity( 1, "value for tenant 2" );
session.save( entity );

backendMock.expectWorks( IndexedEntity.NAME, TENANT_2_ID )
.add( String.valueOf( 1 ), b -> b.field( "text", "value for tenant 2" ) );
} );
backendMock.verifyExpectationsMet();
}

@Entity(name = IndexedEntity.NAME)
@Indexed
public static class IndexedEntity {

static final String NAME = "IndexedEntity";

static volatile AtomicReference<Runnable> getTextConcurrentOperation = new AtomicReference<>( () -> { } );

private Integer id;
private String text;

public IndexedEntity() {
}

public IndexedEntity(Integer id, String text) {
this.id = id;
this.text = text;
}

@Id
public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

@FullTextField
public String getText() {
getTextConcurrentOperation.get().run();
return text;
}

public void setText(String text) {
this.text = text;
}
}

}

0 comments on commit 47296de

Please sign in to comment.