added integration test

use spring-retry instead of custom
add logging
This commit is contained in:
RM
2026-10-05 20:09:26 +03:00
parent 9813f2489e
commit b1e79e1e2b
6 changed files with 191 additions and 138 deletions

View File

@@ -5,11 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableJpaRepositories
@EnableJpaAuditing
@EnableConfigurationProperties
@EnableRetry
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);

View File

@@ -33,10 +33,13 @@ public class DownloadHeadlessController {
private final Set<String> content = new HashSet<>();
private final TorgiEventRepository eventRepository;
private final MissedDataRepository missedDataRepository;
private final DownloadHeadlessService downloadHeadlessService;
public DownloadHeadlessController(TorgiEventRepository eventRepository, MissedDataRepository missedDataRepository) {
public DownloadHeadlessController(TorgiEventRepository eventRepository, MissedDataRepository missedDataRepository,
DownloadHeadlessService downloadHeadlessService) {
this.eventRepository = eventRepository;
this.missedDataRepository = missedDataRepository;
this.downloadHeadlessService = downloadHeadlessService;
eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(0, 10000)).forEach(event -> content.add(event.getKey()));
}
@@ -46,8 +49,7 @@ public class DownloadHeadlessController {
private void addNewToDiff() {
businessLogger.info("startProcessing");
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(0, EXCEL_LINK);
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(EXCEL_LINK);
List<TorgiEvent> newEvents = new ArrayList<>();
for (String key : currentContent.keySet()) {
@@ -58,11 +60,10 @@ public class DownloadHeadlessController {
}
}
eventRepository.saveAll(newEvents);
businessLogger.info("completeProcessing");
businessLogger.info("completeProcessing readed from torgi = {}, added to DB = {}", currentContent.size(), newEvents.size());
}
public void sendNewContent(StartParams params, List<String> cadastral, String email) {
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
LocalDate now = LocalDate.now();
downloadHeadlessService.createNewDocument(eventRepository, cadastral, now);
createArchive(now);
@@ -142,9 +143,8 @@ public class DownloadHeadlessController {
public void richData() {
try {
List<MissedData> all = missedDataRepository.findAll(PageRequest.of(0, 10)).toList();
DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService();
for (MissedData missedCadastral : all) {
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(0, String.format(CUSTOM_EXCEL_LINK, missedCadastral.getCadastral()));
Map<String, Lot> currentContent = downloadHeadlessService.downloadExcel(String.format(CUSTOM_EXCEL_LINK, missedCadastral.getCadastral()));
businessLogger.info("get content by cadastral {} {}", missedCadastral.getCadastral(), currentContent.size());
if (!currentContent.isEmpty()) {
List<TorgiEvent> newEvents = new ArrayList<>();

View File

@@ -10,9 +10,15 @@ import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.FileSystems;
@@ -23,6 +29,7 @@ import java.util.*;
import static com.pledge.torgi.utils.AppUtils.EMPTY;
import static java.util.Objects.nonNull;
@Service
public class DownloadHeadlessService {
private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessService.class);
@@ -40,54 +47,60 @@ public class DownloadHeadlessService {
private static final int COL_FINAL_PRICE = 27;
private static final int COL_FULL_INFO = 29;
public Map<String, Lot> downloadExcel(int retryCount, String link) {
@Retryable(retryFor = UncheckedIOException.class, maxAttempts = 11,
backoff = @Backoff(delay = 1000))
public Map<String, Lot> downloadExcel(String link) {
HttpURLConnection connection;
int responseCode;
try {
HttpURLConnection connection = (HttpURLConnection) new URL(link).openConnection();
connection = (HttpURLConnection) new URL(link).openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
connection.setRequestProperty("Accept", "application/json, text/plain, */*");
connection.setRequestProperty("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7");
connection.setConnectTimeout(5000);
connection.setReadTimeout(10000);
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
Map<String, Lot> data = new HashMap<>();
Workbook wb = new XSSFWorkbook(connection.getInputStream());
Sheet sheetWithData = wb.getSheetAt(0);
int currentRow = 2;
while (sheetWithData.getRow(currentRow) != null) {
List<String> cadastrals = extractCadastral(sheetWithData.getRow(currentRow).getCell(29).getStringCellValue());
if (cadastrals.isEmpty()) {
Lot extractedContent = extractDataFromRow(sheetWithData.getRow(currentRow), "");
data.put(extractedContent.getNotice() + "_" + extractedContent.getLotNumber() + "_" + extractedContent.getStatus(), extractedContent);
} else {
for (String cadastral : cadastrals) {
Lot extractedContent = extractDataFromRow(sheetWithData.getRow(currentRow), cadastral);
data.put(extractedContent.getNotice() + "_" + extractedContent.getLotNumber() + "_" + extractedContent.getStatus() + "_" + cadastral, extractedContent);
}
}
currentRow++;
}
return data;
} else {
logger.error("Wrong response code {} during call link {} ", responseCode, link);
Thread.sleep(1000);
return downloadExcel(retryCount + 1, link);
}
} catch (Exception e) {
responseCode = connection.getResponseCode();
} catch (IOException e) {
logger.error("Error during processing link {} ", link, e);
if (retryCount > 10) {
return new HashMap<>();
throw new UncheckedIOException(e);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
logger.error("Wrong response code {} during call link {} ", responseCode, link);
throw new UncheckedIOException(new IOException("Wrong response code " + responseCode + " during call link " + link));
}
try (Workbook wb = new XSSFWorkbook(connection.getInputStream())) {
Map<String, Lot> data = new HashMap<>();
Sheet sheetWithData = wb.getSheetAt(0);
int currentRow = 2;
while (sheetWithData.getRow(currentRow) != null) {
List<String> cadastrals = extractCadastral(sheetWithData.getRow(currentRow).getCell(29).getStringCellValue());
if (cadastrals.isEmpty()) {
Lot extractedContent = extractDataFromRow(sheetWithData.getRow(currentRow), "");
data.put(extractedContent.getNotice() + "_" + extractedContent.getLotNumber() + "_" + extractedContent.getStatus(), extractedContent);
} else {
for (String cadastral : cadastrals) {
Lot extractedContent = extractDataFromRow(sheetWithData.getRow(currentRow), cadastral);
data.put(extractedContent.getNotice() + "_" + extractedContent.getLotNumber() + "_" + extractedContent.getStatus() + "_" + cadastral, extractedContent);
}
}
currentRow++;
}
return data;
} catch (IOException e) {
logger.error("Error during processing link {} ", link, e);
throw new UncheckedIOException(e);
} catch (Exception e) {
logger.error("Error during parsing content by link {} ", link, e);
throw new UncheckedIOException(new IOException("Failed to parse content by link " + link, e));
}
try {
Thread.sleep(1000);
return downloadExcel(retryCount + 1, link);
} catch (Exception ex) {
logger.error("Exception during retry downloadExcel by link {}", link, ex);
return new HashMap<>();
}
}
@Recover
public Map<String, Lot> downloadExcelFallback(Exception e, String link) {
logger.error("Failed to download excel by link {} after retries", link, e);
return new HashMap<>();
}
private Lot extractDataFromRow(Row row, String cadastral) {

View File

@@ -0,0 +1,34 @@
package com.pledge.torgi;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("torgi_test")
.withUsername("postgres")
.withPassword("postgres");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void connectionEstablished() {
assertThat(postgres.isRunning()).isTrue();
}
}

View File

@@ -56,6 +56,9 @@ class DownloadHeadlessControllerTest {
@Mock
private MissedDataRepository missedDataRepository;
@Mock
private DownloadHeadlessService downloadHeadlessService;
@BeforeEach
void setUp() {
lenient().when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class)))
@@ -73,7 +76,7 @@ class DownloadHeadlessControllerTest {
.thenReturn(page);
// when
new DownloadHeadlessController(eventRepository, missedDataRepository);
new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
// then
verify(eventRepository).findAllByOrderByCreatedAtDesc(any(PageRequest.class));
@@ -90,14 +93,14 @@ class DownloadHeadlessControllerTest {
.thenReturn(new PageImpl<>(List.of(event1, event2)));
// when
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
// then
assertThat(getContent(controller)).containsExactlyInAnyOrder("key-1", "key-2");
}
@Test
void process_shouldSaveNewEvents() {
void process_shouldSaveNewEvents() throws Exception {
// given
Lot lot = new Lot();
lot.setStatus("PUBLISHED");
@@ -106,51 +109,45 @@ class DownloadHeadlessControllerTest {
lot.setCadastral(CADASTRAL);
lot.setFullInfo("Full info");
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<DownloadHeadlessService> ignored = mockConstruction(DownloadHeadlessService.class,
(mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
// when
controller.process();
// when
controller.process();
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
List<TorgiEvent> saved = captor.getValue();
assertThat(saved).hasSize(1);
assertThat(saved.get(0).getKey()).isEqualTo(EVENT_KEY);
assertThat(saved.get(0).getStatus()).isEqualTo("PUBLISHED");
assertThat(saved.get(0).getNotice()).isEqualTo("Notice1");
assertThat(saved.get(0).getLotNumber()).isEqualTo(1);
assertThat(saved.get(0).getCadastral()).isEqualTo(CADASTRAL);
assertThat(saved.get(0).getFullInfo()).isEqualTo("Full info");
assertThat(getContent(controller)).contains(EVENT_KEY);
}
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
List<TorgiEvent> saved = captor.getValue();
assertThat(saved).hasSize(1);
assertThat(saved.get(0).getKey()).isEqualTo(EVENT_KEY);
assertThat(saved.get(0).getStatus()).isEqualTo("PUBLISHED");
assertThat(saved.get(0).getNotice()).isEqualTo("Notice1");
assertThat(saved.get(0).getLotNumber()).isEqualTo(1);
assertThat(saved.get(0).getCadastral()).isEqualTo(CADASTRAL);
assertThat(saved.get(0).getFullInfo()).isEqualTo("Full info");
assertThat(getContent(controller)).contains(EVENT_KEY);
}
@Test
void process_shouldSkipAlreadyKnownKeys() {
void process_shouldSkipAlreadyKnownKeys() throws Exception {
// given
TorgiEvent existing = new TorgiEvent();
existing.setKey(EVENT_KEY);
when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class)))
.thenReturn(new PageImpl<>(List.of(existing)));
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<DownloadHeadlessService> ignored = mockConstruction(DownloadHeadlessService.class,
(mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
// when
controller.process();
// when
controller.process();
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
assertThat(captor.getValue()).isEmpty();
assertThat(getContent(controller)).containsExactly(EVENT_KEY);
}
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
assertThat(captor.getValue()).isEmpty();
assertThat(getContent(controller)).containsExactly(EVENT_KEY);
}
@Test
@@ -161,17 +158,15 @@ class DownloadHeadlessControllerTest {
params.setToken("token");
List<String> cadastral = List.of(CADASTRAL);
try (MockedConstruction<DownloadHeadlessService> serviceConstruction = mockConstruction(DownloadHeadlessService.class);
MockedConstruction<SendEmailService> emailConstruction = mockConstruction(SendEmailService.class)) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
try (MockedConstruction<SendEmailService> emailConstruction = mockConstruction(SendEmailService.class)) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
// when
controller.sendNewContent(params, cadastral, "to@example.com");
// then
DownloadHeadlessService service = serviceConstruction.constructed().get(0);
ArgumentCaptor<LocalDate> dateCaptor = ArgumentCaptor.forClass(LocalDate.class);
verify(service).createNewDocument(eq(eventRepository), eq(cadastral), dateCaptor.capture());
verify(downloadHeadlessService).createNewDocument(eq(eventRepository), eq(cadastral), dateCaptor.capture());
LocalDate now = dateCaptor.getValue();
SendEmailService emailService = emailConstruction.constructed().get(0);
@@ -191,7 +186,7 @@ class DownloadHeadlessControllerTest {
StartParams params = new StartParams();
params.setFrom("from@example.com");
params.setToken("token");
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<SendEmailService> ignored = mockConstruction(SendEmailService.class,
(mock, context) -> doThrow(new RuntimeException("smtp failure"))
@@ -206,7 +201,7 @@ class DownloadHeadlessControllerTest {
}
@Test
void richData_shouldSaveNewEventsForMissedCadastral() {
void richData_shouldSaveNewEventsForMissedCadastral() throws Exception {
// given
MissedData missed = new MissedData();
missed.setCadastral(CADASTRAL);
@@ -219,25 +214,22 @@ class DownloadHeadlessControllerTest {
lot.setLotNumber(1);
lot.setCadastral(CADASTRAL);
Map<String, Lot> downloaded = Map.of(EVENT_KEY, lot);
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<DownloadHeadlessService> ignored = mockConstruction(DownloadHeadlessService.class,
(mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
// when
controller.richData();
// when
controller.richData();
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
assertThat(captor.getValue()).hasSize(1);
assertThat(captor.getValue().get(0).getKey()).isEqualTo(EVENT_KEY);
verify(missedDataRepository).delete(missed);
}
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
assertThat(captor.getValue()).hasSize(1);
assertThat(captor.getValue().get(0).getKey()).isEqualTo(EVENT_KEY);
verify(missedDataRepository).delete(missed);
}
@Test
void richData_shouldSkipKnownEventsForCadastral() {
void richData_shouldSkipKnownEventsForCadastral() throws Exception {
// given
MissedData missed = new MissedData();
missed.setCadastral(CADASTRAL);
@@ -248,44 +240,38 @@ class DownloadHeadlessControllerTest {
when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + CADASTRAL + "%")).thenReturn(List.of(existing));
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<DownloadHeadlessService> ignored = mockConstruction(DownloadHeadlessService.class,
(mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
// when
controller.richData();
// when
controller.richData();
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
assertThat(captor.getValue()).isEmpty();
verify(missedDataRepository).delete(missed);
}
// then
ArgumentCaptor<List<TorgiEvent>> captor = ArgumentCaptor.forClass(List.class);
verify(eventRepository).saveAll(captor.capture());
assertThat(captor.getValue()).isEmpty();
verify(missedDataRepository).delete(missed);
}
@Test
void richData_shouldDeleteMissedDataWhenDownloadedContentEmpty() {
void richData_shouldDeleteMissedDataWhenDownloadedContentEmpty() throws Exception {
// given
MissedData missed = new MissedData();
missed.setCadastral(CADASTRAL);
when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed)));
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(Collections.emptyMap());
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<DownloadHeadlessService> ignored = mockConstruction(DownloadHeadlessService.class,
(mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(Collections.emptyMap()))) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
// when
controller.richData();
// when
controller.richData();
// then
verify(eventRepository, never()).saveAll(anyList());
verify(missedDataRepository).delete(missed);
}
// then
verify(eventRepository, never()).saveAll(anyList());
verify(missedDataRepository).delete(missed);
}
@Test
void richData_shouldAbortProcessingOnRepositoryError() {
void richData_shouldAbortProcessingOnRepositoryError() throws Exception {
// given
MissedData missed = new MissedData();
missed.setCadastral(CADASTRAL);
@@ -294,25 +280,22 @@ class DownloadHeadlessControllerTest {
.thenThrow(new RuntimeException("Database error"));
Map<String, Lot> downloaded = Map.of(EVENT_KEY, new Lot());
when(downloadHeadlessService.downloadExcel(anyString())).thenReturn(downloaded);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try (MockedConstruction<DownloadHeadlessService> ignored = mockConstruction(DownloadHeadlessService.class,
(mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) {
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
// when
controller.richData();
// when
controller.richData();
// then
verify(eventRepository, never()).saveAll(anyList());
verify(missedDataRepository, never()).delete(any());
}
// then
verify(eventRepository, never()).saveAll(anyList());
verify(missedDataRepository, never()).delete(any());
}
@Test
void richData_shouldHandleExceptionGracefully() {
// given
when(missedDataRepository.findAll(any(PageRequest.class))).thenThrow(new RuntimeException("Database error"));
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
// when / then
assertThatCode(controller::richData).doesNotThrowAnyException();
@@ -325,7 +308,7 @@ class DownloadHeadlessControllerTest {
File deeper = new File(nested, "deeper");
assertThat(deeper.mkdirs()).isTrue();
writeFile(new File(deeper, "file.txt"), "content");
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
// when
boolean result = invokeDeleteDirectory(controller, nested);
@@ -388,7 +371,7 @@ class DownloadHeadlessControllerTest {
File nested = new File(sourceDir, "nested");
assertThat(nested.mkdirs()).isTrue();
writeFile(new File(nested, "data.txt"), "archive-content");
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository);
DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository, downloadHeadlessService);
try {
// when