diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/DelegatingTenantResolver.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/DelegatingTenantResolver.java index a2e3c6d8..8c47b6ce 100644 --- a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/DelegatingTenantResolver.java +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/DelegatingTenantResolver.java @@ -17,15 +17,17 @@ package in.cleartax.dropwizard.sharding.hibernate; +import com.google.common.base.Preconditions; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; -import static com.google.common.base.Preconditions.checkNotNull; +import java.util.Stack; public class DelegatingTenantResolver implements CurrentTenantIdentifierResolver { private static DelegatingTenantResolver instance; - private ThreadLocal delegate = new ThreadLocal<>(); + private ThreadLocal> delegate = + ThreadLocal.withInitial(Stack::new); private DelegatingTenantResolver() { @@ -43,18 +45,24 @@ public static DelegatingTenantResolver getInstance() { } public void setDelegate(CurrentTenantIdentifierResolver resolver) { - delegate.set(resolver); + if (resolver != null) { + delegate.get().add(resolver); + } else { + delegate.get().pop(); + } } @Override public String resolveCurrentTenantIdentifier() { - checkNotNull(delegate.get(), "Did you forget to set tenantId"); - - return delegate.get().resolveCurrentTenantIdentifier(); + Preconditions.checkArgument(!delegate.get().isEmpty(), "Did you forget to set tenantId"); + //noinspection ConstantConditions + return delegate.get().peek().resolveCurrentTenantIdentifier(); } @Override public boolean validateExistingCurrentSessions() { - return delegate.get().validateExistingCurrentSessions(); + Preconditions.checkArgument(!delegate.get().isEmpty(), "Did you forget to set tenantId"); + //noinspection ConstantConditions + return delegate.get().peek().validateExistingCurrentSessions(); } } diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantSessionSource.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantSessionSource.java new file mode 100644 index 00000000..e785104c --- /dev/null +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantSessionSource.java @@ -0,0 +1,32 @@ +/* + * Copyright 2018 Saurabh Agrawal (Cleartax) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package in.cleartax.dropwizard.sharding.hibernate; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.hibernate.SessionFactory; + +@AllArgsConstructor +@Value +@Builder +public class MultiTenantSessionSource { + private SessionFactory sessionFactory; + private MultiTenantDataSourceFactory dataSourceFactory; + private MultiTenantUnitOfWorkAwareProxyFactory unitOfWorkAwareProxyFactory; +} diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAspect.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAspect.java new file mode 100644 index 00000000..fb7453c2 --- /dev/null +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAspect.java @@ -0,0 +1,162 @@ +package in.cleartax.dropwizard.sharding.hibernate; + +import io.dropwizard.hibernate.HibernateBundle; +import io.dropwizard.hibernate.UnitOfWork; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.context.internal.ManagedSessionContext; + +import javax.annotation.Nullable; +import java.util.Map; +import java.util.Stack; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +/** + * Created on 14/11/18 + */ +public class MultiTenantUnitOfWorkAspect { + private static ThreadLocal> CONTEXT_OPEN_SESSIONS = ThreadLocal.withInitial(Stack::new); + + private final Map sessionFactories; + // Context variables + @Nullable + private UnitOfWork unitOfWork; + @Nullable + private Session session; + @Nullable + private SessionFactory sessionFactory; + + public MultiTenantUnitOfWorkAspect(Map sessionFactories) { + this.sessionFactories = sessionFactories; + } + + public void beforeStart(@Nullable UnitOfWork unitOfWork) { + if (unitOfWork == null) { + return; + } + this.unitOfWork = unitOfWork; + + sessionFactory = sessionFactories.get(unitOfWork.value()); + if (sessionFactory == null) { + // If the user didn't specify the name of a session factory, + // and we have only one registered, we can assume that it's the right one. + if (unitOfWork.value().equals(HibernateBundle.DEFAULT_NAME) && sessionFactories.size() == 1) { + sessionFactory = sessionFactories.values().iterator().next(); + } else { + throw new IllegalArgumentException("Unregistered Hibernate bundle: '" + unitOfWork.value() + "'"); + } + } + session = sessionFactory.openSession(); + assert session != null; + try { + configureSession(); + bind(session); + beginTransaction(unitOfWork, session); + } catch (Throwable th) { + session.close(); + session = null; + unbind(sessionFactory); + throw th; + } + } + + public void afterEnd() { + if (unitOfWork == null || session == null) { + return; + } + + try { + commitTransaction(unitOfWork, session); + } catch (Exception e) { + rollbackTransaction(unitOfWork, session); + throw e; + } + // We should not close the session to let the lazy loading work during serializing a response to the client. + // If the response successfully serialized, then the session will be closed by the `onFinish` method + } + + public void onError() { + if (unitOfWork == null || session == null) { + return; + } + + try { + rollbackTransaction(unitOfWork, session); + } finally { + onFinish(); + } + } + + public void onFinish() { + try { + if (session != null) { + session.close(); + } + } finally { + session = null; + unbind(sessionFactory); + } + } + + protected void configureSession() { + checkNotNull(unitOfWork); + checkNotNull(session); + session.setDefaultReadOnly(unitOfWork.readOnly()); + session.setCacheMode(unitOfWork.cacheMode()); + session.setHibernateFlushMode(unitOfWork.flushMode()); + } + + private void beginTransaction(UnitOfWork unitOfWork, Session session) { + if (!unitOfWork.transactional()) { + return; + } + session.beginTransaction(); + } + + private void rollbackTransaction(UnitOfWork unitOfWork, Session session) { + if (!unitOfWork.transactional()) { + return; + } + final Transaction txn = session.getTransaction(); + if (txn != null && txn.getStatus().canRollback()) { + txn.rollback(); + } + } + + private void commitTransaction(UnitOfWork unitOfWork, Session session) { + if (!unitOfWork.transactional()) { + return; + } + final Transaction txn = session.getTransaction(); + if (txn != null && txn.getStatus().canRollback()) { + txn.commit(); + } + } + + protected Session getSession() { + return requireNonNull(session); + } + + protected SessionFactory getSessionFactory() { + return requireNonNull(sessionFactory); + } + + private void bind(Session session) { + CONTEXT_OPEN_SESSIONS.get().push(session); + ManagedSessionContext.bind(session); + } + + private void unbind(SessionFactory sessionFactory) { + ManagedSessionContext.unbind(sessionFactory); + // This defensive check is needed as in case of exception onFinish gets called multiple times. + if (!CONTEXT_OPEN_SESSIONS.get().isEmpty()) { + CONTEXT_OPEN_SESSIONS.get().pop(); + } + if (!CONTEXT_OPEN_SESSIONS.get().isEmpty()) { + ManagedSessionContext.bind(CONTEXT_OPEN_SESSIONS.get().peek()); + } + } +} diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAwareProxyFactory.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAwareProxyFactory.java index 4d5aaa2a..b79ad9b9 100644 --- a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAwareProxyFactory.java +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/hibernate/MultiTenantUnitOfWorkAwareProxyFactory.java @@ -18,7 +18,6 @@ package in.cleartax.dropwizard.sharding.hibernate; import com.google.common.collect.ImmutableMap; -import io.dropwizard.hibernate.UnitOfWorkAspect; import org.hibernate.SessionFactory; public class MultiTenantUnitOfWorkAwareProxyFactory { @@ -39,15 +38,15 @@ public MultiTenantUnitOfWorkAwareProxyFactory(MultiTenantHibernateBundle... b /** * @return a new aspect */ - public UnitOfWorkAspect newAspect() { - return new UnitOfWorkAspect(sessionFactories); + public MultiTenantUnitOfWorkAspect newAspect() { + return new MultiTenantUnitOfWorkAspect(sessionFactories); } /** * @param sessionFactories * @return a new aspect */ - public UnitOfWorkAspect newAspect(ImmutableMap sessionFactories) { - return new UnitOfWorkAspect(sessionFactories); + public MultiTenantUnitOfWorkAspect newAspect(ImmutableMap sessionFactories) { + return new MultiTenantUnitOfWorkAspect(sessionFactories); } } diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/TransactionRunner.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/TransactionRunner.java index 33bb4f35..106bf43e 100644 --- a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/TransactionRunner.java +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/TransactionRunner.java @@ -19,9 +19,9 @@ import in.cleartax.dropwizard.sharding.hibernate.ConstTenantIdentifierResolver; import in.cleartax.dropwizard.sharding.hibernate.DelegatingTenantResolver; +import in.cleartax.dropwizard.sharding.hibernate.MultiTenantUnitOfWorkAspect; import in.cleartax.dropwizard.sharding.hibernate.MultiTenantUnitOfWorkAwareProxyFactory; import io.dropwizard.hibernate.UnitOfWork; -import io.dropwizard.hibernate.UnitOfWorkAspect; import lombok.AllArgsConstructor; import org.hibernate.SessionFactory; import org.hibernate.context.internal.ManagedSessionContext; @@ -39,7 +39,7 @@ public T start(boolean reUseSession, UnitOfWork unitOfWork) throws Throwable { return run(); } DelegatingTenantResolver.getInstance().setDelegate(tenantIdentifierResolver); - UnitOfWorkAspect aspect = proxyFactory.newAspect(); + MultiTenantUnitOfWorkAspect aspect = proxyFactory.newAspect(); Exception ex = null; T result = null; try { diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/UnitOfWorkModule.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/UnitOfWorkModule.java index 85eff81d..8c78d89c 100644 --- a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/UnitOfWorkModule.java +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/transactions/UnitOfWorkModule.java @@ -21,19 +21,19 @@ import com.google.inject.Inject; import com.google.inject.matcher.Matchers; import in.cleartax.dropwizard.sharding.hibernate.ConstTenantIdentifierResolver; -import in.cleartax.dropwizard.sharding.hibernate.MultiTenantDataSourceFactory; -import in.cleartax.dropwizard.sharding.hibernate.MultiTenantUnitOfWorkAwareProxyFactory; +import in.cleartax.dropwizard.sharding.hibernate.DelegatingTenantResolver; +import in.cleartax.dropwizard.sharding.hibernate.MultiTenantSessionSource; import in.cleartax.dropwizard.sharding.providers.ShardKeyProvider; import in.cleartax.dropwizard.sharding.resolvers.bucket.BucketResolver; import in.cleartax.dropwizard.sharding.resolvers.shard.ShardResolver; import io.dropwizard.hibernate.UnitOfWork; +import lombok.extern.slf4j.Slf4j; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.hibernate.SessionFactory; -import javax.inject.Named; import java.util.Objects; +@Slf4j public class UnitOfWorkModule extends AbstractModule { @Override @@ -45,11 +45,6 @@ protected void configure() { private static class UnitOfWorkInterceptor implements MethodInterceptor { - @Inject - MultiTenantUnitOfWorkAwareProxyFactory proxyFactory; - @Inject - @Named("session") - SessionFactory sessionFactory; @Inject BucketResolver bucketResolver; @Inject @@ -57,19 +52,21 @@ private static class UnitOfWorkInterceptor implements MethodInterceptor { @Inject ShardKeyProvider shardKeyProvider; @Inject - @Named("multiTenantConfiguration") - MultiTenantDataSourceFactory multiTenantDataSourceFactory; + MultiTenantSessionSource multiTenantSessionSource; private String getTenantIdentifier(MethodInvocation mi) { boolean useDefaultShard = mi.getMethod().isAnnotationPresent(DefaultTenant.class); String tenantId; - if (!useDefaultShard && multiTenantDataSourceFactory.isAllowMultipleTenants()) { + if (!useDefaultShard && multiTenantSessionSource.getDataSourceFactory().isAllowMultipleTenants()) { String shardKey = shardKeyProvider.getKey(); - Objects.requireNonNull(shardKey, "No tenant-identifier set for this session"); - String bucketId = bucketResolver.resolve(shardKey); - tenantId = shardResolver.resolve(bucketId); + if (shardKey != null) { + String bucketId = bucketResolver.resolve(shardKey); + tenantId = shardResolver.resolve(bucketId); + } else { + tenantId = DelegatingTenantResolver.getInstance().resolveCurrentTenantIdentifier(); + } } else { - tenantId = multiTenantDataSourceFactory.getDefaultTenant(); + tenantId = multiTenantSessionSource.getDataSourceFactory().getDefaultTenant(); } return tenantId; } @@ -79,7 +76,8 @@ public Object invoke(MethodInvocation mi) throws Throwable { String tenantId = getTenantIdentifier(mi); Objects.requireNonNull(tenantId, "No tenant-identifier found for this session"); - TransactionRunner runner = new TransactionRunner(proxyFactory, sessionFactory, + TransactionRunner runner = new TransactionRunner(multiTenantSessionSource.getUnitOfWorkAwareProxyFactory(), + multiTenantSessionSource.getSessionFactory(), new ConstTenantIdentifierResolver(tenantId)) { @Override public Object run() throws Throwable { diff --git a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/utils/resolvers/shard/DbBasedShardResolver.java b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/utils/resolvers/shard/DbBasedShardResolver.java index a04efce2..1e7df71a 100644 --- a/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/utils/resolvers/shard/DbBasedShardResolver.java +++ b/sharding-core/src/main/java/in/cleartax/dropwizard/sharding/utils/resolvers/shard/DbBasedShardResolver.java @@ -19,6 +19,7 @@ import in.cleartax.dropwizard.sharding.resolvers.shard.ShardResolver; import in.cleartax.dropwizard.sharding.transactions.DefaultTenant; +import in.cleartax.dropwizard.sharding.transactions.ReuseSession; import in.cleartax.dropwizard.sharding.utils.dao.BucketToShardMappingDAO; import io.dropwizard.hibernate.UnitOfWork; import lombok.RequiredArgsConstructor; @@ -34,6 +35,7 @@ public class DbBasedShardResolver implements ShardResolver { @Override @UnitOfWork @DefaultTenant + @ReuseSession public String resolve(String bucketId) { Optional shardId = dao.getShardId(bucketId); if (!shardId.isPresent()) { diff --git a/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestModule.java b/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestModule.java index df5bd8b4..7a49e4d9 100644 --- a/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestModule.java +++ b/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestModule.java @@ -77,15 +77,12 @@ public MultiTenantHibernateBundle getHibernateBundle() { } @Provides - @Singleton - MultiTenantUnitOfWorkAwareProxyFactory provideUnitOfWorkAwareProxyFactory(MultiTenantHibernateBundle hibernateBundle) { - return new MultiTenantUnitOfWorkAwareProxyFactory(hibernateBundle); - } - - @Provides - @Named("multiTenantConfiguration") - public MultiTenantDataSourceFactory getMultiTenantDataSource(TestConfig config) { - return config.getMultiTenantDataSourceFactory(); + public MultiTenantSessionSource getMultiTenantDataSource(TestConfig config) { + return MultiTenantSessionSource.builder() + .dataSourceFactory(config.getMultiTenantDataSourceFactory()) + .sessionFactory(getSession()) + .unitOfWorkAwareProxyFactory(new MultiTenantUnitOfWorkAwareProxyFactory(hibernateBundle)) + .build(); } private class CustomSessionFactory extends MultiTenantSessionFactoryFactory { diff --git a/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestResource.java b/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestResource.java index 95d083f0..22744e30 100644 --- a/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestResource.java +++ b/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/application/TestResource.java @@ -20,8 +20,8 @@ import com.codahale.metrics.annotation.ExceptionMetered; import com.codahale.metrics.annotation.Timed; import in.cleartax.dropwizard.sharding.dto.OrderDto; -import in.cleartax.dropwizard.sharding.services.OrderService; import in.cleartax.dropwizard.sharding.services.CustomerService; +import in.cleartax.dropwizard.sharding.services.OrderService; import io.dropwizard.hibernate.UnitOfWork; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -64,6 +64,7 @@ public OrderDto createOrUpdateInvoice(@NotNull OrderDto order) { @Path("{id}") @Produces(MediaType.APPLICATION_JSON) @PermitAll + @UnitOfWork public OrderDto getOrder(@PathParam("id") long id) { return orderService.getOrder(id); } diff --git a/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/services/OrderServiceImpl.java b/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/services/OrderServiceImpl.java index 249663c7..eb5536ce 100644 --- a/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/services/OrderServiceImpl.java +++ b/sharding-example/src/main/java/in/cleartax/dropwizard/sharding/services/OrderServiceImpl.java @@ -21,7 +21,6 @@ import in.cleartax.dropwizard.sharding.dto.OrderDto; import in.cleartax.dropwizard.sharding.dto.OrderMapper; import in.cleartax.dropwizard.sharding.entities.Order; -import io.dropwizard.hibernate.UnitOfWork; import lombok.RequiredArgsConstructor; import javax.inject.Inject; @@ -33,14 +32,12 @@ public class OrderServiceImpl implements OrderService { private final OrderMapper orderMapper = new OrderMapper(); @Override - @UnitOfWork public OrderDto createOrder(OrderDto orderDto) { Order order = orderDao.save(orderMapper.from(orderDto)); return orderMapper.to(order); } @Override - @UnitOfWork public OrderDto getOrder(long orderId) { Order order = orderDao.get(orderId); return orderMapper.to(order); diff --git a/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/OrderIntegrationTestWithMultiTenancy.java b/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/OrderIntegrationTestWithMultiTenancy.java index 9e320dbf..971da3ba 100644 --- a/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/OrderIntegrationTestWithMultiTenancy.java +++ b/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/OrderIntegrationTestWithMultiTenancy.java @@ -19,8 +19,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; -import com.google.inject.Key; -import com.google.inject.name.Names; import in.cleartax.dropwizard.sharding.application.TestApplication; import in.cleartax.dropwizard.sharding.application.TestConfig; import in.cleartax.dropwizard.sharding.dao.OrderDao; @@ -28,13 +26,12 @@ import in.cleartax.dropwizard.sharding.dto.OrderItemDto; import in.cleartax.dropwizard.sharding.entities.Order; import in.cleartax.dropwizard.sharding.hibernate.ConstTenantIdentifierResolver; -import in.cleartax.dropwizard.sharding.hibernate.MultiTenantUnitOfWorkAwareProxyFactory; +import in.cleartax.dropwizard.sharding.hibernate.MultiTenantSessionSource; import in.cleartax.dropwizard.sharding.transactions.DefaultUnitOfWorkImpl; import in.cleartax.dropwizard.sharding.transactions.TransactionRunner; import io.dropwizard.testing.junit.DropwizardAppRule; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.tuple.ImmutablePair; -import org.hibernate.SessionFactory; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -75,12 +72,11 @@ public static void setUp() { private void assertOrderPresentOnShard(final String expectedOnShard, final OrderDto orderDto) throws Throwable { final GuiceBundle guiceBundle = ((TestApplication) RULE.getApplication()).getGuiceBundle(); final OrderDao orderDao = guiceBundle.getInjector().getInstance(OrderDao.class); - final MultiTenantUnitOfWorkAwareProxyFactory proxyFactory = guiceBundle.getInjector() - .getInstance(MultiTenantUnitOfWorkAwareProxyFactory.class); - final SessionFactory sessionFactory = guiceBundle.getInjector() - .getInstance(Key.get(SessionFactory.class, Names.named("session"))); + final MultiTenantSessionSource multiTenantSessionSource = guiceBundle.getInjector() + .getInstance(MultiTenantSessionSource.class); for (final String eachShard : shards) { - new TransactionRunner(proxyFactory, sessionFactory, new ConstTenantIdentifierResolver(eachShard)) { + new TransactionRunner(multiTenantSessionSource.getUnitOfWorkAwareProxyFactory(), + multiTenantSessionSource.getSessionFactory(), new ConstTenantIdentifierResolver(eachShard)) { @Override public Order run() { Order order = orderDao.get(orderDto.getId()); diff --git a/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/TestHelper.java b/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/TestHelper.java index 9481398f..28170221 100644 --- a/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/TestHelper.java +++ b/sharding-example/src/test/java/in/cleartax/dropwizard/sharding/test/sampleapp/TestHelper.java @@ -46,7 +46,7 @@ public static Client onSuiteRun(DropwizardAppRule rule) JerseyClientConfiguration jerseyClientConfiguration = new JerseyClientConfiguration(); // increasing minThreads from 1 (default) to 2 to ensure async requests run in parallel. jerseyClientConfiguration.setMinThreads(2); - jerseyClientConfiguration.setTimeout(Duration.seconds(60)); + jerseyClientConfiguration.setTimeout(Duration.seconds(300)); return new JerseyClientBuilder(rule.getEnvironment()) .using(jerseyClientConfiguration).build("test-client"); }