diff --git a/build.gradle b/build.gradle index e5d23f4..aa97e64 100644 --- a/build.gradle +++ b/build.gradle @@ -63,18 +63,20 @@ dependencies { implementation "org.testeditor.web:org.testeditor.web.dropwizard:$versions.testEditorDropwizard" implementation "commons-io:commons-io:2.6" implementation "org.apache.commons:commons-text:1.4" - implementation "org.testeditor.web:org.testeditor.web.dropwizard:$versions.testEditorDropwizard" - implementation 'org.eclipse.jgit:org.eclipse.jgit:5.1.2.201810061102-r' + implementation "org.testeditor.web:org.testeditor.web.dropwizard:$versions.testEditorDropwizard" + implementation "org.eclipse.jgit:org.eclipse.jgit:5.1.2.201810061102-r" + implementation "org.glassfish.jersey.ext.rx:jersey-rx-client-java8:2.25.1" + implementation "io.dropwizard:dropwizard-client:$versions.dropwizard" testImplementation "junit:junit:4.12" testImplementation "io.dropwizard:dropwizard-testing:$versions.dropwizard" testImplementation "org.assertj:assertj-core:3.12.0" testImplementation "org.mockito:mockito-core:3.1.0" testImplementation "org.eclipse.xtext:org.eclipse.xtext.testing:$versions.xtext" - testImplementation "org.testeditor.web:org.testeditor.web.dropwizard.testing:$versions.testEditorDropwizard" - testImplementation 'org.eclipse.jgit:org.eclipse.jgit.junit:5.1.2.201810061102-r' + testImplementation "org.testeditor.web:org.testeditor.web.dropwizard.testing:$versions.testEditorDropwizard" + testImplementation 'org.eclipse.jgit:org.eclipse.jgit.junit:5.1.2.201810061102-r' - annotationProcessor 'com.github.moditect.deptective:deptective-javac-plugin:master-SNAPSHOT' + annotationProcessor 'com.github.moditect.deptective:deptective-javac-plugin:master-SNAPSHOT' } mainClassName = 'org.testeditor.web.backend.testexecution.dropwizard.TestExecutionApplication' @@ -142,4 +144,4 @@ compileJava { 'reporting_policy=WARN ' + 'visualize=true ' + "config_file=${projectDir}/src/main/resources/META-INF/deptective.json" -} \ No newline at end of file +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/RestClient.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/RestClient.xtend new file mode 100644 index 0000000..d797b6d --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/RestClient.xtend @@ -0,0 +1,151 @@ +package org.testeditor.web.backend.testexecution.distributed.common + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import java.net.URI +import java.util.concurrent.CompletionStage +import java.util.concurrent.ExecutorService +import java.util.concurrent.ForkJoinPool +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import javax.inject.Singleton +import javax.ws.rs.client.Entity +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.StreamingOutput +import org.glassfish.jersey.client.rx.RxClient +import org.glassfish.jersey.client.rx.java8.RxCompletionStageInvoker +import org.slf4j.LoggerFactory + +import static javax.ws.rs.client.Entity.json +import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE +import static org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT +import static org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT +import static org.glassfish.jersey.client.HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING + +/** + * Abstraction around an HTTP client for easy mocking + */ +interface RestClient { + + public static val int READ_TIMEOUT_MILLIS = 10000 + + def CompletionStage postAsync(URI uri, T body) + + def CompletionStage postAsync(URI uri, StreamingOutput body) + + def CompletionStage putAsync(URI uri, T body) + + def CompletionStage getAsync(URI uri) + + def CompletionStage getAsync(URI uri, MediaType accept) + + def CompletionStage deleteAsync(URI uri) + + def Response post(URI uri, T body) + + def Response put(URI uri, T body) + + def Response get(URI uri) + + def Response get(URI uri, MediaType accept) + + def Response delete(URI uri) + +} + +abstract class AbstractRestClient implements RestClient { + + override post(URI uri, T body) { + return uri.postAsync(body).toCompletableFuture.join + } + + override put(URI uri, T body) { + return uri.putAsync(body).toCompletableFuture.join + } + + override get(URI uri) { + return uri.getAsync.toCompletableFuture.join + } + + override delete(URI uri) { + return uri.deleteAsync.toCompletableFuture.join + } + + override get(URI uri, MediaType accept) { + return uri.getAsync(accept).toCompletableFuture.join + } + +} + +@Singleton +class JerseyBasedRestClient extends AbstractRestClient { + + static val logger = LoggerFactory.getLogger(JerseyBasedRestClient) + + val Provider> httpClientProvider + val ExecutorService executor + + @Inject + new(Provider> httpClientProvider, @Named('httpClientExecutor') ForkJoinPool executor) { + this.httpClientProvider = httpClientProvider + this.executor = executor + } + + override CompletionStage postAsync(URI uri, T body) { + val entity = json(body) + logger.info('''sending POST request to «uri.toString» with body:\n«entity.toString»''') + return uri.invoker.post(entity) + } + + override CompletionStage postAsync(URI uri, StreamingOutput body) { + val entity = Entity.entity(body, MediaType.APPLICATION_OCTET_STREAM_TYPE) + logger.info('''sending POST request to «uri.toString» with streaming data''') + return uri.streamingInvoker.post(entity) + } + + override CompletionStage putAsync(URI uri, T body) { + val entity = json(body) + logger.info('''sending PUT request to «uri.toString» with body:\n«entity.toString»''') + return uri.invoker.put(entity) + } + + override CompletionStage getAsync(URI uri) { + return uri.invoker.get + } + + override CompletionStage getAsync(URI uri, MediaType accept) { + return uri.getInvoker(accept).get + } + + override deleteAsync(URI uri) { + return uri.invoker.delete + } + + private def getInvoker(URI uri) { + return uri.getInvoker(APPLICATION_JSON_TYPE) + } + + private def getInvoker(URI uri, MediaType accept) { + return httpClientProvider.get.property(READ_TIMEOUT, READ_TIMEOUT_MILLIS).target(uri).request(accept).header( + 'Authorization', '''Bearer «dummyToken»''').rx(executor) + } + + private def getStreamingInvoker(URI uri) { + return httpClientProvider.get.property(USE_FIXED_LENGTH_STREAMING, true).property(READ_TIMEOUT, 0).property(CONNECT_TIMEOUT, 0).target(uri). + request.header('Authorization', '''Bearer «dummyToken»''').rx(executor) + } + + val static String dummyToken = createToken('test.execution', 'Test Execution User', 'testeditor.eng@gmail.com') + + static def String createToken(String id, String name, String eMail) { + val builder = JWT.create => [ + withClaim('id', id) + withClaim('name', name) + withClaim('email', eMail) + ] + return builder.sign(Algorithm.HMAC256("secret")) + } + +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/TestExecutionManager.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/TestExecutionManager.xtend new file mode 100644 index 0000000..e51d2b7 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/TestExecutionManager.xtend @@ -0,0 +1,12 @@ +package org.testeditor.web.backend.testexecution.distributed.common + +import java.util.Set +import org.testeditor.web.backend.testexecution.common.TestExecutionKey + +interface TestExecutionManager extends StatusAwareTestJobStore { + + def void cancelJob(TestExecutionKey key) + + def TestExecutionKey addJob(Iterable testFiles, Set requiredCapabilities) + +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/Worker.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/Worker.xtend index 728811f..67b4039 100644 --- a/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/Worker.xtend +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/Worker.xtend @@ -12,6 +12,16 @@ interface WorkerInfo { def Set getProvidedCapabilities() + static val NONE = new WorkerInfo { + val uri = new URI('') + val providedCapabilities = emptySet + + override getUri() { uri } + + override getProvidedCapabilities() { providedCapabilities } + + } + } interface Worker extends RunningTest, WorkerInfo, TestJobStore { diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/WorkerAPI.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/WorkerAPI.xtend new file mode 100644 index 0000000..9dcd8e2 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/WorkerAPI.xtend @@ -0,0 +1,13 @@ +package org.testeditor.web.backend.testexecution.distributed.common + +interface WorkerAPI { + + def R isRegistered() + + def R executeTestJob(TestJob job) + + def R cancelTestJob() + + def R getTestJobState(Boolean wait) + +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/WorkerManagerAPI.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/WorkerManagerAPI.xtend new file mode 100644 index 0000000..f6f7757 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/common/WorkerManagerAPI.xtend @@ -0,0 +1,16 @@ +package org.testeditor.web.backend.testexecution.distributed.common + +import org.testeditor.web.backend.testexecution.common.TestExecutionKey +import org.testeditor.web.backend.testexecution.common.TestStatus + +interface WorkerManagerAPI { + + def R registerWorker(Worker worker) + + def R unregisterWorker(String id) + + def R upload(String workerId, TestExecutionKey jobId, String fileName, S content) + + def R updateStatus(String workerId, TestExecutionKey jobId, TestStatus status) + +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/TestExecutionManager.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/DefaultExecutionManager.xtend similarity index 78% rename from src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/TestExecutionManager.xtend rename to src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/DefaultExecutionManager.xtend index 89e6c86..329b396 100644 --- a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/TestExecutionManager.xtend +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/DefaultExecutionManager.xtend @@ -1,6 +1,5 @@ package org.testeditor.web.backend.testexecution.distributed.manager -import java.util.Optional import java.util.Set import java.util.concurrent.atomic.AtomicLong import javax.inject.Inject @@ -8,43 +7,34 @@ import javax.inject.Singleton import org.slf4j.LoggerFactory import org.testeditor.web.backend.testexecution.common.TestExecutionKey import org.testeditor.web.backend.testexecution.common.TestStatus -import org.testeditor.web.backend.testexecution.distributed.common.StatusAwareTestJobStore +import org.testeditor.web.backend.testexecution.distributed.common.TestExecutionManager import org.testeditor.web.backend.testexecution.distributed.common.TestJob import org.testeditor.web.backend.testexecution.distributed.common.TestJobInfo import org.testeditor.web.backend.testexecution.distributed.common.TestJobInfo.JobState import org.testeditor.web.backend.testexecution.distributed.common.WritableStatusAwareTestJobStore -interface TestExecutionManager extends StatusAwareTestJobStore { - - def void cancelJob(TestExecutionKey key) - - def TestExecutionKey addJob(Iterable testFiles, Set requiredCapabilities) - -} - @Singleton -class LocalSingleWorkerExecutionManager implements TestExecutionManager { - static val logger = LoggerFactory.getLogger(LocalSingleWorkerExecutionManager) +class DefaultExecutionManager implements TestExecutionManager { + static val logger = LoggerFactory.getLogger(DefaultExecutionManager) @Inject extension WorkerProvider workerProvider @Inject extension WritableStatusAwareTestJobStore jobStore var AtomicLong runningTestSuiteRunId = new AtomicLong(0) - var Optional currentJob = Optional.empty + val currentJob = newHashMap override cancelJob(TestExecutionKey key) { - currentJob.filter[id == key].ifPresent[ - workers.head.cancel + currentJob.remove(key) => [ + workerForJob.cancel setState(JobState.COMPLETED_CANCELLED).store - currentJob = Optional.empty ] } override addJob(Iterable testFiles, Set requiredCapabilities) { return (new TestJob(new TestExecutionKey("0").deriveFreshRunId, emptySet, testFiles) => [ store - workers.head.assign(it).thenAccept[status|updateStatus(status)] - currentJob = Optional.of(it) + idleWorkers.head?.assign(it)?.thenAccept[status|updateStatus(status)] + currentJob.put(id, it) ]).id } diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/LocalSingleWorkerManager.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/LocalSingleWorkerManager.xtend index 893cee3..c3c6109 100644 --- a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/LocalSingleWorkerManager.xtend +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/LocalSingleWorkerManager.xtend @@ -3,6 +3,7 @@ package org.testeditor.web.backend.testexecution.distributed.manager import javax.inject.Inject import org.eclipse.xtend.lib.annotations.Delegate import org.testeditor.web.backend.testexecution.common.TestExecutionKey +import org.testeditor.web.backend.testexecution.common.TestStatus import org.testeditor.web.backend.testexecution.distributed.common.TestJob import org.testeditor.web.backend.testexecution.distributed.common.TestJobInfo import org.testeditor.web.backend.testexecution.distributed.common.TestJobStore @@ -17,6 +18,14 @@ class LocalSingleWorkerManager implements WorkerProvider { override getWorkers() { return #[worker] } + + override idleWorkers() { + return #[worker].filter[checkStatus !== TestStatus.RUNNING].filter(WorkerInfo) + } + + override workerForJob(TestJobInfo job) { + return if (currentJob?.id == job.id) { worker } else { WorkerInfo.NONE } + } override assign(WorkerInfo worker, TestJob job) { return if (worker === this.worker) { diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/WorkerProvider.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/WorkerProvider.xtend index f983a82..aebfd0e 100644 --- a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/WorkerProvider.xtend +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/WorkerProvider.xtend @@ -5,10 +5,15 @@ import org.testeditor.web.backend.testexecution.common.TestStatus import org.testeditor.web.backend.testexecution.distributed.common.StatusAwareTestJobStore import org.testeditor.web.backend.testexecution.distributed.common.TestJob import org.testeditor.web.backend.testexecution.distributed.common.WorkerInfo +import org.testeditor.web.backend.testexecution.distributed.common.TestJobInfo interface WorkerProvider extends StatusAwareTestJobStore { def Iterable getWorkers() + + def Iterable idleWorkers() + + def WorkerInfo workerForJob(TestJobInfo job) def CompletionStage assign(WorkerInfo worker, TestJob job) diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/RestWorkerClient.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/RestWorkerClient.xtend new file mode 100644 index 0000000..150fcb4 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/RestWorkerClient.xtend @@ -0,0 +1,130 @@ +package org.testeditor.web.backend.testexecution.distributed.manager.rest + +import com.fasterxml.jackson.annotation.JacksonInject +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.net.URI +import java.util.Set +import java.util.concurrent.CompletionStage +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.SynchronousQueue +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.core.UriBuilder +import org.eclipse.xtend.lib.annotations.Data +import org.eclipse.xtend.lib.annotations.EqualsHashCode +import org.slf4j.LoggerFactory +import org.testeditor.web.backend.testexecution.common.TestExecutionKey +import org.testeditor.web.backend.testexecution.common.TestStatus +import org.testeditor.web.backend.testexecution.distributed.common.RestClient +import org.testeditor.web.backend.testexecution.distributed.common.TestJobInfo +import org.testeditor.web.backend.testexecution.distributed.common.Worker + +import static javax.ws.rs.core.Response.Status.CREATED + +@Data +@EqualsHashCode +class RestWorkerClient implements Worker { + public static val RestWorkerClient NONE = new RestWorkerClient(null) + + static val logger = LoggerFactory.getLogger(RestWorkerClient) + + val URI uri + val Set providedCapabilities + val transient extension RestClient client + val transient ExecutorService executor + val transient currentJobs = >newHashMap + + new(URI uri, Set capabilities, RestClient client, ExecutorService executor) { + this.uri = uri + this.providedCapabilities = capabilities + this.client = client + this.executor = executor + } + + @JsonCreator + new(@JsonProperty('uri') URI uri, @JsonProperty('capabilities') Set capabilities, + @JacksonInject('restClient') RestClient client) { + this(uri, capabilities, client, Executors.newSingleThreadExecutor) + } + + new(URI uri) { + this(uri, emptySet, null) + } + + new(URI uri, Set capabilities) { + this(uri, capabilities, null) + } + + override CompletionStage startJob(TestJobInfo job) { + currentJobs.put(job.id, new SynchronousQueue) + + return requestStartJob(job).thenApplyAsync[waitForCompletion(job)] + } + + override checkStatus() { + return TestStatus.valueOf(executor.submit [ + jobUri.build.getAsync(MediaType.TEXT_PLAIN_TYPE).toCompletableFuture.get.readEntity(String) + ].get) + } + + override waitForStatus() { + logger.info('''enqueueing request to wait for status of worker at "«uri»"''') + return TestStatus.valueOf(executor.submit [ + logger.info('''now requesting to wait for status of worker at "«uri»"''') + val status = jobUri.queryParam('wait', true).build.getAsync(MediaType.TEXT_PLAIN_TYPE).toCompletableFuture. + get.readEntity(String) + logger.info('''received status "«status»" of worker at "«uri»"''') + return status + ].get) + } + + override kill() { + executor.submit [ + jobUri.build.deleteAsync + ] + + } + + def void updateStatus(TestExecutionKey key, TestStatus status) { + if (status !== TestStatus.RUNNING) { + currentJobs.remove(key)?.put(status) + } + } + + private def UriBuilder jobUri() { + return UriBuilder.fromUri(uri).path('job') + } + + private def CompletionStage requestStartJob(TestJobInfo job) { + jobUri.build.postAsync(job).exceptionally [ + logger.error('''exception occurred while trying to assign job "«job?.id»" to worker at "«uri»"''', it) + Response.serverError.entity('exception thrown on client side').build + ].thenApplyAsync [ + return (status === CREATED.statusCode) => [ success | + if (!success) { + logger. + warn('''job "«job?.id»" was rejected by worker at "«uri»" with status code «status»: «readEntity(String)»''') + } + ] + ] + } + + private def waitForCompletion(boolean successfullyStarted, TestJobInfo job) { + if (successfullyStarted) { + currentJobs.get(job.id).take + } else { + TestStatus.FAILED // TODO failed to start vs. test failed – separate status? + } + } + + override testJobExists(TestExecutionKey key) { + return currentJobs.containsKey(key) + } + + override getJsonCallTree(TestExecutionKey key) { + throw new UnsupportedOperationException("TODO: auto-generated method stub") + } + +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/RestWorkerManager.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/RestWorkerManager.xtend new file mode 100644 index 0000000..509001b --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/RestWorkerManager.xtend @@ -0,0 +1,73 @@ +package org.testeditor.web.backend.testexecution.distributed.manager.rest + +import javax.inject.Singleton +import org.testeditor.web.backend.testexecution.common.TestExecutionKey +import org.testeditor.web.backend.testexecution.distributed.common.TestJob +import org.testeditor.web.backend.testexecution.distributed.common.TestJobInfo +import org.testeditor.web.backend.testexecution.distributed.common.Worker +import org.testeditor.web.backend.testexecution.distributed.common.WorkerInfo +import org.testeditor.web.backend.testexecution.distributed.manager.WorkerProvider +import java.util.Optional +import org.testeditor.web.backend.testexecution.common.TestStatus + +@Singleton +class RestWorkerManager implements WorkerProvider { + + val workers = newHashMap + + override getWorkers() { + return newHashSet => [ addAll(workers.keySet)] + } + + override idleWorkers() { + return workers.filter[worker, job|job == TestJob.NONE].keySet.filter(WorkerInfo) + } + + override workerForJob(TestJobInfo job) { + return workers.keySet.findFirst[workers.get(it) == job] + } + + override assign(WorkerInfo workerInfo, TestJob job) { + return if (workers.get(workerInfo) === TestJob.NONE) { + val worker = workerInfo as Worker + worker.startJob(job) => [ + thenRunAsync[workers.replace(worker, TestJob.NONE)] + ] + } else { + //TODO throw exception + } + } + + override cancel(WorkerInfo workerInfo) { + if (workers.containsKey(workerInfo)) { + (workerInfo as Worker).cancel + } else { + //TODO throw exception + } + } + + override testJobExists(TestExecutionKey key) { + return workers.values.exists[id == key] + } + + override getJsonCallTree(TestExecutionKey key) { + return key.worker.flatMap[getJsonCallTree(key)] + } + + override getStatusAll() { + return workers.filter[__, job| job !== TestJob.NONE].keySet.toMap([workers.get(it).id],[checkStatus]) + } + + override getStatus(TestExecutionKey key) { + return key.worker.map[checkStatus].orElse(TestStatus.IDLE) + } + + override waitForStatus(TestExecutionKey key) { + return key.worker.map[waitForStatus].orElse(TestStatus.IDLE) + } + + private def getWorker(TestExecutionKey key) { + return Optional.ofNullable(workers.filter[__, job| job.id == key].keySet.head) + } + +} \ No newline at end of file diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/TestExecutionManagerResource.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/TestExecutionManagerResource.xtend new file mode 100644 index 0000000..dd39201 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/manager/rest/TestExecutionManagerResource.xtend @@ -0,0 +1,34 @@ +package org.testeditor.web.backend.testexecution.distributed.manager.rest + +import java.io.InputStream +import javax.inject.Inject +import javax.ws.rs.core.Response +import org.testeditor.web.backend.testexecution.common.TestExecutionKey +import org.testeditor.web.backend.testexecution.common.TestStatus +import org.testeditor.web.backend.testexecution.distributed.common.TestExecutionManager +import org.testeditor.web.backend.testexecution.distributed.common.Worker +import org.testeditor.web.backend.testexecution.distributed.common.WorkerManagerAPI +import org.testeditor.web.backend.testexecution.distributed.manager.WorkerProvider + +class TestExecutionManagerResource implements WorkerManagerAPI{ + @Inject TestExecutionManager manager + @Inject extension WorkerProvider workerProvider + + override registerWorker(Worker worker) { + throw new UnsupportedOperationException("TODO: auto-generated method stub") + } + + override unregisterWorker(String id) { + throw new UnsupportedOperationException("TODO: auto-generated method stub") + } + + override upload(String workerId, TestExecutionKey jobId, String fileName, InputStream content) { + throw new UnsupportedOperationException("TODO: auto-generated method stub") + } + + override updateStatus(String workerId, TestExecutionKey jobId, TestStatus status) { + workers.filter(RestWorkerClient).findFirst[uri == workerId]?.updateStatus(jobId, status) + return Response.ok.build + } + +} \ No newline at end of file diff --git a/src/main/java/org/testeditor/web/backend/testexecution/distributed/worker/rest/WorkerResource.xtend b/src/main/java/org/testeditor/web/backend/testexecution/distributed/worker/rest/WorkerResource.xtend new file mode 100644 index 0000000..094575b --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/distributed/worker/rest/WorkerResource.xtend @@ -0,0 +1,172 @@ +package org.testeditor.web.backend.testexecution.distributed.worker.rest + +import java.util.Map +import javax.inject.Inject +import javax.inject.Singleton +import javax.ws.rs.DELETE +import javax.ws.rs.GET +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.Produces +import javax.ws.rs.QueryParam +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import org.eclipse.xtend.lib.annotations.FinalFieldsConstructor +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testeditor.web.backend.testexecution.common.TestStatus +import org.testeditor.web.backend.testexecution.distributed.common.TestJob +import org.testeditor.web.backend.testexecution.distributed.common.Worker +import org.testeditor.web.backend.testexecution.distributed.common.WorkerAPI + +import static javax.ws.rs.core.Response.Status.CONFLICT +import static javax.ws.rs.core.Response.Status.NOT_FOUND +import static org.testeditor.web.backend.testexecution.distributed.worker.rest.WorkerStateEnum.* + +@Path('/worker') +@Singleton +class WorkerResource implements WorkerAPI, WorkerStateContext { + + val Map states + var WorkerState state + + @Inject + new(Worker delegateWorker) { + states = #{ + IDLE -> new IdleWorker(this, delegateWorker), + BUSY -> new BusyWorker(this, delegateWorker) + } + setState(IDLE) + } + + override setState(WorkerStateEnum state) { + this.state = states.get(state) + this.state.onEntry + } + + static val logger = LoggerFactory.getLogger(WorkerResource) + + override Logger getLogger() { return logger } + + @GET + @Path('registered') + override isRegistered() { + return state.isRegistered + } + + @GET + @Path('job') + @Produces(MediaType.TEXT_PLAIN) + override synchronized Response getTestJobState(@QueryParam('wait') Boolean wait) { + return state.getTestJobState(wait ?: false) + } + + @POST + @Path('job') + override synchronized executeTestJob(TestJob job) { + return state.executeTestJob(job) + } + + @DELETE + @Path('job') + override synchronized Response cancelTestJob() { + return state.cancelTestJob + } + + override transitionTo(WorkerStateEnum state, ()=>Void action) { + setState(state) + action.apply + } +} + +enum WorkerStateEnum { + + IDLE, + BUSY + +} + +interface WorkerState extends WorkerAPI { + + def void onEntry() + +} + +interface WorkerStateContext { + + def void setState(WorkerStateEnum state) + + def void transitionTo(WorkerStateEnum state, ()=>Void action) + + def Logger getLogger() + +} + +abstract class BaseWorkerState implements WorkerState { + + override isRegistered() { + return Response.ok(true).build + } + +} + +@FinalFieldsConstructor +class IdleWorker extends BaseWorkerState { + + val extension WorkerStateContext + val Worker delegate + + override onEntry() { + logger.info('''worker has entered idle state''') + } + + override Response executeTestJob(TestJob job) { + state = BUSY + delegate.startJob(job) + return Response.ok.build + } + + override cancelTestJob() { + return Response.status(NOT_FOUND).entity('worker is idle').build + } + + override getTestJobState(Boolean wait) { + return Response.ok(delegate.checkStatus.name).build + } +} + +@FinalFieldsConstructor +class BusyWorker extends BaseWorkerState { + + extension val WorkerStateContext context + val Worker delegate + + override onEntry() { + logger.info('''worker has entered busy state''') + } + + override executeTestJob(TestJob job) { + return Response.status(CONFLICT).entity('worker is busy').build + } + + override cancelTestJob() { + delegate.kill + state = IDLE + return Response.ok.build + } + + override getTestJobState(Boolean wait) { + val status = if (wait) { + delegate.waitForStatus + } else { + delegate.checkStatus + } + + if (status !== TestStatus.RUNNING) { + state = IDLE + } + + return Response.ok(status.name).build + } + +} \ No newline at end of file diff --git a/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/LocalSingleWorkerModule.xtend b/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/LocalSingleWorkerModule.xtend index 6df223c..7176556 100644 --- a/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/LocalSingleWorkerModule.xtend +++ b/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/LocalSingleWorkerModule.xtend @@ -1,19 +1,19 @@ package org.testeditor.web.backend.testexecution.dropwizard import com.google.inject.AbstractModule +import org.testeditor.web.backend.testexecution.distributed.common.TestExecutionManager import org.testeditor.web.backend.testexecution.distributed.common.Worker import org.testeditor.web.backend.testexecution.distributed.common.WritableStatusAwareTestJobStore -import org.testeditor.web.backend.testexecution.distributed.manager.LocalSingleWorkerExecutionManager +import org.testeditor.web.backend.testexecution.distributed.manager.DefaultExecutionManager import org.testeditor.web.backend.testexecution.distributed.manager.LocalSingleWorkerJobStore import org.testeditor.web.backend.testexecution.distributed.manager.LocalSingleWorkerManager -import org.testeditor.web.backend.testexecution.distributed.manager.TestExecutionManager import org.testeditor.web.backend.testexecution.distributed.manager.WorkerProvider import org.testeditor.web.backend.testexecution.distributed.worker.LocalSingleWorker class LocalSingleWorkerModule extends AbstractModule { override protected configure() { binder => [ - bind(TestExecutionManager).to(LocalSingleWorkerExecutionManager) + bind(TestExecutionManager).to(DefaultExecutionManager) bind(WorkerProvider).to(LocalSingleWorkerManager) bind(Worker).to(LocalSingleWorker) bind(WritableStatusAwareTestJobStore).to(LocalSingleWorkerJobStore) diff --git a/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/RestManagerModule.xtend b/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/RestManagerModule.xtend new file mode 100644 index 0000000..88a31f7 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/RestManagerModule.xtend @@ -0,0 +1,19 @@ +package org.testeditor.web.backend.testexecution.dropwizard + +import com.google.inject.AbstractModule +import org.testeditor.web.backend.testexecution.distributed.common.TestExecutionManager +import org.testeditor.web.backend.testexecution.distributed.common.WritableStatusAwareTestJobStore +import org.testeditor.web.backend.testexecution.distributed.manager.DefaultExecutionManager +import org.testeditor.web.backend.testexecution.distributed.manager.LocalSingleWorkerJobStore +import org.testeditor.web.backend.testexecution.distributed.manager.WorkerProvider +import org.testeditor.web.backend.testexecution.distributed.manager.rest.RestWorkerManager + +class RestManagerModule extends AbstractModule { + override protected configure() { + binder => [ + bind(TestExecutionManager).to(DefaultExecutionManager) + bind(WorkerProvider).to(RestWorkerManager) + bind(WritableStatusAwareTestJobStore).to(LocalSingleWorkerJobStore) + ] + } +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/RestWorkerModule.xtend b/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/RestWorkerModule.xtend new file mode 100644 index 0000000..d8edbb8 --- /dev/null +++ b/src/main/java/org/testeditor/web/backend/testexecution/dropwizard/RestWorkerModule.xtend @@ -0,0 +1,13 @@ +package org.testeditor.web.backend.testexecution.dropwizard + +import com.google.inject.AbstractModule +import org.testeditor.web.backend.testexecution.distributed.common.Worker +import org.testeditor.web.backend.testexecution.distributed.worker.LocalSingleWorker + +class RestWorkerModule extends AbstractModule { + override protected configure() { + binder => [ + bind(Worker).to(LocalSingleWorker) + ] + } +} diff --git a/src/main/java/org/testeditor/web/backend/testexecution/webapi/TestSuiteResource.xtend b/src/main/java/org/testeditor/web/backend/testexecution/webapi/TestSuiteResource.xtend index 9becfbc..5d6c9ce 100644 --- a/src/main/java/org/testeditor/web/backend/testexecution/webapi/TestSuiteResource.xtend +++ b/src/main/java/org/testeditor/web/backend/testexecution/webapi/TestSuiteResource.xtend @@ -27,7 +27,7 @@ import org.testeditor.web.backend.testexecution.common.LogLevel import org.testeditor.web.backend.testexecution.common.TestExecutionKey import org.testeditor.web.backend.testexecution.common.TestStatus import org.testeditor.web.backend.testexecution.common.TestSuiteStatusInfo -import org.testeditor.web.backend.testexecution.distributed.manager.TestExecutionManager +import org.testeditor.web.backend.testexecution.distributed.common.TestExecutionManager import org.testeditor.web.backend.testexecution.loglines.LogFinder import org.testeditor.web.backend.testexecution.screenshots.ScreenshotFinder diff --git a/src/main/resources/META-INF/deptective.json b/src/main/resources/META-INF/deptective.json index aaeb633..c7162b3 100644 --- a/src/main/resources/META-INF/deptective.json +++ b/src/main/resources/META-INF/deptective.json @@ -45,32 +45,37 @@ "org.testeditor.web.backend.testexecution.distributed.common" ], "reads": [ + "Auth0", "Commons", "Jackson", + "JAX-RS", + "Jersey Client", "Network" ] }, { "name": "TestExecutionManager", "contains": [ - "org.testeditor.web.backend.testexecution.distributed.manager" + "org.testeditor.web.backend.testexecution.distributed.manager*" ], "reads": [ "CommonAPI", "Commons", + "JAX-RS", "TestArtifactManagement" ] }, { "name": "TestExecutionWorker", "contains": [ - "org.testeditor.web.backend.testexecution.distributed.worker" + "org.testeditor.web.backend.testexecution.distributed.worker*" ], "reads": [ "CommonAPI", "TestProcessExecution", "Commons", "IO", + "JAX-RS", "Network" ] }, @@ -162,11 +167,13 @@ { "name": "JAX-RS", "contains": [ - "javax.ws.rs", - "javax.ws.rs.container", - "javax.ws.rs.core", - "javax.ws.rs.ext", - "javax.ws.rs.container.ext" + "javax.ws.rs*" + ] + }, + { + "name": "Jersey Client", + "contains": [ + "org.glassfish.jersey.client*" ] }, { @@ -189,6 +196,12 @@ "org.apache.commons.text" ] }, + { + "name": "Auth0", + "contains": [ + "com.auth0*" + ] + }, { "name": "IO", "contains": [