From 7a1530dd282797b4053ae7f040e42649168d5132 Mon Sep 17 00:00:00 2001 From: rkukharenka Date: Tue, 14 Mar 2023 14:55:06 +0300 Subject: [PATCH 01/27] EPMRPP-82327-job-service-google-style google-style applied --- Jenkinsfile-candidate | 26 +- README.md | 1 + .../reportportal/ServiceJobApplication.java | 8 +- .../analyzer/RabbitMqManagementClient.java | 3 +- .../RabbitMqManagementClientTemplate.java | 55 ++-- .../analyzer/index/IndexerServiceClient.java | 38 +-- .../index/IndexerServiceClientImpl.java | 104 +++---- .../calculation/BatchProcessing.java | 87 +++--- .../calculation/RetryCalculation.java | 10 +- .../calculation/RetryProcessing.java | 25 +- .../reportportal/config/DataSourceConfig.java | 13 +- .../config/DataStorageConfig.java | 266 +++++++++--------- .../reportportal/config/ExecutorConfig.java | 21 +- .../reportportal/config/ShedLockConfig.java | 17 +- .../rabbit/AnalyzerRabbitMqConfiguration.java | 71 ++--- .../BackgroundProcessingConfiguration.java | 34 +-- .../ProcessingRabbitMqConfiguration.java | 62 ++-- .../elastic/ElasticSearchClient.java | 6 +- .../elastic/EmptyElasticSearchClient.java | 18 +- .../elastic/SimpleElasticSearchClient.java | 145 +++++----- .../com/epam/reportportal/jobs/BaseJob.java | 29 +- .../reportportal/jobs/clean/BaseCleanJob.java | 59 ++-- .../jobs/clean/CleanAttachmentJob.java | 58 ++-- .../jobs/clean/CleanLaunchJob.java | 198 ++++++------- .../reportportal/jobs/clean/CleanLogJob.java | 132 ++++----- .../jobs/clean/CleanMaterializedViewJob.java | 170 +++++------ .../jobs/clean/CleanStorageJob.java | 91 +++--- .../jobs/processing/SaveLogMessageJob.java | 24 +- .../storage/CalculateAllocatedStorageJob.java | 88 +++--- .../com/epam/reportportal/log/LogMessage.java | 157 ++++++----- .../epam/reportportal/log/LogProcessing.java | 28 +- .../model/StaleMaterializedView.java | 32 +-- .../model/index/CleanIndexByDateRangeRq.java | 68 ++--- .../model/index/CleanIndexRq.java | 45 ++- .../storage/DataStorageService.java | 3 +- .../storage/LocalDataStorageService.java | 31 +- .../storage/S3DataStorageService.java | 67 +++-- src/main/resources/application.properties | 1 - src/main/resources/application.yml | 8 +- .../CalculateAllocatedStorageJobTest.java | 85 +++--- 40 files changed, 1213 insertions(+), 1171 deletions(-) diff --git a/Jenkinsfile-candidate b/Jenkinsfile-candidate index 0764764..dae8716 100644 --- a/Jenkinsfile-candidate +++ b/Jenkinsfile-candidate @@ -1,17 +1,17 @@ #!groovy -properties([ - parameters ([ - string( - name: "VERSION", - defaultValue: "", - description: "Release candidate version tag" - ), - string( - name: "BRANCH", - defaultValue: "", - description: "Specify the GitHub branch from which the image will be built" - ) - ]) +properties([ + parameters([ + string( + name: "VERSION", + defaultValue: "", + description: "Release candidate version tag" + ), + string( + name: "BRANCH", + defaultValue: "", + description: "Specify the GitHub branch from which the image will be built" + ) + ]) ]) node { diff --git a/README.md b/README.md index 9cda6d9..c62e444 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # service-jobs + ReportPortal cron jobs diff --git a/src/main/java/com/epam/reportportal/ServiceJobApplication.java b/src/main/java/com/epam/reportportal/ServiceJobApplication.java index e79fe9f..9918b0f 100644 --- a/src/main/java/com/epam/reportportal/ServiceJobApplication.java +++ b/src/main/java/com/epam/reportportal/ServiceJobApplication.java @@ -19,11 +19,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication(scanBasePackages = { "com.epam.reportportal" }) +@SpringBootApplication(scanBasePackages = {"com.epam.reportportal"}) public class ServiceJobApplication { - public static void main(String[] args) { - SpringApplication.run(ServiceJobApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(ServiceJobApplication.class, args); + } } diff --git a/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClient.java b/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClient.java index c31cca7..63dadb3 100644 --- a/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClient.java +++ b/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClient.java @@ -17,7 +17,6 @@ package com.epam.reportportal.analyzer; import com.rabbitmq.http.client.domain.ExchangeInfo; - import java.util.List; /** @@ -25,6 +24,6 @@ */ public interface RabbitMqManagementClient { - List getAnalyzerExchangesInfo(); + List getAnalyzerExchangesInfo(); } diff --git a/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClientTemplate.java b/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClientTemplate.java index 6f31e6d..1963d77 100644 --- a/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClientTemplate.java +++ b/src/main/java/com/epam/reportportal/analyzer/RabbitMqManagementClientTemplate.java @@ -16,43 +16,44 @@ package com.epam.reportportal.analyzer; +import static java.util.Comparator.comparingInt; +import static java.util.Optional.ofNullable; + import com.fasterxml.jackson.core.JsonProcessingException; import com.rabbitmq.http.client.Client; import com.rabbitmq.http.client.domain.ExchangeInfo; -import org.apache.commons.lang3.math.NumberUtils; - import java.util.List; import java.util.function.ToIntFunction; import java.util.stream.Collectors; - -import static java.util.Comparator.comparingInt; -import static java.util.Optional.ofNullable; +import org.apache.commons.lang3.math.NumberUtils; /** * @author Ihar Kahadouski */ public class RabbitMqManagementClientTemplate implements RabbitMqManagementClient { - public static final String ANALYZER_KEY = "analyzer"; - static final String ANALYZER_PRIORITY = "analyzer_priority"; - private final String virtualHost; - - public static final ToIntFunction EXCHANGE_PRIORITY = it -> ofNullable(it.getArguments() - .get(ANALYZER_PRIORITY)).map(val -> NumberUtils.toInt(val.toString(), Integer.MAX_VALUE)).orElse(Integer.MAX_VALUE); - - private final Client rabbitClient; - - public RabbitMqManagementClientTemplate(Client rabbitClient, String virtualHost) throws JsonProcessingException { - this.rabbitClient = rabbitClient; - this.virtualHost = virtualHost; - rabbitClient.createVhost(virtualHost); - } - - public List getAnalyzerExchangesInfo() { - return ofNullable(rabbitClient.getExchanges(virtualHost)).map(client -> client.stream() - .filter(it -> it.getArguments().get(ANALYZER_KEY) != null) - .sorted(comparingInt(EXCHANGE_PRIORITY)) - .collect(Collectors.toList())) - .orElseThrow(() -> new RuntimeException("Unable to resolve exchanges for key: " + ANALYZER_KEY)); - } + public static final String ANALYZER_KEY = "analyzer"; + static final String ANALYZER_PRIORITY = "analyzer_priority"; + public static final ToIntFunction EXCHANGE_PRIORITY = it -> ofNullable( + it.getArguments() + .get(ANALYZER_PRIORITY)).map(val -> NumberUtils.toInt(val.toString(), Integer.MAX_VALUE)) + .orElse(Integer.MAX_VALUE); + private final String virtualHost; + private final Client rabbitClient; + + public RabbitMqManagementClientTemplate(Client rabbitClient, String virtualHost) + throws JsonProcessingException { + this.rabbitClient = rabbitClient; + this.virtualHost = virtualHost; + rabbitClient.createVhost(virtualHost); + } + + public List getAnalyzerExchangesInfo() { + return ofNullable(rabbitClient.getExchanges(virtualHost)).map(client -> client.stream() + .filter(it -> it.getArguments().get(ANALYZER_KEY) != null) + .sorted(comparingInt(EXCHANGE_PRIORITY)) + .collect(Collectors.toList())) + .orElseThrow( + () -> new RuntimeException("Unable to resolve exchanges for key: " + ANALYZER_KEY)); + } } diff --git a/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClient.java b/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClient.java index b0e1940..80c0079 100644 --- a/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClient.java +++ b/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClient.java @@ -24,25 +24,27 @@ */ public interface IndexerServiceClient { - /** - * Remove documents with specified ids from index - * - * @param index Index to be cleaned - * @param ids Document ids to be deleted from index - * @return Amount of deleted logs - */ - Long cleanIndex(Long index, List ids); + /** + * Remove documents with specified ids from index + * + * @param index Index to be cleaned + * @param ids Document ids to be deleted from index + * @return Amount of deleted logs + */ + Long cleanIndex(Long index, List ids); - /** - * Remove documents from index by index and log time range. - * @param index Index to be cleaned. - */ - void removeFromIndexLessThanLogDate(Long index, LocalDateTime lessThanDate); + /** + * Remove documents from index by index and log time range. + * + * @param index Index to be cleaned. + */ + void removeFromIndexLessThanLogDate(Long index, LocalDateTime lessThanDate); - /** - * Remove documents from index by index and log time range. - * @param index Index to be cleaned - */ - void removeFromIndexLessThanLaunchDate(Long index, LocalDateTime lessThanDate); + /** + * Remove documents from index by index and log time range. + * + * @param index Index to be cleaned + */ + void removeFromIndexLessThanLaunchDate(Long index, LocalDateTime lessThanDate); } diff --git a/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java b/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java index fe17e0e..0af6c18 100644 --- a/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java +++ b/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java @@ -1,21 +1,20 @@ package com.epam.reportportal.analyzer.index; +import static com.epam.reportportal.analyzer.RabbitMqManagementClientTemplate.EXCHANGE_PRIORITY; + import com.epam.reportportal.analyzer.RabbitMqManagementClient; import com.epam.reportportal.model.index.CleanIndexByDateRangeRq; import com.epam.reportportal.model.index.CleanIndexRq; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.stereotype.Service; - import java.time.LocalDateTime; import java.util.AbstractMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import static com.epam.reportportal.analyzer.RabbitMqManagementClientTemplate.EXCHANGE_PRIORITY; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; /** * @author Pavel Bortnik @@ -23,55 +22,56 @@ @Service public class IndexerServiceClientImpl implements IndexerServiceClient { - private static final String CLEAN_ROUTE = "clean"; - private static final String CLEAN_BY_LOG_DATE_ROUTE = "remove_by_log_time"; - private static final String CLEAN_BY_LAUNCH_DATE_ROUTE = "remove_by_launch_start_time"; - private static final String EXCHANGE_NAME = "analyzer-default"; - // need to be in line with analyzer API, better to fix api and remove it in future. - private static final LocalDateTime OLDEST_DATE = LocalDateTime.now().minusYears(10L); + private static final String CLEAN_ROUTE = "clean"; + private static final String CLEAN_BY_LOG_DATE_ROUTE = "remove_by_log_time"; + private static final String CLEAN_BY_LAUNCH_DATE_ROUTE = "remove_by_launch_start_time"; + private static final String EXCHANGE_NAME = "analyzer-default"; + // need to be in line with analyzer API, better to fix api and remove it in future. + private static final LocalDateTime OLDEST_DATE = LocalDateTime.now().minusYears(10L); - private final RabbitMqManagementClient rabbitMqManagementClient; - private final RabbitTemplate rabbitTemplate; + private final RabbitMqManagementClient rabbitMqManagementClient; + private final RabbitTemplate rabbitTemplate; - @Autowired - public IndexerServiceClientImpl(RabbitMqManagementClient rabbitMqManagementClient, - @Qualifier("analyzerRabbitTemplate") RabbitTemplate rabbitTemplate) { - this.rabbitMqManagementClient = rabbitMqManagementClient; - this.rabbitTemplate = rabbitTemplate; - } + @Autowired + public IndexerServiceClientImpl(RabbitMqManagementClient rabbitMqManagementClient, + @Qualifier("analyzerRabbitTemplate") RabbitTemplate rabbitTemplate) { + this.rabbitMqManagementClient = rabbitMqManagementClient; + this.rabbitTemplate = rabbitTemplate; + } - @Override - public Long cleanIndex(Long index, List ids) { - final Map priorityToCleanedLogsCountMapping = rabbitMqManagementClient.getAnalyzerExchangesInfo() - .stream() - .collect(Collectors.toMap(EXCHANGE_PRIORITY::applyAsInt, - exchange -> rabbitTemplate.convertSendAndReceiveAsType(exchange.getName(), - CLEAN_ROUTE, - new CleanIndexRq(index, ids), - new ParameterizedTypeReference<>() { - } - ) - )); - return priorityToCleanedLogsCountMapping.entrySet() - .stream() - .min(Map.Entry.comparingByKey()) - .orElseGet(() -> new AbstractMap.SimpleEntry<>(0, 0L)) - .getValue(); - } + @Override + public Long cleanIndex(Long index, List ids) { + final Map priorityToCleanedLogsCountMapping = rabbitMqManagementClient.getAnalyzerExchangesInfo() + .stream() + .collect(Collectors.toMap(EXCHANGE_PRIORITY::applyAsInt, + exchange -> rabbitTemplate.convertSendAndReceiveAsType(exchange.getName(), + CLEAN_ROUTE, + new CleanIndexRq(index, ids), + new ParameterizedTypeReference<>() { + } + ) + )); + return priorityToCleanedLogsCountMapping.entrySet() + .stream() + .min(Map.Entry.comparingByKey()) + .orElseGet(() -> new AbstractMap.SimpleEntry<>(0, 0L)) + .getValue(); + } - @Override - public void removeFromIndexLessThanLogDate(Long index, LocalDateTime lessThanDate) { - sendRangeRemovingMessageToRoute(index, lessThanDate, CLEAN_BY_LOG_DATE_ROUTE); - } + @Override + public void removeFromIndexLessThanLogDate(Long index, LocalDateTime lessThanDate) { + sendRangeRemovingMessageToRoute(index, lessThanDate, CLEAN_BY_LOG_DATE_ROUTE); + } - @Override - public void removeFromIndexLessThanLaunchDate(Long index, LocalDateTime lessThanDate) { - sendRangeRemovingMessageToRoute(index, lessThanDate, CLEAN_BY_LAUNCH_DATE_ROUTE); - } + @Override + public void removeFromIndexLessThanLaunchDate(Long index, LocalDateTime lessThanDate) { + sendRangeRemovingMessageToRoute(index, lessThanDate, CLEAN_BY_LAUNCH_DATE_ROUTE); + } - private void sendRangeRemovingMessageToRoute(Long index, LocalDateTime lessThanDate, String route) { - CleanIndexByDateRangeRq message = new CleanIndexByDateRangeRq(index, OLDEST_DATE, lessThanDate); - rabbitTemplate.convertAndSend(EXCHANGE_NAME, route, message); - } + private void sendRangeRemovingMessageToRoute(Long index, LocalDateTime lessThanDate, + String route) { + CleanIndexByDateRangeRq message = new CleanIndexByDateRangeRq(index, OLDEST_DATE, lessThanDate); + rabbitTemplate.convertAndSend(EXCHANGE_NAME, route, message); + } } diff --git a/src/main/java/com/epam/reportportal/calculation/BatchProcessing.java b/src/main/java/com/epam/reportportal/calculation/BatchProcessing.java index 2481c97..72a2e3c 100644 --- a/src/main/java/com/epam/reportportal/calculation/BatchProcessing.java +++ b/src/main/java/com/epam/reportportal/calculation/BatchProcessing.java @@ -1,65 +1,66 @@ package com.epam.reportportal.calculation; -import org.springframework.scheduling.TaskScheduler; - import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ScheduledFuture; +import org.springframework.scheduling.TaskScheduler; /** * Batch processing, grouping in batch based on amount or time unit. + * * @param */ public abstract class BatchProcessing { - private List objectList; - private final TaskScheduler scheduler; - private volatile ScheduledFuture scheduledTask; - private int batchSize; - private long timeout; + private final TaskScheduler scheduler; + private List objectList; + private volatile ScheduledFuture scheduledTask; + private int batchSize; + private long timeout; - public BatchProcessing(int batchSize, long timeout, TaskScheduler scheduler) { - if (timeout < 0 || scheduler == null) { - throw new IllegalArgumentException("Timeout must be greater than 0 and scheduler must be not null"); - } - objectList = new ArrayList<>(); - this.batchSize = batchSize; - this.timeout = timeout; - this.scheduler = scheduler; - this.scheduledTask = this.scheduler.schedule(this::processAndSchedule, getNextTime()); - } + public BatchProcessing(int batchSize, long timeout, TaskScheduler scheduler) { + if (timeout < 0 || scheduler == null) { + throw new IllegalArgumentException( + "Timeout must be greater than 0 and scheduler must be not null"); + } + objectList = new ArrayList<>(); + this.batchSize = batchSize; + this.timeout = timeout; + this.scheduler = scheduler; + this.scheduledTask = this.scheduler.schedule(this::processAndSchedule, getNextTime()); + } - private Date getNextTime() { - return new Date(System.currentTimeMillis() + this.timeout); - } + private Date getNextTime() { + return new Date(System.currentTimeMillis() + this.timeout); + } - public void add(T message) { - synchronized (this) { - this.objectList.add(message); - if (this.objectList.size() >= this.batchSize) { - processAndSchedule(); - } - } - } + public void add(T message) { + synchronized (this) { + this.objectList.add(message); + if (this.objectList.size() >= this.batchSize) { + processAndSchedule(); + } + } + } - private void processAndSchedule() { - List copyObjectList; - synchronized (this) { - copyObjectList = new ArrayList<>(this.objectList); - this.objectList.clear(); - } + private void processAndSchedule() { + List copyObjectList; + synchronized (this) { + copyObjectList = new ArrayList<>(this.objectList); + this.objectList.clear(); + } - if (!copyObjectList.isEmpty()) { - process(copyObjectList); - } + if (!copyObjectList.isEmpty()) { + process(copyObjectList); + } - if (this.scheduledTask != null) { - this.scheduledTask.cancel(true); - } + if (this.scheduledTask != null) { + this.scheduledTask.cancel(true); + } - this.scheduledTask = this.scheduler.schedule(this::processAndSchedule, getNextTime()); - } + this.scheduledTask = this.scheduler.schedule(this::processAndSchedule, getNextTime()); + } - protected abstract void process(List objectList); + protected abstract void process(List objectList); } diff --git a/src/main/java/com/epam/reportportal/calculation/RetryCalculation.java b/src/main/java/com/epam/reportportal/calculation/RetryCalculation.java index 14466c9..64c5289 100644 --- a/src/main/java/com/epam/reportportal/calculation/RetryCalculation.java +++ b/src/main/java/com/epam/reportportal/calculation/RetryCalculation.java @@ -1,18 +1,18 @@ package com.epam.reportportal.calculation; -import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler; import org.springframework.stereotype.Component; @Component // example of usage public class RetryCalculation { - private final RetryProcessing retryProcessing; - public RetryCalculation() { - retryProcessing = new RetryProcessing(5, 3000, new DefaultManagedTaskScheduler()); - } + private final RetryProcessing retryProcessing; + + public RetryCalculation() { + retryProcessing = new RetryProcessing(5, 3000, new DefaultManagedTaskScheduler()); + } /* example of using fill be removed during integration @RabbitListener(queues = "some_queue") diff --git a/src/main/java/com/epam/reportportal/calculation/RetryProcessing.java b/src/main/java/com/epam/reportportal/calculation/RetryProcessing.java index 9fb3118..f52d919 100644 --- a/src/main/java/com/epam/reportportal/calculation/RetryProcessing.java +++ b/src/main/java/com/epam/reportportal/calculation/RetryProcessing.java @@ -1,26 +1,25 @@ package com.epam.reportportal.calculation; +import java.util.List; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.CollectionUtils; -import java.util.List; - /** * Class for retrying batching, possible Object will be converted to some specific object */ public class RetryProcessing extends BatchProcessing { - public RetryProcessing(int batchSize, long timeout, TaskScheduler scheduler) { - super(batchSize, timeout, scheduler); - } + public RetryProcessing(int batchSize, long timeout, TaskScheduler scheduler) { + super(batchSize, timeout, scheduler); + } - @Override - protected void process(List objectList) { - if (CollectionUtils.isEmpty(objectList)) { - System.out.println("Collection is empty"); - } else { - System.out.println("Processing..."); - objectList.forEach(System.out::println); - } + @Override + protected void process(List objectList) { + if (CollectionUtils.isEmpty(objectList)) { + System.out.println("Collection is empty"); + } else { + System.out.println("Processing..."); + objectList.forEach(System.out::println); } + } } diff --git a/src/main/java/com/epam/reportportal/config/DataSourceConfig.java b/src/main/java/com/epam/reportportal/config/DataSourceConfig.java index 64ab085..9163267 100644 --- a/src/main/java/com/epam/reportportal/config/DataSourceConfig.java +++ b/src/main/java/com/epam/reportportal/config/DataSourceConfig.java @@ -18,14 +18,13 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import javax.sql.DataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.transaction.annotation.EnableTransactionManagement; -import javax.sql.DataSource; - /** * @author Ihar Kahadouski */ @@ -34,9 +33,9 @@ @EnableTransactionManagement public class DataSourceConfig extends HikariConfig { - @Primary - @Bean - public DataSource dataSource() { - return new HikariDataSource(this); - } + @Primary + @Bean + public DataSource dataSource() { + return new HikariDataSource(this); + } } diff --git a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java index 74e15ef..9b15b5e 100644 --- a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java +++ b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java @@ -9,6 +9,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.inject.Module; +import java.util.Set; import org.jclouds.ContextBuilder; import org.jclouds.aws.s3.config.AWSS3HttpApiModule; import org.jclouds.blobstore.BlobStore; @@ -23,137 +24,142 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; -import java.util.Set; - @Configuration public class DataStorageConfig { - /** - * Amazon has a general work flow they publish that allows clients to always find the correct URL endpoint for a given bucket: - * 1) ask s3.amazonaws.com for the bucket location - * 2) use the url returned to make the container specific request (get/put, etc) - * Jclouds cache the results from the first getBucketLocation call and use that region-specific URL, as needed. - * In this custom implementation of {@link AWSS3HttpApiModule} we are providing location from environment variable, so that - * we don't need to make getBucketLocation call - */ - @ConfiguresHttpApi - private static class CustomBucketToRegionModule extends AWSS3HttpApiModule { - private final String region; - - public CustomBucketToRegionModule(String region) { - this.region = region; - } - - @Override - @SuppressWarnings("Guava") - protected CacheLoader> bucketToRegion(Supplier> regionSupplier, S3Client client) { - Set regions = regionSupplier.get(); - if (regions.isEmpty()) { - return new CacheLoader<>() { - - @Override - @SuppressWarnings({ "Guava", "NullableProblems" }) - public Optional load(String bucket) { - if (CustomBucketToRegionModule.this.region != null) { - return Optional.of(CustomBucketToRegionModule.this.region); - } - return Optional.absent(); - } - - @Override - public String toString() { - return "noRegions()"; - } - }; - } else if (regions.size() == 1) { - final String onlyRegion = Iterables.getOnlyElement(regions); - return new CacheLoader<>() { - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - final Optional onlyRegionOption = Optional.of(onlyRegion); - - @Override - @SuppressWarnings("NullableProblems") - public Optional load(String bucket) { - if (CustomBucketToRegionModule.this.region != null) { - return Optional.of(CustomBucketToRegionModule.this.region); - } - return onlyRegionOption; - } - - @Override - public String toString() { - return "onlyRegion(" + onlyRegion + ")"; - } - }; - } else { - return new CacheLoader<>() { - @Override - @SuppressWarnings("NullableProblems") - public Optional load(String bucket) { - if (CustomBucketToRegionModule.this.region != null) { - return Optional.of(CustomBucketToRegionModule.this.region); - } - try { - return Optional.fromNullable(client.getBucketLocation(bucket)); - } catch (ContainerNotFoundException e) { - return Optional.absent(); - } - } - - @Override - public String toString() { - return "bucketToRegion()"; - } - }; - } - } - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") - public DataStorageService localDataStore(@Value("${datastore.default.path:/data/store}") String storagePath) { - return new LocalDataStorageService(storagePath); - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") - public BlobStore minioBlobStore(@Value("${datastore.minio.accessKey}") String accessKey, - @Value("${datastore.minio.secretKey}") String secretKey, @Value("${datastore.minio.endpoint}") String endpoint) { - - BlobStoreContext blobStoreContext = ContextBuilder.newBuilder("s3") - .endpoint(endpoint) - .credentials(accessKey, secretKey) - .buildView(BlobStoreContext.class); - - return blobStoreContext.getBlobStore(); - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") - public DataStorageService minioDataStore(@Autowired BlobStore blobStore, @Value("${datastore.minio.bucketPrefix}") String bucketPrefix, - @Value("${datastore.minio.defaultBucketName}") String defaultBucketName) { - return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName); - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") - public BlobStore blobStore(@Value("${datastore.s3.accessKey}") String accessKey, @Value("${datastore.s3.secretKey}") String secretKey, - @Value("${datastore.s3.region}") String region) { - Iterable modules = ImmutableSet.of(new CustomBucketToRegionModule(region)); - - BlobStoreContext blobStoreContext = ContextBuilder.newBuilder("aws-s3") - .modules(modules) - .credentials(accessKey, secretKey) - .buildView(BlobStoreContext.class); - - return blobStoreContext.getBlobStore(); - } - - @Bean - @Primary - @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") - public DataStorageService s3DataStore(@Autowired BlobStore blobStore, @Value("${datastore.s3.bucketPrefix}") String bucketPrefix, - @Value("${datastore.s3.defaultBucketName}") String defaultBucketName) { - return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName); - } + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") + public DataStorageService localDataStore( + @Value("${datastore.default.path:/data/store}") String storagePath) { + return new LocalDataStorageService(storagePath); + } + + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") + public BlobStore minioBlobStore(@Value("${datastore.minio.accessKey}") String accessKey, + @Value("${datastore.minio.secretKey}") String secretKey, + @Value("${datastore.minio.endpoint}") String endpoint) { + + BlobStoreContext blobStoreContext = ContextBuilder.newBuilder("s3") + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .buildView(BlobStoreContext.class); + + return blobStoreContext.getBlobStore(); + } + + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") + public DataStorageService minioDataStore(@Autowired BlobStore blobStore, + @Value("${datastore.minio.bucketPrefix}") String bucketPrefix, + @Value("${datastore.minio.defaultBucketName}") String defaultBucketName) { + return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName); + } + + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") + public BlobStore blobStore(@Value("${datastore.s3.accessKey}") String accessKey, + @Value("${datastore.s3.secretKey}") String secretKey, + @Value("${datastore.s3.region}") String region) { + Iterable modules = ImmutableSet.of(new CustomBucketToRegionModule(region)); + + BlobStoreContext blobStoreContext = ContextBuilder.newBuilder("aws-s3") + .modules(modules) + .credentials(accessKey, secretKey) + .buildView(BlobStoreContext.class); + + return blobStoreContext.getBlobStore(); + } + + @Bean + @Primary + @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") + public DataStorageService s3DataStore(@Autowired BlobStore blobStore, + @Value("${datastore.s3.bucketPrefix}") String bucketPrefix, + @Value("${datastore.s3.defaultBucketName}") String defaultBucketName) { + return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName); + } + + /** + * Amazon has a general work flow they publish that allows clients to always find the correct URL + * endpoint for a given bucket: 1) ask s3.amazonaws.com for the bucket location 2) use the url + * returned to make the container specific request (get/put, etc) Jclouds cache the results from + * the first getBucketLocation call and use that region-specific URL, as needed. In this custom + * implementation of {@link AWSS3HttpApiModule} we are providing location from environment + * variable, so that we don't need to make getBucketLocation call + */ + @ConfiguresHttpApi + private static class CustomBucketToRegionModule extends AWSS3HttpApiModule { + + private final String region; + + public CustomBucketToRegionModule(String region) { + this.region = region; + } + + @Override + @SuppressWarnings("Guava") + protected CacheLoader> bucketToRegion( + Supplier> regionSupplier, S3Client client) { + Set regions = regionSupplier.get(); + if (regions.isEmpty()) { + return new CacheLoader<>() { + + @Override + @SuppressWarnings({"Guava", "NullableProblems"}) + public Optional load(String bucket) { + if (CustomBucketToRegionModule.this.region != null) { + return Optional.of(CustomBucketToRegionModule.this.region); + } + return Optional.absent(); + } + + @Override + public String toString() { + return "noRegions()"; + } + }; + } else if (regions.size() == 1) { + final String onlyRegion = Iterables.getOnlyElement(regions); + return new CacheLoader<>() { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + final Optional onlyRegionOption = Optional.of(onlyRegion); + + @Override + @SuppressWarnings("NullableProblems") + public Optional load(String bucket) { + if (CustomBucketToRegionModule.this.region != null) { + return Optional.of(CustomBucketToRegionModule.this.region); + } + return onlyRegionOption; + } + + @Override + public String toString() { + return "onlyRegion(" + onlyRegion + ")"; + } + }; + } else { + return new CacheLoader<>() { + @Override + @SuppressWarnings("NullableProblems") + public Optional load(String bucket) { + if (CustomBucketToRegionModule.this.region != null) { + return Optional.of(CustomBucketToRegionModule.this.region); + } + try { + return Optional.fromNullable(client.getBucketLocation(bucket)); + } catch (ContainerNotFoundException e) { + return Optional.absent(); + } + } + + @Override + public String toString() { + return "bucketToRegion()"; + } + }; + } + } + } } diff --git a/src/main/java/com/epam/reportportal/config/ExecutorConfig.java b/src/main/java/com/epam/reportportal/config/ExecutorConfig.java index 9259e31..4f4c69f 100644 --- a/src/main/java/com/epam/reportportal/config/ExecutorConfig.java +++ b/src/main/java/com/epam/reportportal/config/ExecutorConfig.java @@ -12,14 +12,15 @@ @Configuration public class ExecutorConfig { - @Bean - public TaskExecutor projectAllocatedStorageExecutor(@Value("${rp.environment.variable.executor.pool.storage.project.core}") Integer corePoolSize, - @Value("${rp.environment.variable.executor.pool.storage.project.max}") Integer maxPoolSize) { - final ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); - threadPoolTaskExecutor.setCorePoolSize(corePoolSize); - threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize); - threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true); - threadPoolTaskExecutor.setThreadNamePrefix("prj-alloc-storage"); - return threadPoolTaskExecutor; - } + @Bean + public TaskExecutor projectAllocatedStorageExecutor( + @Value("${rp.environment.variable.executor.pool.storage.project.core}") Integer corePoolSize, + @Value("${rp.environment.variable.executor.pool.storage.project.max}") Integer maxPoolSize) { + final ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setCorePoolSize(corePoolSize); + threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize); + threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true); + threadPoolTaskExecutor.setThreadNamePrefix("prj-alloc-storage"); + return threadPoolTaskExecutor; + } } diff --git a/src/main/java/com/epam/reportportal/config/ShedLockConfig.java b/src/main/java/com/epam/reportportal/config/ShedLockConfig.java index 71f8a8f..e699804 100644 --- a/src/main/java/com/epam/reportportal/config/ShedLockConfig.java +++ b/src/main/java/com/epam/reportportal/config/ShedLockConfig.java @@ -1,5 +1,6 @@ package com.epam.reportportal.config; +import javax.sql.DataSource; import net.javacrumbs.shedlock.core.LockProvider; import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; @@ -8,8 +9,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.EnableScheduling; -import javax.sql.DataSource; - /** * @author Pavel Bortnik */ @@ -18,11 +17,11 @@ @EnableSchedulerLock(defaultLockAtMostFor = "15m") public class ShedLockConfig { - @Bean - public LockProvider lockProvider(DataSource dataSource) { - return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder() - .withJdbcTemplate(new JdbcTemplate(dataSource)) - .usingDbTime() - .build()); - } + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .usingDbTime() + .build()); + } } diff --git a/src/main/java/com/epam/reportportal/config/rabbit/AnalyzerRabbitMqConfiguration.java b/src/main/java/com/epam/reportportal/config/rabbit/AnalyzerRabbitMqConfiguration.java index 147faab..71e0a6f 100644 --- a/src/main/java/com/epam/reportportal/config/rabbit/AnalyzerRabbitMqConfiguration.java +++ b/src/main/java/com/epam/reportportal/config/rabbit/AnalyzerRabbitMqConfiguration.java @@ -21,6 +21,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.rabbitmq.http.client.Client; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -33,10 +36,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; - /** * @author Pavel Bortnik */ @@ -44,41 +43,43 @@ @Configuration public class AnalyzerRabbitMqConfiguration { - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - @Autowired - public AnalyzerRabbitMqConfiguration(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } + @Autowired + public AnalyzerRabbitMqConfiguration(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } - @Bean - public MessageConverter jsonMessageConverter() { - return new Jackson2JsonMessageConverter(objectMapper); - } + @Bean + public MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(objectMapper); + } - @Bean - public RabbitMqManagementClient managementTemplate(@Value("${rp.amqp.api-address}") String address, - @Value("${rp.amqp.analyzer-vhost}") String virtualHost) - throws MalformedURLException, URISyntaxException, JsonProcessingException { - final Client rabbitClient = new Client(address); - return new RabbitMqManagementClientTemplate(rabbitClient, virtualHost); - } + @Bean + public RabbitMqManagementClient managementTemplate( + @Value("${rp.amqp.api-address}") String address, + @Value("${rp.amqp.analyzer-vhost}") String virtualHost) + throws MalformedURLException, URISyntaxException, JsonProcessingException { + final Client rabbitClient = new Client(address); + return new RabbitMqManagementClientTemplate(rabbitClient, virtualHost); + } - @Bean(name = "analyzerConnectionFactory") - public ConnectionFactory analyzerConnectionFactory(@Value("${rp.amqp.addresses}") URI addresses, - @Value("${rp.amqp.analyzer-vhost}") String virtualHost) { - CachingConnectionFactory factory = new CachingConnectionFactory(addresses); - factory.setVirtualHost(virtualHost); - return factory; - } + @Bean(name = "analyzerConnectionFactory") + public ConnectionFactory analyzerConnectionFactory(@Value("${rp.amqp.addresses}") URI addresses, + @Value("${rp.amqp.analyzer-vhost}") String virtualHost) { + CachingConnectionFactory factory = new CachingConnectionFactory(addresses); + factory.setVirtualHost(virtualHost); + return factory; + } - @Bean(name = "analyzerRabbitTemplate") - public RabbitTemplate analyzerRabbitTemplate(@Autowired @Qualifier("analyzerConnectionFactory") ConnectionFactory connectionFactory, - @Value("${rp.amqp.reply-timeout}") long replyTimeout) { - RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); - rabbitTemplate.setMessageConverter(jsonMessageConverter()); - rabbitTemplate.setReplyTimeout(replyTimeout); - return rabbitTemplate; - } + @Bean(name = "analyzerRabbitTemplate") + public RabbitTemplate analyzerRabbitTemplate( + @Autowired @Qualifier("analyzerConnectionFactory") ConnectionFactory connectionFactory, + @Value("${rp.amqp.reply-timeout}") long replyTimeout) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(jsonMessageConverter()); + rabbitTemplate.setReplyTimeout(replyTimeout); + return rabbitTemplate; + } } diff --git a/src/main/java/com/epam/reportportal/config/rabbit/BackgroundProcessingConfiguration.java b/src/main/java/com/epam/reportportal/config/rabbit/BackgroundProcessingConfiguration.java index f101c78..2e05a11 100644 --- a/src/main/java/com/epam/reportportal/config/rabbit/BackgroundProcessingConfiguration.java +++ b/src/main/java/com/epam/reportportal/config/rabbit/BackgroundProcessingConfiguration.java @@ -11,28 +11,30 @@ /** * Rabbitmq background queue configuration. + * * @author Maksim Antonov */ @Configuration @ConditionalOnProperty(prefix = "rp.elasticsearch", name = "host") public class BackgroundProcessingConfiguration { - public static final String LOG_MESSAGE_SAVING_QUEUE_NAME = "log_message_saving"; - public static final String LOG_MESSAGE_SAVING_ROUTING_KEY = "log_message_saving"; - public static final String PROCESSING_EXCHANGE_NAME = "processing"; - @Bean - Queue logMessageSavingQueue() { - return new Queue(LOG_MESSAGE_SAVING_QUEUE_NAME); - } + public static final String LOG_MESSAGE_SAVING_QUEUE_NAME = "log_message_saving"; + public static final String LOG_MESSAGE_SAVING_ROUTING_KEY = "log_message_saving"; + public static final String PROCESSING_EXCHANGE_NAME = "processing"; - @Bean - DirectExchange exchangeProcessing() { - return new DirectExchange(PROCESSING_EXCHANGE_NAME); - } + @Bean + Queue logMessageSavingQueue() { + return new Queue(LOG_MESSAGE_SAVING_QUEUE_NAME); + } - @Bean - Binding bindingSavingLogs(@Qualifier("logMessageSavingQueue") Queue queue, - @Qualifier("exchangeProcessing") DirectExchange exchange) { - return BindingBuilder.bind(queue).to(exchange).with(LOG_MESSAGE_SAVING_ROUTING_KEY); - } + @Bean + DirectExchange exchangeProcessing() { + return new DirectExchange(PROCESSING_EXCHANGE_NAME); + } + + @Bean + Binding bindingSavingLogs(@Qualifier("logMessageSavingQueue") Queue queue, + @Qualifier("exchangeProcessing") DirectExchange exchange) { + return BindingBuilder.bind(queue).to(exchange).with(LOG_MESSAGE_SAVING_ROUTING_KEY); + } } diff --git a/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java b/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java index 25bea9e..cb74095 100644 --- a/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java +++ b/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java @@ -16,6 +16,7 @@ package com.epam.reportportal.config.rabbit; +import java.net.URI; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -28,8 +29,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.net.URI; - /** * @author Pavel Bortnik */ @@ -37,35 +36,38 @@ @Configuration public class ProcessingRabbitMqConfiguration { - @Bean(name = "processingConnectionFactory") - public ConnectionFactory processingConnectionFactory(@Value("${rp.amqp.addresses}") URI addresses, - @Value("${rp.amqp.base-vhost}") String virtualHost) { - CachingConnectionFactory factory = new CachingConnectionFactory(addresses); - factory.setVirtualHost(virtualHost); - return factory; - } + @Bean(name = "processingConnectionFactory") + public ConnectionFactory processingConnectionFactory(@Value("${rp.amqp.addresses}") URI addresses, + @Value("${rp.amqp.base-vhost}") String virtualHost) { + CachingConnectionFactory factory = new CachingConnectionFactory(addresses); + factory.setVirtualHost(virtualHost); + return factory; + } - @Bean - public SimpleRabbitListenerContainerFactory processingRabbitListenerContainerFactory( - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory, MessageConverter jsonMessageConverter, - @Value("${rp.amqp.maxLogConsumer}") int maxLogConsumer) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory); - factory.setMaxConcurrentConsumers(maxLogConsumer); - factory.setMessageConverter(jsonMessageConverter); - return factory; - } + @Bean + public SimpleRabbitListenerContainerFactory processingRabbitListenerContainerFactory( + @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory, + MessageConverter jsonMessageConverter, + @Value("${rp.amqp.maxLogConsumer}") int maxLogConsumer) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setMaxConcurrentConsumers(maxLogConsumer); + factory.setMessageConverter(jsonMessageConverter); + return factory; + } - @Bean - public RabbitAdmin processingRabbitAdmin(@Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { - return new RabbitAdmin(connectionFactory); - } + @Bean + public RabbitAdmin processingRabbitAdmin( + @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { + return new RabbitAdmin(connectionFactory); + } - @Bean - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( - SimpleRabbitListenerContainerFactoryConfigurer configurer, @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - configurer.configure(factory, connectionFactory); - return factory; - } + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( + SimpleRabbitListenerContainerFactoryConfigurer configurer, + @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + configurer.configure(factory, connectionFactory); + return factory; + } } diff --git a/src/main/java/com/epam/reportportal/elastic/ElasticSearchClient.java b/src/main/java/com/epam/reportportal/elastic/ElasticSearchClient.java index cf3ee22..b81ed57 100644 --- a/src/main/java/com/epam/reportportal/elastic/ElasticSearchClient.java +++ b/src/main/java/com/epam/reportportal/elastic/ElasticSearchClient.java @@ -1,16 +1,16 @@ package com.epam.reportportal.elastic; import com.epam.reportportal.log.LogMessage; - import java.util.List; /** * Client interface to work with Elasticsearch. + * * @author Maksim Antonov */ public interface ElasticSearchClient { - void save(List logMessageList); + void save(List logMessageList); - void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId); + void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId); } diff --git a/src/main/java/com/epam/reportportal/elastic/EmptyElasticSearchClient.java b/src/main/java/com/epam/reportportal/elastic/EmptyElasticSearchClient.java index 1040a51..11bc6ab 100644 --- a/src/main/java/com/epam/reportportal/elastic/EmptyElasticSearchClient.java +++ b/src/main/java/com/epam/reportportal/elastic/EmptyElasticSearchClient.java @@ -1,22 +1,22 @@ package com.epam.reportportal.elastic; import com.epam.reportportal.log.LogMessage; -import org.springframework.stereotype.Service; - import java.util.List; +import org.springframework.stereotype.Service; /** * Empty client to work with Elasticsearch. + * * @author Maksim Antonov */ @Service -public class EmptyElasticSearchClient implements ElasticSearchClient { +public class EmptyElasticSearchClient implements ElasticSearchClient { - @Override - public void save(List logMessageList) { - } + @Override + public void save(List logMessageList) { + } - @Override - public void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId) { - } + @Override + public void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId) { + } } diff --git a/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java b/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java index d68bdf8..8bfc59b 100644 --- a/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java +++ b/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java @@ -1,6 +1,9 @@ package com.epam.reportportal.elastic; import com.epam.reportportal.log.LogMessage; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,12 +18,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestTemplate; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Simple client to work with Elasticsearch. + * * @author Maksim Antonov */ @Primary @@ -28,85 +28,88 @@ @ConditionalOnProperty(prefix = "rp.elasticsearch", name = "host") public class SimpleElasticSearchClient implements ElasticSearchClient { - protected final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticSearchClient.class); - - private final String host; - private final RestTemplate restTemplate; + protected final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticSearchClient.class); - public SimpleElasticSearchClient(@Value("${rp.elasticsearch.host}") String host, - @Value("${rp.elasticsearch.username}") String username, - @Value("${rp.elasticsearch.password}") String password) { - restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + private final String host; + private final RestTemplate restTemplate; - this.host = host; - } - - @Override - public void save(List logMessageList) { - if (CollectionUtils.isEmpty(logMessageList)) return; - Map logsByIndex = new HashMap<>(); - - String create = "{\"create\":{ }}\n"; - - logMessageList.forEach(logMessage -> { - String indexName = "logs-reportportal-" + logMessage.getProjectId(); - String logCreateBody = create + convertToJson(logMessage) + "\n"; + public SimpleElasticSearchClient(@Value("${rp.elasticsearch.host}") String host, + @Value("${rp.elasticsearch.username}") String username, + @Value("${rp.elasticsearch.password}") String password) { + restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); - if (logsByIndex.containsKey(indexName)) { - logsByIndex.put(indexName, logsByIndex.get(indexName) + logCreateBody); - } else { - logsByIndex.put(indexName, logCreateBody); - } - }); + this.host = host; + } - logsByIndex.forEach((indexName, body) -> { - restTemplate.put(host + "/" + indexName + "/_bulk?refresh", getStringHttpEntity(body)); - }); + @Override + public void save(List logMessageList) { + if (CollectionUtils.isEmpty(logMessageList)) { + return; } - - @Override - public void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId) { - String indexName = "logs-reportportal-" + projectId; - try { - JSONObject deleteByLaunch = getDeleteLaunchJson(launchId); - HttpEntity deleteRequest = getStringHttpEntity(deleteByLaunch.toString()); - - restTemplate.postForObject(host + "/" + indexName + "/_delete_by_query", deleteRequest, JSONObject.class); - } catch (Exception exception) { - // to avoid checking of exists stream or not - LOGGER.info("DELETE logs from stream ES error " + indexName + " " + exception.getMessage()); - } + Map logsByIndex = new HashMap<>(); + + String create = "{\"create\":{ }}\n"; + + logMessageList.forEach(logMessage -> { + String indexName = "logs-reportportal-" + logMessage.getProjectId(); + String logCreateBody = create + convertToJson(logMessage) + "\n"; + + if (logsByIndex.containsKey(indexName)) { + logsByIndex.put(indexName, logsByIndex.get(indexName) + logCreateBody); + } else { + logsByIndex.put(indexName, logCreateBody); + } + }); + + logsByIndex.forEach((indexName, body) -> { + restTemplate.put(host + "/" + indexName + "/_bulk?refresh", getStringHttpEntity(body)); + }); + } + + @Override + public void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId) { + String indexName = "logs-reportportal-" + projectId; + try { + JSONObject deleteByLaunch = getDeleteLaunchJson(launchId); + HttpEntity deleteRequest = getStringHttpEntity(deleteByLaunch.toString()); + + restTemplate.postForObject(host + "/" + indexName + "/_delete_by_query", deleteRequest, + JSONObject.class); + } catch (Exception exception) { + // to avoid checking of exists stream or not + LOGGER.info("DELETE logs from stream ES error " + indexName + " " + exception.getMessage()); } + } - private JSONObject getDeleteLaunchJson(Long launchId) { - JSONObject match = new JSONObject(); - match.put("launchId", launchId); + private JSONObject getDeleteLaunchJson(Long launchId) { + JSONObject match = new JSONObject(); + match.put("launchId", launchId); - JSONObject query = new JSONObject(); - query.put("match", match); + JSONObject query = new JSONObject(); + query.put("match", match); - JSONObject deleteByLaunch = new JSONObject(); - deleteByLaunch.put("query", query); + JSONObject deleteByLaunch = new JSONObject(); + deleteByLaunch.put("query", query); - return deleteByLaunch; - } + return deleteByLaunch; + } - private JSONObject convertToJson(LogMessage logMessage) { - JSONObject personJsonObject = new JSONObject(); - personJsonObject.put("id", logMessage.getId()); - personJsonObject.put("message", logMessage.getLogMessage()); - personJsonObject.put("itemId", logMessage.getItemId()); - personJsonObject.put("@timestamp", logMessage.getLogTime()); - personJsonObject.put("launchId", logMessage.getLaunchId()); + private JSONObject convertToJson(LogMessage logMessage) { + JSONObject personJsonObject = new JSONObject(); + personJsonObject.put("id", logMessage.getId()); + personJsonObject.put("message", logMessage.getLogMessage()); + personJsonObject.put("itemId", logMessage.getItemId()); + personJsonObject.put("@timestamp", logMessage.getLogTime()); + personJsonObject.put("launchId", logMessage.getLaunchId()); - return personJsonObject; - } + return personJsonObject; + } - private HttpEntity getStringHttpEntity(String body) { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); + private HttpEntity getStringHttpEntity(String body) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); - return new HttpEntity<>(body, headers); - } + return new HttpEntity<>(body, headers); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/BaseJob.java b/src/main/java/com/epam/reportportal/jobs/BaseJob.java index 98fc4ad..77d240d 100644 --- a/src/main/java/com/epam/reportportal/jobs/BaseJob.java +++ b/src/main/java/com/epam/reportportal/jobs/BaseJob.java @@ -5,22 +5,23 @@ import org.springframework.jdbc.core.JdbcTemplate; public abstract class BaseJob { - protected JdbcTemplate jdbcTemplate; - protected final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); - public BaseJob(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } + protected final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + protected JdbcTemplate jdbcTemplate; - protected void logStart() { - LOGGER.info("Job {} has been started.", this.getClass().getSimpleName()); - } + public BaseJob(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } - protected void logFinish(Object result) { - LOGGER.info("Job {} has been finished. Result {}", this.getClass().getSimpleName(), result); - } + protected void logStart() { + LOGGER.info("Job {} has been started.", this.getClass().getSimpleName()); + } - protected void logFinish() { - logFinish(null); - } + protected void logFinish(Object result) { + LOGGER.info("Job {} has been finished. Result {}", this.getClass().getSimpleName(), result); + } + + protected void logFinish() { + logFinish(null); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java b/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java index aa775be..6ea4088 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java @@ -1,42 +1,43 @@ package com.epam.reportportal.jobs.clean; -import com.epam.reportportal.jobs.BaseJob; -import org.springframework.jdbc.core.JdbcTemplate; +import static java.time.Duration.ofSeconds; +import com.epam.reportportal.jobs.BaseJob; import java.time.Duration; import java.util.HashMap; import java.util.Map; - -import static java.time.Duration.ofSeconds; +import org.springframework.jdbc.core.JdbcTemplate; /** * @author Pavel Bortnik */ public class BaseCleanJob extends BaseJob { - protected static final String KEEP_LAUNCHES = "job.keepLaunches"; - protected static final String KEEP_LOGS = "job.keepLogs"; - protected static final String KEEP_SCREENSHOTS = "job.keepScreenshots"; - - protected final String SELECT_PROJECTS_ATTRIBUTES = "SELECT pa.project_id AS id, pa.value AS attribute_value FROM project_attribute pa " - + "JOIN attribute a ON pa.attribute_id = a.id WHERE a.name = ? AND pa.value != '0' AND TRIM(pa.value) != '';"; - - public BaseCleanJob(JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - protected Map getProjectsWithAttribute(String attributeKey) { - return jdbcTemplate.query(SELECT_PROJECTS_ATTRIBUTES, rs -> { - Map result = new HashMap<>(); - while (rs.next()) { - String attributeValue = rs.getString("attribute_value"); - try { - result.put(rs.getLong("id"), ofSeconds(Long.parseLong(attributeValue))); - } catch (NumberFormatException e) { - LOGGER.error("Bad attribute value format for {}. Expected a number, actual is {}", attributeKey, attributeValue); - } - } - return result; - }, attributeKey); - } + protected static final String KEEP_LAUNCHES = "job.keepLaunches"; + protected static final String KEEP_LOGS = "job.keepLogs"; + protected static final String KEEP_SCREENSHOTS = "job.keepScreenshots"; + + protected final String SELECT_PROJECTS_ATTRIBUTES = + "SELECT pa.project_id AS id, pa.value AS attribute_value FROM project_attribute pa " + + "JOIN attribute a ON pa.attribute_id = a.id WHERE a.name = ? AND pa.value != '0' AND TRIM(pa.value) != '';"; + + public BaseCleanJob(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + protected Map getProjectsWithAttribute(String attributeKey) { + return jdbcTemplate.query(SELECT_PROJECTS_ATTRIBUTES, rs -> { + Map result = new HashMap<>(); + while (rs.next()) { + String attributeValue = rs.getString("attribute_value"); + try { + result.put(rs.getLong("id"), ofSeconds(Long.parseLong(attributeValue))); + } catch (NumberFormatException e) { + LOGGER.error("Bad attribute value format for {}. Expected a number, actual is {}", + attributeKey, attributeValue); + } + } + return result; + }, attributeKey); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java index 34f9bc8..6c35c30 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java @@ -1,47 +1,47 @@ package com.epam.reportportal.jobs.clean; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.concurrent.atomic.AtomicInteger; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.concurrent.atomic.AtomicInteger; - /** - * Moving data from attachment table to attachment_deletion by storage policy - * for future deletion that attachment from storage by another job. + * Moving data from attachment table to attachment_deletion by storage policy for future deletion + * that attachment from storage by another job. * * @author Pavel Bortnik */ @Service public class CleanAttachmentJob extends BaseCleanJob { - private static final String MOVING_QUERY = - "WITH moved_rows AS (DELETE FROM attachment WHERE project_id = ? AND creation_date <= ?::TIMESTAMP RETURNING *) " - + "INSERT INTO attachment_deletion (id, file_id, thumbnail_id, creation_attachment_date, deletion_date) " - + "SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows;"; + private static final String MOVING_QUERY = + "WITH moved_rows AS (DELETE FROM attachment WHERE project_id = ? AND creation_date <= ?::TIMESTAMP RETURNING *) " + + "INSERT INTO attachment_deletion (id, file_id, thumbnail_id, creation_attachment_date, deletion_date) " + + "SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows;"; - public CleanAttachmentJob(JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } + public CleanAttachmentJob(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } - @Scheduled(cron = "${rp.environment.variable.clean.attachment.cron}") - @SchedulerLock(name = "cleanAttachment", lockAtMostFor = "24h") - public void execute() { - moveAttachments(); - } + @Scheduled(cron = "${rp.environment.variable.clean.attachment.cron}") + @SchedulerLock(name = "cleanAttachment", lockAtMostFor = "24h") + public void execute() { + moveAttachments(); + } - void moveAttachments() { - logStart(); - AtomicInteger counter = new AtomicInteger(0); - getProjectsWithAttribute(KEEP_SCREENSHOTS).forEach((projectId, duration) -> { - LocalDateTime lessThanDate = LocalDateTime.now(ZoneOffset.UTC).minus(duration); - int movedCount = jdbcTemplate.update(MOVING_QUERY, projectId, lessThanDate); - counter.addAndGet(movedCount); - LOGGER.info("Moved {} attachments to the deletion table for project {}, lessThanDate {} ", movedCount, projectId, lessThanDate); - }); - logFinish(counter.get()); - } + void moveAttachments() { + logStart(); + AtomicInteger counter = new AtomicInteger(0); + getProjectsWithAttribute(KEEP_SCREENSHOTS).forEach((projectId, duration) -> { + LocalDateTime lessThanDate = LocalDateTime.now(ZoneOffset.UTC).minus(duration); + int movedCount = jdbcTemplate.update(MOVING_QUERY, projectId, lessThanDate); + counter.addAndGet(movedCount); + LOGGER.info("Moved {} attachments to the deletion table for project {}, lessThanDate {} ", + movedCount, projectId, lessThanDate); + }); + logFinish(counter.get()); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanLaunchJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanLaunchJob.java index 2904a19..5413e19 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanLaunchJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanLaunchJob.java @@ -2,16 +2,7 @@ import com.epam.reportportal.analyzer.index.IndexerServiceClient; import com.epam.reportportal.elastic.ElasticSearchClient; -import com.epam.reportportal.events.ElementsDeletedEvent; import com.google.common.collect.Lists; -import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; @@ -19,6 +10,13 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; /** * @author Pavel Bortnik @@ -26,104 +24,110 @@ @Service public class CleanLaunchJob extends BaseCleanJob { - private static final String IDS_PARAM = "ids"; - private static final String PROJECT_ID_PARAM = "projectId"; - private static final String START_TIME_PARAM = "startTime"; - private final Integer batchSize; - - private static final String SELECT_LAUNCH_ID_QUERY = "SELECT id FROM launch WHERE project_id = :projectId AND start_time <= :startTime::TIMESTAMP;"; - private static final String DELETE_CLUSTER_QUERY = "DELETE FROM clusters WHERE clusters.launch_id IN (:ids);"; - private static final String DELETE_LAUNCH_QUERY = "DELETE FROM launch WHERE id IN (:ids);"; - - private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - private final CleanLogJob cleanLogJob; - private final IndexerServiceClient indexerServiceClient; - private final ApplicationEventPublisher eventPublisher; - private final ElasticSearchClient elasticSearchClient; + private static final String IDS_PARAM = "ids"; + private static final String PROJECT_ID_PARAM = "projectId"; + private static final String START_TIME_PARAM = "startTime"; + private static final String SELECT_LAUNCH_ID_QUERY = "SELECT id FROM launch WHERE project_id = :projectId AND start_time <= :startTime::TIMESTAMP;"; + private static final String DELETE_CLUSTER_QUERY = "DELETE FROM clusters WHERE clusters.launch_id IN (:ids);"; + private static final String DELETE_LAUNCH_QUERY = "DELETE FROM launch WHERE id IN (:ids);"; + private final Integer batchSize; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final CleanLogJob cleanLogJob; + private final IndexerServiceClient indexerServiceClient; + private final ApplicationEventPublisher eventPublisher; + private final ElasticSearchClient elasticSearchClient; - public CleanLaunchJob(@Value("${rp.environment.variable.elements-counter.batch-size}") Integer batchSize, JdbcTemplate jdbcTemplate, - NamedParameterJdbcTemplate namedParameterJdbcTemplate, CleanLogJob cleanLogJob, IndexerServiceClient indexerServiceClient, - ApplicationEventPublisher eventPublisher, ElasticSearchClient elasticSearchClient) { - super(jdbcTemplate); - this.batchSize = batchSize; - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - this.cleanLogJob = cleanLogJob; - this.indexerServiceClient = indexerServiceClient; - this.eventPublisher = eventPublisher; - this.elasticSearchClient = elasticSearchClient; - } + public CleanLaunchJob( + @Value("${rp.environment.variable.elements-counter.batch-size}") Integer batchSize, + JdbcTemplate jdbcTemplate, + NamedParameterJdbcTemplate namedParameterJdbcTemplate, CleanLogJob cleanLogJob, + IndexerServiceClient indexerServiceClient, + ApplicationEventPublisher eventPublisher, ElasticSearchClient elasticSearchClient) { + super(jdbcTemplate); + this.batchSize = batchSize; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + this.cleanLogJob = cleanLogJob; + this.indexerServiceClient = indexerServiceClient; + this.eventPublisher = eventPublisher; + this.elasticSearchClient = elasticSearchClient; + } - @Scheduled(cron = "${rp.environment.variable.clean.launch.cron}") - @SchedulerLock(name = "cleanLaunch", lockAtMostFor = "24h") - public void execute() { - removeLaunches(); - cleanLogJob.removeLogs(); - } + @Scheduled(cron = "${rp.environment.variable.clean.launch.cron}") + @SchedulerLock(name = "cleanLaunch", lockAtMostFor = "24h") + public void execute() { + removeLaunches(); + cleanLogJob.removeLogs(); + } - private void removeLaunches() { - logStart(); - AtomicInteger counter = new AtomicInteger(0); - getProjectsWithAttribute(KEEP_LAUNCHES).forEach((projectId, duration) -> { - final LocalDateTime lessThanDate = LocalDateTime.now(ZoneOffset.UTC).minus(duration); - final List launchIds = getLaunchIds(projectId, lessThanDate); - if (!launchIds.isEmpty()) { - deleteClusters(launchIds); + private void removeLaunches() { + logStart(); + AtomicInteger counter = new AtomicInteger(0); + getProjectsWithAttribute(KEEP_LAUNCHES).forEach((projectId, duration) -> { + final LocalDateTime lessThanDate = LocalDateTime.now(ZoneOffset.UTC).minus(duration); + final List launchIds = getLaunchIds(projectId, lessThanDate); + if (!launchIds.isEmpty()) { + deleteClusters(launchIds); // final Long numberOfLaunchElements = countNumberOfLaunchElements(launchIds); - int deleted = namedParameterJdbcTemplate.update(DELETE_LAUNCH_QUERY, Map.of(IDS_PARAM, launchIds)); - counter.addAndGet(deleted); - LOGGER.info("Delete {} launches for project {}", deleted, projectId); - // to avoid error message in analyzer log, doesn't find index - if (deleted > 0) { - indexerServiceClient.removeFromIndexLessThanLaunchDate(projectId, lessThanDate); - LOGGER.info("Send message for deletion to analyzer for project {}", projectId); + int deleted = namedParameterJdbcTemplate.update(DELETE_LAUNCH_QUERY, + Map.of(IDS_PARAM, launchIds)); + counter.addAndGet(deleted); + LOGGER.info("Delete {} launches for project {}", deleted, projectId); + // to avoid error message in analyzer log, doesn't find index + if (deleted > 0) { + indexerServiceClient.removeFromIndexLessThanLaunchDate(projectId, lessThanDate); + LOGGER.info("Send message for deletion to analyzer for project {}", projectId); - deleteLogsFromElasticsearchByLaunchIdsAndProjectId(launchIds, projectId); + deleteLogsFromElasticsearchByLaunchIdsAndProjectId(launchIds, projectId); // eventPublisher.publishEvent(new ElementsDeletedEvent(launchIds, projectId, numberOfLaunchElements)); // LOGGER.info("Send event with elements deleted number {} for project {}", deleted, projectId); - } - } - }); - logFinish(counter.get()); - } + } + } + }); + logFinish(counter.get()); + } - private void deleteLogsFromElasticsearchByLaunchIdsAndProjectId(List launchIds, Long projectId) { - for (Long launchId : launchIds) { - elasticSearchClient.deleteLogsByLaunchIdAndProjectId(launchId, projectId); - LOGGER.info("Delete logs from ES by launch {} and project {}", launchId, projectId); - } - } + private void deleteLogsFromElasticsearchByLaunchIdsAndProjectId(List launchIds, + Long projectId) { + for (Long launchId : launchIds) { + elasticSearchClient.deleteLogsByLaunchIdAndProjectId(launchId, projectId); + LOGGER.info("Delete logs from ES by launch {} and project {}", launchId, projectId); + } + } - private List getLaunchIds(Long projectId, LocalDateTime lessThanDate) { - return namedParameterJdbcTemplate.queryForList(SELECT_LAUNCH_ID_QUERY, - Map.of(PROJECT_ID_PARAM, projectId, START_TIME_PARAM, lessThanDate), - Long.class - ); - } + private List getLaunchIds(Long projectId, LocalDateTime lessThanDate) { + return namedParameterJdbcTemplate.queryForList(SELECT_LAUNCH_ID_QUERY, + Map.of(PROJECT_ID_PARAM, projectId, START_TIME_PARAM, lessThanDate), + Long.class + ); + } - private void deleteClusters(List launchIds) { - namedParameterJdbcTemplate.update(DELETE_CLUSTER_QUERY, Map.of(IDS_PARAM, launchIds)); - } + private void deleteClusters(List launchIds) { + namedParameterJdbcTemplate.update(DELETE_CLUSTER_QUERY, Map.of(IDS_PARAM, launchIds)); + } - private Long countNumberOfLaunchElements(List launchIds) { - final AtomicLong resultedNumber = new AtomicLong(launchIds.size()); - final List itemIds = namedParameterJdbcTemplate.queryForList("SELECT item_id FROM test_item WHERE launch_id IN (:ids) UNION " - + "SELECT item_id FROM test_item WHERE retry_of IS NOT NULL AND retry_of IN " - + "(SELECT item_id FROM test_item WHERE launch_id IN (:ids))", - Map.of(IDS_PARAM, launchIds), - Long.class - ); - resultedNumber.addAndGet(itemIds.size()); - Lists.partition(itemIds, batchSize) - .forEach(batch -> resultedNumber.addAndGet(Optional.ofNullable(namedParameterJdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM log WHERE item_id IN (:ids);", - Map.of(IDS_PARAM, batch), - Long.class - )).orElse(0L))); - resultedNumber.addAndGet(Optional.ofNullable(namedParameterJdbcTemplate.queryForObject("SELECT COUNT(*) FROM log WHERE log.launch_id IN (:ids);", - Map.of(IDS_PARAM, launchIds), - Long.class - )).orElse(0L)); - return resultedNumber.longValue(); - } + private Long countNumberOfLaunchElements(List launchIds) { + final AtomicLong resultedNumber = new AtomicLong(launchIds.size()); + final List itemIds = namedParameterJdbcTemplate.queryForList( + "SELECT item_id FROM test_item WHERE launch_id IN (:ids) UNION " + + "SELECT item_id FROM test_item WHERE retry_of IS NOT NULL AND retry_of IN " + + "(SELECT item_id FROM test_item WHERE launch_id IN (:ids))", + Map.of(IDS_PARAM, launchIds), + Long.class + ); + resultedNumber.addAndGet(itemIds.size()); + Lists.partition(itemIds, batchSize) + .forEach(batch -> resultedNumber.addAndGet( + Optional.ofNullable(namedParameterJdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM log WHERE item_id IN (:ids);", + Map.of(IDS_PARAM, batch), + Long.class + )).orElse(0L))); + resultedNumber.addAndGet(Optional.ofNullable(namedParameterJdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM log WHERE log.launch_id IN (:ids);", + Map.of(IDS_PARAM, launchIds), + Long.class + )).orElse(0L)); + return resultedNumber.longValue(); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java index 5428d24..a982cf3 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java @@ -2,7 +2,11 @@ import com.epam.reportportal.analyzer.index.IndexerServiceClient; import com.epam.reportportal.elastic.ElasticSearchClient; -import com.epam.reportportal.events.ElementsDeletedEvent; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.context.ApplicationEventPublisher; import org.springframework.jdbc.core.JdbcTemplate; @@ -10,87 +14,83 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - /** * @author Pavel Bortnik */ @Service public class CleanLogJob extends BaseCleanJob { - private static final String PROJECT_ID_PARAM = "projectId"; - private static final String START_TIME_PARAM = "startTime"; + private static final String PROJECT_ID_PARAM = "projectId"; + private static final String START_TIME_PARAM = "startTime"; - private static final String DELETE_LOGS_QUERY = "DELETE FROM log WHERE project_id = ? AND log_time <= ?::TIMESTAMP;"; - private static final String SELECT_LAUNCH_ID_QUERY = "SELECT id FROM launch WHERE project_id = :projectId AND start_time <= :startTime::TIMESTAMP;"; + private static final String DELETE_LOGS_QUERY = "DELETE FROM log WHERE project_id = ? AND log_time <= ?::TIMESTAMP;"; + private static final String SELECT_LAUNCH_ID_QUERY = "SELECT id FROM launch WHERE project_id = :projectId AND start_time <= :startTime::TIMESTAMP;"; - private final CleanAttachmentJob cleanAttachmentJob; - private final IndexerServiceClient indexerServiceClient; - private final ApplicationEventPublisher eventPublisher; - private final ElasticSearchClient elasticSearchClient; - private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final CleanAttachmentJob cleanAttachmentJob; + private final IndexerServiceClient indexerServiceClient; + private final ApplicationEventPublisher eventPublisher; + private final ElasticSearchClient elasticSearchClient; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - public CleanLogJob(JdbcTemplate jdbcTemplate, CleanAttachmentJob cleanAttachmentJob, - IndexerServiceClient indexerServiceClient, ApplicationEventPublisher eventPublisher, - ElasticSearchClient elasticSearchClient, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - super(jdbcTemplate); - this.cleanAttachmentJob = cleanAttachmentJob; - this.indexerServiceClient = indexerServiceClient; - this.eventPublisher = eventPublisher; - this.elasticSearchClient = elasticSearchClient; - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - } + public CleanLogJob(JdbcTemplate jdbcTemplate, CleanAttachmentJob cleanAttachmentJob, + IndexerServiceClient indexerServiceClient, ApplicationEventPublisher eventPublisher, + ElasticSearchClient elasticSearchClient, + NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate); + this.cleanAttachmentJob = cleanAttachmentJob; + this.indexerServiceClient = indexerServiceClient; + this.eventPublisher = eventPublisher; + this.elasticSearchClient = elasticSearchClient; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } - @Scheduled(cron = "${rp.environment.variable.clean.log.cron}") - @SchedulerLock(name = "cleanLog", lockAtMostFor = "24h") - public void execute() { - removeLogs(); - cleanAttachmentJob.moveAttachments(); - } + @Scheduled(cron = "${rp.environment.variable.clean.log.cron}") + @SchedulerLock(name = "cleanLog", lockAtMostFor = "24h") + public void execute() { + removeLogs(); + cleanAttachmentJob.moveAttachments(); + } - void removeLogs() { - logStart(); - AtomicInteger counter = new AtomicInteger(0); - // TODO: Need to refactor Logs to keep real it's launchId and combine code with - // CleanLaunch to avoid duplication - getProjectsWithAttribute(KEEP_LOGS).forEach((projectId, duration) -> { - final LocalDateTime lessThanDate = LocalDateTime.now(ZoneOffset.UTC).minus(duration); - int deleted = jdbcTemplate.update(DELETE_LOGS_QUERY, projectId, lessThanDate); - counter.addAndGet(deleted); - LOGGER.info("Delete {} logs for project {}", deleted, projectId); - // to avoid error message in analyzer log, doesn't find index - if (deleted > 0) { - indexerServiceClient.removeFromIndexLessThanLogDate(projectId, lessThanDate); - LOGGER.info("Send message for deletion to analyzer for project {}", projectId); + void removeLogs() { + logStart(); + AtomicInteger counter = new AtomicInteger(0); + // TODO: Need to refactor Logs to keep real it's launchId and combine code with + // CleanLaunch to avoid duplication + getProjectsWithAttribute(KEEP_LOGS).forEach((projectId, duration) -> { + final LocalDateTime lessThanDate = LocalDateTime.now(ZoneOffset.UTC).minus(duration); + int deleted = jdbcTemplate.update(DELETE_LOGS_QUERY, projectId, lessThanDate); + counter.addAndGet(deleted); + LOGGER.info("Delete {} logs for project {}", deleted, projectId); + // to avoid error message in analyzer log, doesn't find index + if (deleted > 0) { + indexerServiceClient.removeFromIndexLessThanLogDate(projectId, lessThanDate); + LOGGER.info("Send message for deletion to analyzer for project {}", projectId); - final List launchIds = getLaunchIds(projectId, lessThanDate); - if (!launchIds.isEmpty()) { - deleteLogsFromElasticsearchByLaunchIdsAndProjectId(launchIds, projectId); - } + final List launchIds = getLaunchIds(projectId, lessThanDate); + if (!launchIds.isEmpty()) { + deleteLogsFromElasticsearchByLaunchIdsAndProjectId(launchIds, projectId); + } // eventPublisher.publishEvent(new ElementsDeletedEvent(this, projectId, deleted)); // LOGGER.info("Send event with elements deleted number {} for project {}", deleted, projectId); - } - }); + } + }); - logFinish(counter.get()); - } + logFinish(counter.get()); + } - private void deleteLogsFromElasticsearchByLaunchIdsAndProjectId(List launchIds, Long projectId) { - for (Long launchId : launchIds) { - elasticSearchClient.deleteLogsByLaunchIdAndProjectId(launchId, projectId); - LOGGER.info("Delete logs from ES by launch {} and project {}", launchId, projectId); - } - } + private void deleteLogsFromElasticsearchByLaunchIdsAndProjectId(List launchIds, + Long projectId) { + for (Long launchId : launchIds) { + elasticSearchClient.deleteLogsByLaunchIdAndProjectId(launchId, projectId); + LOGGER.info("Delete logs from ES by launch {} and project {}", launchId, projectId); + } + } - private List getLaunchIds(Long projectId, LocalDateTime lessThanDate) { - return namedParameterJdbcTemplate.queryForList(SELECT_LAUNCH_ID_QUERY, - Map.of(PROJECT_ID_PARAM, projectId, START_TIME_PARAM, lessThanDate), - Long.class - ); - } + private List getLaunchIds(Long projectId, LocalDateTime lessThanDate) { + return namedParameterJdbcTemplate.queryForList(SELECT_LAUNCH_ID_QUERY, + Map.of(PROJECT_ID_PARAM, projectId, START_TIME_PARAM, lessThanDate), + Long.class + ); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanMaterializedViewJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanMaterializedViewJob.java index db4d963..c03200d 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanMaterializedViewJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanMaterializedViewJob.java @@ -2,14 +2,6 @@ import com.epam.reportportal.jobs.BaseJob; import com.epam.reportportal.model.StaleMaterializedView; -import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.jdbc.core.BeanPropertyRowMapper; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -18,6 +10,13 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import java.util.stream.Collectors; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; /** * @author Ivan Budayeu @@ -25,77 +24,86 @@ @Service public class CleanMaterializedViewJob extends BaseJob { - private static final String TIME_BOUND_PARAM = "timeBound"; - private static final String BATCH_SIZE_PARAM = "batchSize"; - - private static final String NAMES_PARAM = "names"; - - private static final String SELECT_STALE_VIEWS = "SELECT id, name FROM stale_materialized_view WHERE creation_date <= :timeBound::TIMESTAMP ORDER BY id LIMIT :batchSize"; - private static final String SELECT_EXISTING_VIEWS_BY_NAME_IN = "SELECT matviewname FROM pg_matviews WHERE matviewname IN (:names)"; - private static final String DELETE_STALE_VIEWS_BY_NAMES = "DELETE FROM stale_materialized_view WHERE name IN (:names)"; - - private static final String DROP_MATERIALIZED_VIEW = "DROP MATERIALIZED VIEW IF EXISTS %s"; - - private static final Pattern VIEW_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_-]*$"); - - private final Integer batchSize; - private final Integer liveTimeout; - private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - - public CleanMaterializedViewJob(JdbcTemplate jdbcTemplate, @Value("${rp.environment.variable.clean.view.batch}") Integer batchSize, - @Value("${rp.environment.variable.clean.view.liveTimeout}") Integer liveTimeout, - NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - super(jdbcTemplate); - this.batchSize = batchSize; - this.liveTimeout = liveTimeout; - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - } - - @Scheduled(cron = "${rp.environment.variable.clean.view.cron}") - @SchedulerLock(name = "cleanMaterializedView", lockAtMostFor = "24h") - public void execute() { - logStart(); - final AtomicInteger existingCounter = new AtomicInteger(0); - final AtomicInteger staleCounter = new AtomicInteger(0); - - final LocalDateTime timeBound = LocalDateTime.now(ZoneOffset.UTC).minus(Duration.ofSeconds(liveTimeout)); - List staleViews = getStaleViews(timeBound); - - while (!staleViews.isEmpty()) { - final List viewNames = staleViews.stream() - .map(StaleMaterializedView::getName) - .filter(name -> VIEW_NAME_PATTERN.matcher(name).matches()) - .collect(Collectors.toList()); - - final int existingRemoved = removeExistingViews(viewNames); - existingCounter.addAndGet(existingRemoved); - - final int staleRemoved = removeStaleViews(viewNames); - staleCounter.addAndGet(staleRemoved); - - staleViews = getStaleViews(timeBound); - } - - logFinish(String.format("Stale removed: %d, Existing removed: %d", staleCounter.get(), existingCounter.get())); - - } - - private List getStaleViews(LocalDateTime timeBound) { - final Map selectParams = Map.of(TIME_BOUND_PARAM, timeBound, BATCH_SIZE_PARAM, batchSize); - return namedParameterJdbcTemplate.query(SELECT_STALE_VIEWS, selectParams, new BeanPropertyRowMapper<>(StaleMaterializedView.class)); - } - - private int removeExistingViews(List viewNames) { - final List existingViews = namedParameterJdbcTemplate.queryForList(SELECT_EXISTING_VIEWS_BY_NAME_IN, - Map.of(NAMES_PARAM, viewNames), - String.class - ); - - existingViews.forEach(name -> namedParameterJdbcTemplate.update(String.format(DROP_MATERIALIZED_VIEW, name), Map.of())); - return existingViews.size(); - } - - private int removeStaleViews(List viewNames) { - return namedParameterJdbcTemplate.update(DELETE_STALE_VIEWS_BY_NAMES, Map.of(NAMES_PARAM, viewNames)); - } + private static final String TIME_BOUND_PARAM = "timeBound"; + private static final String BATCH_SIZE_PARAM = "batchSize"; + + private static final String NAMES_PARAM = "names"; + + private static final String SELECT_STALE_VIEWS = "SELECT id, name FROM stale_materialized_view WHERE creation_date <= :timeBound::TIMESTAMP ORDER BY id LIMIT :batchSize"; + private static final String SELECT_EXISTING_VIEWS_BY_NAME_IN = "SELECT matviewname FROM pg_matviews WHERE matviewname IN (:names)"; + private static final String DELETE_STALE_VIEWS_BY_NAMES = "DELETE FROM stale_materialized_view WHERE name IN (:names)"; + + private static final String DROP_MATERIALIZED_VIEW = "DROP MATERIALIZED VIEW IF EXISTS %s"; + + private static final Pattern VIEW_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_-]*$"); + + private final Integer batchSize; + private final Integer liveTimeout; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + public CleanMaterializedViewJob(JdbcTemplate jdbcTemplate, + @Value("${rp.environment.variable.clean.view.batch}") Integer batchSize, + @Value("${rp.environment.variable.clean.view.liveTimeout}") Integer liveTimeout, + NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate); + this.batchSize = batchSize; + this.liveTimeout = liveTimeout; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Scheduled(cron = "${rp.environment.variable.clean.view.cron}") + @SchedulerLock(name = "cleanMaterializedView", lockAtMostFor = "24h") + public void execute() { + logStart(); + final AtomicInteger existingCounter = new AtomicInteger(0); + final AtomicInteger staleCounter = new AtomicInteger(0); + + final LocalDateTime timeBound = LocalDateTime.now(ZoneOffset.UTC) + .minus(Duration.ofSeconds(liveTimeout)); + List staleViews = getStaleViews(timeBound); + + while (!staleViews.isEmpty()) { + final List viewNames = staleViews.stream() + .map(StaleMaterializedView::getName) + .filter(name -> VIEW_NAME_PATTERN.matcher(name).matches()) + .collect(Collectors.toList()); + + final int existingRemoved = removeExistingViews(viewNames); + existingCounter.addAndGet(existingRemoved); + + final int staleRemoved = removeStaleViews(viewNames); + staleCounter.addAndGet(staleRemoved); + + staleViews = getStaleViews(timeBound); + } + + logFinish(String.format("Stale removed: %d, Existing removed: %d", staleCounter.get(), + existingCounter.get())); + + } + + private List getStaleViews(LocalDateTime timeBound) { + final Map selectParams = Map.of(TIME_BOUND_PARAM, timeBound, BATCH_SIZE_PARAM, + batchSize); + return namedParameterJdbcTemplate.query(SELECT_STALE_VIEWS, selectParams, + new BeanPropertyRowMapper<>(StaleMaterializedView.class)); + } + + private int removeExistingViews(List viewNames) { + final List existingViews = namedParameterJdbcTemplate.queryForList( + SELECT_EXISTING_VIEWS_BY_NAME_IN, + Map.of(NAMES_PARAM, viewNames), + String.class + ); + + existingViews.forEach( + name -> namedParameterJdbcTemplate.update(String.format(DROP_MATERIALIZED_VIEW, name), + Map.of())); + return existingViews.size(); + } + + private int removeStaleViews(List viewNames) { + return namedParameterJdbcTemplate.update(DELETE_STALE_VIEWS_BY_NAMES, + Map.of(NAMES_PARAM, viewNames)); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java index 149dbd4..b7d7676 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java @@ -2,6 +2,9 @@ import com.epam.reportportal.jobs.BaseJob; import com.epam.reportportal.storage.DataStorageService; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicInteger; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; @@ -11,10 +14,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.concurrent.atomic.AtomicInteger; - /** * Removing data from storage. * @@ -23,52 +22,54 @@ @Service public class CleanStorageJob extends BaseJob { - private static final String ROLLBACK_ERROR_MESSAGE = "Rollback deleting transaction."; - private static final String SELECT_AND_DELETE_DATA_CHUNK_QUERY = "DELETE FROM attachment_deletion WHERE id in " + - "(SELECT id FROM attachment_deletion ORDER BY id LIMIT ?) RETURNING *"; - private final DataStorageService storageService; - private final int chunkSize; + private static final String ROLLBACK_ERROR_MESSAGE = "Rollback deleting transaction."; + private static final String SELECT_AND_DELETE_DATA_CHUNK_QUERY = + "DELETE FROM attachment_deletion WHERE id in " + + "(SELECT id FROM attachment_deletion ORDER BY id LIMIT ?) RETURNING *"; + private final DataStorageService storageService; + private final int chunkSize; - public CleanStorageJob(JdbcTemplate jdbcTemplate, DataStorageService storageService, - @Value("${rp.environment.variable.clean.storage.chunkSize}") int chunkSize) { - super(jdbcTemplate); - this.chunkSize = chunkSize; - this.storageService = storageService; - } + public CleanStorageJob(JdbcTemplate jdbcTemplate, DataStorageService storageService, + @Value("${rp.environment.variable.clean.storage.chunkSize}") int chunkSize) { + super(jdbcTemplate); + this.chunkSize = chunkSize; + this.storageService = storageService; + } - @Scheduled(cron = "${rp.environment.variable.clean.storage.cron}") - @SchedulerLock(name = "cleanStorage", lockAtMostFor = "24h") - @Transactional - public void execute() { - logStart(); - AtomicInteger counter = new AtomicInteger(0); + @Scheduled(cron = "${rp.environment.variable.clean.storage.cron}") + @SchedulerLock(name = "cleanStorage", lockAtMostFor = "24h") + @Transactional + public void execute() { + logStart(); + AtomicInteger counter = new AtomicInteger(0); - jdbcTemplate.query(SELECT_AND_DELETE_DATA_CHUNK_QUERY, rs -> { - try { - delete(rs.getString("file_id"), rs.getString("thumbnail_id")); - counter.incrementAndGet(); - while (rs.next()) { - delete(rs.getString("file_id"), rs.getString("thumbnail_id")); - counter.incrementAndGet(); - } - } catch (Exception e) { - throw new RuntimeException(ROLLBACK_ERROR_MESSAGE, e); - } - }, chunkSize); + jdbcTemplate.query(SELECT_AND_DELETE_DATA_CHUNK_QUERY, rs -> { + try { + delete(rs.getString("file_id"), rs.getString("thumbnail_id")); + counter.incrementAndGet(); + while (rs.next()) { + delete(rs.getString("file_id"), rs.getString("thumbnail_id")); + counter.incrementAndGet(); + } + } catch (Exception e) { + throw new RuntimeException(ROLLBACK_ERROR_MESSAGE, e); + } + }, chunkSize); - logFinish(counter.get()); - } + logFinish(counter.get()); + } - private void delete(String fileId, String thumbnailId) throws Exception { - if (Strings.isNotBlank(fileId)) { - storageService.delete(decode(fileId)); - } - if (Strings.isNotBlank(thumbnailId)) { - storageService.delete(decode(thumbnailId)); - } + private void delete(String fileId, String thumbnailId) throws Exception { + if (Strings.isNotBlank(fileId)) { + storageService.delete(decode(fileId)); } - - private String decode(String data) { - return StringUtils.isEmpty(data) ? data : new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); + if (Strings.isNotBlank(thumbnailId)) { + storageService.delete(decode(thumbnailId)); } + } + + private String decode(String data) { + return StringUtils.isEmpty(data) ? data + : new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/processing/SaveLogMessageJob.java b/src/main/java/com/epam/reportportal/jobs/processing/SaveLogMessageJob.java index e2c92fe..22519ca 100644 --- a/src/main/java/com/epam/reportportal/jobs/processing/SaveLogMessageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/processing/SaveLogMessageJob.java @@ -2,13 +2,12 @@ import com.epam.reportportal.log.LogMessage; import com.epam.reportportal.log.LogProcessing; +import java.util.Objects; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Service; -import java.util.Objects; - /** * Log consumer. * @@ -17,17 +16,18 @@ @Service @ConditionalOnProperty(prefix = "rp.elasticsearch", name = "host") public class SaveLogMessageJob { - public static final String LOG_MESSAGE_SAVING_QUEUE_NAME = "log_message_saving"; - private final LogProcessing logProcessing; - public SaveLogMessageJob(LogProcessing logProcessing) { - this.logProcessing = logProcessing; - } + public static final String LOG_MESSAGE_SAVING_QUEUE_NAME = "log_message_saving"; + private final LogProcessing logProcessing; + + public SaveLogMessageJob(LogProcessing logProcessing) { + this.logProcessing = logProcessing; + } - @RabbitListener(queues = LOG_MESSAGE_SAVING_QUEUE_NAME, containerFactory = "processingRabbitListenerContainerFactory") - public void execute(@Payload LogMessage logMessage) { - if (Objects.nonNull(logMessage)) { - this.logProcessing.add(logMessage); - } + @RabbitListener(queues = LOG_MESSAGE_SAVING_QUEUE_NAME, containerFactory = "processingRabbitListenerContainerFactory") + public void execute(@Payload LogMessage logMessage) { + if (Objects.nonNull(logMessage)) { + this.logProcessing.add(logMessage); } + } } diff --git a/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java b/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java index 32bc71e..b75989f 100644 --- a/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java @@ -1,6 +1,8 @@ package com.epam.reportportal.jobs.storage; import com.epam.reportportal.jobs.BaseJob; +import java.util.List; +import java.util.concurrent.CompletableFuture; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.task.TaskExecutor; @@ -8,53 +10,53 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.concurrent.CompletableFuture; - /** * @author Ivan Budayeu */ @Service public class CalculateAllocatedStorageJob extends BaseJob { - private static final String SELECT_PROJECT_IDS_QUERY = "SELECT id FROM project ORDER BY id"; - private static final String SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY = "" + - "SELECT coalesce(sum(file_size), 0) FROM attachment WHERE attachment.project_id = ?"; - private static final String UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY = - "UPDATE project SET allocated_storage = ? WHERE id = ?"; - - private final TaskExecutor projectAllocatedStorageExecutor; - - @Autowired - public CalculateAllocatedStorageJob(TaskExecutor projectAllocatedStorageExecutor, JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - this.projectAllocatedStorageExecutor = projectAllocatedStorageExecutor; - } - - @Scheduled(cron = "${rp.environment.variable.storage.project.cron}") - @SchedulerLock(name = "calculateAllocatedStorage", lockAtMostFor = "24h") - public void calculate() { - logStart(); - CompletableFuture.allOf(getProjectIds().stream() - .map(id -> CompletableFuture.runAsync(() -> updateAllocatedStorage(id), projectAllocatedStorageExecutor)) - .toArray(CompletableFuture[]::new)).join(); - logFinish(); - } - - private List getProjectIds() { - return jdbcTemplate.queryForList(SELECT_PROJECT_IDS_QUERY, Long.class); - } - - private void updateAllocatedStorage(Long projectId) { - final Long allocatedStorage = getAllocatedStorage(projectId); - updateAllocatedStorage(allocatedStorage, projectId); - } - - private Long getAllocatedStorage(Long projectId) { - return jdbcTemplate.queryForObject(SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY, Long.class, projectId); - } - - private void updateAllocatedStorage(Long allocatedStorage, Long projectId) { - jdbcTemplate.update(UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY, allocatedStorage, projectId); - } + private static final String SELECT_PROJECT_IDS_QUERY = "SELECT id FROM project ORDER BY id"; + private static final String SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY = "" + + "SELECT coalesce(sum(file_size), 0) FROM attachment WHERE attachment.project_id = ?"; + private static final String UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY = + "UPDATE project SET allocated_storage = ? WHERE id = ?"; + + private final TaskExecutor projectAllocatedStorageExecutor; + + @Autowired + public CalculateAllocatedStorageJob(TaskExecutor projectAllocatedStorageExecutor, + JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + this.projectAllocatedStorageExecutor = projectAllocatedStorageExecutor; + } + + @Scheduled(cron = "${rp.environment.variable.storage.project.cron}") + @SchedulerLock(name = "calculateAllocatedStorage", lockAtMostFor = "24h") + public void calculate() { + logStart(); + CompletableFuture.allOf(getProjectIds().stream() + .map(id -> CompletableFuture.runAsync(() -> updateAllocatedStorage(id), + projectAllocatedStorageExecutor)) + .toArray(CompletableFuture[]::new)).join(); + logFinish(); + } + + private List getProjectIds() { + return jdbcTemplate.queryForList(SELECT_PROJECT_IDS_QUERY, Long.class); + } + + private void updateAllocatedStorage(Long projectId) { + final Long allocatedStorage = getAllocatedStorage(projectId); + updateAllocatedStorage(allocatedStorage, projectId); + } + + private Long getAllocatedStorage(Long projectId) { + return jdbcTemplate.queryForObject(SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY, Long.class, + projectId); + } + + private void updateAllocatedStorage(Long allocatedStorage, Long projectId) { + jdbcTemplate.update(UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY, allocatedStorage, projectId); + } } diff --git a/src/main/java/com/epam/reportportal/log/LogMessage.java b/src/main/java/com/epam/reportportal/log/LogMessage.java index 134bbcd..5631fbb 100644 --- a/src/main/java/com/epam/reportportal/log/LogMessage.java +++ b/src/main/java/com/epam/reportportal/log/LogMessage.java @@ -6,82 +6,87 @@ public class LogMessage implements Serializable { - private Long id; - private LocalDateTime logTime; - private String logMessage; - private Long itemId; - private Long launchId; - private Long projectId; - - public LogMessage(Long id, LocalDateTime logTime, String logMessage, Long itemId, Long launchId, Long projectId) { - this.id = id; - this.logTime = logTime; - this.logMessage = logMessage; - this.itemId = itemId; - this.launchId = launchId; - this.projectId = projectId; + private Long id; + private LocalDateTime logTime; + private String logMessage; + private Long itemId; + private Long launchId; + private Long projectId; + + public LogMessage(Long id, LocalDateTime logTime, String logMessage, Long itemId, Long launchId, + Long projectId) { + this.id = id; + this.logTime = logTime; + this.logMessage = logMessage; + this.itemId = itemId; + this.launchId = launchId; + this.projectId = projectId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public LocalDateTime getLogTime() { + return logTime; + } + + public void setLogTime(LocalDateTime logTime) { + this.logTime = logTime; + } + + public String getLogMessage() { + return logMessage; + } + + public void setLogMessage(String logMessage) { + this.logMessage = logMessage; + } + + public Long getItemId() { + return itemId; + } + + public void setItemId(Long itemId) { + this.itemId = itemId; + } + + public Long getLaunchId() { + return launchId; + } + + public void setLaunchId(Long launchId) { + this.launchId = launchId; + } + + public Long getProjectId() { + return projectId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public LocalDateTime getLogTime() { - return logTime; - } - - public void setLogTime(LocalDateTime logTime) { - this.logTime = logTime; - } - - public String getLogMessage() { - return logMessage; - } - - public void setLogMessage(String logMessage) { - this.logMessage = logMessage; - } - - public Long getItemId() { - return itemId; - } - - public void setItemId(Long itemId) { - this.itemId = itemId; - } - - public Long getLaunchId() { - return launchId; - } - - public void setLaunchId(Long launchId) { - this.launchId = launchId; - } - - public Long getProjectId() { - return projectId; - } - - public void setProjectId(Long projectId) { - this.projectId = projectId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - LogMessage that = (LogMessage) o; - return Objects.equals(id, that.id) && Objects.equals(logTime, that.logTime) - && Objects.equals(logMessage, that.logMessage) && Objects.equals(itemId, that.itemId) - && Objects.equals(launchId, that.launchId) && Objects.equals(projectId, that.projectId); - } - - @Override - public int hashCode() { - return Objects.hash(id, logTime, logMessage, itemId, launchId, projectId); + if (o == null || getClass() != o.getClass()) { + return false; } + LogMessage that = (LogMessage) o; + return Objects.equals(id, that.id) && Objects.equals(logTime, that.logTime) + && Objects.equals(logMessage, that.logMessage) && Objects.equals(itemId, that.itemId) + && Objects.equals(launchId, that.launchId) && Objects.equals(projectId, that.projectId); + } + + @Override + public int hashCode() { + return Objects.hash(id, logTime, logMessage, itemId, launchId, projectId); + } } diff --git a/src/main/java/com/epam/reportportal/log/LogProcessing.java b/src/main/java/com/epam/reportportal/log/LogProcessing.java index db74bc7..2781596 100644 --- a/src/main/java/com/epam/reportportal/log/LogProcessing.java +++ b/src/main/java/com/epam/reportportal/log/LogProcessing.java @@ -2,35 +2,35 @@ import com.epam.reportportal.calculation.BatchProcessing; import com.epam.reportportal.elastic.ElasticSearchClient; +import java.util.List; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; -import java.util.List; - /** * Batch processing for log. + * * @author Maksim Antonov */ @Component @ConditionalOnProperty(prefix = "rp.elasticsearch", name = "host") public class LogProcessing extends BatchProcessing { - private final ElasticSearchClient elasticSearchClient; + private final ElasticSearchClient elasticSearchClient; - public LogProcessing(ElasticSearchClient elasticSearchClient, - @Value("${rp.processing.log.maxBatchSize}") int batchSize, - @Value("${rp.processing.log.maxBatchTimeout}") int timeout) { - super(batchSize, timeout, new DefaultManagedTaskScheduler()); - this.elasticSearchClient = elasticSearchClient; - } + public LogProcessing(ElasticSearchClient elasticSearchClient, + @Value("${rp.processing.log.maxBatchSize}") int batchSize, + @Value("${rp.processing.log.maxBatchTimeout}") int timeout) { + super(batchSize, timeout, new DefaultManagedTaskScheduler()); + this.elasticSearchClient = elasticSearchClient; + } - @Override - protected void process(List logMessageList) { - if (!CollectionUtils.isEmpty(logMessageList)) { - elasticSearchClient.save(logMessageList); - } + @Override + protected void process(List logMessageList) { + if (!CollectionUtils.isEmpty(logMessageList)) { + elasticSearchClient.save(logMessageList); } + } } diff --git a/src/main/java/com/epam/reportportal/model/StaleMaterializedView.java b/src/main/java/com/epam/reportportal/model/StaleMaterializedView.java index 1c8ffa6..fadb036 100644 --- a/src/main/java/com/epam/reportportal/model/StaleMaterializedView.java +++ b/src/main/java/com/epam/reportportal/model/StaleMaterializedView.java @@ -5,26 +5,26 @@ */ public class StaleMaterializedView { - private Long id; - private String name; + private Long id; + private String name; - public StaleMaterializedView() { + public StaleMaterializedView() { - } + } - public Long getId() { - return id; - } + public Long getId() { + return id; + } - public void setId(Long id) { - this.id = id; - } + public void setId(Long id) { + this.id = id; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(String name) { + this.name = name; + } } diff --git a/src/main/java/com/epam/reportportal/model/index/CleanIndexByDateRangeRq.java b/src/main/java/com/epam/reportportal/model/index/CleanIndexByDateRangeRq.java index 98ac9ed..afd26f4 100644 --- a/src/main/java/com/epam/reportportal/model/index/CleanIndexByDateRangeRq.java +++ b/src/main/java/com/epam/reportportal/model/index/CleanIndexByDateRangeRq.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.LocalDateTime; /** @@ -10,47 +9,48 @@ */ public class CleanIndexByDateRangeRq { - @JsonProperty("project") - private Long projectId; + @JsonProperty("project") + private Long projectId; - @JsonProperty("interval_start_date") - @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") - private LocalDateTime intervalStartDate; + @JsonProperty("interval_start_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime intervalStartDate; - @JsonProperty("interval_end_date") - @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") - private LocalDateTime intervalEndDate; + @JsonProperty("interval_end_date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime intervalEndDate; - public CleanIndexByDateRangeRq() { - } + public CleanIndexByDateRangeRq() { + } - public CleanIndexByDateRangeRq(Long projectId, LocalDateTime intervalStartDate, LocalDateTime intervalEndDate) { - this.projectId = projectId; - this.intervalStartDate = intervalStartDate; - this.intervalEndDate = intervalEndDate; - } + public CleanIndexByDateRangeRq(Long projectId, LocalDateTime intervalStartDate, + LocalDateTime intervalEndDate) { + this.projectId = projectId; + this.intervalStartDate = intervalStartDate; + this.intervalEndDate = intervalEndDate; + } - public Long getProjectId() { - return projectId; - } + public Long getProjectId() { + return projectId; + } - public void setProjectId(Long projectId) { - this.projectId = projectId; - } + public void setProjectId(Long projectId) { + this.projectId = projectId; + } - public LocalDateTime getIntervalStartDate() { - return intervalStartDate; - } + public LocalDateTime getIntervalStartDate() { + return intervalStartDate; + } - public void setIntervalStartDate(LocalDateTime intervalStartDate) { - this.intervalStartDate = intervalStartDate; - } + public void setIntervalStartDate(LocalDateTime intervalStartDate) { + this.intervalStartDate = intervalStartDate; + } - public LocalDateTime getIntervalEndDate() { - return intervalEndDate; - } + public LocalDateTime getIntervalEndDate() { + return intervalEndDate; + } - public void setIntervalEndDate(LocalDateTime intervalEndDate) { - this.intervalEndDate = intervalEndDate; - } + public void setIntervalEndDate(LocalDateTime intervalEndDate) { + this.intervalEndDate = intervalEndDate; + } } diff --git a/src/main/java/com/epam/reportportal/model/index/CleanIndexRq.java b/src/main/java/com/epam/reportportal/model/index/CleanIndexRq.java index c2966c6..cf94809 100644 --- a/src/main/java/com/epam/reportportal/model/index/CleanIndexRq.java +++ b/src/main/java/com/epam/reportportal/model/index/CleanIndexRq.java @@ -1,7 +1,6 @@ package com.epam.reportportal.model.index; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; /** @@ -9,33 +8,33 @@ */ public class CleanIndexRq { - @JsonProperty("project") - private Long projectId; + @JsonProperty("project") + private Long projectId; - @JsonProperty("ids") - private List logIds; + @JsonProperty("ids") + private List logIds; - public CleanIndexRq() { - } + public CleanIndexRq() { + } - public CleanIndexRq(Long projectId, List logIds) { - this.projectId = projectId; - this.logIds = logIds; - } + public CleanIndexRq(Long projectId, List logIds) { + this.projectId = projectId; + this.logIds = logIds; + } - public Long getProjectId() { - return projectId; - } + public Long getProjectId() { + return projectId; + } - public void setProjectId(Long projectId) { - this.projectId = projectId; - } + public void setProjectId(Long projectId) { + this.projectId = projectId; + } - public List getLogIds() { - return logIds; - } + public List getLogIds() { + return logIds; + } - public void setLogIds(List logIds) { - this.logIds = logIds; - } + public void setLogIds(List logIds) { + this.logIds = logIds; + } } diff --git a/src/main/java/com/epam/reportportal/storage/DataStorageService.java b/src/main/java/com/epam/reportportal/storage/DataStorageService.java index d7db8a1..295f6b4 100644 --- a/src/main/java/com/epam/reportportal/storage/DataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/DataStorageService.java @@ -20,5 +20,6 @@ * Storage service interface */ public interface DataStorageService { - void delete(String filePath) throws Exception; + + void delete(String filePath) throws Exception; } \ No newline at end of file diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index 03e45ca..d26f266 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -16,33 +16,32 @@ package com.epam.reportportal.storage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Local storage service */ public class LocalDataStorageService implements DataStorageService { - private static final Logger LOGGER = LoggerFactory.getLogger(LocalDataStorageService.class); + private static final Logger LOGGER = LoggerFactory.getLogger(LocalDataStorageService.class); - private final String storageRootPath; + private final String storageRootPath; - public LocalDataStorageService(String storageRootPath) { - this.storageRootPath = storageRootPath; - } + public LocalDataStorageService(String storageRootPath) { + this.storageRootPath = storageRootPath; + } - @Override - public void delete(String filePath) throws IOException { - try { - Files.deleteIfExists(Paths.get(storageRootPath, filePath)); - } catch (IOException e) { - LOGGER.error("Unable to delete file '{}'", filePath, e); - throw e; - } + @Override + public void delete(String filePath) throws IOException { + try { + Files.deleteIfExists(Paths.get(storageRootPath, filePath)); + } catch (IOException e) { + LOGGER.error("Unable to delete file '{}'", filePath, e); + throw e; } + } } diff --git a/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java b/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java index 1e910e8..938774e 100644 --- a/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java @@ -16,55 +16,54 @@ package com.epam.reportportal.storage; +import java.nio.file.Path; +import java.nio.file.Paths; import org.jclouds.blobstore.BlobStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Path; -import java.nio.file.Paths; - /** * S3 storage service */ public class S3DataStorageService implements DataStorageService { - private static final Logger LOGGER = LoggerFactory.getLogger(S3DataStorageService.class); - - private final BlobStore blobStore; - private final String bucketPrefix; - private final String defaultBucketName; + private static final Logger LOGGER = LoggerFactory.getLogger(S3DataStorageService.class); - public S3DataStorageService(BlobStore blobStore, String bucketPrefix, String defaultBucketName) { - this.blobStore = blobStore; - this.bucketPrefix = bucketPrefix; - this.defaultBucketName = defaultBucketName; - } + private final BlobStore blobStore; + private final String bucketPrefix; + private final String defaultBucketName; - @Override - public void delete(String filePath) throws Exception { - Path targetPath = Paths.get(filePath); - int nameCount = targetPath.getNameCount(); + public S3DataStorageService(BlobStore blobStore, String bucketPrefix, String defaultBucketName) { + this.blobStore = blobStore; + this.bucketPrefix = bucketPrefix; + this.defaultBucketName = defaultBucketName; + } - String bucket; - String objectName; + @Override + public void delete(String filePath) throws Exception { + Path targetPath = Paths.get(filePath); + int nameCount = targetPath.getNameCount(); - if (nameCount > 1) { - bucket = bucketPrefix + retrievePath(targetPath, 0, 1); - objectName = retrievePath(targetPath, 1, nameCount); - } else { - bucket = defaultBucketName; - objectName = retrievePath(targetPath, 0, 1); - } + String bucket; + String objectName; - try { - blobStore.removeBlob(bucket, objectName); - } catch (Exception e) { - LOGGER.error("Unable to delete file '{}'", filePath, e); - throw e; - } + if (nameCount > 1) { + bucket = bucketPrefix + retrievePath(targetPath, 0, 1); + objectName = retrievePath(targetPath, 1, nameCount); + } else { + bucket = defaultBucketName; + objectName = retrievePath(targetPath, 0, 1); } - private String retrievePath(Path path, int beginIndex, int endIndex) { - return String.valueOf(path.subpath(beginIndex, endIndex)); + try { + blobStore.removeBlob(bucket, objectName); + } catch (Exception e) { + LOGGER.error("Unable to delete file '{}'", filePath, e); + throw e; } + } + + private String retrievePath(Path path, int beginIndex, int endIndex) { + return String.valueOf(path.subpath(beginIndex, endIndex)); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 526fa52..78eae83 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,7 +3,6 @@ info.build.description=ReportPortal Jobs Service info.build.version=${version}${buildNumber} info.build.branch=${branch} info.build.repo=${repo} - server.port=8686 management.endpoints.web.exposure.include=info, health management.endpoints.web.base-path=/ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b1550a3..33bfe7d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -33,10 +33,10 @@ rp: project: core: 5 max: 10 -# elasticsearch: -# host: http://elasticsearch:9200 -# username: -# password: + # elasticsearch: + # host: http://elasticsearch:9200 + # username: + # password: processing: log: diff --git a/src/test/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJobTest.java b/src/test/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJobTest.java index d98cc33..6dc6216 100644 --- a/src/test/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJobTest.java +++ b/src/test/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJobTest.java @@ -1,5 +1,13 @@ package com.epam.reportportal.jobs.storage; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -8,60 +16,59 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.List; - -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - /** * @author Ivan Budayeu */ class CalculateAllocatedStorageJobTest { - private static final ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); - private static final String SELECT_PROJECT_IDS_QUERY = "SELECT id FROM project ORDER BY id"; - private static final String SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY = - "SELECT coalesce(sum(file_size), 0) FROM attachment WHERE attachment.project_id = ?"; - private static final String UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY = - "UPDATE project SET allocated_storage = ? WHERE id = ?"; + private static final ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + private static final String SELECT_PROJECT_IDS_QUERY = "SELECT id FROM project ORDER BY id"; + private static final String SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY = + "SELECT coalesce(sum(file_size), 0) FROM attachment WHERE attachment.project_id = ?"; + private static final String UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY = + "UPDATE project SET allocated_storage = ? WHERE id = ?"; - private final JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); + private final JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class); - private final CalculateAllocatedStorageJob calculateAllocatedStorageJob = new CalculateAllocatedStorageJob(taskExecutor, - jdbcTemplate - ); + private final CalculateAllocatedStorageJob calculateAllocatedStorageJob = new CalculateAllocatedStorageJob( + taskExecutor, + jdbcTemplate + ); - @BeforeAll - static void initExecutor() { - taskExecutor.setCorePoolSize(2); - taskExecutor.setMaxPoolSize(2); - taskExecutor.setWaitForTasksToCompleteOnShutdown(true); - taskExecutor.afterPropertiesSet(); - } + @BeforeAll + static void initExecutor() { + taskExecutor.setCorePoolSize(2); + taskExecutor.setMaxPoolSize(2); + taskExecutor.setWaitForTasksToCompleteOnShutdown(true); + taskExecutor.afterPropertiesSet(); + } - @AfterAll - static void shutDownExecutor() { - taskExecutor.shutdown(); - } + @AfterAll + static void shutDownExecutor() { + taskExecutor.shutdown(); + } - @Test - void shouldUpdateAllocatedStorageForAllProjects() { + @Test + void shouldUpdateAllocatedStorageForAllProjects() { - final List projectIds = List.of(1L, 2L); + final List projectIds = List.of(1L, 2L); - when(jdbcTemplate.queryForList(SELECT_PROJECT_IDS_QUERY, Long.class)).thenReturn(projectIds); - when(jdbcTemplate.queryForObject(eq(SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY), eq(Long.class), anyLong())).thenReturn(1000L); + when(jdbcTemplate.queryForList(SELECT_PROJECT_IDS_QUERY, Long.class)).thenReturn(projectIds); + when(jdbcTemplate.queryForObject(eq(SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY), eq(Long.class), + anyLong())).thenReturn(1000L); - calculateAllocatedStorageJob.calculate(); + calculateAllocatedStorageJob.calculate(); - verify(jdbcTemplate, times(2)).queryForObject(eq(SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY), eq(Long.class), anyLong()); + verify(jdbcTemplate, times(2)).queryForObject(eq(SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY), + eq(Long.class), anyLong()); - final ArgumentCaptor projectIdCaptor = ArgumentCaptor.forClass(Long.class); - verify(jdbcTemplate, times(2)).update(eq(UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY), anyLong(), projectIdCaptor.capture()); - final List updatedIds = projectIdCaptor.getAllValues(); + final ArgumentCaptor projectIdCaptor = ArgumentCaptor.forClass(Long.class); + verify(jdbcTemplate, times(2)).update(eq(UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY), + anyLong(), projectIdCaptor.capture()); + final List updatedIds = projectIdCaptor.getAllValues(); - Assertions.assertEquals(projectIds.size(), updatedIds.size()); - Assertions.assertTrue(projectIds.containsAll(updatedIds)); - } + Assertions.assertEquals(projectIds.size(), updatedIds.size()); + Assertions.assertTrue(projectIds.containsAll(updatedIds)); + } } \ No newline at end of file From d70a65e08bd2a1d210043793e4f8e93d23e032c0 Mon Sep 17 00:00:00 2001 From: APiankouski <109206864+APiankouski@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:44:27 +0300 Subject: [PATCH 02/27] Epmrpp-84251 || merge to develop (#76) * EPMRPP-81362 || Fix security vulnerabilities (#58) * EPMRPP-81362 || Update gson version to make able jcloud work (#59) * EPMRPP-82673-exec-jar promote.yml update (added exec jar) * Merge master to 5.7.5 (#66) * EPMRPP-80865|| Update bom version * [Gradle Release Plugin] - new version commit: '5.7.5'. * EPMRPP-82673-exec-jar promote.yml update (added exec jar) --------- Co-authored-by: miracle8484 <76156909+miracle8484@users.noreply.github.com> Co-authored-by: reportportal.io Co-authored-by: rkukharenka Co-authored-by: Ryhor <125865748+rkukharenka@users.noreply.github.com> * Update version * EPMRPP-82707 || Add single bucket configuration (#67) * EPMRPP-82707 || Add single bucket configuration * EPMRPP-82707 || Refactor according to checkstyle * EPMRPP-79722 || Replace RuntimeException with checked exception when file is not found in CleanStorageJob (#68) * Merge master to hotfix/next (#72) * Release 5.8.0 (#71) * EPMRPP-81362 || Fix security vulnerabilities (#58) * EPMRPP-81362 || Update gson version to make able jcloud work (#59) * Merge master to 5.7.5 (#66) * EPMRPP-80865|| Update bom version * [Gradle Release Plugin] - new version commit: '5.7.5'. * EPMRPP-82673-exec-jar promote.yml update (added exec jar) --------- Co-authored-by: miracle8484 <76156909+miracle8484@users.noreply.github.com> Co-authored-by: reportportal.io Co-authored-by: rkukharenka Co-authored-by: Ryhor <125865748+rkukharenka@users.noreply.github.com> * Update version * EPMRPP-83538 || Job service version is missing on Login page * Update version --------- Co-authored-by: miracle8484 <76156909+miracle8484@users.noreply.github.com> Co-authored-by: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Co-authored-by: reportportal.io Co-authored-by: rkukharenka Co-authored-by: Ryhor <125865748+rkukharenka@users.noreply.github.com> Co-authored-by: Andrei Piankouski * [Gradle Release Plugin] - new version commit: '5.8.1'. --------- Co-authored-by: miracle8484 <76156909+miracle8484@users.noreply.github.com> Co-authored-by: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Co-authored-by: reportportal.io Co-authored-by: rkukharenka Co-authored-by: Ryhor <125865748+rkukharenka@users.noreply.github.com> Co-authored-by: Andrei Piankouski * EPRMPP-83651 || Clean storage job out of memory (#74) * EPMRPP-83651 || Create batching for clean storage job * EPMRPP-83651 || Refactor CleanStorageJob * EPMRPP-83651 || Add check for empty attachment_deletion table * EPMRPP-83651 || Clean attachments list every batch * EPMRPP-83651 || Add default value for feature flags * EPMRPP-83651 || Change logic for CleanStorageJob when using multibucket * EPMRPP-83651 || Fix bug interrupting remove files when bucket is not found * EPMRPP-83651 || Refactor CodeStyle * EPMRPP-83098 || Update datastore variables naming (#75) * Update gradle scripts version * Remove dockerPrepareEnvironment --------- Co-authored-by: miracle8484 <76156909+miracle8484@users.noreply.github.com> Co-authored-by: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Co-authored-by: rkukharenka Co-authored-by: Ryhor <125865748+rkukharenka@users.noreply.github.com> Co-authored-by: reportportal.io Co-authored-by: Andrei Piankouski Co-authored-by: Ivan_Kustau --- .github/workflows/promote.yml | 2 +- .github/workflows/release.yml | 2 +- Dockerfile | 9 +- build.gradle | 8 +- gradle.properties | 4 +- .../config/DataStorageConfig.java | 170 +++++++++++------- .../jobs/clean/CleanStorageJob.java | 77 +++++--- .../model/BlobNotFoundException.java | 35 ++++ .../storage/DataStorageService.java | 5 +- .../storage/LocalDataStorageService.java | 20 ++- .../storage/S3DataStorageService.java | 73 +++++--- .../epam/reportportal/utils/FeatureFlag.java | 35 ++++ .../utils/FeatureFlagHandler.java | 37 ++++ src/main/resources/application.properties | 3 +- src/main/resources/application.yml | 17 +- 15 files changed, 364 insertions(+), 133 deletions(-) create mode 100644 src/main/java/com/epam/reportportal/model/BlobNotFoundException.java create mode 100644 src/main/java/com/epam/reportportal/utils/FeatureFlag.java create mode 100644 src/main/java/com/epam/reportportal/utils/FeatureFlagHandler.java diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index 0174d39..6233633 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -23,7 +23,7 @@ on: env: REPOSITORY_URL: 'https://maven.pkg.github.com' UPSTREAM_REPOSITORY_URL: 'https://oss.sonatype.org' - PACKAGE_SUFFIXES: '-javadoc.jar,-javadoc.jar.asc,-sources.jar,-sources.jar.asc,.jar,.jar.asc,.pom,.pom.asc' + PACKAGE_SUFFIXES: '-exec.jar,-exec.jar.asc,-javadoc.jar,-javadoc.jar.asc,-sources.jar,-sources.jar.asc,.jar,.jar.asc,.pom,.pom.asc' PACKAGE: 'com.epam.reportportal' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 744523d..3374fca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ on: env: GH_USER_NAME: github.actor SCRIPTS_VERSION: 5.7.0 - BOM_VERSION: 5.7.4 + BOM_VERSION: 5.7.5 REPOSITORY_URL: 'https://maven.pkg.github.com/' jobs: diff --git a/Dockerfile b/Dockerfile index ea4124f..dcdfde6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ -FROM alpine:latest +FROM amazoncorretto:11.0.17 LABEL version=5.7.4 description="EPAM Report portal. Service jobs" maintainer="Andrei Varabyeu , Hleb Kanonik " ARG GH_TOKEN -RUN apk -U -q upgrade && apk --no-cache -q add openjdk11 ca-certificates && \ - echo 'exec java ${JAVA_OPTS} -jar service-jobs-5.7.4-exec.jar' > /start.sh && chmod +x /start.sh && \ - wget --header="Authorization: Bearer ${GH_TOKEN}" -q https://maven.pkg.github.com/reportportal/service-jobs/com/epam/reportportal/service-jobs/5.7.4/service-jobs-5.7.4-exec.jar +ARG GH_URL=https://__:$GH_TOKEN@maven.pkg.github.com/reportportal/service-jobs/com/epam/reportportal/service-jobs/5.7.4/service-jobs-5.7.4-exec.jar +RUN curl -O -L $GH_URL \ + --output service-jobs-5.7.3-exec.jar && \ + echo 'exec java ${JAVA_OPTS} -jar service-jobs-5.7.4-exec.jar' > /start.sh && chmod +x /start.sh ENV JAVA_OPTS="-Xmx512m -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=70 -Djava.security.egd=file:/dev/./urandom" VOLUME ["/tmp"] EXPOSE 8080 diff --git a/build.gradle b/build.gradle index 8ef9b9f..4b4ad80 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.5.14' + id 'org.springframework.boot' version '2.7.0' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } @@ -9,7 +9,7 @@ project.ext { releaseMode = project.hasProperty("releaseMode") } -def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? getProperty('scripts.version') : '5.5.0') +def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? getProperty('scripts.version') : 'develop') apply from: "$scriptsUrl/build-docker.gradle" apply from: "$scriptsUrl/build-commons.gradle" @@ -74,11 +74,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'org.apache.jclouds.api:s3:2.5.0' implementation 'org.apache.jclouds.provider:aws-s3:2.5.0' + //Needed for correct jcloud work + implementation 'com.google.code.gson:gson:2.8.9' implementation 'org.apache.httpcomponents:httpclient:4.5.13' // https://avd.aquasec.com/nvd/cve-2020-8908 // implementation 'com.google.guava:guava:30.0-jre'; compile "com.rabbitmq:http-client:2.1.0.RELEASE" + //Fix CVE + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2' runtimeOnly 'org.postgresql:postgresql' diff --git a/gradle.properties b/gradle.properties index 8a54ff2..eda4b33 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=5.7.5 +version=5.8.1 description=EPAM Report portal. Service jobs dockerServerUrl=unix:///var/run/docker.sock -dockerPrepareEnvironment=apk -U -q upgrade && apk --no-cache -q add openjdk11 ca-certificates +dockerPrepareEnvironment= dockerJavaOpts=-Xmx512m -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=70 -Djava.security.egd=file:/dev/./urandom \ No newline at end of file diff --git a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java index 9b15b5e..19cdc53 100644 --- a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java +++ b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java @@ -1,8 +1,25 @@ +/* + * Copyright 2019 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.epam.reportportal.config; import com.epam.reportportal.storage.DataStorageService; import com.epam.reportportal.storage.LocalDataStorageService; import com.epam.reportportal.storage.S3DataStorageService; +import com.epam.reportportal.utils.FeatureFlagHandler; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.cache.CacheLoader; @@ -24,73 +41,26 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +/** + * Blob storage configuration. + * + * @author Dzianis_Shybeka + */ @Configuration public class DataStorageConfig { - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") - public DataStorageService localDataStore( - @Value("${datastore.default.path:/data/store}") String storagePath) { - return new LocalDataStorageService(storagePath); - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") - public BlobStore minioBlobStore(@Value("${datastore.minio.accessKey}") String accessKey, - @Value("${datastore.minio.secretKey}") String secretKey, - @Value("${datastore.minio.endpoint}") String endpoint) { - - BlobStoreContext blobStoreContext = ContextBuilder.newBuilder("s3") - .endpoint(endpoint) - .credentials(accessKey, secretKey) - .buildView(BlobStoreContext.class); - - return blobStoreContext.getBlobStore(); - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") - public DataStorageService minioDataStore(@Autowired BlobStore blobStore, - @Value("${datastore.minio.bucketPrefix}") String bucketPrefix, - @Value("${datastore.minio.defaultBucketName}") String defaultBucketName) { - return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName); - } - - @Bean - @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") - public BlobStore blobStore(@Value("${datastore.s3.accessKey}") String accessKey, - @Value("${datastore.s3.secretKey}") String secretKey, - @Value("${datastore.s3.region}") String region) { - Iterable modules = ImmutableSet.of(new CustomBucketToRegionModule(region)); - - BlobStoreContext blobStoreContext = ContextBuilder.newBuilder("aws-s3") - .modules(modules) - .credentials(accessKey, secretKey) - .buildView(BlobStoreContext.class); - - return blobStoreContext.getBlobStore(); - } - - @Bean - @Primary - @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") - public DataStorageService s3DataStore(@Autowired BlobStore blobStore, - @Value("${datastore.s3.bucketPrefix}") String bucketPrefix, - @Value("${datastore.s3.defaultBucketName}") String defaultBucketName) { - return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName); - } - /** - * Amazon has a general work flow they publish that allows clients to always find the correct URL - * endpoint for a given bucket: 1) ask s3.amazonaws.com for the bucket location 2) use the url - * returned to make the container specific request (get/put, etc) Jclouds cache the results from - * the first getBucketLocation call and use that region-specific URL, as needed. In this custom - * implementation of {@link AWSS3HttpApiModule} we are providing location from environment - * variable, so that we don't need to make getBucketLocation call + * Amazon has a general work flow they publish that allows clients to always find the correct + * URL endpoint for a given bucket: + * 1) ask s3.amazonaws.com for the bucket location + * 2) use the url returned to make the container specific request (get/put, etc) + * Jclouds cache the results from the first getBucketLocation call and use that + * region-specific URL, as needed. + * In this custom implementation of {@link AWSS3HttpApiModule} we are providing location + * from environment variable, so that we don't need to make getBucketLocation call */ @ConfiguresHttpApi private static class CustomBucketToRegionModule extends AWSS3HttpApiModule { - private final String region; public CustomBucketToRegionModule(String region) { @@ -106,7 +76,7 @@ protected CacheLoader> bucketToRegion( return new CacheLoader<>() { @Override - @SuppressWarnings({"Guava", "NullableProblems"}) + @SuppressWarnings({ "Guava", "NullableProblems" }) public Optional load(String bucket) { if (CustomBucketToRegionModule.this.region != null) { return Optional.of(CustomBucketToRegionModule.this.region); @@ -162,4 +132,82 @@ public String toString() { } } } + + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") + public DataStorageService localDataStore( + @Value("${datastore.path:/data/store}") String storagePath) { + return new LocalDataStorageService(storagePath); + } + + /** + * Creates BlobStore bean, that works with MinIO. + * + * @param accessKey accessKey to use + * @param secretKey secretKey to use + * @param endpoint MinIO endpoint + * @return {@link BlobStore} + */ + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") + public BlobStore minioBlobStore(@Value("${datastore.accessKey}") String accessKey, + @Value("${datastore.secretKey}") String secretKey, + @Value("${datastore.endpoint}") String endpoint) { + + BlobStoreContext blobStoreContext = + ContextBuilder.newBuilder("s3").endpoint(endpoint).credentials(accessKey, secretKey) + .buildView(BlobStoreContext.class); + + return blobStoreContext.getBlobStore(); + } + + /** + * Creates DataStore bean to work with MinIO. + * + * @param blobStore {@link BlobStore} object + * @param bucketPrefix Prefix for bucket name + * @param defaultBucketName Name of default bucket to use + * @param featureFlagHandler Instance of {@link FeatureFlagHandler} to check enabled features + * @return {@link DataStorageService} object + */ + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "minio") + public DataStorageService minioDataStore(@Autowired BlobStore blobStore, + @Value("${datastore.bucketPrefix}") String bucketPrefix, + @Value("${datastore.defaultBucketName}") String defaultBucketName, + FeatureFlagHandler featureFlagHandler) { + return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName, featureFlagHandler); + } + + /** + * Creates BlobStore bean, that works with AWS S3. + * + * @param accessKey accessKey to use + * @param secretKey secretKey to use + * @param region AWS S3 region to use. + * @return {@link BlobStore} + */ + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") + public BlobStore blobStore(@Value("${datastore.accessKey}") String accessKey, + @Value("${datastore.secretKey}") String secretKey, + @Value("${datastore.region}") String region) { + Iterable modules = ImmutableSet.of(new CustomBucketToRegionModule(region)); + + BlobStoreContext blobStoreContext = + ContextBuilder.newBuilder("aws-s3").modules(modules).credentials(accessKey, secretKey) + .buildView(BlobStoreContext.class); + + return blobStoreContext.getBlobStore(); + } + + @Bean + @Primary + @ConditionalOnProperty(name = "datastore.type", havingValue = "s3") + public DataStorageService s3DataStore(@Autowired BlobStore blobStore, + @Value("${datastore.bucketPrefix}") String bucketPrefix, + @Value("${datastore.defaultBucketName}") String defaultBucketName, + FeatureFlagHandler featureFlagHandler) { + return new S3DataStorageService(blobStore, bucketPrefix, defaultBucketName, featureFlagHandler); + } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java index b7d7676..fb61a06 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java @@ -1,13 +1,16 @@ package com.epam.reportportal.jobs.clean; import com.epam.reportportal.jobs.BaseJob; +import com.epam.reportportal.model.BlobNotFoundException; import com.epam.reportportal.storage.DataStorageService; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.Scheduled; @@ -24,18 +27,33 @@ public class CleanStorageJob extends BaseJob { private static final String ROLLBACK_ERROR_MESSAGE = "Rollback deleting transaction."; private static final String SELECT_AND_DELETE_DATA_CHUNK_QUERY = - "DELETE FROM attachment_deletion WHERE id in " + - "(SELECT id FROM attachment_deletion ORDER BY id LIMIT ?) RETURNING *"; + "DELETE FROM attachment_deletion WHERE id IN " + + "(SELECT id FROM attachment_deletion ORDER BY id LIMIT ?) RETURNING *"; + + private static final int MAX_BATCH_SIZE = 200000; private final DataStorageService storageService; private final int chunkSize; + private final int batchSize; + + /** + * Initializes {@link CleanStorageJob}. + * + * @param jdbcTemplate {@link JdbcTemplate} + * @param storageService {@link DataStorageService} + * @param chunkSize Size of elements deleted at once + */ public CleanStorageJob(JdbcTemplate jdbcTemplate, DataStorageService storageService, @Value("${rp.environment.variable.clean.storage.chunkSize}") int chunkSize) { super(jdbcTemplate); this.chunkSize = chunkSize; this.storageService = storageService; + this.batchSize = chunkSize <= MAX_BATCH_SIZE ? chunkSize : MAX_BATCH_SIZE; } + /** + * Deletes attachments, which are set to be deleted. + */ @Scheduled(cron = "${rp.environment.variable.clean.storage.cron}") @SchedulerLock(name = "cleanStorage", lockAtMostFor = "24h") @Transactional @@ -43,33 +61,48 @@ public void execute() { logStart(); AtomicInteger counter = new AtomicInteger(0); - jdbcTemplate.query(SELECT_AND_DELETE_DATA_CHUNK_QUERY, rs -> { + int batchNumber = 1; + while (batchNumber * batchSize <= chunkSize) { + List attachments = new ArrayList<>(); + List thumbnails = new ArrayList<>(); + jdbcTemplate.query(SELECT_AND_DELETE_DATA_CHUNK_QUERY, rs -> { + do { + String attachment = rs.getString("file_id"); + String thumbnail = rs.getString("thumbnail_id"); + if (attachment != null) { + attachments.add(attachment); + } + if (thumbnail != null) { + thumbnails.add(thumbnail); + } + } while (rs.next()); + }, batchSize); + + int attachmentsSize = thumbnails.size() + attachments.size(); + if (attachmentsSize == 0) { + break; + } try { - delete(rs.getString("file_id"), rs.getString("thumbnail_id")); - counter.incrementAndGet(); - while (rs.next()) { - delete(rs.getString("file_id"), rs.getString("thumbnail_id")); - counter.incrementAndGet(); - } + storageService.deleteAll( + thumbnails.stream().map(this::decode).collect(Collectors.toList())); + storageService.deleteAll( + attachments.stream().map(this::decode).collect(Collectors.toList())); + } catch (BlobNotFoundException e) { + LOGGER.info("File is not found when executing clean storage job"); } catch (Exception e) { throw new RuntimeException(ROLLBACK_ERROR_MESSAGE, e); } - }, chunkSize); - - logFinish(counter.get()); - } - private void delete(String fileId, String thumbnailId) throws Exception { - if (Strings.isNotBlank(fileId)) { - storageService.delete(decode(fileId)); - } - if (Strings.isNotBlank(thumbnailId)) { - storageService.delete(decode(thumbnailId)); + counter.addAndGet(attachmentsSize); + LOGGER.info("Iteration {}, deleted {} attachments", batchNumber, attachmentsSize); + batchNumber++; } + + logFinish(counter.get()); } private String decode(String data) { - return StringUtils.isEmpty(data) ? data - : new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); + return StringUtils.isEmpty(data) ? data : + new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); } } diff --git a/src/main/java/com/epam/reportportal/model/BlobNotFoundException.java b/src/main/java/com/epam/reportportal/model/BlobNotFoundException.java new file mode 100644 index 0000000..fbdabb4 --- /dev/null +++ b/src/main/java/com/epam/reportportal/model/BlobNotFoundException.java @@ -0,0 +1,35 @@ +package com.epam.reportportal.model; + +/** + * Checked exception for cases when file is not found in blob storage. + */ +public class BlobNotFoundException extends Exception { + + private String fileName; + + public BlobNotFoundException() { + super(); + } + + public BlobNotFoundException(String message) { + super(message); + } + + public BlobNotFoundException(String fileName, Throwable cause) { + super(cause); + this.fileName = fileName; + } + + public BlobNotFoundException(Throwable cause) { + super(cause); + } + + protected BlobNotFoundException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public String getFileName() { + return fileName; + } +} diff --git a/src/main/java/com/epam/reportportal/storage/DataStorageService.java b/src/main/java/com/epam/reportportal/storage/DataStorageService.java index 295f6b4..d28fc96 100644 --- a/src/main/java/com/epam/reportportal/storage/DataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/DataStorageService.java @@ -16,10 +16,11 @@ package com.epam.reportportal.storage; +import java.util.List; + /** * Storage service interface */ public interface DataStorageService { - - void delete(String filePath) throws Exception; + void deleteAll(List paths) throws Exception; } \ No newline at end of file diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index d26f266..c1f8cad 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -16,11 +16,13 @@ package com.epam.reportportal.storage; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Local storage service @@ -36,12 +38,14 @@ public LocalDataStorageService(String storageRootPath) { } @Override - public void delete(String filePath) throws IOException { - try { - Files.deleteIfExists(Paths.get(storageRootPath, filePath)); - } catch (IOException e) { - LOGGER.error("Unable to delete file '{}'", filePath, e); - throw e; + public void deleteAll(List paths) throws IOException { + for (String path : paths) { + try { + Files.deleteIfExists(Paths.get(storageRootPath, path)); + } catch (IOException e) { + LOGGER.error("Unable to delete file '{}'", path, e); + throw e; + } } } } diff --git a/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java b/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java index 938774e..a235465 100644 --- a/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java @@ -16,14 +16,21 @@ package com.epam.reportportal.storage; +import com.epam.reportportal.utils.FeatureFlag; +import com.epam.reportportal.utils.FeatureFlagHandler; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.jclouds.blobstore.BlobStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; /** - * S3 storage service + * S3 storage service. */ public class S3DataStorageService implements DataStorageService { @@ -33,37 +40,61 @@ public class S3DataStorageService implements DataStorageService { private final String bucketPrefix; private final String defaultBucketName; - public S3DataStorageService(BlobStore blobStore, String bucketPrefix, String defaultBucketName) { + private final FeatureFlagHandler featureFlagHandler; + + /** + * Creates instance of {@link S3DataStorageService}. + * + * @param blobStore {@link BlobStore} + * @param bucketPrefix Prefix for bucket name + * @param defaultBucketName Name for the default bucket(plugins, etc.) + * @param featureFlagHandler {@link FeatureFlagHandler} + */ + public S3DataStorageService(BlobStore blobStore, String bucketPrefix, String defaultBucketName, + FeatureFlagHandler featureFlagHandler) { this.blobStore = blobStore; this.bucketPrefix = bucketPrefix; this.defaultBucketName = defaultBucketName; + this.featureFlagHandler = featureFlagHandler; } @Override - public void delete(String filePath) throws Exception { - Path targetPath = Paths.get(filePath); - int nameCount = targetPath.getNameCount(); - - String bucket; - String objectName; - - if (nameCount > 1) { - bucket = bucketPrefix + retrievePath(targetPath, 0, 1); - objectName = retrievePath(targetPath, 1, nameCount); - } else { - bucket = defaultBucketName; - objectName = retrievePath(targetPath, 0, 1); + public void deleteAll(List paths) throws Exception { + if (CollectionUtils.isEmpty(paths)) { + return; } - - try { - blobStore.removeBlob(bucket, objectName); - } catch (Exception e) { - LOGGER.error("Unable to delete file '{}'", filePath, e); - throw e; + if (featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { + removeFiles(defaultBucketName, paths); + } else { + Map> bucketPathMap = new HashMap<>(); + for (String path : paths) { + Path targetPath = Paths.get(path); + int nameCount = targetPath.getNameCount(); + String bucket = retrievePath(targetPath, 0, 1); + String cutPath = retrievePath(targetPath, 1, nameCount); + if (bucketPathMap.containsKey(bucket)) { + bucketPathMap.get(bucket).add(cutPath); + } else { + List bucketPaths = new ArrayList<>(); + bucketPaths.add(cutPath); + bucketPathMap.put(bucket, bucketPaths); + } + } + for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { + removeFiles(bucketPrefix + bucketPaths.getKey(), bucketPaths.getValue()); + } } } private String retrievePath(Path path, int beginIndex, int endIndex) { return String.valueOf(path.subpath(beginIndex, endIndex)); } + + private void removeFiles(String bucketName, List paths) { + try { + blobStore.removeBlobs(bucketName, paths); + } catch (Exception e) { + LOGGER.warn("Exception {} is occurred during deleting file", e.getMessage()); + } + } } diff --git a/src/main/java/com/epam/reportportal/utils/FeatureFlag.java b/src/main/java/com/epam/reportportal/utils/FeatureFlag.java new file mode 100644 index 0000000..43b99ed --- /dev/null +++ b/src/main/java/com/epam/reportportal/utils/FeatureFlag.java @@ -0,0 +1,35 @@ +package com.epam.reportportal.utils; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Enumeration of current feature flags. + * + * @author Ivan Kustau + */ +public enum FeatureFlag { + SINGLE_BUCKET("singleBucket"); + + private final String name; + + FeatureFlag(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + /** + * Returns {@link Optional} of {@link FeatureFlag} by string. + * + * @param name Name of feature flag + * @return {@link Optional} of {@link FeatureFlag} by string + */ + public static Optional fromString(String name) { + return Optional.ofNullable(name).flatMap( + str -> Arrays.stream(values()).filter(it -> it.name.equalsIgnoreCase(str)).findAny()); + + } +} \ No newline at end of file diff --git a/src/main/java/com/epam/reportportal/utils/FeatureFlagHandler.java b/src/main/java/com/epam/reportportal/utils/FeatureFlagHandler.java new file mode 100644 index 0000000..9c286f9 --- /dev/null +++ b/src/main/java/com/epam/reportportal/utils/FeatureFlagHandler.java @@ -0,0 +1,37 @@ +package com.epam.reportportal.utils; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +/** + * Component for checking enabled feature flags. + * + * @author Ivan Kustau + */ +@Component +public class FeatureFlagHandler { + + private final Set enabledFeatureFlagsSet = new HashSet<>(); + + /** + * Initialises {@link FeatureFlagHandler} by environment variable with enabled feature flags. + * + * @param featureFlags Set of enabled feature flags + */ + public FeatureFlagHandler( + @Value("#{'${rp.feature.flags}'.split(',')}") Set featureFlags) { + + if (!CollectionUtils.isEmpty(featureFlags)) { + featureFlags.stream().map(FeatureFlag::fromString).filter(Optional::isPresent) + .map(Optional::get).forEach(enabledFeatureFlagsSet::add); + } + } + + public boolean isEnabled(FeatureFlag featureFlag) { + return enabledFeatureFlagsSet.contains(featureFlag); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 78eae83..ae846c2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,4 +6,5 @@ info.build.repo=${repo} server.port=8686 management.endpoints.web.exposure.include=info, health management.endpoints.web.base-path=/ -management.endpoint.info.enabled=true \ No newline at end of file +management.endpoint.info.enabled=true +management.info.env.enabled=true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 33bfe7d..8d360b7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,6 @@ rp: + feature: + flags: environment: variable: elements-counter: @@ -8,6 +10,7 @@ rp: ## 30 seconds cron: '*/30 * * * * *' chunkSize: 1000 + batchSize: 100 attachment: ## 2 minutes cron: '0 */2 * * * *' @@ -76,14 +79,12 @@ rp: datastore: path: /data/storage - minio: - endpoint: http://minio:9000 - accessKey: - secretKey: - bucketPrefix: prj- - defaultBucketName: rp-bucket - region: #{null} - # could be one of [filesystem, s3, minio] type: minio + endpoint: http://minio:9000 + accessKey: + secretKey: + bucketPrefix: prj- + defaultBucketName: rp-bucket + region: #{null} From eb6942d7973cdf10caa6377fdceb49887dab8733 Mon Sep 17 00:00:00 2001 From: hlebkanonik Date: Wed, 6 Sep 2023 12:10:08 +0200 Subject: [PATCH 03/27] added github.run_number to rc build --- .github/workflows/rc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rc.yaml b/.github/workflows/rc.yaml index 7491849..a10b4a1 100644 --- a/.github/workflows/rc.yaml +++ b/.github/workflows/rc.yaml @@ -38,7 +38,7 @@ jobs: - name: Create variables id: vars run: | - echo "tag=$(echo ${{ github.ref_name }} | tr '/' '-')" >> $GITHUB_OUTPUT + echo "tag=$(echo ${{ github.ref_name }}-${{ github.run_number }} | tr '/' '-')" >> $GITHUB_OUTPUT echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT echo "version=$(echo '${{ github.ref_name }}' | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" >> $GITHUB_OUTPUT From e91f0cf8fdcc297c633b44f6bb86aa13a2598477 Mon Sep 17 00:00:00 2001 From: Ivan_Kustau Date: Wed, 4 Oct 2023 16:03:10 +0300 Subject: [PATCH 04/27] Add possibility to have no auth in ES --- .../ProcessingRabbitMqConfiguration.java | 73 ------------------- .../elastic/SimpleElasticSearchClient.java | 19 +++-- 2 files changed, 12 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java diff --git a/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java b/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java deleted file mode 100644 index cb74095..0000000 --- a/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 EPAM Systems - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.epam.reportportal.config.rabbit; - -import java.net.URI; -import org.springframework.amqp.rabbit.annotation.EnableRabbit; -import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Pavel Bortnik - */ -@EnableRabbit -@Configuration -public class ProcessingRabbitMqConfiguration { - - @Bean(name = "processingConnectionFactory") - public ConnectionFactory processingConnectionFactory(@Value("${rp.amqp.addresses}") URI addresses, - @Value("${rp.amqp.base-vhost}") String virtualHost) { - CachingConnectionFactory factory = new CachingConnectionFactory(addresses); - factory.setVirtualHost(virtualHost); - return factory; - } - - @Bean - public SimpleRabbitListenerContainerFactory processingRabbitListenerContainerFactory( - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory, - MessageConverter jsonMessageConverter, - @Value("${rp.amqp.maxLogConsumer}") int maxLogConsumer) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory); - factory.setMaxConcurrentConsumers(maxLogConsumer); - factory.setMessageConverter(jsonMessageConverter); - return factory; - } - - @Bean - public RabbitAdmin processingRabbitAdmin( - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { - return new RabbitAdmin(connectionFactory); - } - - @Bean - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( - SimpleRabbitListenerContainerFactoryConfigurer configurer, - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - configurer.configure(factory, connectionFactory); - return factory; - } -} diff --git a/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java b/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java index 8bfc59b..233f437 100644 --- a/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java +++ b/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java @@ -34,10 +34,13 @@ public class SimpleElasticSearchClient implements ElasticSearchClient { private final RestTemplate restTemplate; public SimpleElasticSearchClient(@Value("${rp.elasticsearch.host}") String host, - @Value("${rp.elasticsearch.username}") String username, - @Value("${rp.elasticsearch.password}") String password) { + @Value("${rp.elasticsearch.username:}") String username, + @Value("${rp.elasticsearch.password:}") String password) { restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + + if (!username.isEmpty() && !password.isEmpty()) { + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + } this.host = host; } @@ -62,9 +65,10 @@ public void save(List logMessageList) { } }); - logsByIndex.forEach((indexName, body) -> { - restTemplate.put(host + "/" + indexName + "/_bulk?refresh", getStringHttpEntity(body)); - }); + logsByIndex.forEach( + (indexName, body) -> restTemplate.put(host + "/" + indexName + "/_bulk?refresh", + getStringHttpEntity(body) + )); } @Override @@ -75,7 +79,8 @@ public void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId) { HttpEntity deleteRequest = getStringHttpEntity(deleteByLaunch.toString()); restTemplate.postForObject(host + "/" + indexName + "/_delete_by_query", deleteRequest, - JSONObject.class); + JSONObject.class + ); } catch (Exception exception) { // to avoid checking of exists stream or not LOGGER.info("DELETE logs from stream ES error " + indexName + " " + exception.getMessage()); From 3c6bd8c99e3035a47fc4a518ae62805c57c71bfe Mon Sep 17 00:00:00 2001 From: Ivan_Kustau Date: Wed, 4 Oct 2023 16:03:10 +0300 Subject: [PATCH 05/27] EPMRPP-86775 || Add possibility to have no auth in ES --- .../ProcessingRabbitMqConfiguration.java | 73 ------------------- .../elastic/SimpleElasticSearchClient.java | 19 +++-- 2 files changed, 12 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java diff --git a/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java b/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java deleted file mode 100644 index cb74095..0000000 --- a/src/main/java/com/epam/reportportal/config/rabbit/ProcessingRabbitMqConfiguration.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 EPAM Systems - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.epam.reportportal.config.rabbit; - -import java.net.URI; -import org.springframework.amqp.rabbit.annotation.EnableRabbit; -import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Pavel Bortnik - */ -@EnableRabbit -@Configuration -public class ProcessingRabbitMqConfiguration { - - @Bean(name = "processingConnectionFactory") - public ConnectionFactory processingConnectionFactory(@Value("${rp.amqp.addresses}") URI addresses, - @Value("${rp.amqp.base-vhost}") String virtualHost) { - CachingConnectionFactory factory = new CachingConnectionFactory(addresses); - factory.setVirtualHost(virtualHost); - return factory; - } - - @Bean - public SimpleRabbitListenerContainerFactory processingRabbitListenerContainerFactory( - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory, - MessageConverter jsonMessageConverter, - @Value("${rp.amqp.maxLogConsumer}") int maxLogConsumer) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory); - factory.setMaxConcurrentConsumers(maxLogConsumer); - factory.setMessageConverter(jsonMessageConverter); - return factory; - } - - @Bean - public RabbitAdmin processingRabbitAdmin( - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { - return new RabbitAdmin(connectionFactory); - } - - @Bean - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( - SimpleRabbitListenerContainerFactoryConfigurer configurer, - @Qualifier("processingConnectionFactory") ConnectionFactory connectionFactory) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - configurer.configure(factory, connectionFactory); - return factory; - } -} diff --git a/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java b/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java index 8bfc59b..233f437 100644 --- a/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java +++ b/src/main/java/com/epam/reportportal/elastic/SimpleElasticSearchClient.java @@ -34,10 +34,13 @@ public class SimpleElasticSearchClient implements ElasticSearchClient { private final RestTemplate restTemplate; public SimpleElasticSearchClient(@Value("${rp.elasticsearch.host}") String host, - @Value("${rp.elasticsearch.username}") String username, - @Value("${rp.elasticsearch.password}") String password) { + @Value("${rp.elasticsearch.username:}") String username, + @Value("${rp.elasticsearch.password:}") String password) { restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + + if (!username.isEmpty() && !password.isEmpty()) { + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, password)); + } this.host = host; } @@ -62,9 +65,10 @@ public void save(List logMessageList) { } }); - logsByIndex.forEach((indexName, body) -> { - restTemplate.put(host + "/" + indexName + "/_bulk?refresh", getStringHttpEntity(body)); - }); + logsByIndex.forEach( + (indexName, body) -> restTemplate.put(host + "/" + indexName + "/_bulk?refresh", + getStringHttpEntity(body) + )); } @Override @@ -75,7 +79,8 @@ public void deleteLogsByLaunchIdAndProjectId(Long launchId, Long projectId) { HttpEntity deleteRequest = getStringHttpEntity(deleteByLaunch.toString()); restTemplate.postForObject(host + "/" + indexName + "/_delete_by_query", deleteRequest, - JSONObject.class); + JSONObject.class + ); } catch (Exception exception) { // to avoid checking of exists stream or not LOGGER.info("DELETE logs from stream ES error " + indexName + " " + exception.getMessage()); From 9ae10b0a8e5d3faea4e44b6d4f1aed032ac87135 Mon Sep 17 00:00:00 2001 From: Siarhei Hrabko <45555481+grabsefx@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:07:45 +0300 Subject: [PATCH 06/27] EPMRPP-86812 || show health info detailed by default (#106) * EPMRPP-86812 || show health info detailed by default --- src/main/resources/application.properties | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ae846c2..529387e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,4 +7,7 @@ server.port=8686 management.endpoints.web.exposure.include=info, health management.endpoints.web.base-path=/ management.endpoint.info.enabled=true -management.info.env.enabled=true \ No newline at end of file +management.info.env.enabled=true + +# Health info +management.endpoint.health.show-details=always From fa8ec112a486349e7a6b5b462adab71fcd9769bf Mon Sep 17 00:00:00 2001 From: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Date: Wed, 18 Oct 2023 10:47:05 +0300 Subject: [PATCH 07/27] EPMRPP-86973 || Fix filesystem storage and delete expired users job (#107) --- .../jobs/clean/DeleteExpiredUsersJob.java | 100 +++++++++--------- .../storage/DataStorageService.java | 2 + .../storage/LocalDataStorageService.java | 12 +++ .../storage/S3DataStorageService.java | 12 +++ 4 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java index 63668e9..d5249c0 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java @@ -23,16 +23,21 @@ import com.epam.reportportal.model.activity.event.UnassignUserEvent; import com.epam.reportportal.model.activity.event.UserDeletedEvent; import com.epam.reportportal.service.MessageBus; +import com.epam.reportportal.storage.DataStorageService; +import com.epam.reportportal.utils.FeatureFlag; +import com.epam.reportportal.utils.FeatureFlagHandler; import com.epam.reportportal.utils.ValidationUtil; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -import org.jclouds.blobstore.BlobStore; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -52,8 +57,7 @@ * @author Andrei Piankouski */ @Service -@ConditionalOnProperty(prefix = "rp.environment.variable", - name = "clean.expiredUser.retentionPeriod") +@ConditionalOnProperty(prefix = "rp.environment.variable", name = "clean.expiredUser.retentionPeriod") public class DeleteExpiredUsersJob extends BaseJob { public static final Logger LOGGER = LoggerFactory.getLogger(DeleteExpiredUsersJob.class); @@ -64,32 +68,21 @@ public class DeleteExpiredUsersJob extends BaseJob { private static final String USER_DELETION_TEMPLATE = "userDeletionNotification"; - private static final String SELECT_EXPIRED_USERS = "SELECT u.id AS user_id, " - + "p.id AS project_id, u.email as user_email " - + "FROM users u " - + "LEFT JOIN api_keys ak ON u.id = ak.user_id " - + "LEFT JOIN project p ON u.login || '_personal' = p.name AND p.project_type = 'PERSONAL' " - + "WHERE (u.metadata->'metadata'->>'last_login')::BIGINT <= :retentionPeriod " - + "AND ( " - + "ak.user_id IS NULL " - + "OR (EXTRACT(EPOCH FROM ak.last_used_at) * 1000)::BIGINT <= :retentionPeriod " - + "OR NOT EXISTS (SELECT 1 FROM api_keys WHERE user_id = u.id AND last_used_at IS NOT NULL) " - + ") " - + "AND u.role != 'ADMINISTRATOR' " - + "GROUP BY u.id, p.id"; - - private static final String MOVE_ATTACHMENTS_TO_DELETE = - "WITH moved_rows AS (DELETE FROM attachment " - + "WHERE project_id = :projectId RETURNING id, file_id, thumbnail_id, creation_date) " - + "INSERT INTO attachment_deletion " - + "(id, file_id, thumbnail_id, creation_attachment_date, deletion_date) " - + "SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows"; + private static final String SELECT_EXPIRED_USERS = + "SELECT u.id AS user_id, " + "p.id AS project_id, u.email AS user_email " + "FROM users u " + + "LEFT JOIN api_keys ak ON u.id = ak.user_id " + + "LEFT JOIN project p ON u.login || '_personal' = p.name AND p.project_type = 'PERSONAL' " + + "WHERE (u.metadata->'metadata'->>'last_login')::BIGINT <= :retentionPeriod " + "AND ( " + + "ak.user_id IS NULL " + + "OR (EXTRACT(EPOCH FROM ak.last_used_at) * 1000)::BIGINT <= :retentionPeriod " + + "OR NOT EXISTS (SELECT 1 FROM api_keys WHERE user_id = u.id AND last_used_at IS NOT NULL) " + + ") " + "AND u.role != 'ADMINISTRATOR' " + "GROUP BY u.id, p.id"; + + private static final String DELETE_ATTACHMENTS_BY_PROJECT = + "DELETE FROM attachment WHERE project_id = :projectId RETURNING file_id"; private static final String DELETE_PROJECT_ISSUE_TYPES = - "DELETE FROM issue_type " - + "WHERE id IN (" - + " SELECT it.id " - + " FROM issue_type it " + "DELETE FROM issue_type " + "WHERE id IN (" + " SELECT it.id " + " FROM issue_type it " + " JOIN issue_type_project itp ON it.id = itp.issue_type_id " + " WHERE itp.project_id = :projectId " + " AND it.locator NOT IN ('pb001', 'ab001', 'si001', 'ti001', 'nd001'))"; @@ -99,33 +92,32 @@ public class DeleteExpiredUsersJob extends BaseJob { private static final String DELETE_PROJECTS_BY_ID_LIST = "DELETE FROM project WHERE id IN (:projectIds)"; - private static final String FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS = "SELECT p.id " - + "FROM project_user pu " - + "JOIN project p ON pu.project_id = p.id " - + "WHERE p.project_type != 'PERSONAL' AND pu.user_id IN (:userIds)"; + private static final String FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS = + "SELECT p.id " + "FROM project_user pu " + "JOIN project p ON pu.project_id = p.id " + + "WHERE p.project_type != 'PERSONAL' AND pu.user_id IN (:userIds)"; @Value("${rp.environment.variable.clean.expiredUser.retentionPeriod}") private Long retentionPeriod; - private final BlobStore blobStore; + private final DataStorageService dataStorageService; private final IndexerServiceClient indexerServiceClient; - private final MessageBus messageBus; + private final FeatureFlagHandler featureFlagHandler; - @Value("${datastore.bucketPrefix}") - private String bucketPrefix; + private final MessageBus messageBus; @Autowired public DeleteExpiredUsersJob(JdbcTemplate jdbcTemplate, - NamedParameterJdbcTemplate namedParameterJdbcTemplate, - BlobStore blobStore, IndexerServiceClient indexerServiceClient, - MessageBus messageBus) { + NamedParameterJdbcTemplate namedParameterJdbcTemplate, DataStorageService dataStorageService, + IndexerServiceClient indexerServiceClient, MessageBus messageBus, + FeatureFlagHandler featureFlagHandler) { super(jdbcTemplate); this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - this.blobStore = blobStore; + this.dataStorageService = dataStorageService; this.indexerServiceClient = indexerServiceClient; this.messageBus = messageBus; + this.featureFlagHandler = featureFlagHandler; } @Override @@ -166,10 +158,10 @@ private void publishUnassignUserEvents(List nonPersonalProjectsByUserIds) } private List findNonPersonalProjectIdsByUserIds(List userIds) { - return CollectionUtils.isEmpty(userIds) - ? Collections.emptyList() - : namedParameterJdbcTemplate.queryForList(FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS, - Map.of("userIds", userIds), Long.class); + return CollectionUtils.isEmpty(userIds) ? Collections.emptyList() : + namedParameterJdbcTemplate.queryForList(FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS, + Map.of("userIds", userIds), Long.class + ); } private List findUsersAndPersonalProjects() { @@ -188,13 +180,17 @@ private List findUsersAndPersonalProjects() { } private void deleteProjectAssociatedData(Long projectId) { - deleteAttachmentsByProjectId(projectId); + List paths = deleteAttachmentsByProjectId(projectId); deleteProjectIssueTypes(projectId); indexerServiceClient.removeSuggest(projectId); try { - blobStore.deleteContainer(bucketPrefix + projectId); + if (featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { + dataStorageService.deleteAll(paths.stream().map(this::decode).collect(Collectors.toList())); + } else { + dataStorageService.deleteContainer(projectId.toString()); + } } catch (Exception e) { - LOGGER.warn("Cannot delete attachments bucket " + bucketPrefix + projectId); + LOGGER.warn("Cannot delete attachments bucket for project {} ", projectId); } indexerServiceClient.deleteIndex(projectId); } @@ -214,10 +210,9 @@ private void deleteProjectIssueTypes(Long projectId) { namedParameterJdbcTemplate.update(DELETE_PROJECT_ISSUE_TYPES, params); } - private void deleteAttachmentsByProjectId(Long projectId) { - MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("projectId", projectId); - namedParameterJdbcTemplate.update(MOVE_ATTACHMENTS_TO_DELETE, params); + private List deleteAttachmentsByProjectId(Long projectId) { + return namedParameterJdbcTemplate.queryForList( + DELETE_ATTACHMENTS_BY_PROJECT, Map.of("projectId", projectId), String.class); } private void deleteProjectsByIds(List projectIds) { @@ -276,4 +271,9 @@ public void setProjectId(long projectId) { this.projectId = projectId; } } + + private String decode(String data) { + return StringUtils.isEmpty(data) ? data : + new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); + } } diff --git a/src/main/java/com/epam/reportportal/storage/DataStorageService.java b/src/main/java/com/epam/reportportal/storage/DataStorageService.java index d28fc96..d450c2b 100644 --- a/src/main/java/com/epam/reportportal/storage/DataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/DataStorageService.java @@ -23,4 +23,6 @@ */ public interface DataStorageService { void deleteAll(List paths) throws Exception; + + void deleteContainer(String containerName) throws Exception; } \ No newline at end of file diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index c1f8cad..b418e75 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -16,7 +16,9 @@ package com.epam.reportportal.storage; +import java.io.File; import java.util.List; +import javax.lang.model.type.ErrorType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,4 +50,14 @@ public void deleteAll(List paths) throws IOException { } } } + + @Override + public void deleteContainer(String containerName) throws IOException{ + try { + Files.deleteIfExists(Paths.get(storageRootPath, containerName)); + } catch (IOException e) { + LOGGER.error("Unable to delete container '{}'", containerName, e); + throw e; + } + } } diff --git a/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java b/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java index 14163bb..f35f632 100644 --- a/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/S3DataStorageService.java @@ -43,6 +43,8 @@ public class S3DataStorageService implements DataStorageService { private final FeatureFlagHandler featureFlagHandler; + private static final String PROJECT_PREFIX = "/project-data/"; + /** * Creates instance of {@link S3DataStorageService}. * @@ -89,6 +91,15 @@ public void deleteAll(List paths) throws Exception { } } + @Override + public void deleteContainer(String containerName) { + try { + blobStore.deleteContainer(bucketPrefix + containerName + bucketPostfix); + } catch (Exception e) { + LOGGER.warn("Exception {} is occurred during deleting container", e.getMessage()); + } + } + private String retrievePath(Path path, int beginIndex, int endIndex) { return String.valueOf(path.subpath(beginIndex, endIndex)); } @@ -100,4 +111,5 @@ private void removeFiles(String bucketName, List paths) { LOGGER.warn("Exception {} is occurred during deleting file", e.getMessage()); } } + } From 4eb8b3ba282bf58d32fb0401211d8cd19e113af8 Mon Sep 17 00:00:00 2001 From: APiankouski <109206864+APiankouski@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:08:31 +0300 Subject: [PATCH 08/27] EPMRPP-87017 || Events Retention policy job --- .../jobs/clean/EventsRetentionPolicyJob.java | 70 +++++++++++++++++++ src/main/resources/application.yml | 3 + 2 files changed, 73 insertions(+) create mode 100644 src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java diff --git a/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java b/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java new file mode 100644 index 0000000..48b6683 --- /dev/null +++ b/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java @@ -0,0 +1,70 @@ +/* + * Copyright 2023 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.jobs.clean; + +import com.epam.reportportal.jobs.BaseJob; +import com.epam.reportportal.utils.ValidationUtil; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** + * Deleting Events by retention policy. + * + * @author Andrei Piankouski + */ +@Service +@ConditionalOnProperty(prefix = "rp.environment.variable", + name = "clean.events.retentionPeriod") +public class EventsRetentionPolicyJob extends BaseJob { + + private static final String DELETE_ACTIVITY_BY_RETENTION = + "DELETE FROM activity " + + "WHERE created_at < NOW() - (:retentionPeriod * interval '1 day')"; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Value("${rp.environment.variable.clean.events.retentionPeriod}") + private Long retentionPeriod; + + public EventsRetentionPolicyJob(JdbcTemplate jdbcTemplate, + NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate); + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + @Scheduled(cron = "${rp.environment.variable.clean.events.cron}") + @SchedulerLock(name = "eventsRetentionPolicy", lockAtMostFor = "24h") + public void execute() { + if (ValidationUtil.isInvalidRetentionPeriod(retentionPeriod)) { + LOGGER.info("No events are deleted"); + return; + } + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("retentionPeriod", retentionPeriod); + int deletedRows = namedParameterJdbcTemplate.update(DELETE_ACTIVITY_BY_RETENTION, params); + LOGGER.info("{} - events were deleted due to retention policy", deletedRows); + } + + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5768f16..d60d20d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,6 +32,9 @@ rp: expiredUser: ## 24 hours cron: '0 0 */24 * * *' + events: + ## 24 hours + cron: '0 0 */24 * * *' storage: project: ## 1 minute From c0e0f49051ca427e962d43d9c264c8861873a54f Mon Sep 17 00:00:00 2001 From: APiankouski <109206864+APiankouski@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:48:29 +0300 Subject: [PATCH 09/27] Epmrpp 85756 update java 21(#109) --- .github/workflows/build.yml | 4 +- .github/workflows/manually-release.yml | 4 +- .github/workflows/release.yml | 4 +- Dockerfile | 4 +- build.gradle | 25 +- gradle/wrapper/gradle-wrapper.jar | Bin 58694 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 282 +++++++++++------- gradlew.bat | 35 +-- .../reportportal/jobs/clean/BaseCleanJob.java | 5 +- .../jobs/clean/CleanAttachmentJob.java | 9 +- .../reportportal/jobs/clean/CleanLogJob.java | 3 - .../jobs/clean/CleanStorageJob.java | 5 +- .../jobs/clean/DeleteExpiredUsersJob.java | 53 ++-- .../jobs/clean/EventsRetentionPolicyJob.java | 6 +- .../notification/NotifyUserExpirationJob.java | 49 +-- .../storage/CalculateAllocatedStorageJob.java | 2 +- 17 files changed, 282 insertions(+), 212 deletions(-) mode change 100755 => 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a508772..e89896f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,11 +18,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: distribution: 'adopt' - java-version: '11' + java-version: '21' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/manually-release.yml b/.github/workflows/manually-release.yml index 9af4925..e628a0f 100644 --- a/.github/workflows/manually-release.yml +++ b/.github/workflows/manually-release.yml @@ -19,11 +19,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: distribution: 'adopt' - java-version: '11' + java-version: '21' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80cbee9..f278e16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,11 +20,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: distribution: 'adopt' - java-version: '11' + java-version: '21' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/Dockerfile b/Dockerfile index ac5e088..a40b846 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:6.8.3-jdk11 AS build +FROM gradle:8.4.0-jdk21 AS build ARG RELEASE_MODE ARG APP_VERSION ARG GITHUB_USER @@ -14,7 +14,7 @@ RUN if [ "${RELEASE_MODE}" = true ]; then \ else gradle build --exclude-task test -Dorg.gradle.project.version=${APP_VERSION}; fi # For ARM build use flag: `--platform linux/arm64` -FROM --platform=$BUILDPLATFORM amazoncorretto:11.0.20 +FROM --platform=$BUILDPLATFORM amazoncorretto:21.0.1 LABEL version=${APP_VERSION} description="EPAM Report portal. Jobs Service" maintainer="Andrei Varabyeu , Hleb Kanonik " ARG APP_VERSION=${APP_VERSION} ENV APP_DIR=/usr/app diff --git a/build.gradle b/build.gradle index 0e4fc16..49d63d9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.7.0' + id 'org.springframework.boot' version '2.7.16' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } @@ -9,7 +9,7 @@ project.ext { releaseMode = project.hasProperty("releaseMode") } -def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? '5.10.0' : 'master') +def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? '5.10.0' : 'EPMRPP-85756') apply from: "$scriptsUrl/build-docker.gradle" apply from: "$scriptsUrl/build-commons.gradle" @@ -18,10 +18,10 @@ apply from: "$scriptsUrl/build-info.gradle" apply from: "$scriptsUrl/release-service.gradle" apply from: "$scriptsUrl/signing.gradle" -sourceCompatibility = '11' +sourceCompatibility = '21' wrapper { - gradleVersion = '6.8' + gradleVersion = '8.4' } bootJar { @@ -53,11 +53,6 @@ ext['snakeyaml.version'] = '1.31' // dependencies { - if (releaseMode) { - implementation 'com.github.reportportal:commons-events:5.10.0' - } else { - implementation 'com.github.reportportal:commons-events:e337f8b7be' - } implementation group: 'org.json', name: 'json', version: '20220320' @@ -81,17 +76,17 @@ dependencies { // https://avd.aquasec.com/nvd/cve-2020-8908 // implementation 'com.google.guava:guava:30.0-jre'; - compile "com.rabbitmq:http-client:2.1.0.RELEASE" + implementation "com.rabbitmq:http-client:2.1.0.RELEASE" //Fix CVE implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2' runtimeOnly 'org.postgresql:postgresql' - testCompile 'org.junit.jupiter:junit-jupiter:5.5.2' - testCompile 'org.junit.jupiter:junit-jupiter-api:5.5.2' - testCompile 'org.junit.jupiter:junit-jupiter-params:5.5.2' - testCompile 'org.junit.jupiter:junit-jupiter-engine:5.5.2' - testCompile 'org.mockito:mockito-junit-jupiter:3.1.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.5.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.5.2' + testImplementation 'org.mockito:mockito-junit-jupiter:3.1.0' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100755 new mode 100644 index 490fda8577df6c95960ba7077c43220e5bb2c0d9..7f93135c49b765f8051ef9d0a6055ff8e46073d8 GIT binary patch delta 44832 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 agh8O80uQ~P|J4;iKprmiHhuWlfBz3&BC9R{ delta 39743 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 I0x&TC2akg(od5s; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da9702f..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 2fe81a7..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,111 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,87 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 9109989..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java b/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java index b9aa808..727a338 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/BaseCleanJob.java @@ -18,8 +18,9 @@ public abstract class BaseCleanJob extends BaseJob { protected static final String KEEP_SCREENSHOTS = "job.keepScreenshots"; protected final String SELECT_PROJECTS_ATTRIBUTES = - "SELECT pa.project_id AS id, pa.value AS attribute_value FROM project_attribute pa " - + "JOIN attribute a ON pa.attribute_id = a.id WHERE a.name = ? AND pa.value != '0' AND TRIM(pa.value) != '';"; + """ + SELECT pa.project_id AS id, pa.value AS attribute_value FROM project_attribute pa\s + JOIN attribute a ON pa.attribute_id = a.id WHERE a.name = ? AND pa.value != '0' AND TRIM(pa.value) != '';"""; public BaseCleanJob(JdbcTemplate jdbcTemplate) { super(jdbcTemplate); diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java index 942fdfe..afc14a5 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanAttachmentJob.java @@ -17,10 +17,11 @@ @Service public class CleanAttachmentJob extends BaseCleanJob { - private static final String MOVING_QUERY = - "WITH moved_rows AS (DELETE FROM attachment WHERE project_id = ? AND creation_date <= ?::TIMESTAMP RETURNING *) " - + "INSERT INTO attachment_deletion (id, file_id, thumbnail_id, creation_attachment_date, deletion_date) " - + "SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows;"; + private static final String MOVING_QUERY = + """ + WITH moved_rows AS (DELETE FROM attachment WHERE project_id = ? AND creation_date <= ?::TIMESTAMP RETURNING *) \s + INSERT INTO attachment_deletion (id, file_id, thumbnail_id, creation_attachment_date, deletion_date)\s + SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows;"""; public CleanAttachmentJob(JdbcTemplate jdbcTemplate) { super(jdbcTemplate); diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java index 71796cb..0446356 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanLogJob.java @@ -70,9 +70,6 @@ void removeLogs() { if (!launchIds.isEmpty()) { deleteLogsFromElasticsearchByLaunchIdsAndProjectId(launchIds, projectId); } - -// eventPublisher.publishEvent(new ElementsDeletedEvent(this, projectId, deleted)); -// LOGGER.info("Send event with elements deleted number {} for project {}", deleted, projectId); } }); } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java index 16ebe84..eb92c09 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java @@ -27,8 +27,9 @@ public class CleanStorageJob extends BaseJob { private static final String ROLLBACK_ERROR_MESSAGE = "Rollback deleting transaction."; private static final String SELECT_AND_DELETE_DATA_CHUNK_QUERY = - "DELETE FROM attachment_deletion WHERE id IN " - + "(SELECT id FROM attachment_deletion ORDER BY id LIMIT ?) RETURNING *"; + """ + DELETE FROM attachment_deletion WHERE id IN\s + (SELECT id FROM attachment_deletion ORDER BY id LIMIT ?) RETURNING *"""; private static final int MAX_BATCH_SIZE = 200000; private final DataStorageService storageService; diff --git a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java index d5249c0..be702b7 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java @@ -57,7 +57,8 @@ * @author Andrei Piankouski */ @Service -@ConditionalOnProperty(prefix = "rp.environment.variable", name = "clean.expiredUser.retentionPeriod") +@ConditionalOnProperty(prefix = "rp.environment.variable", + name = "clean.expiredUser.retentionPeriod") public class DeleteExpiredUsersJob extends BaseJob { public static final Logger LOGGER = LoggerFactory.getLogger(DeleteExpiredUsersJob.class); @@ -68,33 +69,49 @@ public class DeleteExpiredUsersJob extends BaseJob { private static final String USER_DELETION_TEMPLATE = "userDeletionNotification"; - private static final String SELECT_EXPIRED_USERS = - "SELECT u.id AS user_id, " + "p.id AS project_id, u.email AS user_email " + "FROM users u " - + "LEFT JOIN api_keys ak ON u.id = ak.user_id " - + "LEFT JOIN project p ON u.login || '_personal' = p.name AND p.project_type = 'PERSONAL' " - + "WHERE (u.metadata->'metadata'->>'last_login')::BIGINT <= :retentionPeriod " + "AND ( " - + "ak.user_id IS NULL " - + "OR (EXTRACT(EPOCH FROM ak.last_used_at) * 1000)::BIGINT <= :retentionPeriod " - + "OR NOT EXISTS (SELECT 1 FROM api_keys WHERE user_id = u.id AND last_used_at IS NOT NULL) " - + ") " + "AND u.role != 'ADMINISTRATOR' " + "GROUP BY u.id, p.id"; + private static final String SELECT_EXPIRED_USERS = """ + SELECT u.id AS user_id,\s + p.id AS project_id, u.email as user_email\s + FROM users u\s + LEFT JOIN api_keys ak ON u.id = ak.user_id\s + LEFT JOIN project p ON u.login || '_personal' = p.name AND p.project_type = 'PERSONAL'\s + WHERE (u.metadata->'metadata'->>'last_login')::BIGINT <= :retentionPeriod\s + AND (\s + ak.user_id IS NULL\s + OR (EXTRACT(EPOCH FROM ak.last_used_at) * 1000)::BIGINT <= :retentionPeriod\s + OR NOT EXISTS (SELECT 1 FROM api_keys WHERE user_id = u.id AND last_used_at IS NOT NULL)\s + )\s + AND u.role != 'ADMINISTRATOR'\s + GROUP BY u.id, p.id"""; private static final String DELETE_ATTACHMENTS_BY_PROJECT = - "DELETE FROM attachment WHERE project_id = :projectId RETURNING file_id"; + """ + WITH moved_rows AS (DELETE FROM attachment\s + WHERE project_id = :projectId RETURNING id, file_id, thumbnail_id, creation_date)\s + INSERT INTO attachment_deletion\s + (id, file_id, thumbnail_id, creation_attachment_date, deletion_date)\s + SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows"""; private static final String DELETE_PROJECT_ISSUE_TYPES = - "DELETE FROM issue_type " + "WHERE id IN (" + " SELECT it.id " + " FROM issue_type it " - + " JOIN issue_type_project itp ON it.id = itp.issue_type_id " - + " WHERE itp.project_id = :projectId " - + " AND it.locator NOT IN ('pb001', 'ab001', 'si001', 'ti001', 'nd001'))"; + """ + DELETE FROM issue_type\s + WHERE id IN ( + SELECT it.id\s + FROM issue_type it\s + JOIN issue_type_project itp ON it.id = itp.issue_type_id\s + WHERE itp.project_id = :projectId\s + AND it.locator NOT IN ('pb001', 'ab001', 'si001', 'ti001', 'nd001'))"""; private static final String DELETE_USERS = "DELETE FROM users WHERE id IN (:userIds)"; private static final String DELETE_PROJECTS_BY_ID_LIST = "DELETE FROM project WHERE id IN (:projectIds)"; - private static final String FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS = - "SELECT p.id " + "FROM project_user pu " + "JOIN project p ON pu.project_id = p.id " - + "WHERE p.project_type != 'PERSONAL' AND pu.user_id IN (:userIds)"; + private static final String FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS = """ + SELECT p.id\s + FROM project_user pu\s + JOIN project p ON pu.project_id = p.id\s + WHERE p.project_type != 'PERSONAL' AND pu.user_id IN (:userIds)"""; @Value("${rp.environment.variable.clean.expiredUser.retentionPeriod}") private Long retentionPeriod; diff --git a/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java b/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java index 48b6683..9f87d23 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/EventsRetentionPolicyJob.java @@ -37,9 +37,9 @@ name = "clean.events.retentionPeriod") public class EventsRetentionPolicyJob extends BaseJob { - private static final String DELETE_ACTIVITY_BY_RETENTION = - "DELETE FROM activity " - + "WHERE created_at < NOW() - (:retentionPeriod * interval '1 day')"; + private static final String DELETE_ACTIVITY_BY_RETENTION = """ + DELETE FROM activity\s + WHERE created_at < NOW() - (:retentionPeriod * interval '1 day')"""; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; diff --git a/src/main/java/com/epam/reportportal/jobs/notification/NotifyUserExpirationJob.java b/src/main/java/com/epam/reportportal/jobs/notification/NotifyUserExpirationJob.java index 2889f29..414e60b 100644 --- a/src/main/java/com/epam/reportportal/jobs/notification/NotifyUserExpirationJob.java +++ b/src/main/java/com/epam/reportportal/jobs/notification/NotifyUserExpirationJob.java @@ -55,30 +55,31 @@ public class NotifyUserExpirationJob extends BaseJob { private static final String DEADLINE_DATE = "deadlineDate"; private static final String DAYS = " days"; - private static final String SELECT_USERS_FOR_NOTIFY = "WITH user_last_action AS ( " - + "SELECT " - + " u.id as user_id, " - + " u.email as email, " - + "DATE_PART('day', NOW() - GREATEST(" - + " DATE(to_timestamp(CAST(u.metadata->'metadata'->>'last_login' AS bigint) / 1000)), " - + "MAX(ak.last_used_at))) AS inactivityPeriod " - + "FROM " - + " users u " - + " LEFT JOIN api_keys ak ON u.id = ak.user_id " - + "WHERE " - + " u.role != 'ADMINISTRATOR' " - + "GROUP BY " - + " u.id " - + ") " - + "SELECT " - + " user_last_action.user_id, " - + " user_last_action.email, " - + " user_last_action.inactivityPeriod, " - + " :retentionPeriod - inactivityPeriod as remainingTime " - + "FROM " - + " user_last_action " - + "WHERE " - + " :retentionPeriod - inactivityPeriod IN (1, 30, 60)"; + private static final String SELECT_USERS_FOR_NOTIFY = """ + WITH user_last_action AS (\s + SELECT\s + u.id as user_id,\s + u.email as email,\s + DATE_PART('day', NOW() - GREATEST( + DATE(to_timestamp(CAST(u.metadata->'metadata'->>'last_login' AS bigint) / 1000)),\s + MAX(ak.last_used_at))) AS inactivityPeriod\s + FROM\s + users u\s + LEFT JOIN api_keys ak ON u.id = ak.user_id\s + WHERE\s + u.role != 'ADMINISTRATOR'\s + GROUP BY\s + u.id\s + )\s + SELECT\s + user_last_action.user_id,\s + user_last_action.email,\s + user_last_action.inactivityPeriod,\s + :retentionPeriod - inactivityPeriod as remainingTime\s + FROM\s + user_last_action\s + WHERE\s + :retentionPeriod - inactivityPeriod IN (1, 30, 60)"""; private static final String RETENTION_PERIOD = "retentionPeriod"; diff --git a/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java b/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java index ba83be4..2361962 100644 --- a/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/storage/CalculateAllocatedStorageJob.java @@ -17,7 +17,7 @@ public class CalculateAllocatedStorageJob extends BaseJob { private static final String SELECT_PROJECT_IDS_QUERY = "SELECT id FROM project ORDER BY id"; - private static final String SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY = "" + + private static final String SELECT_FILE_SIZE_SUM_BY_PROJECT_ID_QUERY = "SELECT coalesce(sum(file_size), 0) FROM attachment WHERE attachment.project_id = ?"; private static final String UPDATE_ALLOCATED_STORAGE_BY_PROJECT_ID_QUERY = "UPDATE project SET allocated_storage = ? WHERE id = ?"; From da920f74316582e383b77c132e7f787819e0c2aa Mon Sep 17 00:00:00 2001 From: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:05:28 +0300 Subject: [PATCH 10/27] EPMRPP-87272 || Fix DeleteExpiredUsersJob for case when user has no attachments (#110) --- .../jobs/clean/DeleteExpiredUsersJob.java | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java index be702b7..726346e 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java @@ -57,8 +57,7 @@ * @author Andrei Piankouski */ @Service -@ConditionalOnProperty(prefix = "rp.environment.variable", - name = "clean.expiredUser.retentionPeriod") +@ConditionalOnProperty(prefix = "rp.environment.variable", name = "clean.expiredUser.retentionPeriod") public class DeleteExpiredUsersJob extends BaseJob { public static final Logger LOGGER = LoggerFactory.getLogger(DeleteExpiredUsersJob.class); @@ -71,7 +70,7 @@ public class DeleteExpiredUsersJob extends BaseJob { private static final String SELECT_EXPIRED_USERS = """ SELECT u.id AS user_id,\s - p.id AS project_id, u.email as user_email\s + p.id AS project_id, u.email AS user_email\s FROM users u\s LEFT JOIN api_keys ak ON u.id = ak.user_id\s LEFT JOIN project p ON u.login || '_personal' = p.name AND p.project_type = 'PERSONAL'\s @@ -85,22 +84,16 @@ OR NOT EXISTS (SELECT 1 FROM api_keys WHERE user_id = u.id AND last_used_at IS N GROUP BY u.id, p.id"""; private static final String DELETE_ATTACHMENTS_BY_PROJECT = - """ - WITH moved_rows AS (DELETE FROM attachment\s - WHERE project_id = :projectId RETURNING id, file_id, thumbnail_id, creation_date)\s - INSERT INTO attachment_deletion\s - (id, file_id, thumbnail_id, creation_attachment_date, deletion_date)\s - SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows"""; - - private static final String DELETE_PROJECT_ISSUE_TYPES = - """ - DELETE FROM issue_type\s - WHERE id IN ( - SELECT it.id\s - FROM issue_type it\s - JOIN issue_type_project itp ON it.id = itp.issue_type_id\s - WHERE itp.project_id = :projectId\s - AND it.locator NOT IN ('pb001', 'ab001', 'si001', 'ti001', 'nd001'))"""; + "DELETE FROM attachment WHERE project_id = :projectId RETURNING file_id"; + + private static final String DELETE_PROJECT_ISSUE_TYPES = """ + DELETE FROM issue_type\s + WHERE id IN ( + SELECT it.id\s + FROM issue_type it\s + JOIN issue_type_project itp ON it.id = itp.issue_type_id\s + WHERE itp.project_id = :projectId\s + AND it.locator NOT IN ('pb001', 'ab001', 'si001', 'ti001', 'nd001'))"""; private static final String DELETE_USERS = "DELETE FROM users WHERE id IN (:userIds)"; From a2feeba8ddf52610e89b087b79f05471e6d4d237 Mon Sep 17 00:00:00 2001 From: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Date: Thu, 26 Oct 2023 17:41:11 +0300 Subject: [PATCH 11/27] EPMRPP-87272 || Delete expired users no attachments (#111) * EPMRPP-87272 || Fix DeleteExpiredUsersJob for case when user has no attachments * EPMRPP-87272 || Move attachments to attachments deletion when user is expired --- .../jobs/clean/DeleteExpiredUsersJob.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java index 726346e..1c1a367 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java @@ -83,8 +83,12 @@ OR NOT EXISTS (SELECT 1 FROM api_keys WHERE user_id = u.id AND last_used_at IS N AND u.role != 'ADMINISTRATOR'\s GROUP BY u.id, p.id"""; - private static final String DELETE_ATTACHMENTS_BY_PROJECT = - "DELETE FROM attachment WHERE project_id = :projectId RETURNING file_id"; + private static final String DELETE_ATTACHMENTS_BY_PROJECT = """ + WITH moved_rows AS (DELETE FROM attachment\s + WHERE project_id = :projectId RETURNING id, file_id, thumbnail_id, creation_date)\s + INSERT INTO attachment_deletion\s + (id, file_id, thumbnail_id, creation_attachment_date, deletion_date)\s + SELECT id, file_id, thumbnail_id, creation_date, NOW() FROM moved_rows"""; private static final String DELETE_PROJECT_ISSUE_TYPES = """ DELETE FROM issue_type\s @@ -190,13 +194,11 @@ private List findUsersAndPersonalProjects() { } private void deleteProjectAssociatedData(Long projectId) { - List paths = deleteAttachmentsByProjectId(projectId); + deleteAttachmentsByProjectId(projectId); deleteProjectIssueTypes(projectId); indexerServiceClient.removeSuggest(projectId); try { - if (featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { - dataStorageService.deleteAll(paths.stream().map(this::decode).collect(Collectors.toList())); - } else { + if (!featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { dataStorageService.deleteContainer(projectId.toString()); } } catch (Exception e) { @@ -220,9 +222,10 @@ private void deleteProjectIssueTypes(Long projectId) { namedParameterJdbcTemplate.update(DELETE_PROJECT_ISSUE_TYPES, params); } - private List deleteAttachmentsByProjectId(Long projectId) { - return namedParameterJdbcTemplate.queryForList( - DELETE_ATTACHMENTS_BY_PROJECT, Map.of("projectId", projectId), String.class); + private void deleteAttachmentsByProjectId(Long projectId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("projectId", projectId); + namedParameterJdbcTemplate.update(DELETE_ATTACHMENTS_BY_PROJECT, params); } private void deleteProjectsByIds(List projectIds) { From b7361e95a7d2076f0b9f90a5f361f85df84441a2 Mon Sep 17 00:00:00 2001 From: Ivan_Kustau Date: Fri, 27 Oct 2023 10:46:45 +0300 Subject: [PATCH 12/27] EPMRPP-79482 || Add JCloud filesystem implementation --- build.gradle | 1 + .../config/DataStorageConfig.java | 29 +++++-- .../storage/LocalDataStorageService.java | 78 ++++++++++++++----- 3 files changed, 82 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 49d63d9..86ab766 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'org.apache.jclouds.api:s3:2.5.0' implementation 'org.apache.jclouds.provider:aws-s3:2.5.0' + implementation 'org.apache.jclouds.api:filesystem:2.5.0' //Needed for correct jcloud work implementation 'com.google.code.gson:gson:2.8.9' implementation 'org.apache.httpcomponents:httpclient:4.5.13' diff --git a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java index 40894b6..55d9478 100644 --- a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java +++ b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java @@ -26,12 +26,14 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.inject.Module; +import java.util.Properties; import java.util.Set; import org.jclouds.ContextBuilder; import org.jclouds.aws.s3.config.AWSS3HttpApiModule; import org.jclouds.blobstore.BlobStore; import org.jclouds.blobstore.BlobStoreContext; import org.jclouds.blobstore.ContainerNotFoundException; +import org.jclouds.filesystem.reference.FilesystemConstants; import org.jclouds.rest.ConfiguresHttpApi; import org.jclouds.s3.S3Client; import org.springframework.beans.factory.annotation.Autowired; @@ -135,9 +137,24 @@ public String toString() { @Bean @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") - public DataStorageService localDataStore( - @Value("${datastore.path:/data/store}") String storagePath) { - return new LocalDataStorageService(storagePath); + public BlobStore filesystemBlobStore( + @Value("${datastore.path:/data/store}") String baseDirectory) { + + Properties properties = new Properties(); + properties.setProperty(FilesystemConstants.PROPERTY_BASEDIR, baseDirectory); + + BlobStoreContext blobStoreContext = + ContextBuilder.newBuilder("filesystem").overrides(properties) + .buildView(BlobStoreContext.class); + + return blobStoreContext.getBlobStore(); + } + + @Bean + @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") + public DataStorageService localDataStore(@Autowired BlobStore blobStore, + FeatureFlagHandler featureFlagHandler) { + return new LocalDataStorageService(blobStore, featureFlagHandler); } /** @@ -179,7 +196,8 @@ public DataStorageService minioDataStore(@Autowired BlobStore blobStore, @Value("${datastore.defaultBucketName}") String defaultBucketName, FeatureFlagHandler featureFlagHandler) { return new S3DataStorageService(blobStore, bucketPrefix, bucketPostfix, defaultBucketName, - featureFlagHandler); + featureFlagHandler + ); } /** @@ -213,6 +231,7 @@ public DataStorageService s3DataStore(@Autowired BlobStore blobStore, @Value("${datastore.defaultBucketName}") String defaultBucketName, FeatureFlagHandler featureFlagHandler) { return new S3DataStorageService(blobStore, bucketPrefix, bucketPostfix, defaultBucketName, - featureFlagHandler); + featureFlagHandler + ); } } diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index b418e75..0d49d54 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -16,15 +16,18 @@ package com.epam.reportportal.storage; -import java.io.File; +import com.epam.reportportal.utils.FeatureFlag; +import com.epam.reportportal.utils.FeatureFlagHandler; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import javax.lang.model.type.ErrorType; +import java.util.Map; +import org.jclouds.blobstore.BlobStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; +import org.springframework.util.CollectionUtils; /** * Local storage service @@ -33,31 +36,64 @@ public class LocalDataStorageService implements DataStorageService { private static final Logger LOGGER = LoggerFactory.getLogger(LocalDataStorageService.class); - private final String storageRootPath; + private final BlobStore blobStore; + + private final FeatureFlagHandler featureFlagHandler; - public LocalDataStorageService(String storageRootPath) { - this.storageRootPath = storageRootPath; + private static final String SINGLE_BUCKET_NAME = "store"; + + public LocalDataStorageService(BlobStore blobStore, FeatureFlagHandler featureFlagHandler) { + this.blobStore = blobStore; + this.featureFlagHandler = featureFlagHandler; } @Override - public void deleteAll(List paths) throws IOException { - for (String path : paths) { - try { - Files.deleteIfExists(Paths.get(storageRootPath, path)); - } catch (IOException e) { - LOGGER.error("Unable to delete file '{}'", path, e); - throw e; + public void deleteAll(List paths) throws Exception { + if (CollectionUtils.isEmpty(paths)) { + return; + } + if (featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { + removeFiles(SINGLE_BUCKET_NAME, paths); + } else { + Map> bucketPathMap = new HashMap<>(); + for (String path : paths) { + Path targetPath = Paths.get(path); + int nameCount = targetPath.getNameCount(); + String bucket = retrievePath(targetPath, 0, 1); + String cutPath = retrievePath(targetPath, 1, nameCount); + if (bucketPathMap.containsKey(bucket)) { + bucketPathMap.get(bucket).add(cutPath); + } else { + List bucketPaths = new ArrayList<>(); + bucketPaths.add(cutPath); + bucketPathMap.put(bucket, bucketPaths); + } + } + for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { + removeFiles(bucketPaths.getKey(), bucketPaths.getValue()); } } } @Override - public void deleteContainer(String containerName) throws IOException{ + public void deleteContainer(String containerName) { try { - Files.deleteIfExists(Paths.get(storageRootPath, containerName)); - } catch (IOException e) { - LOGGER.error("Unable to delete container '{}'", containerName, e); - throw e; + blobStore.deleteContainer(containerName); + } catch (Exception e) { + LOGGER.warn("Exception {} is occurred during deleting container", e.getMessage()); } } + + private String retrievePath(Path path, int beginIndex, int endIndex) { + return String.valueOf(path.subpath(beginIndex, endIndex)); + } + + private void removeFiles(String bucketName, List paths) { + try { + blobStore.removeBlobs(bucketName, paths); + } catch (Exception e) { + LOGGER.warn("Exception {} is occurred during deleting file", e.getMessage()); + } + } + } From e3b1e4edc959f56d65e234967695da70f07ea07c Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Tue, 31 Oct 2023 18:39:54 +0400 Subject: [PATCH 13/27] Add GitHub actions for building dev images (#113) --- .github/workflows/build-dev-image.yml | 69 +++++++++++++++++++ .../{rc.yaml => build-rc-image.yaml} | 0 2 files changed, 69 insertions(+) create mode 100644 .github/workflows/build-dev-image.yml rename .github/workflows/{rc.yaml => build-rc-image.yaml} (100%) diff --git a/.github/workflows/build-dev-image.yml b/.github/workflows/build-dev-image.yml new file mode 100644 index 0000000..150d447 --- /dev/null +++ b/.github/workflows/build-dev-image.yml @@ -0,0 +1,69 @@ +name: Build develop Docker image + +on: + push: + branches: + - develop + paths-ignore: + - '.github/**' + - README.md + +env: + AWS_REGION: ${{ vars.AWS_REGION }} # set this to your preferred AWS region, e.g. us-west-1 + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} # set this to your Amazon ECR repository name + PLATFORMS: ${{ vars.BUILD_PLATFORMS }} # set target build platforms. By default linux/amd64 + IMAGE_TAG: develop-${{ github.run_number }} # set the image tag + +jobs: + build-and-export: + name: Build and export to AWS ECR + runs-on: ubuntu-latest + environment: develop + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: 'true' + + - name: Create variables + id: vars + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build + uses: docker/build-push-action@v4 + env: + VERSION: ${{ github.ref_name }}-${{ steps.vars.outputs.sha_short }} + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + with: + context: . + push: true + build-args: | + APP_VERSION=${{ env.VERSION }} + platforms: ${{ env.PLATFORMS }} + tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} + - name: Summarize + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + echo "## General information about the build:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- :gift: Docker image in Amazon ECR: ecr/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_STEP_SUMMARY + echo "- :octocat: The commit SHA from which the build was performed: [$GITHUB_SHA](https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/rc.yaml b/.github/workflows/build-rc-image.yaml similarity index 100% rename from .github/workflows/rc.yaml rename to .github/workflows/build-rc-image.yaml From 3f0d3060bd39c830fbaee1f0b551728b22397296 Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:46:31 +0400 Subject: [PATCH 14/27] Patch build-dev-image.yml --- .github/workflows/build-dev-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-dev-image.yml b/.github/workflows/build-dev-image.yml index 150d447..ac530d0 100644 --- a/.github/workflows/build-dev-image.yml +++ b/.github/workflows/build-dev-image.yml @@ -18,7 +18,7 @@ jobs: build-and-export: name: Build and export to AWS ECR runs-on: ubuntu-latest - environment: develop + environment: development steps: - name: Checkout uses: actions/checkout@v3 @@ -66,4 +66,4 @@ jobs: echo "## General information about the build:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- :gift: Docker image in Amazon ECR: ecr/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_STEP_SUMMARY - echo "- :octocat: The commit SHA from which the build was performed: [$GITHUB_SHA](https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "- :octocat: The commit SHA from which the build was performed: [$GITHUB_SHA](https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" >> $GITHUB_STEP_SUMMARY From 56fa3b22ee654a654ad4b61cc8f2e9beff0d66d0 Mon Sep 17 00:00:00 2001 From: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Date: Sat, 11 Nov 2023 12:24:52 +0300 Subject: [PATCH 15/27] EPMRPP-87504 || Remove empty directories after removing attachments and fix DeleteExpiredUsersJob (#114) --- .../config/DataStorageConfig.java | 5 +- .../jobs/clean/DeleteExpiredUsersJob.java | 29 +------ .../storage/LocalDataStorageService.java | 86 +++++++++++++++---- 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java index 55d9478..fa88bc0 100644 --- a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java +++ b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java @@ -153,8 +153,9 @@ public BlobStore filesystemBlobStore( @Bean @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") public DataStorageService localDataStore(@Autowired BlobStore blobStore, - FeatureFlagHandler featureFlagHandler) { - return new LocalDataStorageService(blobStore, featureFlagHandler); + FeatureFlagHandler featureFlagHandler, + @Value("${datastore.path:/data/store}") String baseDirectory) { + return new LocalDataStorageService(blobStore, featureFlagHandler, baseDirectory); } /** diff --git a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java index 1c1a367..ac7fc1e 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java @@ -23,21 +23,15 @@ import com.epam.reportportal.model.activity.event.UnassignUserEvent; import com.epam.reportportal.model.activity.event.UserDeletedEvent; import com.epam.reportportal.service.MessageBus; -import com.epam.reportportal.storage.DataStorageService; -import com.epam.reportportal.utils.FeatureFlag; -import com.epam.reportportal.utils.FeatureFlagHandler; import com.epam.reportportal.utils.ValidationUtil; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -113,25 +107,18 @@ WHERE id IN ( @Value("${rp.environment.variable.clean.expiredUser.retentionPeriod}") private Long retentionPeriod; - private final DataStorageService dataStorageService; - private final IndexerServiceClient indexerServiceClient; - private final FeatureFlagHandler featureFlagHandler; - private final MessageBus messageBus; @Autowired public DeleteExpiredUsersJob(JdbcTemplate jdbcTemplate, - NamedParameterJdbcTemplate namedParameterJdbcTemplate, DataStorageService dataStorageService, - IndexerServiceClient indexerServiceClient, MessageBus messageBus, - FeatureFlagHandler featureFlagHandler) { + NamedParameterJdbcTemplate namedParameterJdbcTemplate, + IndexerServiceClient indexerServiceClient, MessageBus messageBus) { super(jdbcTemplate); this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - this.dataStorageService = dataStorageService; this.indexerServiceClient = indexerServiceClient; this.messageBus = messageBus; - this.featureFlagHandler = featureFlagHandler; } @Override @@ -197,13 +184,6 @@ private void deleteProjectAssociatedData(Long projectId) { deleteAttachmentsByProjectId(projectId); deleteProjectIssueTypes(projectId); indexerServiceClient.removeSuggest(projectId); - try { - if (!featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { - dataStorageService.deleteContainer(projectId.toString()); - } - } catch (Exception e) { - LOGGER.warn("Cannot delete attachments bucket for project {} ", projectId); - } indexerServiceClient.deleteIndex(projectId); } @@ -284,9 +264,4 @@ public void setProjectId(long projectId) { this.projectId = projectId; } } - - private String decode(String data) { - return StringUtils.isEmpty(data) ? data : - new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); - } } diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index 0d49d54..9372fed 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -18,6 +18,9 @@ import com.epam.reportportal.utils.FeatureFlag; import com.epam.reportportal.utils.FeatureFlagHandler; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -40,11 +43,17 @@ public class LocalDataStorageService implements DataStorageService { private final FeatureFlagHandler featureFlagHandler; + private final String baseDirectory; + + private static final String PROJECT_PREFIX = "project-data"; + private static final String SINGLE_BUCKET_NAME = "store"; - public LocalDataStorageService(BlobStore blobStore, FeatureFlagHandler featureFlagHandler) { + public LocalDataStorageService(BlobStore blobStore, FeatureFlagHandler featureFlagHandler, + String baseDirectory) { this.blobStore = blobStore; this.featureFlagHandler = featureFlagHandler; + this.baseDirectory = baseDirectory; } @Override @@ -53,24 +62,17 @@ public void deleteAll(List paths) throws Exception { return; } if (featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { - removeFiles(SINGLE_BUCKET_NAME, paths); - } else { - Map> bucketPathMap = new HashMap<>(); - for (String path : paths) { - Path targetPath = Paths.get(path); - int nameCount = targetPath.getNameCount(); - String bucket = retrievePath(targetPath, 0, 1); - String cutPath = retrievePath(targetPath, 1, nameCount); - if (bucketPathMap.containsKey(bucket)) { - bucketPathMap.get(bucket).add(cutPath); - } else { - List bucketPaths = new ArrayList<>(); - bucketPaths.add(cutPath); - bucketPathMap.put(bucket, bucketPaths); - } + Map> bucketPathMap = retrieveBucketPathMap(paths); + for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { + removeFiles(SINGLE_BUCKET_NAME, bucketPaths.getValue()); + deleteEmptyDirs( + Paths.get(baseDirectory, SINGLE_BUCKET_NAME, PROJECT_PREFIX, bucketPaths.getKey())); } + } else { + Map> bucketPathMap = retrieveBucketPathMap(paths); for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { removeFiles(bucketPaths.getKey(), bucketPaths.getValue()); + deleteEmptyDirs(Paths.get(baseDirectory, bucketPaths.getKey())); } } } @@ -84,6 +86,25 @@ public void deleteContainer(String containerName) { } } + private Map> retrieveBucketPathMap(List paths) { + Map> bucketPathMap = new HashMap<>(); + for (String path : paths) { + Path targetPath = Paths.get(path); + int nameCount = targetPath.getNameCount(); + String bucket = retrievePath(targetPath, 0, 1); + String cutPath = retrievePath(targetPath, 1, nameCount); + if (bucketPathMap.containsKey(bucket)) { + bucketPathMap.get(bucket).add(cutPath); + } else { + List bucketPaths = new ArrayList<>(); + bucketPaths.add(cutPath); + bucketPathMap.put(bucket, bucketPaths); + } + } + + return bucketPathMap; + } + private String retrievePath(Path path, int beginIndex, int endIndex) { return String.valueOf(path.subpath(beginIndex, endIndex)); } @@ -96,4 +117,37 @@ private void removeFiles(String bucketName, List paths) { } } + private void deleteEmptyDirs(Path dir) { + if (!Files.isDirectory(dir)) { + return; + } + + // List all files/directories in the given directory + try (DirectoryStream stream = Files.newDirectoryStream(dir)) { + for (Path entry : stream) { + deleteEmptyDirs(entry); + } + } catch (IOException e) { + LOGGER.warn("Exception {} is occurred during checking directory", e.getMessage()); + } + + // Delete the directory if empty + try { + if (isDirectoryEmpty(dir)) { + Files.delete(dir); + } + } catch (IOException e) { + LOGGER.warn("Exception {} is occurred during deleting empty directory", e.getMessage()); + } + } + + private boolean isDirectoryEmpty(Path dir) { + try (DirectoryStream stream = Files.newDirectoryStream(dir)) { + return !stream.iterator().hasNext(); + } catch (IOException e) { + LOGGER.warn("Exception {} is occurred during checking directory", e.getMessage()); + return false; + } + } + } From 0624fea7abea74a469a56970543508bc21f374fe Mon Sep 17 00:00:00 2001 From: Siarhei Hrabko <45555481+grabsefx@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:10:15 +0300 Subject: [PATCH 16/27] EPMRPP-87382 || delete expired user's photo (#115) * EPMRPP-87382 || delete expired user's photo --- .../jobs/clean/CleanStorageJob.java | 12 ++---- .../jobs/clean/DeleteExpiredUsersJob.java | 35 ++++++++++++++++-- .../reportportal/utils/DataStorageUtils.java | 37 +++++++++++++++++++ 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/epam/reportportal/utils/DataStorageUtils.java diff --git a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java index eb92c09..35bab20 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/CleanStorageJob.java @@ -3,14 +3,12 @@ import com.epam.reportportal.jobs.BaseJob; import com.epam.reportportal.model.BlobNotFoundException; import com.epam.reportportal.storage.DataStorageService; -import java.nio.charset.StandardCharsets; +import com.epam.reportportal.utils.DataStorageUtils; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.Scheduled; @@ -85,9 +83,9 @@ public void execute() { } try { storageService.deleteAll( - thumbnails.stream().map(this::decode).collect(Collectors.toList())); + thumbnails.stream().map(DataStorageUtils::decode).collect(Collectors.toList())); storageService.deleteAll( - attachments.stream().map(this::decode).collect(Collectors.toList())); + attachments.stream().map(DataStorageUtils::decode).collect(Collectors.toList())); attachments.clear(); thumbnails.clear(); } catch (BlobNotFoundException e) { @@ -102,8 +100,4 @@ public void execute() { } } - private String decode(String data) { - return StringUtils.isEmpty(data) ? data : - new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); - } } diff --git a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java index ac7fc1e..ea665b2 100644 --- a/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java +++ b/src/main/java/com/epam/reportportal/jobs/clean/DeleteExpiredUsersJob.java @@ -23,6 +23,8 @@ import com.epam.reportportal.model.activity.event.UnassignUserEvent; import com.epam.reportportal.model.activity.event.UserDeletedEvent; import com.epam.reportportal.service.MessageBus; +import com.epam.reportportal.storage.DataStorageService; +import com.epam.reportportal.utils.DataStorageUtils; import com.epam.reportportal.utils.ValidationUtil; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -55,6 +57,7 @@ public class DeleteExpiredUsersJob extends BaseJob { public static final Logger LOGGER = LoggerFactory.getLogger(DeleteExpiredUsersJob.class); + public static final String USER_IDS = "userIds"; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; @@ -95,6 +98,11 @@ WHERE id IN ( private static final String DELETE_USERS = "DELETE FROM users WHERE id IN (:userIds)"; + private static final String SELECT_USERS_ATTACHMENTS = """ + SELECT attachment FROM users WHERE (id IN (:userIds) AND attachment IS NOT NULL)\s + UNION\s + SELECT attachment_thumbnail FROM users WHERE (id IN (:userIds) AND attachment_thumbnail IS NOT NULL)"""; + private static final String DELETE_PROJECTS_BY_ID_LIST = "DELETE FROM project WHERE id IN (:projectIds)"; @@ -106,6 +114,7 @@ WHERE id IN ( @Value("${rp.environment.variable.clean.expiredUser.retentionPeriod}") private Long retentionPeriod; + private final DataStorageService dataStorageService; private final IndexerServiceClient indexerServiceClient; @@ -114,9 +123,11 @@ WHERE id IN ( @Autowired public DeleteExpiredUsersJob(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate, - IndexerServiceClient indexerServiceClient, MessageBus messageBus) { + DataStorageService dataStorageService, IndexerServiceClient indexerServiceClient, + MessageBus messageBus) { super(jdbcTemplate); this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + this.dataStorageService = dataStorageService; this.indexerServiceClient = indexerServiceClient; this.messageBus = messageBus; } @@ -136,6 +147,7 @@ public void execute() { List personalProjectIds = getProjectIds(userProjects); List nonPersonalProjectsByUserIds = findNonPersonalProjectIdsByUserIds(userIds); + deleteUsersPhoto(userIds); deleteUsersByIds(userIds); publishUnassignUserEvents(nonPersonalProjectsByUserIds); personalProjectIds.forEach(this::deleteProjectAssociatedData); @@ -146,6 +158,23 @@ public void execute() { LOGGER.info("{} - users was deleted due to retention policy", userIds.size()); } + private void deleteUsersPhoto(List userIds) { + if (!CollectionUtils.isEmpty(userIds)) { + MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue(USER_IDS, userIds); + var userAttachments = namedParameterJdbcTemplate + .queryForList(SELECT_USERS_ATTACHMENTS, parameters, String.class) + .stream() + .map(DataStorageUtils::decode) + .toList(); + try { + dataStorageService.deleteAll(userAttachments); + } catch (Exception e) { + LOGGER.error("Failed to delete users photo from data storage: {}", userAttachments, e); + } + } + } + private void publishEmailNotificationEvents(List userEmails) { List notifications = userEmails.stream() .map(recipient -> new EmailNotificationRequest(recipient, USER_DELETION_TEMPLATE)) @@ -161,7 +190,7 @@ private void publishUnassignUserEvents(List nonPersonalProjectsByUserIds) private List findNonPersonalProjectIdsByUserIds(List userIds) { return CollectionUtils.isEmpty(userIds) ? Collections.emptyList() : namedParameterJdbcTemplate.queryForList(FIND_NON_PERSONAL_PROJECTS_BY_USER_IDS, - Map.of("userIds", userIds), Long.class + Map.of(USER_IDS, userIds), Long.class ); } @@ -190,7 +219,7 @@ private void deleteProjectAssociatedData(Long projectId) { private void deleteUsersByIds(List userIds) { if (!userIds.isEmpty()) { MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("userIds", userIds); + params.addValue(USER_IDS, userIds); namedParameterJdbcTemplate.update(DELETE_USERS, params); messageBus.publishActivity(new UserDeletedEvent(userIds.size())); } diff --git a/src/main/java/com/epam/reportportal/utils/DataStorageUtils.java b/src/main/java/com/epam/reportportal/utils/DataStorageUtils.java new file mode 100644 index 0000000..0ffab22 --- /dev/null +++ b/src/main/java/com/epam/reportportal/utils/DataStorageUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.utils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.apache.commons.lang3.StringUtils; + +/** + * Utility class for data storage functionality. + * + * @author Siarhe Hrabko + */ +public final class DataStorageUtils { + + private DataStorageUtils() { + } + + public static String decode(String data) { + return StringUtils.isEmpty(data) ? data : + new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8); + } +} From 0faeddc0be24abfefad96bca0665af51a1a7aeb5 Mon Sep 17 00:00:00 2001 From: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:07:54 +0300 Subject: [PATCH 17/27] EPMRPP-87601 || Fix deletion for single bucket (#117) * EPMRPP-87601 || Fix deletion for single bucket * EPMRPP-87601 || Refactor code smell --- .../reportportal/storage/LocalDataStorageService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index 9372fed..d2cce15 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -64,9 +64,11 @@ public void deleteAll(List paths) throws Exception { if (featureFlagHandler.isEnabled(FeatureFlag.SINGLE_BUCKET)) { Map> bucketPathMap = retrieveBucketPathMap(paths); for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { - removeFiles(SINGLE_BUCKET_NAME, bucketPaths.getValue()); - deleteEmptyDirs( - Paths.get(baseDirectory, SINGLE_BUCKET_NAME, PROJECT_PREFIX, bucketPaths.getKey())); + removeFiles( + SINGLE_BUCKET_NAME, + bucketPaths.getValue().stream().map(s -> bucketPaths.getKey() + "/" + s).toList() + ); + deleteEmptyDirs(Paths.get(baseDirectory, SINGLE_BUCKET_NAME, PROJECT_PREFIX)); } } else { Map> bucketPathMap = retrieveBucketPathMap(paths); From 3b7a827137df644614b14fadcfa13db0c30f6f05 Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:24:29 +0400 Subject: [PATCH 18/27] Platform specification for Docker build stage #116 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a40b846..d6a7b1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:8.4.0-jdk21 AS build +FROM --platform=$BUILDPLATFORM gradle:8.4.0-jdk21 AS build ARG RELEASE_MODE ARG APP_VERSION ARG GITHUB_USER From 9f958778ac11fbe58cb37733d9784f15f11fb4cf Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:07:52 +0400 Subject: [PATCH 19/27] Refactor java build workflow configuration (#119) --- .github/workflows/build.yml | 40 ------------------------------- .github/workflows/java-checks.yml | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 40 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/java-checks.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e89896f..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build - -on: - pull_request: - push: - branches: - - master - - develop - paths-ignore: - - '.github/**' - - README.md - - gradle.properties - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up JDK 21 - uses: actions/setup-java@v2 - with: - distribution: 'adopt' - java-version: '21' - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Setup git credentials - uses: oleksiyrudenko/gha-git-credentials@v2 - with: - name: 'reportportal.io' - email: 'support@reportportal.io' - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build with Gradle - id: build - run: | - ./gradlew build diff --git a/.github/workflows/java-checks.yml b/.github/workflows/java-checks.yml new file mode 100644 index 0000000..fec9a27 --- /dev/null +++ b/.github/workflows/java-checks.yml @@ -0,0 +1,24 @@ +name: Java checks + +on: + pull_request: + types: [opened, synchronize, reopened] + paths-ignore: + - '.github/**' + - README.md + - gradle.properties + push: + branches: + - master + - develop + paths-ignore: + - '.github/**' + - README.md + - gradle.properties + +jobs: + call-java-cheks: + name: Call Java checks + uses: reportportal/.github/.github/workflows/java-checks.yaml@main + with: + java-version: '21' From ca7de3e2274269d64012b12caab9c09349df2d27 Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:47:29 +0400 Subject: [PATCH 20/27] Refactor build workflows for Docker images (#118) --- .github/workflows/build-dev-image.yml | 64 ++++----------- .github/workflows/build-feature-image.yaml | 39 +++++++++ .github/workflows/build-rc-image.yaml | 92 ++++++---------------- 3 files changed, 77 insertions(+), 118 deletions(-) create mode 100644 .github/workflows/build-feature-image.yaml diff --git a/.github/workflows/build-dev-image.yml b/.github/workflows/build-dev-image.yml index ac530d0..302021d 100644 --- a/.github/workflows/build-dev-image.yml +++ b/.github/workflows/build-dev-image.yml @@ -8,62 +8,30 @@ on: - '.github/**' - README.md -env: - AWS_REGION: ${{ vars.AWS_REGION }} # set this to your preferred AWS region, e.g. us-west-1 - ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} # set this to your Amazon ECR repository name - PLATFORMS: ${{ vars.BUILD_PLATFORMS }} # set target build platforms. By default linux/amd64 - IMAGE_TAG: develop-${{ github.run_number }} # set the image tag - jobs: - build-and-export: - name: Build and export to AWS ECR + variables-setup: + name: Setting variables for docker build runs-on: ubuntu-latest - environment: development steps: - name: Checkout uses: actions/checkout@v3 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - with: - mask-password: 'true' - - name: Create variables id: vars run: | + echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + outputs: + date: ${{ steps.vars.outputs.date }} + sha_short: ${{ steps.vars.outputs.sha_short }} - - name: Build - uses: docker/build-push-action@v4 - env: - VERSION: ${{ github.ref_name }}-${{ steps.vars.outputs.sha_short }} - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - with: - context: . - push: true - build-args: | - APP_VERSION=${{ env.VERSION }} - platforms: ${{ env.PLATFORMS }} - tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} - - name: Summarize - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - run: | - echo "## General information about the build:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- :gift: Docker image in Amazon ECR: ecr/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_STEP_SUMMARY - echo "- :octocat: The commit SHA from which the build was performed: [$GITHUB_SHA](https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" >> $GITHUB_STEP_SUMMARY + call-docker-build: + name: Call develop Docker build + needs: variables-setup + uses: reportportal/.github/.github/workflows/build-docker-image.yaml@main + with: + aws-region: ${{ vars.AWS_REGION }} + image-tag: 'develop-${{ github.run_number }}' + version: '${{ github.ref_name }}-${{ needs.variables-setup.outputs.sha_short }}' + date: ${{ needs.variables-setup.outputs.date }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-feature-image.yaml b/.github/workflows/build-feature-image.yaml new file mode 100644 index 0000000..5b4ec36 --- /dev/null +++ b/.github/workflows/build-feature-image.yaml @@ -0,0 +1,39 @@ +name: Build feature Docker image + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - 'develop' + +jobs: + variables-setup: + name: Setting variables for docker build + runs-on: ubuntu-latest + if: (!startsWith(github.head_ref, 'rc/') || !startsWith(github.head_ref, 'hotfix/') || !startsWith(github.head_ref, 'master') || !startsWith(github.head_ref, 'main')) + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Create variables + id: vars + run: | + echo "tag=$(echo ${{ github.head_ref }}-${{ github.run_number }} | tr '/' '-')" >> $GITHUB_OUTPUT + echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + outputs: + tag: ${{ steps.vars.outputs.tag }} + date: ${{ steps.vars.outputs.date }} + sha_short: ${{ steps.vars.outputs.sha_short }} + + call-docker-build: + name: Call feature Docker build + needs: variables-setup + uses: reportportal/.github/.github/workflows/build-docker-image.yaml@main + with: + aws-region: ${{ vars.AWS_REGION }} + image-tag: ${{ needs.variables-setup.outputs.tag }} + version: '${{ github.head_ref }}-${{ needs.variables-setup.outputs.sha_short }}' + branch: ${{ github.head_ref }} + date: ${{ needs.variables-setup.outputs.date }} + secrets: inherit diff --git a/.github/workflows/build-rc-image.yaml b/.github/workflows/build-rc-image.yaml index a10b4a1..31dc2ff 100644 --- a/.github/workflows/build-rc-image.yaml +++ b/.github/workflows/build-rc-image.yaml @@ -6,86 +6,38 @@ on: - "rc/*" - "hotfix/*" -env: - AWS_REGION: ${{ vars.AWS_REGION }} # set this to your preferred AWS region, e.g. us-west-1 - ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} # set this to your Amazon ECR repository name - PLATFORMS: ${{ vars.BUILD_PLATFORMS }} # set target build platforms. By default linux/amd64 - RELEASE_MODE: ${{ vars.RELEASE_MODE }} - jobs: - build-and-export: - name: Build and export to AWS ECR + variables-setup: + name: Setting variables for docker build runs-on: ubuntu-latest environment: rc steps: - name: Checkout uses: actions/checkout@v3 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - # role-to-assume: arn:aws:iam::123456789012:role/my-github-actions-role - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - with: - mask-password: 'true' - - name: Create variables id: vars run: | + echo "platforms=${{ vars.BUILD_PLATFORMS }}" >> $GITHUB_OUTPUT + echo "version=$(echo '${{ github.ref_name }}' | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" >> $GITHUB_OUTPUT echo "tag=$(echo ${{ github.ref_name }}-${{ github.run_number }} | tr '/' '-')" >> $GITHUB_OUTPUT echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - echo "version=$(echo '${{ github.ref_name }}' | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" >> $GITHUB_OUTPUT + outputs: + platforms: ${{ steps.vars.outputs.platforms }} + version: ${{ steps.vars.outputs.version }} + tag: ${{ steps.vars.outputs.tag }} + date: ${{ steps.vars.outputs.date }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build - uses: docker/build-push-action@v4 - env: - VERSION: ${{ steps.vars.outputs.version }} - DATE: ${{ steps.vars.outputs.date }} - IMAGE_TAG: ${{ steps.vars.outputs.tag }} - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - with: - context: . - push: true - build-args: | - APP_VERSION=${{ env.VERSION }} - BUILD_DATE=${{ env.DATE }} - GITHUB_USER=${{ secrets.GH_USER }} - GITHUB_TOKEN=${{ secrets.GH_TOKEN }} - RELEASE_MODE=${{ env.RELEASE_MODE }} - platforms: ${{ env.PLATFORMS }} - tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - env: - IMAGE_TAG: ${{ steps.vars.outputs.tag }} - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - with: - image-ref: '${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}' - format: 'table' - exit-code: '0' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' - - - name: Summarize - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ steps.vars.outputs.tag }} - run: | - echo "## General information about the build:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- :gift: Docker image in Amazon ECR: ecr/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_STEP_SUMMARY - echo "- :octocat: The commit SHA from which the build was performed: [$GITHUB_SHA](https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + call-docker-build: + name: Call release candidate Docker build + needs: variables-setup + uses: reportportal/.github/.github/workflows/build-docker-image.yaml@main + with: + aws-region: ${{ vars.AWS_REGION }} + image-tag: ${{ needs.variables-setup.outputs.tag }} + release-mode: true + additional-tag: 'latest' + build-platforms: ${{ needs.variables-setup.outputs.platforms }} + version: ${{ needs.variables-setup.outputs.version }} + date: ${{ needs.variables-setup.outputs.date }} + secrets: inherit From 5dd9c0301ff31dbd704d2ef520e112b7072e5478 Mon Sep 17 00:00:00 2001 From: Ivan Kustau <86599591+IvanKustau@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:12:39 +0300 Subject: [PATCH 21/27] EPMRPP-86835 || Update releaseMode to use Maven instead of Github (#121) --- Dockerfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index d6a7b1a..9a1ceee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,11 @@ FROM --platform=$BUILDPLATFORM gradle:8.4.0-jdk21 AS build ARG RELEASE_MODE ARG APP_VERSION -ARG GITHUB_USER -ARG GITHUB_TOKEN WORKDIR /usr/app COPY . /usr/app RUN if [ "${RELEASE_MODE}" = true ]; then \ gradle build --exclude-task test \ -PreleaseMode=true \ - -PgithubUserName=${GITHUB_USER} \ - -PgithubToken=${GITHUB_TOKEN} \ -Dorg.gradle.project.version=${APP_VERSION}; \ else gradle build --exclude-task test -Dorg.gradle.project.version=${APP_VERSION}; fi From 96df74f74c3cdcc2089081a2b9b76709a888f1d0 Mon Sep 17 00:00:00 2001 From: Siarhei Hrabko <45555481+grabsefx@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:34:01 +0300 Subject: [PATCH 22/27] EPMRPP-87595 fix CVEs (#120) --- build.gradle | 45 +++++++++++-------- .../index/IndexerServiceClientImpl.java | 1 - 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index 86ab766..f23d5b1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.7.16' + id 'org.springframework.boot' version '2.7.17' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } @@ -9,6 +9,8 @@ project.ext { releaseMode = project.hasProperty("releaseMode") } +ext['junit-jupiter.version'] = '5.10.0' + def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? '5.10.0' : 'EPMRPP-85756') apply from: "$scriptsUrl/build-docker.gradle" @@ -18,7 +20,10 @@ apply from: "$scriptsUrl/build-info.gradle" apply from: "$scriptsUrl/release-service.gradle" apply from: "$scriptsUrl/signing.gradle" -sourceCompatibility = '21' +tasks.withType(JavaCompile).configureEach { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} wrapper { gradleVersion = '8.4' @@ -45,22 +50,22 @@ processResources { //https://nvd.nist.gov/vuln/detail/CVE-2020-9488 and https://nvd.nist.gov/vuln/detail/CVE-2021-44228 and https://nvd.nist.gov/vuln/detail/CVE-2021-45046 and //https://nvd.nist.gov/vuln/detail/CVE-2021-45105 -ext['log4j2.version'] = '2.17.1' -ext['log4j-to-slf4j.version'] = '2.17.1' +ext['log4j2.version'] = '2.21.1' +ext['log4j-to-slf4j.version'] = '2.21.1' //https://nvd.nist.gov/vuln/detail/CVE-2022-26520 -ext['postgresql.version'] = '42.4.1' -ext['snakeyaml.version'] = '1.31' +ext['postgresql.version'] = '42.6.0' +ext['snakeyaml.version'] = '1.33' // dependencies { - implementation group: 'org.json', name: 'json', version: '20220320' + implementation 'org.json:json:20231013' - implementation 'net.javacrumbs.shedlock:shedlock-spring:4.21.0' - implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:4.21.0' + implementation 'net.javacrumbs.shedlock:shedlock-spring:4.46.0' + implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:4.46.0' // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' + implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'org.springframework.boot:spring-boot-starter-aop' @@ -73,21 +78,23 @@ dependencies { implementation 'org.apache.jclouds.api:filesystem:2.5.0' //Needed for correct jcloud work implementation 'com.google.code.gson:gson:2.8.9' - implementation 'org.apache.httpcomponents:httpclient:4.5.13' + implementation 'org.apache.httpcomponents:httpclient:4.5.14' // https://avd.aquasec.com/nvd/cve-2020-8908 -// implementation 'com.google.guava:guava:30.0-jre'; + implementation 'com.google.guava:guava:32.1.3-jre' - implementation "com.rabbitmq:http-client:2.1.0.RELEASE" + implementation "com.rabbitmq:http-client:5.2.0" //Fix CVE - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3' runtimeOnly 'org.postgresql:postgresql' - testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.5.2' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.5.2' - testImplementation 'org.mockito:mockito-junit-jupiter:3.1.0' + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.mockito:mockito-core:5.7.0' + testImplementation 'net.bytebuddy:byte-buddy:1.14.5' + testImplementation 'net.bytebuddy:byte-buddy-agent:1.14.5' } diff --git a/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java b/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java index f3cbe89..37d2cc7 100644 --- a/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java +++ b/src/main/java/com/epam/reportportal/analyzer/index/IndexerServiceClientImpl.java @@ -24,7 +24,6 @@ import java.util.stream.Collectors; import static com.epam.reportportal.analyzer.AnalyzerUtils.DOES_SUPPORT_SUGGEST; -import static com.epam.reportportal.analyzer.RabbitMqManagementClientTemplate.EXCHANGE_PRIORITY; /** * @author Pavel Bortnik From 8d696cd28b0d3b23c16df5068fd6a1d2b63e7e62 Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:29:55 +0400 Subject: [PATCH 23/27] Update build-dev-image.yml --- .github/workflows/build-dev-image.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-dev-image.yml b/.github/workflows/build-dev-image.yml index 302021d..9dbee6b 100644 --- a/.github/workflows/build-dev-image.yml +++ b/.github/workflows/build-dev-image.yml @@ -20,10 +20,8 @@ jobs: id: vars run: | echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT outputs: date: ${{ steps.vars.outputs.date }} - sha_short: ${{ steps.vars.outputs.sha_short }} call-docker-build: name: Call develop Docker build @@ -32,6 +30,6 @@ jobs: with: aws-region: ${{ vars.AWS_REGION }} image-tag: 'develop-${{ github.run_number }}' - version: '${{ github.ref_name }}-${{ needs.variables-setup.outputs.sha_short }}' + version: 'develop-${{ github.run_number }}' date: ${{ needs.variables-setup.outputs.date }} - secrets: inherit \ No newline at end of file + secrets: inherit From db77b0f08c08882040a47da8834d88cd327914b9 Mon Sep 17 00:00:00 2001 From: Reingold Shekhtel <13565058+raikbitters@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:30:58 +0400 Subject: [PATCH 24/27] Update build-feature-image.yaml --- .github/workflows/build-feature-image.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build-feature-image.yaml b/.github/workflows/build-feature-image.yaml index 5b4ec36..757cf2e 100644 --- a/.github/workflows/build-feature-image.yaml +++ b/.github/workflows/build-feature-image.yaml @@ -20,11 +20,9 @@ jobs: run: | echo "tag=$(echo ${{ github.head_ref }}-${{ github.run_number }} | tr '/' '-')" >> $GITHUB_OUTPUT echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT outputs: tag: ${{ steps.vars.outputs.tag }} date: ${{ steps.vars.outputs.date }} - sha_short: ${{ steps.vars.outputs.sha_short }} call-docker-build: name: Call feature Docker build @@ -33,7 +31,7 @@ jobs: with: aws-region: ${{ vars.AWS_REGION }} image-tag: ${{ needs.variables-setup.outputs.tag }} - version: '${{ github.head_ref }}-${{ needs.variables-setup.outputs.sha_short }}' + version: ${{ needs.variables-setup.outputs.tag }} branch: ${{ github.head_ref }} date: ${{ needs.variables-setup.outputs.date }} secrets: inherit From 80e898aa870ef76b0b79579e63366abbce0892ca Mon Sep 17 00:00:00 2001 From: Ivan_Kustau Date: Wed, 13 Dec 2023 11:04:31 +0300 Subject: [PATCH 25/27] Add prefix and postfix for filesystem --- .../reportportal/config/DataStorageConfig.java | 9 +++++++-- .../storage/LocalDataStorageService.java | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java index fa88bc0..dd3f012 100644 --- a/src/main/java/com/epam/reportportal/config/DataStorageConfig.java +++ b/src/main/java/com/epam/reportportal/config/DataStorageConfig.java @@ -154,8 +154,13 @@ public BlobStore filesystemBlobStore( @ConditionalOnProperty(name = "datastore.type", havingValue = "filesystem") public DataStorageService localDataStore(@Autowired BlobStore blobStore, FeatureFlagHandler featureFlagHandler, - @Value("${datastore.path:/data/store}") String baseDirectory) { - return new LocalDataStorageService(blobStore, featureFlagHandler, baseDirectory); + @Value("${datastore.path:/data/store}") String baseDirectory, + @Value("${datastore.bucketPrefix}") String bucketPrefix, + @Value("${datastore.bucketPostfix}") String bucketPostfix, + @Value("${datastore.defaultBucketName}") String defaultBucketName) { + return new LocalDataStorageService(blobStore, featureFlagHandler, baseDirectory, bucketPrefix, + bucketPostfix, defaultBucketName + ); } /** diff --git a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java index d2cce15..cfd18cb 100644 --- a/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java +++ b/src/main/java/com/epam/reportportal/storage/LocalDataStorageService.java @@ -45,15 +45,24 @@ public class LocalDataStorageService implements DataStorageService { private final String baseDirectory; + private final String bucketPrefix; + + private final String bucketPostfix; + + private final String defaultBucketName; + private static final String PROJECT_PREFIX = "project-data"; private static final String SINGLE_BUCKET_NAME = "store"; public LocalDataStorageService(BlobStore blobStore, FeatureFlagHandler featureFlagHandler, - String baseDirectory) { + String baseDirectory, String bucketPrefix, String bucketPostfix, String defaultBucketName) { this.blobStore = blobStore; this.featureFlagHandler = featureFlagHandler; this.baseDirectory = baseDirectory; + this.bucketPrefix = bucketPrefix; + this.bucketPostfix = bucketPostfix; + this.defaultBucketName = defaultBucketName; } @Override @@ -65,7 +74,7 @@ public void deleteAll(List paths) throws Exception { Map> bucketPathMap = retrieveBucketPathMap(paths); for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { removeFiles( - SINGLE_BUCKET_NAME, + defaultBucketName, bucketPaths.getValue().stream().map(s -> bucketPaths.getKey() + "/" + s).toList() ); deleteEmptyDirs(Paths.get(baseDirectory, SINGLE_BUCKET_NAME, PROJECT_PREFIX)); @@ -73,7 +82,7 @@ public void deleteAll(List paths) throws Exception { } else { Map> bucketPathMap = retrieveBucketPathMap(paths); for (Map.Entry> bucketPaths : bucketPathMap.entrySet()) { - removeFiles(bucketPaths.getKey(), bucketPaths.getValue()); + removeFiles(bucketPrefix + bucketPaths.getKey() + bucketPostfix, bucketPaths.getValue()); deleteEmptyDirs(Paths.get(baseDirectory, bucketPaths.getKey())); } } From c8f7223a3db9896135a153e3f10774e5a22aa175 Mon Sep 17 00:00:00 2001 From: Pavel Bortnik Date: Mon, 18 Dec 2023 11:29:07 +0300 Subject: [PATCH 26/27] rc/5.11.0 || Update release version --- build.gradle | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f23d5b1..a833d20 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ project.ext { ext['junit-jupiter.version'] = '5.10.0' -def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? '5.10.0' : 'EPMRPP-85756') +def scriptsUrl = 'https://raw.githubusercontent.com/reportportal/gradle-scripts/' + (releaseMode ? '5.11.0' : 'EPMRPP-85756') apply from: "$scriptsUrl/build-docker.gradle" apply from: "$scriptsUrl/build-commons.gradle" diff --git a/gradle.properties b/gradle.properties index d25a90b..d59033e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=develop +version=5.11.0 description=EPAM Report portal. Service jobs dockerServerUrl=unix:///var/run/docker.sock dockerPrepareEnvironment= From 2e3a698d3ae5cdaa306bf4d9bcffaaadbc81edf7 Mon Sep 17 00:00:00 2001 From: Pavel Bortnik Date: Sun, 3 Mar 2024 18:26:20 +0300 Subject: [PATCH 27/27] rc/5.11.0 || Add dockerhub-release workflow --- .github/workflows/dockerhub-release.yaml | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/dockerhub-release.yaml diff --git a/.github/workflows/dockerhub-release.yaml b/.github/workflows/dockerhub-release.yaml new file mode 100644 index 0000000..003fa89 --- /dev/null +++ b/.github/workflows/dockerhub-release.yaml @@ -0,0 +1,72 @@ +name: Retag RC Docker image + +on: + pull_request_review: + types: [ submitted ] + workflow_dispatch: + +env: + AWS_REGION: ${{ vars.AWS_REGION }} # set this to your preferred AWS region, e.g. us-west-1 + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} # set this to your Amazon ECR repository name + TARGET_REGISTRY: ${{ vars.TARGET_REGISTRY }} # set to target regestry (DockerHub, GitHub & etc) + TARGET_REPOSITORY: ${{ vars.TARGET_REPOSITORY }} # set to target repository + PLATFORMS: ${{ vars.BUILD_PLATFORMS }} # set target build platforms. By default linux/amd64 + RELEASE_MODE: ${{ vars.RELEASE_MODE }} + +jobs: + retag-image: + name: Retag and push image + runs-on: ubuntu-latest + environment: rc + if: github.event.pull_request.base.ref == 'master' || github.event.pull_request.base.ref == 'main' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + # role-to-assume: arn:aws:iam::123456789012:role/my-github-actions-role + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: 'true' + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.REGESTRY_USERNAME }} + password: ${{ secrets.REGESTRY_PASSWORD }} + + - name: Create variables + id: vars + run: | + echo "tag=$(echo '${{ github.event.pull_request.title }}' | sed -nE 's/.*([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')" >> $GITHUB_OUTPUT + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Retag and Push Docker Image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ steps.vars.outputs.tag }} + run: | + docker buildx imagetools create $ECR_REGISTRY/$ECR_REPOSITORY:latest --tag $TARGET_REGISTRY/$TARGET_REPOSITORY:$IMAGE_TAG --tag $TARGET_REGISTRY/$TARGET_REPOSITORY:latest + + - name: Summarize + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ steps.vars.outputs.tag }} + run: | + echo "## General information about the build:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- :whale: Docker image: $TARGET_REGISTRY/$TARGET_REPOSITORY:$IMAGE_TAG" >> $GITHUB_STEP_SUMMARY + echo "- :octocat: The commit SHA from which the build was performed: [$GITHUB_SHA](https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" >> $GITHUB_STEP_SUMMARY