Я разрабатываю сервер для оплаты кредитной картой для системы бронирования отелей, которую можно использовать вручную через веб-интерфейс или автоматически при бронировании с невозвращаемой оплатой. Каждая транзакция проходит через прокси-сервис (стороннее хранилище 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 перед функцией оплаты. Затем я выполнил запланированное весеннее задание, которое каждую минуту проверяет очередь и, если оно не пусто, берет из очереди один заряд и обрабатывает его. Это решило проблему, но я все еще не доволен этим решением.