package com.griddynamics.jagger.master; import com.griddynamics.jagger.jaas.storage.model.LoadScenarioEntity; import com.griddynamics.jagger.jaas.storage.model.TestEnvUtils; import com.griddynamics.jagger.jaas.storage.model.TestEnvironmentEntity; import com.griddynamics.jagger.jaas.storage.model.TestEnvironmentEntity.TestEnvironmentStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.io.Closeable; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * Handles communication to JaaS environment API - * initial registration and further status updates with commands parsing from JaaS response. */ public class JaasEnvApiClient implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(JaasEnvApiClient.class); private volatile boolean standBy = true; private final RestTemplate restTemplate = new RestTemplate(); private volatile String envId; private final URI jaasEndpoint; private final int statusReportIntervalSeconds; private final Set<String> availableConfigs; private boolean registered = false; private StatusExchangeThread statusExchangeThread; public JaasEnvApiClient(String envId, String jaasEndpoint, int statusReportIntervalSeconds, Set<String> availableConfigs) { this.envId = envId; try { this.jaasEndpoint = new URI(jaasEndpoint); } catch (URISyntaxException e) { throw new IllegalStateException(String.format("Incorrect JaaS endpoint %s", jaasEndpoint), e); } if (availableConfigs == null) { availableConfigs = Collections.emptySet(); } this.availableConfigs = availableConfigs; if (statusReportIntervalSeconds <= 0) { LOGGER.warn( "Provided status reporting interval seconds is less or equals to zero - {}.\n" + "Using the default value - 15 seconds", statusReportIntervalSeconds ); statusReportIntervalSeconds = 15; } this.statusReportIntervalSeconds = statusReportIntervalSeconds; } public void register() throws InterruptedException { final String sessionCookie = doRegister(); statusExchangeThread = new StatusExchangeThread(sessionCookie); statusExchangeThread.start(); registered = true; } public JaasResponse awaitNextExecution() throws TerminateException, InterruptedException { if (!registered) { throw new IllegalStateException("must be registered"); } statusExchangeThread.setPendingRequestEntity(); JaasResponse jaasResponse; do { jaasResponse = statusExchangeThread.nextConfigToExecute.poll(1, TimeUnit.MINUTES); if (!standBy) { throw getTerminateException(); } } while (jaasResponse == null); statusExchangeThread.setRunningRequestEntity(); return jaasResponse; } private TerminateException getTerminateException() throws TerminateException { throw new TerminateException("Communication to JaaS Env API can't be proceeded"); } private String doRegister() throws InterruptedException { URI envsUri = UriComponentsBuilder.newInstance().uri(jaasEndpoint).path("/envs").build().toUri(); TestEnvironmentEntity testEnvironmentEntity = buildTestEnvironmentEntityWith(TestEnvironmentStatus.PENDING); ResponseEntity<String> responseEntity = null; boolean posted = false; do { try { responseEntity = restTemplate.postForEntity(envsUri, testEnvironmentEntity, String.class); posted = true; LOGGER.info("POST request sent to {} with body {}.", envsUri, testEnvironmentEntity); } catch (HttpClientErrorException e) { if (!isEnvIdUnacceptable(e)) { throw e; } envId = UUID.randomUUID().toString(); LOGGER.warn("Changing env id from {} to {}", testEnvironmentEntity.getEnvironmentId(), envId); testEnvironmentEntity.setEnvironmentId(envId); } catch (HttpServerErrorException | ResourceAccessException e) { LOGGER.warn("Error during registration to '{}'", envsUri, e); TimeUnit.SECONDS.sleep(statusReportIntervalSeconds); } } while (!posted); final String sessionCookie = extractSessionCookie(responseEntity); LOGGER.info("Environment {} successfully registered to JaaS with session cookie - {}", envId, sessionCookie); return sessionCookie; } private TestEnvironmentEntity buildTestEnvironmentEntityWith(TestEnvironmentStatus status) { TestEnvironmentEntity testEnvironmentEntity = new TestEnvironmentEntity(); testEnvironmentEntity.setEnvironmentId(envId); testEnvironmentEntity.setStatus(status); testEnvironmentEntity.setLoadScenarios(availableConfigs.stream() .map(LoadScenarioEntity::new) .collect(Collectors.toList()) ); return testEnvironmentEntity; } private boolean isEnvIdUnacceptable(HttpClientErrorException exception) { if (exception.getStatusCode() == HttpStatus.CONFLICT) { LOGGER.warn("Env with id {} already registered", envId); return true; } // If env id doesn't match acceptable pattern. if (exception.getStatusCode() == HttpStatus.BAD_REQUEST && exception.getResponseBodyAsString().contains("doesn't match pattern")) { LOGGER.warn("Env id {} doesn't match pattern", envId); return true; } return false; } private String extractSessionCookie(ResponseEntity<String> responseEntity) { List<String> sessionCookies = responseEntity.getHeaders().get(HttpHeaders.SET_COOKIE).stream() .filter(s -> s.contains(TestEnvUtils.SESSION_COOKIE)) .collect(Collectors.toList()); if (sessionCookies.size() != 1) { throw new IllegalStateException(String.format("There are %s values for '%s' cookie", sessionCookies.size(), TestEnvUtils.SESSION_COOKIE )); } String rawSessionCookie = sessionCookies.get(0); int endIndex = rawSessionCookie.indexOf(";"); if (endIndex < 0) { endIndex = rawSessionCookie.length() - 1; } return rawSessionCookie.substring(0, endIndex); } public boolean isStandBy() { return standBy; } public boolean isRegistered() { return registered; } @Override public void close() { standBy = false; } private final class StatusExchangeThread extends Thread { private String sessionCookie; private final SynchronousQueue<JaasResponse> nextConfigToExecute = new SynchronousQueue<>(); private volatile RequestEntity<TestEnvironmentEntity> requestEntity; StatusExchangeThread(String sessionCookie) { this.sessionCookie = sessionCookie; this.requestEntity = buildPendingRequestEntity(); setName(this.getClass().getName()); } private RequestEntity<TestEnvironmentEntity> buildPendingRequestEntity() { TestEnvironmentEntity testEnvEntity = buildTestEnvironmentEntityWith(TestEnvironmentStatus.PENDING); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.COOKIE, sessionCookie); URI updatesUri = UriComponentsBuilder.newInstance() .uri(jaasEndpoint) .path("/envs") .path("/" + envId) .build().toUri(); return new RequestEntity<>(testEnvEntity, httpHeaders, HttpMethod.PUT, updatesUri); } void setPendingRequestEntity() { this.requestEntity = buildPendingRequestEntity(); } void setRunningRequestEntity() { RequestEntity<TestEnvironmentEntity> requestEntity = buildPendingRequestEntity(); requestEntity.getBody().setStatus(TestEnvironmentStatus.RUNNING); this.requestEntity = requestEntity; } @Override public void run() { do { try { updateStatus(); TimeUnit.SECONDS.sleep(statusReportIntervalSeconds); } catch (InterruptedException e) { standBy = false; } } while (standBy); } private void updateStatus() throws InterruptedException { LOGGER.debug("Performing status update..."); try { ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class); LOGGER.info("PUT request sent to {} with body {}.", requestEntity.getUrl(), requestEntity.getBody()); tryToOfferNextConfigToExecute(responseEntity); } catch (HttpServerErrorException | ResourceAccessException e) { LOGGER.warn("Server error during update by url '{}'", requestEntity.getUrl(), e); } catch (HttpClientErrorException e) { if (e.getStatusCode() == HttpStatus.NOT_FOUND) { reRegister(); } else { LOGGER.error("Finishing work due to HTTP client exception.", e); standBy = false; } } } private void reRegister() throws InterruptedException { this.sessionCookie = doRegister(); if (requestEntity.getBody().getStatus() == TestEnvironmentStatus.PENDING) { setPendingRequestEntity(); } else { setRunningRequestEntity(); } } private void tryToOfferNextConfigToExecute(ResponseEntity<String> responseEntity) throws InterruptedException { if (requestEntity.getBody().getStatus() != TestEnvironmentStatus.PENDING) { return; } JaasResponse jaasResponse = extractJaasResponse(responseEntity); if (jaasResponse == null) { return; } if (!nextConfigToExecute.offer(jaasResponse, 1, TimeUnit.MINUTES)) { LOGGER.warn("Didn't manage to put next config name into a queue"); } } private JaasResponse extractJaasResponse(ResponseEntity<String> responseEntity) { String executionId = extractHeader(responseEntity, TestEnvUtils.EXECUTION_ID_HEADER); if (executionId == null) { return null; } JaasResponse jaasResponse = new JaasResponse(); jaasResponse.executionId = executionId; return jaasResponse; } private String extractHeader(final ResponseEntity<String> responseEntity, final String headerName) { List<String> configNameHeaders = responseEntity.getHeaders().get(headerName); if (CollectionUtils.isEmpty(configNameHeaders)) { return null; } if (configNameHeaders.size() > 1) { LOGGER.warn("There are more then 1 {} header value in response. Using the 1st one", headerName); } return configNameHeaders.get(0); } } public static class JaasResponse { private String executionId; public String getExecutionId() { return executionId; } } }