Java WebClient теряет прерывистые ответы при увеличении нагрузки - PullRequest
0 голосов
/ 12 февраля 2020

Я разрабатываю сервер для оплаты кредитной картой для системы бронирования отелей, которую можно использовать вручную через веб-интерфейс или автоматически при бронировании с невозвращаемой оплатой. Каждая транзакция проходит через прокси-сервис (стороннее хранилище cc, бронирование PCI) и оттуда фактическому провайдеру платежей. Затем я сохраняю ответы (Успешно, карта отклонена и т. Д. c), имя поставщика и время, необходимое для зарядки. Когда я заправляю вручную, все работает нормально, но при одновременном срабатывании нескольких автоматических запросов c я не получаю все ответы. Иногда я ничего не получаю, а иногда запускается обработчик elapsed (), а затем ничего.

Вот код, о котором идет речь:

public Mono<PaymentResponse> doRequest( PaymentTransaction pt ) {
    Long bookId = pt.getCardToken().getBookId();
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    logToBoth( "Book ID: " + bookId + ", Total number of threads: " + bean.getThreadCount() );
    logToBoth( "Book ID: " + bookId + ", Total number of daemon threads: " 
        + bean.getDaemonThreadCount() );
    logToBoth( "Book ID: " + bookId + ", Peak thread count: " + bean.getPeakThreadCount() );
    if( pt == null ) {
        logService.logError( this, "Book ID: " + pt.getCardToken().getBookId() 
            + ", doRequest: pt is null!" );
    }
    else {
        logService.logInfo( this, "Book ID: " + pt.getCardToken().getBookId() 
            + ", PaymentTransaction health: " + pt.getHealth() );
    }
    return client.post()
        .uri( builder -> builder.path( "/api/paymentGateway" ).build() )
        .contentType( MediaType.APPLICATION_JSON )
        .body( Mono.just( pt ), PaymentTransaction.class )
        .header( "authorization", "APIKEY " + getApiKey() )
        .exchange()
        .defaultIfEmpty( ClientResponse.create( HttpStatus.I_AM_A_TEAPOT ).build() )
        .timeout( Duration.ofMillis( 90000 ) )
        .onErrorResume( e -> {
            String errorMsg = "Error in payment processing (" 
               + pt.getPaymentGateway().getName() + ", BookID: "
               + pt.getCardToken().getBookId() + ", "
               + pt.getCardToken().getCardNumber() + ", "
               + pt.getAmount() + "): " + parseError( e );
            logToBoth( errorMsg );
            return Mono.error( new Exception( errorMsg )  );
        } )
        .elapsed()
        .flatMap( tuple -> logTime( pt, tuple ) )
        .flatMap( response -> {
            logToBoth( "Book ID " + pt.getCardToken().getBookId() 
              + ". doRequest status: " + response.statusCode().toString() );
            if (response.statusCode().is4xxClientError()
                || response.statusCode().is5xxServerError() ) {
                logService.logError( this, "Book ID: " + pt.getCardToken().getBookId()
                    + ", Error response from provider: " 
                    + response.statusCode().toString() );
                return response.bodyToMono( String.class )
                    .map( s -> {
                        String errorMsg = "Book ID " + pt.getCardToken().getBookId() 
                            + ": " + "Error from PCI Booking: " + s;
                            logToBoth( errorMsg );
                            PaymentResponse pr = new PaymentResponse();
                            String error = encodingService.urlEncode( s );
                            pr.setPciBookingError( error );
                            pr.setOperationResultCode( "Failure" );
                            pr.setOperationResultDescription( "Error" );
                            pr.setGatewayResultCode( response.statusCode().toString() );
                            pr.setGatewayResultDescription( error );
                            pr.setGatewayName( pt.getPaymentGateway().getName() );
                            pr.setAmount( pt.getAmount() );
                            pr.setCurrency( pt.getCurrency() );
                            pr.setGatewayReference( pt.getGatewayReference() );
                            pr.setCreated( new Date() );
                            pr.setOperationType( pt.getOperationType() );
                            return pr;
                        });
            }
            else {
                logService.logInfo( this, "Book ID: " 
                  + pt.getCardToken().getBookId() + 
                  ", returning payment response" );
                return response.bodyToMono( PaymentResponse.class );
            }
        })
        .doOnSuccess( pr -> logToBoth( "Book ID: " 
            + pt.getCardToken().getBookId() + ", doRequest successful" ) )
        .doOnError( e -> logToBoth( "Book ID: " 
            + pt.getCardToken().getBookId() 
            + ", doRequest error: " + e.getMessage() ) );
}

private Mono<ClientResponse> logTime( PaymentTransaction pt, Tuple2<Long, ClientResponse> t ) {
    ClientResponse cr = t.getT2();
    logToBoth( "=> " + pt.getOperationType() + ", Booking: " 
        + pt.getCardToken().getBookId() + ", "
        + "Provider: " + pt.getPaymentGateway().getName() + ", "
        + "Amount: " + pt.getAmount() + " " + pt.getCurrency() + ", "
        + "Card: " + pt.getCardToken().getCardNumber() + ", "
        + "Duration: " + t.getT1() + "ms" + ", Status: " + cr.statusCode().toString() );

    return Mono.just( cr );
}

Это выполняется в компоненте службы Spring, и вот как WebClient создан:

    WebClient client = WebClient.builder()
            .baseUrl( "https://service.pcibooking.net" )
            .build();

У меня возникла проблема, когда одновременно поступало всего 9 запросов. Все они доходят до поставщика платежей, но я, кажется, получаю ответ на каждый второй запрос. В этих случаях не вызывается ни один из flatMaps, но иногда вызывается обработчик elapsed().

Я веду журнал как для консоли, так и для базы данных. Когда возникает эта проблема, последний журнал, который я вижу, это либо журнал с «PaymentTransaction health», либо журнал из logTime().

Я добавил defaultIfEmpty() только для того, чтобы убедиться, что я не получаю пустое тело. Согласно PCI Booking, ответы от платежных систем нормальные, но кажется, что я не получаю их все.

Я использую весеннюю загрузку, и это мой pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>is.godo</groupId>
    <artifactId>pci-server</artifactId>
    <version>1.1.0.RELEASE</version>
    <packaging>war</packaging>

    <name>pci-server</name>
    <description>PCI server</description>

    <properties>
        <java.version>1.8</java.version>
        <maven.build.timestamp.format>yyyy.MM.dd</maven.build.timestamp.format>
        <docker.tag>${project.version}-${maven.build.timestamp}</docker.tag>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <docker.image.prefix> docker.godo.is </docker.image.prefix>
        <docker.tag.prefix></docker.tag.prefix>
    </properties>

    <distributionManagement>
        <snapshotRepository>
            <id>godo-snapshot</id>
            <url>https://repo.godo.is/repository/maven-snapshots/</url>
        </snapshotRepository>
        <repository>
            <id>godo-release</id>
            <url>https://repo.godo.is/repository/maven-releases/</url>
        </repository>
    </distributionManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.9.7</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.6.1</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator</artifactId>
            <version>0.36</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.7</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>is.godo</groupId>
            <artifactId>godo-api-extended</artifactId>
            <version>1.4.8</version>
        </dependency>
        <dependency>
            <groupId>is.godo.core</groupId>
            <artifactId>godo-core</artifactId>
            <version>2.2.9</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <dependency>
            <groupId>is.godo.server.property.dto</groupId>
            <artifactId>godo-property-server-dto</artifactId>
            <version>1.1.9</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <addResources>true</addResources>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                        </manifest>
                        <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.20.1</version>
                <configuration>
                    <skipTests>false</skipTests>
                </configuration>
            </plugin>

            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>dockerfile-maven-plugin</artifactId>
                <version>1.3.6</version>
                <dependencies>
                    <dependency>
                        <groupId>javax.activation</groupId>
                        <artifactId>activation</artifactId>
                        <version>1.1.1</version>
                    </dependency>
                </dependencies>
                <configuration>
                    <repository>${docker.image.prefix}/${project.artifactId}</repository>
                    <buildArgs>
                        <WAR_FILE>target/${project.build.finalName}.war</WAR_FILE>
                    </buildArgs>
                    <tag>${docker.tag.prefix
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Возможно, уместно, что функция doRequest возвращает Mono, который затем является flatMapped, и другую вызванную службу, которая хранит информацию об оплате в системе бронирования отелей. Эта функция может занять несколько секунд.

Я подумал, что это может быть проблема с многопоточностью, поэтому я попытался изменить некоторые параметры, но они, похоже, не дают эффекта. На данный момент у меня установлены следующие параметры tomcat:

server.tomcat.max-threads: 200
server.tomcat.min-spare-threads: 50

Любая помощь будет принята с благодарностью. Я в полном недоумении.

Спасибо, Gísli

== Обновление ==

Не уверен, почему это не просматривается другими людьми, кроме меня. В итоге я поставил BlockingQueue перед функцией оплаты. Затем я выполнил запланированное весеннее задание, которое каждую минуту проверяет очередь и, если оно не пусто, берет из очереди один заряд и обрабатывает его. Это решило проблему, но я все еще не доволен этим решением.

...