Java WebClient losing intermittent responses when load increases

只谈情不闲聊 提交于 2021-02-11 17:19:24

问题


I'm developing a credit card charging server for a hotel booking system that can either be used manually through a web UI or automatically for non-refundable bookings. Each transaction goes through a proxy service (third party cc store, PCI Booking) and from there to the actual payment provider. I then save the responses (Success, card declined, etc), the provider name and the time it took to charge. When I charge manually, everything works fine but when there are several automatic requests firing at the same time, I'm not getting all the responses. Sometimes I get nothing and sometimes the elapsed() handler fires and then nothing.

Here is the code in question:

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 );
}

This is running in a spring service bean and this is how the WebClient is constructed:

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

I have experienced the issue with as few as 9 requests coming in at the same time. They all reach the payment provider but I appear to get the response for every second request. In those cases, neither of the flatMaps are called but sometimes the elapsed() handler gets called.

I'm logging both to console nad to a database. When this issue happens, the last log I'll see is either the one with "PaymentTransaction health" or the one from logTime().

I added the defaultIfEmpty() just to make sure that I was not getting an empty body. According to PCI Booking, the responses from the payment providers are normal but it seems like I am not receiving them all.

I'm using spring boot and this is my 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>

It might be relevant that the doRequest function returns a Mono which is then flatMapped and another service called that stores information about the payment in the hotel booking system. This function can take several seconds to complete.

I thought this might be a threading issue so I tried changing some parameters but they don't seem to have an effect. At the moment, I have these tomcat parameters set:

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

Any help would be greatly appreciated. I'm totally at a loss here.

Thanks, Gísli

== Update ==

Not sure why this is not being viewed by people other than myself. I ended up putting a BlockingQueue in front of the pay function. Then I made a spring scheduled task that checks the queue every minute and if it's non-empty, it takes one charge from the queue and processes it. This has solved the problem but I'm still not happy with this solution.

来源:https://stackoverflow.com/questions/60187111/java-webclient-losing-intermittent-responses-when-load-increases

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!