TD-955: Add implementation (#1)

This commit is contained in:
Baikov Dmitrii 2024-09-04 14:23:32 +03:00 committed by GitHub
parent 23d86bd8ca
commit 44c3657ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1798 additions and 0 deletions

2
.github/settings.yml vendored Normal file
View File

@ -0,0 +1,2 @@
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
_extends: .github

10
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,10 @@
name: Build Maven Artifact
on:
pull_request:
branches:
- '**'
jobs:
build:
uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v3

15
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Deploy Docker Image
on:
push:
branches:
- 'master'
- 'main'
- 'epic/**'
jobs:
build-and-deploy:
uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v3
secrets:
github-token: ${{ secrets.GITHUB_TOKEN }}
mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }}

83
.gitignore vendored Normal file
View File

@ -0,0 +1,83 @@
# Created by .ignore support plugin (hsz.mobi)
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
*.ipr
*.iml
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Java template
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
env.list
# OSX
*.DS_Store
.AppleDouble
.LSOverride
# TestContainers
.testcontainers-*

385
pom.xml Normal file
View File

@ -0,0 +1,385 @@
<?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>dev.vality</groupId>
<artifactId>service-parent-pom</artifactId>
<version>3.0.2</version>
<relativePath/>
</parent>
<artifactId>liminator</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>liminator</name>
<description>Service for interation with the dominant data</description>
<properties>
<dockerfile.registry>${env.REGISTRY}</dockerfile.registry>
<server.port>8022</server.port>
<management.port>8023</management.port>
<exposed.ports>${server.port} ${management.port}</exposed.ports>
<db.url>jdbc:postgresql://localhost:5432/liminator</db.url>
<db.user>postgres</db.user>
<db.password>postgres</db.password>
<db.name>liminator</db.name>
<db.schema>lim</db.schema>
<local.pg.url>jdbc:postgresql://localhost:5432/liminator</local.pg.url>
<local.pg.port>5432</local.pg.port>
<checkstyle.config.suppressions.path>./src/main/resources/checkstyle/checkstyle-suppressions.xml
</checkstyle.config.suppressions.path>
</properties>
<dependencies>
<!--spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.6</version>
</dependency>
<!--Thrirdparty libs-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.19.11</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>4.46.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>4.46.0</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>software.amazon.msk</groupId>
<artifactId>aws-msk-iam-auth</artifactId>
<version>2.0.3</version>
</dependency>
<!--dev.vality-->
<dependency>
<groupId>dev.vality</groupId>
<artifactId>db-common-lib</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>liminator-proto</artifactId>
<version>1.2-63529e6</version>
</dependency>
<dependency>
<groupId>dev.vality.woody</groupId>
<artifactId>woody-thrift</artifactId>
<version>${woody.version}</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>limiter-proto</artifactId>
<version>1.33-31de59b</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>shared-resources</artifactId>
<version>${shared-resources.version}</version>
</dependency>
<dependency>
<groupId>dev.vality.geck</groupId>
<artifactId>serializer</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>dev.vality.geck</groupId>
<artifactId>filter</artifactId>
<version>1.0.1</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>testcontainers-annotations</artifactId>
<version>2.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<targetPath>${project.build.directory}</targetPath>
<includes>
<include>Dockerfile</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>Dockerfile</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>dev.vality.maven.plugins</groupId>
<artifactId>pg-embedded-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<port>${local.pg.port}</port>
<dbName>${db.name}</dbName>
<schemas>
<schema>${db.schema}</schema>
</schemas>
</configuration>
<executions>
<execution>
<id>PG_server_start</id>
<phase>initialize</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>PG_server_stop</id>
<phase>generate-resources</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<configuration>
<url>${local.pg.url}</url>
<user>${db.user}</user>
<password>${db.password}</password>
<schemas>
<schema>${db.schema}</schema>
</schemas>
</configuration>
<executions>
<execution>
<id>migrate</id>
<phase>initialize</phase>
<goals>
<goal>migrate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<configuration>
<jdbc>
<driver>org.postgresql.Driver</driver>
<url>${local.pg.url}</url>
<user>${db.user}</user>
<password>${db.password}</password>
</jdbc>
<generator>
<generate>
<javaTimeTypes>true</javaTimeTypes>
<pojos>true</pojos>
<pojosEqualsAndHashCode>true</pojosEqualsAndHashCode>
<pojosToString>true</pojosToString>
</generate>
<database>
<name>org.jooq.meta.postgres.PostgresDatabase</name>
<includes>.*</includes>
<excludes>
schema_version|.*func
</excludes>
<inputSchema>${db.schema}</inputSchema>
</database>
<target>
<packageName>com.empayre.liminator.domain</packageName>
<directory>target/generated-sources/</directory>
</target>
</generator>
</configuration>
<executions>
<execution>
<id>gen-src</id>
<phase>initialize</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-remote-resources-plugin</artifactId>
<version>3.1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-filtering</artifactId>
<version>3.3.1</version>
</dependency>
</dependencies>
<configuration>
<resourceBundles>
<resourceBundle>dev.vality:shared-resources:${shared-resources.version}</resourceBundle>
</resourceBundles>
<attachToMain>false</attachToMain>
<attachToTest>false</attachToTest>
</configuration>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>p12</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>

4
renovate.json Normal file
View File

@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>valitydev/.github:renovate-config"]
}

View File

@ -0,0 +1,16 @@
package com.empayre.liminator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@ServletComponentScan
@SpringBootApplication(scanBasePackages = {"com.empayre.liminator"})
public class LiminatorApplication {
public static void main(String[] args) {
SpringApplication.run(LiminatorApplication.class, args);
}
}

View File

@ -0,0 +1,14 @@
package com.empayre.liminator.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationConfig {
@Bean
public ObjectMapper mapper() {
return new ObjectMapper();
}
}

View File

@ -0,0 +1,22 @@
package com.empayre.liminator.converter;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import dev.vality.liminator.CreateLimitRequest;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Component
public class CreateLimitRequestToLimitDataConverter implements Converter<CreateLimitRequest, LimitData> {
@Override
public LimitData convert(CreateLimitRequest request) {
LimitData data = new LimitData();
data.setName(request.getLimitName());
data.setCreatedAt(LocalDate.now());
data.setWtime(LocalDateTime.now());
return data;
}
}

View File

@ -0,0 +1,30 @@
package com.empayre.liminator.converter;
import com.empayre.liminator.model.LimitValue;
import dev.vality.liminator.LimitResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class CurrentLimitValuesToLimitResponseConverter implements Converter<List<LimitValue>, List<LimitResponse>> {
@Override
public List<LimitResponse> convert(List<LimitValue> values) {
if (values == null) {
log.info("Received LimitValues array is empty");
return new ArrayList<>();
}
return values.stream()
.map(limitValue -> new LimitResponse(
limitValue.getLimitName(),
limitValue.getCommitValue(),
limitValue.getHoldValue())
)
.toList();
}
}

View File

@ -0,0 +1,9 @@
package com.empayre.liminator.converter;
import com.empayre.liminator.domain.tables.pojos.Operation;
import dev.vality.liminator.LimitRequest;
public interface OperationConverter {
Operation convert(LimitRequest request, Long limitId);
}

View File

@ -0,0 +1,24 @@
package com.empayre.liminator.converter.impl;
import com.empayre.liminator.converter.OperationConverter;
import com.empayre.liminator.domain.enums.OperationState;
import com.empayre.liminator.domain.tables.pojos.Operation;
import dev.vality.liminator.LimitRequest;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class OperationConverterImpl implements OperationConverter {
@Override
public Operation convert(LimitRequest request, Long limitId) {
Operation operation = new Operation();
operation.setLimitId(limitId);
operation.setOperationId(request.getOperationId());
operation.setAmount(request.getValue());
operation.setCreatedAt(LocalDateTime.now());
operation.setState(OperationState.HOLD);
return operation;
}
}

View File

@ -0,0 +1,23 @@
package com.empayre.liminator.dao;
import lombok.Getter;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.impl.DefaultConfiguration;
import javax.sql.DataSource;
@Getter
public abstract class AbstractDao {
private final DSLContext dslContext;
public AbstractDao(DataSource dataSource) {
Configuration configuration = new DefaultConfiguration();
configuration.set(dataSource);
configuration.set(SQLDialect.POSTGRES);
this.dslContext = DSL.using(configuration);
}
}

View File

@ -0,0 +1,8 @@
package com.empayre.liminator.dao;
import com.empayre.liminator.exception.DaoException;
public interface CommonDao<T> {
Long save(T domainObject) throws DaoException;
}

View File

@ -0,0 +1,8 @@
package com.empayre.liminator.dao;
import com.empayre.liminator.domain.tables.pojos.LimitContext;
public interface LimitContextDao extends CommonDao<LimitContext> {
LimitContext getLimitContext(Long limitId);
}

View File

@ -0,0 +1,13 @@
package com.empayre.liminator.dao;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import java.util.Collection;
import java.util.List;
public interface LimitDataDao extends CommonDao<LimitData> {
LimitData get(String limitName);
List<LimitData> get(Collection<String> limitNames);
}

View File

@ -0,0 +1,21 @@
package com.empayre.liminator.dao;
import com.empayre.liminator.domain.tables.pojos.Operation;
import com.empayre.liminator.model.LimitValue;
import java.util.List;
public interface OperationDao extends CommonDao<Operation> {
void saveBatch(List<Operation> operations);
Operation get(Long id);
List<LimitValue> getCurrentLimitValue(List<String> limitNames);
List<LimitValue> getCurrentLimitValue(List<String> limitNames, String operationId);
int commit(List<String> operationIds);
int rollback(List<String> operationIds);
}

View File

@ -0,0 +1,37 @@
package com.empayre.liminator.dao.impl;
import com.empayre.liminator.dao.AbstractDao;
import com.empayre.liminator.dao.LimitContextDao;
import com.empayre.liminator.domain.tables.pojos.LimitContext;
import com.empayre.liminator.exception.DaoException;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import static com.empayre.liminator.domain.Tables.LIMIT_CONTEXT;
@Component
public class LimitContextDaoImpl extends AbstractDao implements LimitContextDao {
public LimitContextDaoImpl(DataSource dataSource) {
super(dataSource);
}
@Override
public Long save(LimitContext limitContext) throws DaoException {
return getDslContext()
.insertInto(LIMIT_CONTEXT)
.set(getDslContext().newRecord(LIMIT_CONTEXT, limitContext))
.returning(LIMIT_CONTEXT.ID)
.fetchOne()
.getId();
}
@Override
public LimitContext getLimitContext(Long limitId) {
return getDslContext()
.selectFrom(LIMIT_CONTEXT)
.where(LIMIT_CONTEXT.LIMIT_ID.eq(limitId))
.fetchOneInto(LimitContext.class);
}
}

View File

@ -0,0 +1,52 @@
package com.empayre.liminator.dao.impl;
import com.empayre.liminator.dao.AbstractDao;
import com.empayre.liminator.dao.LimitDataDao;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import com.empayre.liminator.exception.DaoException;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static com.empayre.liminator.domain.Tables.LIMIT_DATA;
@Component
public class LimitDataDaoImpl extends AbstractDao implements LimitDataDao {
public LimitDataDaoImpl(DataSource dataSource) {
super(dataSource);
}
@Override
public Long save(LimitData limitData) throws DaoException {
return getDslContext()
.insertInto(LIMIT_DATA)
.set(getDslContext().newRecord(LIMIT_DATA, limitData))
.onConflict(LIMIT_DATA.NAME)
.doUpdate()
.set(LIMIT_DATA.WTIME, LocalDateTime.now())
.returning(LIMIT_DATA.ID)
.fetchOne()
.getId();
}
@Override
public LimitData get(String limitName) {
return getDslContext()
.selectFrom(LIMIT_DATA)
.where(LIMIT_DATA.NAME.equal(limitName))
.fetchOneInto(LimitData.class);
}
@Override
public List<LimitData> get(Collection<String> limitNames) {
return getDslContext()
.selectFrom(LIMIT_DATA)
.where(LIMIT_DATA.NAME.in(limitNames))
.fetchInto(LimitData.class);
}
}

View File

@ -0,0 +1,143 @@
package com.empayre.liminator.dao.impl;
import com.empayre.liminator.dao.AbstractDao;
import com.empayre.liminator.dao.OperationDao;
import com.empayre.liminator.domain.enums.OperationState;
import com.empayre.liminator.domain.tables.pojos.Operation;
import com.empayre.liminator.domain.tables.records.OperationRecord;
import com.empayre.liminator.exception.DaoException;
import com.empayre.liminator.model.LimitValue;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.List;
import java.util.stream.Collectors;
import static com.empayre.liminator.domain.Tables.OPERATION;
import static org.jooq.impl.DSL.raw;
import static org.jooq.impl.DSL.val;
@Component
public class OperationDaoImpl extends AbstractDao implements OperationDao {
public OperationDaoImpl(DataSource dataSource) {
super(dataSource);
}
private static final String DELIMITER = " ,";
@Override
public Long save(Operation operation) throws DaoException {
return getDslContext()
.insertInto(OPERATION)
.set(getDslContext().newRecord(OPERATION, operation))
.returning(OPERATION.ID)
.fetchOne()
.getId();
}
@Override
public Operation get(Long id) {
return getDslContext()
.selectFrom(OPERATION)
.where(OPERATION.ID.eq(id))
.fetchOneInto(Operation.class);
}
@Override
public void saveBatch(List<Operation> operations) {
List<OperationRecord> records = operations.stream()
.map(operation -> getDslContext().newRecord(OPERATION, operation))
.toList();
getDslContext().batchInsert(records).execute();
}
@Override
public int commit(List<String> operationIds) {
return updateStateForHoldOperation(operationIds, OperationState.COMMIT);
}
@Override
public int rollback(List<String> operationIds) {
return updateStateForHoldOperation(operationIds, OperationState.ROLLBACK);
}
private int updateStateForHoldOperation(List<String> operationIds, OperationState state) {
return getDslContext()
.update(OPERATION)
.set(OPERATION.STATE, state)
.where(OPERATION.OPERATION_ID.in(operationIds))
.and(OPERATION.STATE.eq(OperationState.HOLD))
.execute();
}
@Override
public List<LimitValue> getCurrentLimitValue(List<String> limitNames) {
String sql = """
with hold_data as (
select ld.id, ld.name, coalesce(sum(ops.amount), 0) as hold_value
from lim.limit_data as ld
left join lim.operation as ops
on ops.limit_id = ld.id and ops.state = 'HOLD'
where ld.name in ({0})
group by ld.id, ld.name
), commit_data as (
select ld.id, ld.name, coalesce(sum(ops.amount), 0) as commit_value
from lim.limit_data as ld
left join lim.operation as ops
on ops.limit_id = ld.id and ops.state = 'COMMIT'
where ld.name in ({0})
group by ld.id, ld.name
)
select cd.name as limit_name, cd.commit_value, hd.hold_value
from commit_data as cd
join hold_data as hd on cd.id = hd.id;
""";
return getDslContext()
.resultQuery(sql, raw(arrayToString(limitNames)))
.fetchInto(LimitValue.class);
}
@Override
public List<LimitValue> getCurrentLimitValue(List<String> limitNames, String operationId) {
String sql = """
with operation_timestamp as (
select created_at
from lim.operation
where operation_id = {0}
), hold_data as (
select ld.id, ld.name, coalesce(sum(ops.amount), 0) as hold_amount
from lim.limit_data as ld
left join lim.operation as ops
on ops.limit_id = ld.id
and ops.created_at <= (select created_at from operation_timestamp limit 1)
and ops.state = 'HOLD'
where ld.name in ({1})
group by ld.id, ld.name
), commit_data as (
select ld.id, ld.name, coalesce(sum(ops.amount), 0) as commit_amount
from lim.limit_data as ld
left join lim.operation as ops
on ops.limit_id = ld.id
and ops.created_at <= (select created_at from operation_timestamp limit 1)
and ops.state = 'COMMIT'
where ld.name in ({1})
group by ld.id, ld.name
)
select cd.name as limit_name, cd.commit_amount, hd.hold_amount
from commit_data as cd
join hold_data as hd on cd.id = hd.id;
""";
return getDslContext()
.resultQuery(sql, val(operationId), raw(arrayToString(limitNames)))
.fetchInto(LimitValue.class);
}
private static String arrayToString(List<String> strings) {
return strings.stream()
.map(limit -> "'%s'".formatted(limit))
.collect(Collectors.joining(DELIMITER));
}
}

View File

@ -0,0 +1,7 @@
package com.empayre.liminator.exception;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}

View File

@ -0,0 +1,23 @@
package com.empayre.liminator.exception;
public class DaoException extends RuntimeException {
public DaoException() {
super();
}
public DaoException(String message) {
super(message);
}
public DaoException(String message, Throwable cause) {
super(message, cause);
}
public DaoException(Throwable cause) {
super(cause);
}
protected DaoException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,7 @@
package com.empayre.liminator.exception;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.empayre.liminator.exception;
public class SerializationException extends RuntimeException {
public SerializationException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,44 @@
package com.empayre.liminator.handler;
import com.empayre.liminator.dao.OperationDao;
import com.empayre.liminator.service.LimitsGettingService;
import dev.vality.liminator.LimitRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class CommitLimitAmountHandler implements Handler<List<LimitRequest>, Boolean> {
private final OperationDao operationDao;
private final LimitsGettingService limitsGettingService;
private static final String LOG_PREFIX = "COMMIT";
@Override
@Transactional
public Boolean handle(List<LimitRequest> requestList) throws TException {
if (CollectionUtils.isEmpty(requestList)) {
log.warn("[{}] Received LimitRequest list is empty", LOG_PREFIX);
return true;
}
limitsGettingService.get(requestList, LOG_PREFIX);
List<String> operationsIds = requestList.stream()
.map(request -> request.getOperationId())
.toList();
int updatedRowsCount = operationDao.commit(operationsIds);
if (updatedRowsCount != operationsIds.size()) {
log.warn("[{}] Count of updated rows ({}) is not equal to the count of source commit operations ({})",
LOG_PREFIX, updatedRowsCount, operationsIds.size());
}
return true;
}
}

View File

@ -0,0 +1,68 @@
package com.empayre.liminator.handler;
import com.empayre.liminator.dao.LimitContextDao;
import com.empayre.liminator.dao.LimitDataDao;
import com.empayre.liminator.domain.tables.pojos.LimitContext;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.vality.liminator.CreateLimitRequest;
import dev.vality.liminator.LimitResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class CreateLimitHandler implements Handler<CreateLimitRequest, LimitResponse> {
private final ObjectMapper mapper;
private final Converter<CreateLimitRequest, LimitData> createLimitRequestToLimitDataConverter;
private final LimitDataDao limitDataDao;
private final LimitContextDao limitContextDao;
private static final String LOG_PREFIX = "CREATE";
private static final String EMPTY_JSON = "{}";
@Transactional
@Override
public LimitResponse handle(CreateLimitRequest request) throws TException {
String limitName = request.getLimitName();
LimitData existedLimitData = limitDataDao.get(limitName);
if (existedLimitData != null) {
log.info("[{}] Limit {} already exists", LOG_PREFIX, limitName);
return new LimitResponse(limitName, 0, 0);
}
LimitData limitData = createLimitRequestToLimitDataConverter.convert(request);
Long limitId = limitDataDao.save(limitData);
if (request.context != null) {
limitContextDao.save(convertToLimitContext(limitId, request.context));
}
return new LimitResponse(limitName, 0, 0);
}
private LimitContext convertToLimitContext(Long limitId, Map<String, String> contextMap) {
LimitContext context = new LimitContext();
context.setLimitId(limitId);
context.setContext(getContextString(contextMap));
context.setWtime(LocalDateTime.now());
return context;
}
private String getContextString(Map<String, String> contextMap) {
try {
return mapper.writeValueAsString(contextMap);
} catch (JsonProcessingException e) {
log.error("[{}] ContextJSON processing exception", LOG_PREFIX, e);
return EMPTY_JSON;
}
}
}

View File

@ -0,0 +1,28 @@
package com.empayre.liminator.handler;
import com.empayre.liminator.dao.OperationDao;
import com.empayre.liminator.model.LimitValue;
import dev.vality.liminator.LimitResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class GetLimitAmountHandler implements Handler<List<String>, List<LimitResponse>> {
private final OperationDao operationDao;
private final Converter<List<LimitValue>, List<LimitResponse>> currentLimitValuesToLimitResponseConverter;
@Override
@Transactional
public List<LimitResponse> handle(List<String> limitIdNames) throws TException {
return currentLimitValuesToLimitResponseConverter.convert(operationDao.getCurrentLimitValue(limitIdNames));
}
}

View File

@ -0,0 +1,8 @@
package com.empayre.liminator.handler;
import org.apache.thrift.TException;
public interface Handler<T, R> {
R handle(T source) throws TException;
}

View File

@ -0,0 +1,55 @@
package com.empayre.liminator.handler;
import com.empayre.liminator.converter.OperationConverter;
import com.empayre.liminator.dao.OperationDao;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import com.empayre.liminator.domain.tables.pojos.Operation;
import com.empayre.liminator.model.LimitValue;
import com.empayre.liminator.service.LimitsGettingService;
import com.empayre.liminator.util.LimitDataUtils;
import dev.vality.liminator.LimitRequest;
import dev.vality.liminator.LimitResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class HoldLimitAmountHandler implements Handler<List<LimitRequest>, List<LimitResponse>> {
private final OperationDao operationDao;
private final LimitsGettingService limitsGettingService;
private final Converter<List<LimitValue>, List<LimitResponse>> currentLimitValuesToLimitResponseConverter;
private final OperationConverter operationConverter;
private static final String LOG_PREFIX = "HOLD";
@Transactional
@Override
public List<LimitResponse> handle(List<LimitRequest> requestList) throws TException {
if (CollectionUtils.isEmpty(requestList)) {
return new ArrayList<>();
}
List<LimitData> limitData = limitsGettingService.get(requestList, LOG_PREFIX);
Map<String, Long> limitNamesMap = LimitDataUtils.createLimitNamesMap(limitData);
List<Operation> operations = convertToOperation(requestList, limitNamesMap);
operationDao.saveBatch(operations);
List<String> limitNames = LimitDataUtils.getLimitNames(requestList);
return currentLimitValuesToLimitResponseConverter.convert(operationDao.getCurrentLimitValue(limitNames));
}
private List<Operation> convertToOperation(List<LimitRequest> requestList, Map<String, Long> limitNamesMap) {
return requestList.stream()
.map(request -> operationConverter.convert(request, limitNamesMap.get(request.getLimitName())))
.toList();
}
}

View File

@ -0,0 +1,39 @@
package com.empayre.liminator.handler;
import com.empayre.liminator.dao.OperationDao;
import com.empayre.liminator.service.LimitsGettingService;
import dev.vality.liminator.LimitRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class RollbackLimitAmountHandler implements Handler<List<LimitRequest>, Boolean> {
private final OperationDao operationDao;
private final LimitsGettingService limitsGettingService;
private static final String LOG_PREFIX = "ROLLBACK";
@Override
@Transactional
public Boolean handle(List<LimitRequest> requestList) throws TException {
limitsGettingService.get(requestList, LOG_PREFIX);
List<String> operationsIds = requestList.stream()
.map(request -> request.getOperationId())
.toList();
int updatedRowsCount = operationDao.rollback(operationsIds);
if (updatedRowsCount != operationsIds.size()) {
log.warn("[{}] Count of updated rows ({}) is not equal to the count of source rollback operations ({})",
LOG_PREFIX, updatedRowsCount, operationsIds.size());
}
return true;
}
}

View File

@ -0,0 +1,13 @@
package com.empayre.liminator.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class LimitValue {
private String limitName;
private Long commitValue;
private Long holdValue;
}

View File

@ -0,0 +1,59 @@
package com.empayre.liminator.service;
import com.empayre.liminator.handler.Handler;
import dev.vality.liminator.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class LiminatorService implements LiminatorServiceSrv.Iface {
private final Handler<CreateLimitRequest, LimitResponse> createLimitHandler;
private final Handler<List<LimitRequest>, List<LimitResponse>> holdLimitAmountHandler;
private final Handler<List<LimitRequest>, Boolean> commitLimitAmountHandler;
private final Handler<List<LimitRequest>, Boolean> rollbackLimitAmountHandler;
private final Handler<List<String>, List<LimitResponse>> getLimitAmountHandler;
@Override
public LimitResponse create(CreateLimitRequest createLimitRequest) throws DuplicateLimitName, TException {
return createLimitHandler.handle(createLimitRequest);
}
@Override
public List<LimitResponse> hold(List<LimitRequest> list) throws LimitNotFound, TException {
return holdLimitAmountHandler.handle(list);
}
@Override
public boolean commit(List<LimitRequest> list) throws LimitNotFound, TException {
try {
commitLimitAmountHandler.handle(list);
return true;
} catch (Exception ex) {
log.error("Commit execution exception. Request list: {}", list, ex);
return false;
}
}
@Override
public boolean rollback(List<LimitRequest> list) throws LimitNotFound, TException {
try {
rollbackLimitAmountHandler.handle(list);
return true;
} catch (Exception ex) {
log.error("Commit execution exception. Request list: {}", list, ex);
return false;
}
}
@Override
public List<LimitResponse> get(List<String> limitNames) throws LimitNotFound, TException {
return getLimitAmountHandler.handle(limitNames);
}
}

View File

@ -0,0 +1,37 @@
package com.empayre.liminator.service;
import com.empayre.liminator.dao.LimitDataDao;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import com.empayre.liminator.util.LimitDataUtils;
import dev.vality.liminator.LimitNotFound;
import dev.vality.liminator.LimitRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class LimitsGettingService {
private final LimitDataDao limitDataDao;
public List<LimitData> get(List<LimitRequest> requestList, String source) throws TException {
List<String> limitNames = LimitDataUtils.getLimitNames(requestList);
List<LimitData> limitData = limitDataDao.get(limitNames);
if (CollectionUtils.isEmpty(limitData)) {
log.error("[{}] Limits not found: {}", source, limitNames);
throw new LimitNotFound();
}
if (limitData.size() != limitNames.size()) {
log.error("[{}] Received limit ({}) size is not equal to expected ({}). " +
"Probably one of limits doesn't exist", source, limitData.size(), limitNames.size());
throw new LimitNotFound();
}
return limitData;
}
}

View File

@ -0,0 +1,30 @@
package com.empayre.liminator.servlet;
import dev.vality.liminator.LiminatorServiceSrv;
import dev.vality.woody.thrift.impl.http.THServiceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/liminator/v1")
public class LiminatorServiceServlet extends GenericServlet {
private Servlet thriftServlet;
@Autowired
private LiminatorServiceSrv.Iface requestHandler;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
thriftServlet = new THServiceBuilder()
.build(LiminatorServiceSrv.Iface.class, requestHandler);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
thriftServlet.service(req, res);
}
}

View File

@ -0,0 +1,29 @@
package com.empayre.liminator.util;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import dev.vality.liminator.LimitRequest;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LimitDataUtils {
public static List<String> getLimitNames(List<LimitRequest> requestList) {
return requestList.stream()
.map(LimitRequest::getLimitName)
.distinct()
.toList();
}
public static Map<String, Long> createLimitNamesMap(List<LimitData> limitData) {
Map<String, Long> map = new HashMap<>();
for (LimitData data : limitData) {
map.put(data.getName(), data.getId());
}
return map;
}
}

View File

@ -0,0 +1,97 @@
server:
port: "${server.port}"
spring:
application:
name: "${project.name}"
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
url: "${db.url}"
username: '${db.user}'
password: "${db.password}"
hikari:
idle-timeout: 30000
maximum-pool-size: 10
data-source-properties:
reWriteBatchedInserts: true
flyway:
schemas: lim
jooq:
sql-dialect: Postgres
kafka:
bootstrap-servers: localhost:9092
client-id: dominant_warehouse
consumer:
enable-auto-commit: false
auto-offset-reset: earliest
max-poll-records: 20
properties:
max.poll.interval.ms: 30000
session.timeout.ms: 30000
service:
default:
limit: 20
management:
server:
port: "${management.port}"
metrics:
export:
prometheus:
enabled: false
tags:
application: "${project.name}"
endpoint:
health:
probes:
enabled: true
show-details: always
metrics:
enabled: true
prometheus:
enabled: true
endpoints:
web:
exposure:
include: health,info,prometheus
kafka:
consumer:
group-id: "Liminator-Listener"
party-management-concurrency: 7
wallet-concurrency: 7
identity-concurrency: 7
topics:
party-management:
id: mg-events-party
enabled: false
consumer.group-id: "LiminatorListenerPartyManagement"
identity:
id: mg-events-ff-identity
enabled: false
wallet:
id: mg-events-ff-wallet
enabled: false
dmt:
url: http://dominant:8022/v1/domain/repository
networkTimeout: 5000
polling:
delay: 100
maxQuerySize: 10
enabled: false
cache:
party-shop:
size: 10000
expire:
after:
sec: 600
testcontainers:
postgresql:
tag: '11.4'
kafka:
tag: '6.2.0'

View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.0//EN"
"https://checkstyle.org/dtds/suppressions_1_0.dtd">
<suppressions>
<suppress checks="[a-zA-Z0-9]*"
files="com[\\/]empayre[\\/]dominant[\\/]warehouse[\\/].*.java"/>
<suppress checks="LineLength" files=".*ContractUtil.java"/>
<suppress checks="VariableDeclarationUsageDistance" files=".*DaoTests.java"/>
</suppressions>

View File

@ -0,0 +1,44 @@
CREATE SCHEMA IF NOT EXISTS lim;
CREATE TABLE lim.limit_data
(
id bigserial NOT NULL,
name character varying NOT NULL,
created_at date NOT NULL,
wtime timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
CONSTRAINT limit_data_pkey PRIMARY KEY (id)
);
CREATE UNIQUE INDEX limit_data_unq_idx ON lim.limit_data USING btree (name);
CREATE TABLE lim.limit_context
(
id bigserial NOT NULL,
limit_id bigint NOT NULL,
context character varying,
wtime timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
CONSTRAINT limit_context_pkey PRIMARY KEY (id)
);
CREATE UNIQUE INDEX limit_context_unq_idx ON lim.limit_context USING btree (limit_id);
CREATE TYPE lim.operation_state AS ENUM ('HOLD', 'COMMIT', 'ROLLBACK');
CREATE TABLE lim.operation
(
id bigserial NOT NULL,
limit_id bigint NOT NULL,
operation_id character varying NOT NULL,
state lim.operation_state NOT NULL,
amount bigint NOT NULL,
created_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,
CONSTRAINT operation_pkey PRIMARY KEY (id)
);
CREATE UNIQUE INDEX operation_unq_idx ON lim.operation USING btree (limit_id, operation_id);
CREATE INDEX operation_idx ON lim.operation USING btree (limit_id, state, created_at, operation_id);

View File

@ -0,0 +1,16 @@
package com.empayre.liminator.config;
import dev.vality.testcontainers.annotations.postgresql.PostgresqlTestcontainerSingleton;
import org.springframework.boot.test.autoconfigure.jooq.JooqTest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PostgresqlTestcontainerSingleton
@JooqTest
public @interface PostgresqlJooqSpringBootITest {
}

View File

@ -0,0 +1,16 @@
package com.empayre.liminator.config;
import dev.vality.testcontainers.annotations.DefaultSpringBootTest;
import dev.vality.testcontainers.annotations.postgresql.PostgresqlTestcontainerSingleton;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PostgresqlTestcontainerSingleton
@DefaultSpringBootTest
public @interface PostgresqlSpringBootITest {
}

View File

@ -0,0 +1,143 @@
package com.empayre.liminator.dao;
import com.empayre.liminator.config.PostgresqlSpringBootITest;
import com.empayre.liminator.domain.enums.OperationState;
import com.empayre.liminator.domain.tables.pojos.LimitContext;
import com.empayre.liminator.domain.tables.pojos.LimitData;
import com.empayre.liminator.domain.tables.pojos.Operation;
import com.empayre.liminator.model.LimitValue;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@PostgresqlSpringBootITest
public class DaoTests {
@Autowired
private LimitDataDao limitDataDao;
@Autowired
private LimitContextDao limitContextDao;
@Autowired
private OperationDao operationDao;
@Test
public void limitDataDaoTest() {
List<String> limitNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String limitName = "Limit-" + i;
limitDataDao.save(new LimitData(null, limitName, LocalDate.now(), LocalDateTime.now()));
limitNames.add(limitName);
}
List<LimitData> limitDataList = limitDataDao.get(limitNames);
assertEquals(limitNames.size(), limitDataList.size());
}
@Test
public void limitContextDaoTest() {
LimitContext limitContext = new LimitContext();
long limitId = 123L;
limitContext.setLimitId(limitId);
limitContext.setContext("{\"provider\":\"test\"}");
limitContextDao.save(limitContext);
LimitContext result = limitContextDao.getLimitContext(limitId);
assertNotNull(result);
}
@Test
public void operationDaoTest() {
List<Long> limitIdsList = new ArrayList<>();
List<String> limitNamesList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
String limitName = "Limit-odc-1-" + i;
Long limitId = limitDataDao.save(new LimitData(null, limitName, LocalDate.now(), LocalDateTime.now()));
limitIdsList.add(limitId);
limitNamesList.add(limitName);
}
List<Operation> operations = new ArrayList<>();
for (Long limitId : limitIdsList) {
for (int i = 0; i < 5; i++) {
operations.add(createOperation(limitId, "Operation-odc-1-%s-%s".formatted(limitId, i)));
}
}
operationDao.saveBatch(operations);
List<LimitValue> currentLimitValue = operationDao.getCurrentLimitValue(limitNamesList);
assertEquals(10, currentLimitValue.size());
currentLimitValue.forEach(value -> assertEquals(0, value.getCommitValue()));
currentLimitValue.forEach(value -> assertNotEquals(0, value.getHoldValue()));
List<String> commitOperations = operations.subList(0, 20).stream()
.map(Operation::getOperationId)
.toList();
operationDao.commit(commitOperations);
List<String> rollbackOperations = operations.subList(20, 35).stream()
.map(Operation::getOperationId)
.toList();
operationDao.rollback(rollbackOperations);
List<LimitValue> limitValuesAfterChanges = operationDao.getCurrentLimitValue(limitNamesList);
List<LimitValue> limitValuesWithCommitData = limitValuesAfterChanges.stream()
.filter(value -> value.getCommitValue() > 0 && value.getHoldValue() == 0)
.toList();
assertEquals(4, limitValuesWithCommitData.size());
List<LimitValue> limitValuesWithHoldData = limitValuesAfterChanges.stream()
.filter(value -> value.getCommitValue() == 0 && value.getHoldValue() > 0)
.toList();
assertEquals(3, limitValuesWithHoldData.size());
List<LimitValue> limitValuesWithoutData = limitValuesAfterChanges.stream()
.filter(value -> value.getCommitValue() == 0 && value.getHoldValue() == 0)
.toList();
assertEquals(3, limitValuesWithoutData.size());
}
@Test
public void operationDaoCurrentLimitWithOperationIdTest() {
String limitName = "Limit-odc-2";
Long limitId = limitDataDao.save(new LimitData(null, limitName, LocalDate.now(), LocalDateTime.now()));
List<Operation> operations = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Operation operation = createOperation(
limitId,
"Operation-odc-2-%s-%s".formatted(limitId, i),
LocalDateTime.now().minusMinutes(11L - i));
operationDao.save(operation);
operations.add(operation);
}
List<LimitValue> valuesForFifthOperation =
operationDao.getCurrentLimitValue(List.of(limitName), operations.get(2).getOperationId());
LimitValue limitValue = valuesForFifthOperation.get(0);
assertEquals(300, limitValue.getHoldValue());
valuesForFifthOperation =
operationDao.getCurrentLimitValue(List.of(limitName), operations.get(5).getOperationId());
assertEquals(600, valuesForFifthOperation.get(0).getHoldValue());
}
private Operation createOperation(Long limitId, String operationId) {
return createOperation(limitId, operationId, LocalDateTime.now());
}
private Operation createOperation(Long limitId, String operationId, LocalDateTime createdAt) {
Operation operation = new Operation();
operation.setLimitId(limitId);
operation.setOperationId(operationId);
operation.setState(OperationState.HOLD);
operation.setAmount(100L);
operation.setCreatedAt(createdAt);
return operation;
}
}

View File

@ -0,0 +1,97 @@
package com.empayre.liminator.service;
import com.empayre.liminator.config.PostgresqlSpringBootITest;
import dev.vality.liminator.CreateLimitRequest;
import dev.vality.liminator.LimitRequest;
import dev.vality.liminator.LimitResponse;
import org.apache.thrift.TException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@PostgresqlSpringBootITest
public class LiminatorServiceTest {
@Autowired
private LiminatorService liminatorService;
@Test
void createLimitTest() throws TException {
String limitName = "TestLimitCreate";
CreateLimitRequest request = new CreateLimitRequest()
.setLimitName(limitName);
LimitResponse response = liminatorService.create(request);
assertEquals(limitName, response.getLimitName());
assertEquals(0, response.getHoldValue());
assertEquals(0, response.getCommitValue());
}
@Test
void holdValueTest() throws TException {
String limitName = "TestLimitHold";
CreateLimitRequest createRequest = new CreateLimitRequest()
.setLimitName(limitName);
liminatorService.create(createRequest);
String operationId = "OpHold";
LimitRequest holdRequest = new LimitRequest()
.setLimitName(limitName)
.setOperationId(operationId)
.setValue(500L);
List<LimitResponse> holdResponse = liminatorService.hold(List.of(holdRequest));
assertEquals(1, holdResponse.size());
LimitResponse response = holdResponse.get(0);
assertEquals(500, response.getHoldValue());
assertEquals(0, response.getCommitValue());
assertEquals(limitName, response.getLimitName());
}
@Test
void commitValueTest() throws TException {
String limitName = "TestLimitCommit";
CreateLimitRequest createRequest = new CreateLimitRequest()
.setLimitName(limitName);
liminatorService.create(createRequest);
String operationId = "OpComit";
LimitRequest holdRequest = new LimitRequest()
.setLimitName(limitName)
.setOperationId(operationId)
.setValue(500L);
liminatorService.hold(List.of(holdRequest));
assertTrue(liminatorService.commit(List.of(holdRequest)));
List<LimitResponse> limitResponses = liminatorService.get(List.of(limitName));
assertEquals(1, limitResponses.size());
assertEquals(0, limitResponses.get(0).getHoldValue());
assertEquals(500, limitResponses.get(0).getCommitValue());
assertEquals(limitName, limitResponses.get(0).getLimitName());
}
@Test
void rollbackValueTest() throws TException {
String limitName = "TestLimitRollback";
CreateLimitRequest createRequest = new CreateLimitRequest()
.setLimitName(limitName);
liminatorService.create(createRequest);
String operationId = "Op-112";
LimitRequest holdRequest = new LimitRequest()
.setLimitName(limitName)
.setOperationId(operationId)
.setValue(500L);
liminatorService.hold(List.of(holdRequest));
assertTrue(liminatorService.rollback(List.of(holdRequest)));
List<LimitResponse> limitResponses = liminatorService.get(List.of(limitName));
assertEquals(1, limitResponses.size());
assertEquals(0, limitResponses.get(0).getHoldValue());
assertEquals(0, limitResponses.get(0).getCommitValue());
assertEquals(limitName, limitResponses.get(0).getLimitName());
}
}