|
@@ -0,0 +1,555 @@
|
|
|
|
|
+package cn.yunzhixue.ability.center.examsprint.infrastructure.report.pdf;
|
|
|
|
|
+
|
|
|
|
|
+import ch.qos.logback.classic.Logger;
|
|
|
|
|
+import ch.qos.logback.classic.spi.ILoggingEvent;
|
|
|
|
|
+import ch.qos.logback.core.read.ListAppender;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.time.Duration;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
|
|
+import java.util.concurrent.Executors;
|
|
|
|
|
+import java.util.concurrent.Future;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
|
|
+import java.util.function.Consumer;
|
|
|
|
|
+import java.util.function.Function;
|
|
|
|
|
+
|
|
|
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
|
|
|
+
|
|
|
|
|
+class PooledPlaywrightExamSprintReportPdfGeneratorTest {
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void constructorCreatesPoolSizeWorkers() {
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(3, factory)) {
|
|
|
|
|
+ assertThat(factory.workers()).hasSize(3);
|
|
|
|
|
+ assertThat(factory.workers()).allSatisfy(worker -> assertThat(worker.closeCount()).isZero());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void generateReturnsSuccessfulWorkerToPoolForReuse() {
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, factory)) {
|
|
|
|
|
+ assertThat(asString(generator.generate("first"))).isEqualTo("worker-1:first");
|
|
|
|
|
+ assertThat(asString(generator.generate("second"))).isEqualTo("worker-1:second");
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(factory.workers()).hasSize(1);
|
|
|
|
|
+ assertThat(factory.worker(0).htmlContents()).containsExactly("first", "second");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void generateEmitsStructuredPoolLogsForSuccessPath() {
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+ Logger logger = (Logger) LoggerFactory.getLogger(PooledPlaywrightExamSprintReportPdfGenerator.class);
|
|
|
|
|
+ ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
|
|
|
|
+ appender.start();
|
|
|
|
|
+ logger.addAppender(appender);
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, Duration.ofMillis(100), factory)) {
|
|
|
|
|
+ assertThat(asString(generator.generate("html"))).isEqualTo("worker-1:html");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ logger.detachAppender(appender);
|
|
|
|
|
+ appender.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> messages = appender.list.stream()
|
|
|
|
|
+ .map(ILoggingEvent::getFormattedMessage)
|
|
|
|
|
+ .toList();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_pool_initialized");
|
|
|
|
|
+ assertThat(message).contains("poolSize=1");
|
|
|
|
|
+ assertThat(message).contains("borrowTimeoutMs=100");
|
|
|
|
|
+ });
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_borrowed");
|
|
|
|
|
+ assertThat(message).contains("borrowWaitMs=");
|
|
|
|
|
+ assertThat(message).contains("availableWorkers=");
|
|
|
|
|
+ assertThat(message).contains("activeWorkers=");
|
|
|
|
|
+ assertThat(message).contains("managedWorkers=");
|
|
|
|
|
+ });
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_generation_completed");
|
|
|
|
|
+ assertThat(message).contains("workerRenderMs=");
|
|
|
|
|
+ assertThat(message).contains("pdfByteLength=");
|
|
|
|
|
+ });
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_returned");
|
|
|
|
|
+ assertThat(message).contains("availableWorkers=");
|
|
|
|
|
+ });
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> assertThat(message).contains("exam_sprint_report_pdf_pool_closed"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void concurrentGenerateEntersAtMostPoolSizeWorkers() throws Exception {
|
|
|
|
|
+ CountDownLatch firstBatchEntered = new CountDownLatch(2);
|
|
|
|
|
+ CountDownLatch releaseWorkers = new CountDownLatch(1);
|
|
|
|
|
+ AtomicInteger activeWorkers = new AtomicInteger();
|
|
|
|
|
+ AtomicInteger maxActiveWorkers = new AtomicInteger();
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> worker.generateAction(html -> {
|
|
|
|
|
+ int active = activeWorkers.incrementAndGet();
|
|
|
|
|
+ maxActiveWorkers.accumulateAndGet(active, Math::max);
|
|
|
|
|
+ firstBatchEntered.countDown();
|
|
|
|
|
+ await(releaseWorkers);
|
|
|
|
|
+ activeWorkers.decrementAndGet();
|
|
|
|
|
+ return bytes("worker-" + worker.id() + ":" + html);
|
|
|
|
|
+ }));
|
|
|
|
|
+ ExecutorService executor = Executors.newFixedThreadPool(4);
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(2, Duration.ofSeconds(2), factory)) {
|
|
|
|
|
+ List<Future<byte[]>> futures = new ArrayList<>();
|
|
|
|
|
+ for (int i = 0; i < 4; i++) {
|
|
|
|
|
+ String html = "html-" + i;
|
|
|
|
|
+ futures.add(executor.submit(() -> generator.generate(html)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(firstBatchEntered.await(1, TimeUnit.SECONDS)).isTrue();
|
|
|
|
|
+ assertThat(maxActiveWorkers.get()).isLessThanOrEqualTo(2);
|
|
|
|
|
+
|
|
|
|
|
+ releaseWorkers.countDown();
|
|
|
|
|
+ for (Future<byte[]> future : futures) {
|
|
|
|
|
+ assertThat(asString(future.get(1, TimeUnit.SECONDS))).startsWith("worker-");
|
|
|
|
|
+ }
|
|
|
|
|
+ assertThat(maxActiveWorkers.get()).isLessThanOrEqualTo(2);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ executor.shutdownNow();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void generateTimesOutWhenNoWorkerIsAvailable() throws Exception {
|
|
|
|
|
+ CountDownLatch workerEntered = new CountDownLatch(1);
|
|
|
|
|
+ CountDownLatch releaseWorker = new CountDownLatch(1);
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> worker.generateAction(html -> {
|
|
|
|
|
+ workerEntered.countDown();
|
|
|
|
|
+ await(releaseWorker);
|
|
|
|
|
+ return bytes("done");
|
|
|
|
|
+ }));
|
|
|
|
|
+ ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, Duration.ofMillis(50), factory)) {
|
|
|
|
|
+ Future<byte[]> borrowed = executor.submit(() -> generator.generate("held"));
|
|
|
|
|
+ assertThat(workerEntered.await(1, TimeUnit.SECONDS)).isTrue();
|
|
|
|
|
+
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("waiting"))
|
|
|
|
|
+ .isInstanceOf(IllegalStateException.class)
|
|
|
|
|
+ .hasMessageContaining("Timed out waiting for PDF worker");
|
|
|
|
|
+
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ assertThat(asString(borrowed.get(1, TimeUnit.SECONDS))).isEqualTo("done");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ executor.shutdownNow();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void generateTimeoutEmitsStructuredPoolLog() throws Exception {
|
|
|
|
|
+ CountDownLatch workerEntered = new CountDownLatch(1);
|
|
|
|
|
+ CountDownLatch releaseWorker = new CountDownLatch(1);
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> worker.generateAction(html -> {
|
|
|
|
|
+ workerEntered.countDown();
|
|
|
|
|
+ await(releaseWorker);
|
|
|
|
|
+ return bytes("done");
|
|
|
|
|
+ }));
|
|
|
|
|
+ Logger logger = (Logger) LoggerFactory.getLogger(PooledPlaywrightExamSprintReportPdfGenerator.class);
|
|
|
|
|
+ ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
|
|
|
|
+ appender.start();
|
|
|
|
|
+ logger.addAppender(appender);
|
|
|
|
|
+ ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, Duration.ofMillis(50), factory)) {
|
|
|
|
|
+ Future<byte[]> borrowed = executor.submit(() -> generator.generate("held"));
|
|
|
|
|
+ assertThat(workerEntered.await(1, TimeUnit.SECONDS)).isTrue();
|
|
|
|
|
+
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("waiting"))
|
|
|
|
|
+ .isInstanceOf(IllegalStateException.class)
|
|
|
|
|
+ .hasMessageContaining("Timed out waiting for PDF worker");
|
|
|
|
|
+
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ assertThat(asString(borrowed.get(1, TimeUnit.SECONDS))).isEqualTo("done");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ executor.shutdownNow();
|
|
|
|
|
+ logger.detachAppender(appender);
|
|
|
|
|
+ appender.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> messages = appender.list.stream()
|
|
|
|
|
+ .map(ILoggingEvent::getFormattedMessage)
|
|
|
|
|
+ .toList();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_borrow_timeout");
|
|
|
|
|
+ assertThat(message).contains("poolSize=1");
|
|
|
|
|
+ assertThat(message).contains("borrowTimeoutMs=50");
|
|
|
|
|
+ assertThat(message).contains("borrowWaitMs=");
|
|
|
|
|
+ assertThat(message).contains("exceptionType=IllegalStateException");
|
|
|
|
|
+ assertThat(message).contains("availableWorkers=");
|
|
|
|
|
+ assertThat(message).contains("activeWorkers=");
|
|
|
|
|
+ assertThat(message).contains("managedWorkers=");
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void generateFailureEmitsStructuredFailureAndReplacementLogs() {
|
|
|
|
|
+ RuntimeException generateFailure = new RuntimeException("boom");
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> {
|
|
|
|
|
+ if (worker.id() == 1) {
|
|
|
|
|
+ worker.generateAction(html -> {
|
|
|
|
|
+ throw generateFailure;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ Logger logger = (Logger) LoggerFactory.getLogger(PooledPlaywrightExamSprintReportPdfGenerator.class);
|
|
|
|
|
+ ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
|
|
|
|
+ appender.start();
|
|
|
|
|
+ logger.addAppender(appender);
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, factory)) {
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("failing"))
|
|
|
|
|
+ .isSameAs(generateFailure);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ logger.detachAppender(appender);
|
|
|
|
|
+ appender.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> messages = appender.list.stream()
|
|
|
|
|
+ .map(ILoggingEvent::getFormattedMessage)
|
|
|
|
|
+ .toList();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_generation_failed");
|
|
|
|
|
+ assertThat(message).contains("workerRenderMs=");
|
|
|
|
|
+ assertThat(message).contains("exceptionType=RuntimeException");
|
|
|
|
|
+ assertThat(message).contains("availableWorkers=");
|
|
|
|
|
+ assertThat(message).contains("activeWorkers=");
|
|
|
|
|
+ assertThat(message).contains("managedWorkers=");
|
|
|
|
|
+ });
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_replaced");
|
|
|
|
|
+ assertThat(message).contains("poolSize=1");
|
|
|
|
|
+ assertThat(message).contains("availableWorkers=");
|
|
|
|
|
+ assertThat(message).contains("activeWorkers=");
|
|
|
|
|
+ assertThat(message).contains("managedWorkers=");
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void replacementFailureEmitsStructuredReplacementFailureLog() {
|
|
|
|
|
+ RuntimeException generateFailure = new RuntimeException("boom");
|
|
|
|
|
+ RuntimeException replacementFailure = new RuntimeException("replacement failed");
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> worker.generateAction(html -> {
|
|
|
|
|
+ throw generateFailure;
|
|
|
|
|
+ }));
|
|
|
|
|
+ factory.failOnCreateNumber(2, replacementFailure);
|
|
|
|
|
+ Logger logger = (Logger) LoggerFactory.getLogger(PooledPlaywrightExamSprintReportPdfGenerator.class);
|
|
|
|
|
+ ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
|
|
|
|
+ appender.start();
|
|
|
|
|
+ logger.addAppender(appender);
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, factory)) {
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("failing"))
|
|
|
|
|
+ .isSameAs(generateFailure);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ logger.detachAppender(appender);
|
|
|
|
|
+ appender.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> messages = appender.list.stream()
|
|
|
|
|
+ .map(ILoggingEvent::getFormattedMessage)
|
|
|
|
|
+ .toList();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(messages).anySatisfy(message -> {
|
|
|
|
|
+ assertThat(message).contains("exam_sprint_report_pdf_worker_replacement_failed");
|
|
|
|
|
+ assertThat(message).contains("exceptionType=RuntimeException");
|
|
|
|
|
+ assertThat(message).contains("availableWorkers=");
|
|
|
|
|
+ assertThat(message).contains("activeWorkers=");
|
|
|
|
|
+ assertThat(message).contains("managedWorkers=");
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void generateFailureClosesFailedWorkerCreatesReplacementAndDoesNotRetryCurrentRequest() {
|
|
|
|
|
+ RuntimeException generateFailure = new RuntimeException("boom");
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> {
|
|
|
|
|
+ if (worker.id() == 1) {
|
|
|
|
|
+ worker.generateAction(html -> {
|
|
|
|
|
+ throw generateFailure;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ try (PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, factory)) {
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("failing"))
|
|
|
|
|
+ .isSameAs(generateFailure);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(factory.workers()).hasSize(2);
|
|
|
|
|
+ assertThat(factory.worker(0).closeCount()).isEqualTo(1);
|
|
|
|
|
+ assertThat(factory.worker(1).htmlContents()).isEmpty();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(asString(generator.generate("after-failure"))).isEqualTo("worker-2:after-failure");
|
|
|
|
|
+ assertThat(factory.worker(1).htmlContents()).containsExactly("after-failure");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void closeClosesIdleWorkersOnceAndRejectsGenerate() {
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+ PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(2, factory);
|
|
|
|
|
+
|
|
|
|
|
+ generator.close();
|
|
|
|
|
+ generator.close();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(factory.workers()).allSatisfy(worker -> assertThat(worker.closeCount()).isEqualTo(1));
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("html"))
|
|
|
|
|
+ .isInstanceOf(IllegalStateException.class)
|
|
|
|
|
+ .hasMessageContaining("PDF generator pool is closed");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void closeClosesActiveWorkerWhileGenerateIsRunning() throws Exception {
|
|
|
|
|
+ CountDownLatch workerEntered = new CountDownLatch(1);
|
|
|
|
|
+ CountDownLatch releaseWorker = new CountDownLatch(1);
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> worker.generateAction(html -> {
|
|
|
|
|
+ workerEntered.countDown();
|
|
|
|
|
+ await(releaseWorker);
|
|
|
|
|
+ return bytes("done");
|
|
|
|
|
+ }));
|
|
|
|
|
+ ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, Duration.ofSeconds(5), factory);
|
|
|
|
|
+ Future<byte[]> activeGenerate = executor.submit(() -> generator.generate("active"));
|
|
|
|
|
+ assertThat(workerEntered.await(1, TimeUnit.SECONDS)).isTrue();
|
|
|
|
|
+
|
|
|
|
|
+ generator.close();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(factory.worker(0).closeCount()).isEqualTo(1);
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ assertThat(asString(activeGenerate.get(1, TimeUnit.SECONDS))).isEqualTo("done");
|
|
|
|
|
+ assertThat(factory.worker(0).closeCount()).isEqualTo(1);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ executor.shutdownNow();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void waitingBorrowFailsPromptlyWithPoolClosedWhenCloseIsCalled() throws Exception {
|
|
|
|
|
+ CountDownLatch workerEntered = new CountDownLatch(1);
|
|
|
|
|
+ CountDownLatch releaseWorker = new CountDownLatch(1);
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory(worker -> worker.generateAction(html -> {
|
|
|
|
|
+ workerEntered.countDown();
|
|
|
|
|
+ await(releaseWorker);
|
|
|
|
|
+ return bytes("done");
|
|
|
|
|
+ }));
|
|
|
|
|
+ ExecutorService executor = Executors.newFixedThreadPool(2);
|
|
|
|
|
+ AtomicReference<Throwable> waitingFailure = new AtomicReference<>();
|
|
|
|
|
+ Thread waitingThread = null;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(1, Duration.ofSeconds(5), factory);
|
|
|
|
|
+ Future<byte[]> activeGenerate = executor.submit(() -> generator.generate("active"));
|
|
|
|
|
+ assertThat(workerEntered.await(1, TimeUnit.SECONDS)).isTrue();
|
|
|
|
|
+ waitingThread = new Thread(() -> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ generator.generate("waiting");
|
|
|
|
|
+ } catch (Throwable failure) {
|
|
|
|
|
+ waitingFailure.set(failure);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, "pooled-pdf-waiting-borrow-test");
|
|
|
|
|
+ waitingThread.start();
|
|
|
|
|
+ awaitThreadState(waitingThread, Thread.State.TIMED_WAITING);
|
|
|
|
|
+
|
|
|
|
|
+ generator.close();
|
|
|
|
|
+
|
|
|
|
|
+ waitingThread.join(1_000);
|
|
|
|
|
+ assertThat(waitingThread.isAlive()).isFalse();
|
|
|
|
|
+ assertThat(waitingFailure.get())
|
|
|
|
|
+ .isInstanceOf(IllegalStateException.class)
|
|
|
|
|
+ .hasMessageContaining("PDF generator pool is closed");
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ assertThat(asString(activeGenerate.get(1, TimeUnit.SECONDS))).isEqualTo("done");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (waitingThread != null) {
|
|
|
|
|
+ waitingThread.interrupt();
|
|
|
|
|
+ }
|
|
|
|
|
+ releaseWorker.countDown();
|
|
|
|
|
+ executor.shutdownNow();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void destroyClosesIdleWorkersOnceAndRejectsGenerate() {
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+ PooledPlaywrightExamSprintReportPdfGenerator generator = newGenerator(2, factory);
|
|
|
|
|
+
|
|
|
|
|
+ generator.destroy();
|
|
|
|
|
+ generator.destroy();
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(factory.workers()).allSatisfy(worker -> assertThat(worker.closeCount()).isEqualTo(1));
|
|
|
|
|
+ assertThatThrownBy(() -> generator.generate("html"))
|
|
|
|
|
+ .isInstanceOf(IllegalStateException.class)
|
|
|
|
|
+ .hasMessageContaining("PDF generator pool is closed");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void constructorRejectsInvalidPoolSettings() {
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+
|
|
|
|
|
+ assertThatThrownBy(() -> new PooledPlaywrightExamSprintReportPdfGenerator(0, Duration.ofMillis(1), factory))
|
|
|
|
|
+ .isInstanceOf(IllegalArgumentException.class)
|
|
|
|
|
+ .hasMessageContaining("poolSize");
|
|
|
|
|
+ assertThatThrownBy(() -> new PooledPlaywrightExamSprintReportPdfGenerator(1, Duration.ZERO, factory))
|
|
|
|
|
+ .isInstanceOf(IllegalArgumentException.class)
|
|
|
|
|
+ .hasMessageContaining("borrowTimeout");
|
|
|
|
|
+ assertThatThrownBy(() -> new PooledPlaywrightExamSprintReportPdfGenerator(1, Duration.ofMillis(-1), factory))
|
|
|
|
|
+ .isInstanceOf(IllegalArgumentException.class)
|
|
|
|
|
+ .hasMessageContaining("borrowTimeout");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void constructorClosesAlreadyCreatedWorkersWhenFactoryFails() {
|
|
|
|
|
+ RuntimeException createFailure = new RuntimeException("create failed");
|
|
|
|
|
+ FakeWorkerFactory factory = new FakeWorkerFactory();
|
|
|
|
|
+ factory.failOnCreateNumber(3, createFailure);
|
|
|
|
|
+
|
|
|
|
|
+ assertThatThrownBy(() -> newGenerator(3, factory))
|
|
|
|
|
+ .isSameAs(createFailure);
|
|
|
|
|
+ assertThat(factory.workers()).hasSize(2);
|
|
|
|
|
+ assertThat(factory.workers()).allSatisfy(worker -> assertThat(worker.closeCount()).isEqualTo(1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static PooledPlaywrightExamSprintReportPdfGenerator newGenerator(int poolSize,
|
|
|
|
|
+ FakeWorkerFactory factory) {
|
|
|
|
|
+ return newGenerator(poolSize, Duration.ofMillis(100), factory);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static PooledPlaywrightExamSprintReportPdfGenerator newGenerator(int poolSize,
|
|
|
|
|
+ Duration borrowTimeout,
|
|
|
|
|
+ FakeWorkerFactory factory) {
|
|
|
|
|
+ return new PooledPlaywrightExamSprintReportPdfGenerator(poolSize, borrowTimeout, factory);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String asString(byte[] bytes) {
|
|
|
|
|
+ return new String(bytes, StandardCharsets.UTF_8);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static byte[] bytes(String value) {
|
|
|
|
|
+ return value.getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void await(CountDownLatch latch) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue();
|
|
|
|
|
+ } catch (InterruptedException exception) {
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ throw new IllegalStateException(exception);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void awaitThreadState(Thread thread, Thread.State expectedState) throws InterruptedException {
|
|
|
|
|
+ long deadlineNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(1);
|
|
|
|
|
+ while (System.nanoTime() < deadlineNanos && thread.getState() != expectedState) {
|
|
|
|
|
+ Thread.sleep(10);
|
|
|
|
|
+ }
|
|
|
|
|
+ assertThat(thread.getState()).isEqualTo(expectedState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static final class FakeWorkerFactory implements PlaywrightPdfWorkerFactory {
|
|
|
|
|
+
|
|
|
|
|
+ private final List<FakeWorker> workers = Collections.synchronizedList(new ArrayList<>());
|
|
|
|
|
+ private final Consumer<FakeWorker> customizer;
|
|
|
|
|
+ private int failOnCreateNumber;
|
|
|
|
|
+ private RuntimeException createFailure;
|
|
|
|
|
+
|
|
|
|
|
+ private FakeWorkerFactory() {
|
|
|
|
|
+ this(worker -> {
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FakeWorkerFactory(Consumer<FakeWorker> customizer) {
|
|
|
|
|
+ this.customizer = customizer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public synchronized PlaywrightPdfWorker create() {
|
|
|
|
|
+ int nextId = workers.size() + 1;
|
|
|
|
|
+ if (nextId == failOnCreateNumber) {
|
|
|
|
|
+ throw createFailure;
|
|
|
|
|
+ }
|
|
|
|
|
+ FakeWorker worker = new FakeWorker(nextId);
|
|
|
|
|
+ customizer.accept(worker);
|
|
|
|
|
+ workers.add(worker);
|
|
|
|
|
+ return worker;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<FakeWorker> workers() {
|
|
|
|
|
+ return workers;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FakeWorker worker(int index) {
|
|
|
|
|
+ return workers.get(index);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void failOnCreateNumber(int failOnCreateNumber, RuntimeException createFailure) {
|
|
|
|
|
+ this.failOnCreateNumber = failOnCreateNumber;
|
|
|
|
|
+ this.createFailure = createFailure;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static final class FakeWorker implements PlaywrightPdfWorker {
|
|
|
|
|
+
|
|
|
|
|
+ private final int id;
|
|
|
|
|
+ private final List<String> htmlContents = Collections.synchronizedList(new ArrayList<>());
|
|
|
|
|
+ private final AtomicInteger closeCount = new AtomicInteger();
|
|
|
|
|
+ private Function<String, byte[]> generateAction;
|
|
|
|
|
+
|
|
|
|
|
+ private FakeWorker(int id) {
|
|
|
|
|
+ this.id = id;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public byte[] generate(String htmlContent) {
|
|
|
|
|
+ htmlContents.add(htmlContent);
|
|
|
|
|
+ if (generateAction != null) {
|
|
|
|
|
+ return generateAction.apply(htmlContent);
|
|
|
|
|
+ }
|
|
|
|
|
+ return bytes("worker-" + id + ":" + htmlContent);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void close() {
|
|
|
|
|
+ closeCount.incrementAndGet();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int id() {
|
|
|
|
|
+ return id;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> htmlContents() {
|
|
|
|
|
+ return htmlContents;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int closeCount() {
|
|
|
|
|
+ return closeCount.get();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void generateAction(Function<String, byte[]> generateAction) {
|
|
|
|
|
+ this.generateAction = generateAction;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|