From 5c4429e7a3e347437e2e8901d4e85a1c56dde66b Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Tue, 21 Nov 2017 16:00:19 +0100 Subject: [PATCH] SONAR-10104 async webhooks with full in-memory implementation --- .../container/ComputeEngineContainerImpl.java | 8 +- .../ComputeEngineContainerImplTest.java | 8 +- .../sonar/server/async/AsyncExecution.java | 31 +++++++ .../async/AsyncExecutionExecutorService.java | 24 ++++++ .../AsyncExecutionExecutorServiceImpl.java | 59 +++++++++++++ .../server/async/AsyncExecutionImpl.java | 46 ++++++++++ .../server/async/AsyncExecutionModule.java | 31 +++++++ .../org/sonar/server/async/package-info.java | 23 +++++ ...ProjectAnalysisTaskContainerPopulator.java | 2 - .../platformlevel/PlatformLevel3.java | 4 +- .../AbstractStoppableExecutorService.java | 6 +- .../sonar/server/webhook/WebHooksImpl.java | 11 ++- ...AsyncExecutionExecutorServiceImplTest.java | 58 +++++++++++++ .../server/async/AsyncExecutionImplTest.java | 68 +++++++++++++++ .../webhook/AsynchronousWebHooksImplTest.java | 85 +++++++++++++++++++ ....java => SynchronousWebHooksImplTest.java} | 6 +- 16 files changed, 454 insertions(+), 16 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecution.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorService.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorServiceImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionModule.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/async/package-info.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionExecutorServiceImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/webhook/AsynchronousWebHooksImplTest.java rename server/sonar-server/src/test/java/org/sonar/server/webhook/{WebHooksImplTest.java => SynchronousWebHooksImplTest.java} (97%) diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 9bcaaad2909..febc63da76f 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -78,6 +78,7 @@ import org.sonar.process.NetworkUtilsImpl; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; import org.sonar.process.logging.LogbackHelper; +import org.sonar.server.async.AsyncExecutionModule; import org.sonar.server.component.ComponentFinder; import org.sonar.server.component.index.ComponentIndexer; import org.sonar.server.computation.task.projectanalysis.ProjectAnalysisTaskModule; @@ -153,6 +154,7 @@ import org.sonar.server.user.index.UserIndexer; import org.sonar.server.util.OkHttpClientProvider; import org.sonar.server.view.index.ViewIndex; import org.sonar.server.view.index.ViewIndexer; +import org.sonar.server.webhook.WebhookModule; import org.sonarqube.ws.Rules; import static java.util.Objects.requireNonNull; @@ -309,7 +311,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { ServerIdManager.class, UriReader.class, ServerImpl.class, - DefaultOrganizationProviderImpl.class); + DefaultOrganizationProviderImpl.class, + AsyncExecutionModule.class); } private static void populateLevel4(ComponentContainer container, Props props) { @@ -422,6 +425,9 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { InternalPropertiesImpl.class, ProjectConfigurationFactory.class, + // webhooks + WebhookModule.class, + // cleaning CeCleaningModule.class); diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 8c94f57d8e8..0793ae11024 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -91,7 +91,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getComponentAdapters()) .hasSize( CONTAINER_ITSELF - + 76 // level 4 + + 77 // level 4 + 6 // content of CeConfigurationModule + 4 // content of CeQueueModule + 4 // content of CeHttpModule @@ -100,16 +100,18 @@ public class ComputeEngineContainerImplTest { + 7 // content of CeTaskProcessorModule + 4 // content of ReportAnalysisFailureNotificationModule + 3 // CeCleaningModule + its content + + 4 // WebhookModule + 1 // CeDistributedInformation ); assertThat(picoContainer.getParent().getComponentAdapters()).hasSize( CONTAINER_ITSELF - + 5 // level 3 + + 6 // level 3 + + 2 // AsyncExecutionModule ); assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize( CONTAINER_ITSELF - + 13 // MigrationConfigurationModule + 17 // level 2 + + 13 // MigrationConfigurationModule ); assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION diff --git a/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecution.java b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecution.java new file mode 100644 index 00000000000..f98d5824d72 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecution.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +public interface AsyncExecution { + /** + * Add the specified {@link Runnable} in queue for asynchronous processing. + * + * This method returns instantly and {@code r} is executed in another thread. + * + * @throws NullPointerException if r is {@code null} + */ + void addToQueue(Runnable r); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorService.java b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorService.java new file mode 100644 index 00000000000..bade9978b2c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorService.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +public interface AsyncExecutionExecutorService { + void addToQueue(Runnable r); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorServiceImpl.java b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorServiceImpl.java new file mode 100644 index 00000000000..60c87fc9fa6 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionExecutorServiceImpl.java @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.server.util.AbstractStoppableExecutorService; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class AsyncExecutionExecutorServiceImpl + extends AbstractStoppableExecutorService + implements AsyncExecutionExecutorService { + private static final Logger LOG = Loggers.get(AsyncExecutionExecutorServiceImpl.class); + + private static final int MIN_THREAD_COUNT = 1; + private static final int MAX_THREAD_COUNT = 10; + private static final int MAX_QUEUE_SIZE = Integer.MAX_VALUE; + private static final long KEEP_ALIVE_TIME_IN_MILLISECONDS = 0L; + + public AsyncExecutionExecutorServiceImpl() { + super( + new ThreadPoolExecutor( + MIN_THREAD_COUNT, MAX_THREAD_COUNT, + KEEP_ALIVE_TIME_IN_MILLISECONDS, MILLISECONDS, + new LinkedBlockingQueue<>(MAX_QUEUE_SIZE), + new ThreadFactoryBuilder() + .setDaemon(false) + .setNameFormat("SQ_async-%d") + .setUncaughtExceptionHandler(((t, e) -> LOG.error("Thread " + t + " failed unexpectedly", e))) + .build())); + } + + @Override + public void addToQueue(Runnable r) { + this.submit(r); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionImpl.java b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionImpl.java new file mode 100644 index 00000000000..d730bc309bc --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionImpl.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.util.Objects.requireNonNull; + +public class AsyncExecutionImpl implements AsyncExecution { + private static final Logger LOG = Loggers.get(AsyncExecutionImpl.class); + private final AsyncExecutionExecutorService executorService; + + public AsyncExecutionImpl(AsyncExecutionExecutorService executorService) { + this.executorService = executorService; + } + + @Override + public void addToQueue(Runnable r) { + requireNonNull(r); + executorService.addToQueue(() -> { + try { + r.run(); + } catch (Exception e) { + LOG.error("Asynchronous task failed", e); + } + }); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionModule.java b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionModule.java new file mode 100644 index 00000000000..1e5673c45ee --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/async/AsyncExecutionModule.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +import org.sonar.core.platform.Module; + +public class AsyncExecutionModule extends Module { + @Override + protected void configureModule() { + add( + AsyncExecutionExecutorServiceImpl.class, + AsyncExecutionImpl.class); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/async/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/async/package-info.java new file mode 100644 index 00000000000..0934fefe908 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/async/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.async; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 6c49120776f..88fc743e11e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -122,7 +122,6 @@ import org.sonar.server.computation.task.step.ComputationStepExecutor; import org.sonar.server.computation.task.step.ComputationSteps; import org.sonar.server.computation.taskprocessor.MutableTaskResultHolderImpl; import org.sonar.server.view.index.ViewIndex; -import org.sonar.server.webhook.WebhookModule; public final class ProjectAnalysisTaskContainerPopulator implements ContainerPopulator { private static final ReportAnalysisComponentProvider[] NO_REPORT_ANALYSIS_COMPONENT_PROVIDERS = new ReportAnalysisComponentProvider[0]; @@ -272,7 +271,6 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop SmallChangesetQualityGateSpecialCase.class, // webhooks - WebhookModule.class, WebhookPostTask.class); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java index d7c3a3753b6..9c4ab3de720 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java @@ -21,6 +21,7 @@ package org.sonar.server.platform.platformlevel; import org.sonar.api.utils.UriReader; import org.sonar.core.util.DefaultHttpDownloader; +import org.sonar.server.async.AsyncExecutionModule; import org.sonar.server.organization.DefaultOrganizationProviderImpl; import org.sonar.server.organization.OrganizationFlagsImpl; import org.sonar.server.platform.ServerIdManager; @@ -47,6 +48,7 @@ public class PlatformLevel3 extends PlatformLevel { UriReader.class, DefaultHttpDownloader.class, DefaultOrganizationProviderImpl.class, - OrganizationFlagsImpl.class); + OrganizationFlagsImpl.class, + AsyncExecutionModule.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/util/AbstractStoppableExecutorService.java b/server/sonar-server/src/main/java/org/sonar/server/util/AbstractStoppableExecutorService.java index 5a926a55ca5..a3e331846fc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/util/AbstractStoppableExecutorService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/util/AbstractStoppableExecutorService.java @@ -36,10 +36,10 @@ import static java.lang.String.format; * Abstract implementation of StoppableExecutorService that implements the * stop() method and delegates all methods to the provided ExecutorService instance. */ -public abstract class AbstractStoppableExecutorService implements StoppableExecutorService { - protected final T delegate; +public abstract class AbstractStoppableExecutorService implements StoppableExecutorService { + protected final D delegate; - public AbstractStoppableExecutorService(T delegate) { + public AbstractStoppableExecutorService(D delegate) { this.delegate = delegate; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooksImpl.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooksImpl.java index b839d8c43fe..8554484fe5b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooksImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooksImpl.java @@ -30,6 +30,7 @@ import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.core.config.WebhookProperties; import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.server.async.AsyncExecution; import static java.lang.String.format; import static org.sonar.core.config.WebhookProperties.MAX_WEBHOOKS_PER_TYPE; @@ -41,10 +42,12 @@ public class WebHooksImpl implements WebHooks { private final WebhookCaller caller; private final WebhookDeliveryStorage deliveryStorage; + private final AsyncExecution asyncExecution; - public WebHooksImpl(WebhookCaller caller, WebhookDeliveryStorage deliveryStorage) { + public WebHooksImpl(WebhookCaller caller, WebhookDeliveryStorage deliveryStorage, AsyncExecution asyncExecution) { this.caller = caller; this.deliveryStorage = deliveryStorage; + this.asyncExecution = asyncExecution; } @Override @@ -88,12 +91,12 @@ public class WebHooksImpl implements WebHooks { } WebhookPayload payload = payloadSupplier.get(); - webhooks.forEach(webhook -> { + webhooks.forEach(webhook -> asyncExecution.addToQueue(() -> { WebhookDelivery delivery = caller.call(webhook, payload); log(delivery); deliveryStorage.persist(delivery); - }); - deliveryStorage.purge(analysis.getProjectUuid()); + })); + asyncExecution.addToQueue(() -> deliveryStorage.purge(analysis.getProjectUuid())); } private static void log(WebhookDelivery delivery) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionExecutorServiceImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionExecutorServiceImplTest.java new file mode 100644 index 00000000000..f8e0976bbe0 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionExecutorServiceImplTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AsyncExecutionExecutorServiceImplTest { + private AsyncExecutionExecutorServiceImpl underTest = new AsyncExecutionExecutorServiceImpl(); + + @Test + public void submit_executes_runnable_in_another_thread() { + try (SlowRunnable slowRunnable = new SlowRunnable()) { + underTest.submit(slowRunnable); + assertThat(slowRunnable.executed).isFalse(); + } + } + + private static final class SlowRunnable implements Runnable, AutoCloseable { + private final CountDownLatch latch = new CountDownLatch(1); + private volatile boolean executed = false; + + @Override + public void run() { + try { + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // ignore + } + executed = true; + } + + @Override + public void close() { + latch.countDown(); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionImplTest.java new file mode 100644 index 00000000000..f98001c663e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/async/AsyncExecutionImplTest.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.async; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AsyncExecutionImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public LogTester logTester = new LogTester(); + + private AsyncExecutionExecutorService synchronousExecutorService = Runnable::run; + private AsyncExecutionImpl underTest = new AsyncExecutionImpl(synchronousExecutorService); + + @Test + public void addToQueue_fails_with_NPE_if_Runnable_is_null() { + expectedException.expect(NullPointerException.class); + + underTest.addToQueue(null); + } + + @Test + public void addToQueue_submits_runnable_to_executorService_which_does_not_fail_if_Runnable_argument_throws_exception() { + underTest.addToQueue(() -> { + throw new RuntimeException("Faking an exception thrown by Runnable argument"); + }); + + assertThat(logTester.logs()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.ERROR)).containsOnly("Asynchronous task failed"); + } + + @Test + public void addToQueue_submits_runnable_that_fails_if_Runnable_argument_throws_Error() { + Error expected = new Error("Faking an exception thrown by Runnable argument"); + Runnable runnable = () -> { + throw expected; + }; + + expectedException.expect(Error.class); + expectedException.expectMessage(expected.getMessage()); + + underTest.addToQueue(runnable); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/AsynchronousWebHooksImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/AsynchronousWebHooksImplTest.java new file mode 100644 index 00000000000..5e0bfa22f40 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/AsynchronousWebHooksImplTest.java @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.webhook; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.server.async.AsyncExecution; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class AsynchronousWebHooksImplTest { + private static final long NOW = 1_500_000_000_000L; + private static final String PROJECT_UUID = "P1_UUID"; + + private final MapSettings settings = new MapSettings(); + private final TestWebhookCaller caller = new TestWebhookCaller(); + private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class); + private final WebhookPayload mock = mock(WebhookPayload.class); + private final RecordingAsyncExecution asyncExecution = new RecordingAsyncExecution(); + + private final WebHooksImpl underTest = new WebHooksImpl(caller, deliveryStorage, asyncExecution); + + @Test + public void send_global_webhooks() { + settings.setProperty("sonar.webhooks.global", "1,2"); + settings.setProperty("sonar.webhooks.global.1.name", "First"); + settings.setProperty("sonar.webhooks.global.1.url", "http://url1"); + settings.setProperty("sonar.webhooks.global.2.name", "Second"); + settings.setProperty("sonar.webhooks.global.2.url", "http://url2"); + caller.enqueueSuccess(NOW, 200, 1_234); + caller.enqueueFailure(NOW, new IOException("Fail to connect")); + + underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock); + + assertThat(caller.countSent()).isZero(); + verifyZeroInteractions(deliveryStorage); + + asyncExecution.executeRecorded(); + + assertThat(caller.countSent()).isEqualTo(2); + verify(deliveryStorage, times(2)).persist(any(WebhookDelivery.class)); + verify(deliveryStorage).purge(PROJECT_UUID); + } + + private static class RecordingAsyncExecution implements AsyncExecution { + private final List runnableList = new ArrayList<>(); + + @Override + public void addToQueue(Runnable r) { + runnableList.add(requireNonNull(r)); + } + + public void executeRecorded() { + ArrayList runnables = new ArrayList<>(runnableList); + runnableList.clear(); + runnables.forEach(Runnable::run); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebHooksImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/SynchronousWebHooksImplTest.java similarity index 97% rename from server/sonar-server/src/test/java/org/sonar/server/webhook/WebHooksImplTest.java rename to server/sonar-server/src/test/java/org/sonar/server/webhook/SynchronousWebHooksImplTest.java index abc72c3a316..17fc6de7046 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebHooksImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/SynchronousWebHooksImplTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.sonar.api.config.internal.MapSettings; import org.sonar.api.utils.log.LogTester; import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.server.async.AsyncExecution; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -35,7 +36,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; -public class WebHooksImplTest { +public class SynchronousWebHooksImplTest { private static final long NOW = 1_500_000_000_000L; private static final String PROJECT_UUID = "P1_UUID"; @@ -47,7 +48,8 @@ public class WebHooksImplTest { private final TestWebhookCaller caller = new TestWebhookCaller(); private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class); private final WebhookPayload mock = mock(WebhookPayload.class); - private final WebHooksImpl underTest = new WebHooksImpl(caller, deliveryStorage); + private final AsyncExecution synchronousAsyncExecution = Runnable::run; + private final WebHooksImpl underTest = new WebHooksImpl(caller, deliveryStorage, synchronousAsyncExecution); @Test public void isEnabled_returns_false_if_no_webHoolds() { -- 2.39.5