diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cf60b9a9..fb80702b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,51 @@ All notable changes to this project will be documented in this file. +## [[8.3.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.3.0) 2024-01-11 + +### New Features + +- Create `iexec-core-library` sub-project to split shared code/apis from specific scheduler application code. (#623) +- Move first DTO classes to `iexec-core-library` subproject. (#626) +- Move `PlatformMetric` to `iexec-core-library` subproject, modify it to become immutable. (#628 #629) +- Add prometheus endpoint with custom metrics. (#632) +- Expose version through prometheus endpoint. (#637, #639) +- Stop fetching completed tasks count from DB. (#638) +- Expose current task statuses count to Prometheus and `/metrics` endpoint. (#640, #654) +- Add `tasks` endpoints to `iexec-core-library`. (#645) + +### Quality + +- Add and use a non-root user in the dockerfile. (#627) +- Replace single thread executor with synchronized keyword. (#633) +- Move contribution status checks from `iexec-commons-poco`. (#636) +- Use `BlockchainAdapterService` from `iexec-blockchain-adapter-api-library`. (#641) +- `ResultRepositoryConfiguration` and `WorkerConfiguration` classes are now immutable with `@Value` lombok annotation. (#650) + +### Bug Fixes + +- Fix web security depreciation warning. (#624) +- Move `TaskModel` and `ReplicateModel` instances creation methods to `Task` and `Replicate` classes. (#625) +- Expose `TaskLogsModel` on `TaskController` instead of `TaskLogs`. (#631) +- Remove duplicated MongoDB read on `ReplicatesList` during replicate status update. (#647) +- Use less MongoDB calls when updating a task to a final status. (#649) +- Save contribution and result updload replicate data when `CONTRIBUTE_AND_FINALIZE_DONE`. (#651) +- Fix potential `NullPointerException` during first worker replicate request. (#652) +- Fix missed replicate status update detectors to avoid false positives by mixing `CONTRIBUTE-REVEAL-FINALIZE` and `CONTRIBUTE_AND_FINALIZE` workflows. (#653) + +### Dependency Upgrades + +- Upgrade to `eclipse-temurin:11.0.21_9-jre-focal`. (#635) +- Upgrade to Spring Boot 2.7.17. (#634) +- Upgrade to Spring Dependency Management Plugin 1.1.4. (#634) +- Upgrade to Spring Doc Openapi 1.7.0. (#637) +- Upgrade to `jenkins-library` 2.7.4. (#630) +- Upgrade to `iexec-commons-poco` 3.2.0. (#648) +- Upgrade to `iexec-common` 8.3.1. (#648) +- Upgrade to `iexec-blockchain-adapter-api-library` 8.3.0. (#655) +- Upgrade to `iexec-result-proxy-library` 8.3.0. (#655) +- Upgrade to `iexec-sms-library` 8.4.0. (#655) + ## [[8.2.3]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.2.3) 2023-12-14 ### Bug Fixes @@ -23,23 +68,30 @@ All notable changes to this project will be documented in this file. ## [[8.2.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.2.0) 2023-09-29 ### New Features + - Add blockchain connection health indicator. (#601) - Block some connections and messages when blockchain connection is down. (#604) - Block deal watching mechanisms when communication with the blockchain node is lost. (#606) - Use `isEligibleToContributeAndFinalize` method from `TaskDescription`. (#619) + ### Bug Fixes + - Clean call to `iexecHubService#getTaskDescriptionFromChain` in test. (#597) - Reject deal if TEE tag but trust not in {0,1}. (#598) - Fix and harmonize `Dockerfile entrypoint` in all Spring Boot applications. (#614) - Use `mongo:4.4` in tests with `MongoDBContainer`. Replace `getContainerIpAddress` with `getHost`. (#616) + ### Quality + - Remove `nexus.intra.iex.ec` repository. (#605) - Remove `Graylog` support. Fetch logs with a sidecar to push them to your log infrastructure. (#607) - Events are now immutable with `@Value` lombok annotation. (#608) - Fix several code smells. (#609) - Upgrade to Gradle 8.2.1 with up-to-date plugins. (#612) - Remove `VersionService#isSnapshot`. (#618) + ### Dependency Upgrades + - Remove `logstash-gelf` dependency. (#607) - Upgrade to `eclipse-temurin` 11.0.20. (#610) - Upgrade to Spring Boot 2.7.14. (#611) @@ -55,13 +107,17 @@ All notable changes to this project will be documented in this file. ## [[8.1.2]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.1.2) 2023-06-29 ## Bug fixes + - Prevent race conditions in `WorkerService`. (#602) + ### Dependency Upgrades + - Upgrade to `iexec-commons-poco` 3.0.5. (#602) ## [[8.1.1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.1.1) 2023-06-23 ### Dependency Upgrades + - Upgrade to `iexec-common` 8.2.1. (#599) - Upgrade to `iexec-commons-poco` 3.0.4. (#599) - Upgrade to `iexec-blockchain-adapter-api-library` 8.1.1. (#599) @@ -71,16 +127,21 @@ All notable changes to this project will be documented in this file. ## [[8.1.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.1.0) 2023-06-09 ### New Features + - Add ContributeAndFinalize to `ReplicateWorkflow`. (#574) - Add check for ContributeAndFinalize in `ReplicatesService`. (#576 #582) - Add `running2Finalized2Completed` in `TaskUpdateManager`. (#577 #578) - Disable `contributeAndFinalize` with CallBack. (#579 #581) - Add purge cached task descriptions ability. (#587) - Add detectors for `ContributeAndFinalize` flow. (#590 #593) + ### Bug Fixes + - Prevent race condition on replicate update. (#568) - Use builders in test classes. (#589) + ### Quality + - Remove unused methods in `IexecHubService`. (#572) - Clean unused Replicate methods and update tests. (#573) - Clean unused `ReplicateStatus#RESULT_UPLOAD_REQUEST_FAILED`. (#575) @@ -89,7 +150,9 @@ All notable changes to this project will be documented in this file. - Rearrange checks order to avoid call to database. (#585) - Move methods to get event blocks from `iexec-commons-poco`. (#588) - Rename detectors' methods and fields to match Ongoing/Done standard. (#591) + ### Dependency Upgrades + - Upgrade to `iexec-common` 8.2.0. (#571 #575 #586 #594) - Add new `iexec-commons-poco` 3.0.2 dependency. (#571 #574 #586 #587 #588 #592 #594) - Upgrade to `iexec-blockchain-adapter-api-library` 8.1.0. (#594) @@ -99,6 +162,7 @@ All notable changes to this project will be documented in this file. ## [[8.0.1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.0.1) 2023-03-20 ### Bug Fixes + - Remove explicit version on `micrometer-registry-prometheus` dependency. (#563) - Send a `TaskNotificationType` to worker with a 2XX HTTP status code. (#564) - Remove `com.iexec.core.dataset` package. (#565) @@ -107,24 +171,31 @@ All notable changes to this project will be documented in this file. ## [[8.0.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v8.0.0) 2023-03-08 ### New Features + * Support Gramine framework for TEE tasks. * Retrieve location of SMS services through an _iExec Platform Registry_. * Improve authentication on scheduler. - * burn challenge after login. - * handle JWT expiration through the expiration claim. - * cache JWT until expiration. - * better claims usage. + * burn challenge after login. + * handle JWT expiration through the expiration claim. + * cache JWT until expiration. + * better claims usage. * Show application version on banner. + ### Bug Fixes + * Always return a `TaskNotificationType` on replicate status update when it has been authorized. * Handle task added twice. + ### Quality + * Improve code quality and tests. * Removed unused variables in configuration. * Use existing `toString()` method to serialize and hash scheduler public configuration. * Use recommended annotation in `MetricController`. * Remove `spring-cloud-starter-openfeign` dependency. + ### Dependency Upgrades + * Replace the deprecated `openjdk` Docker base image with `eclipse-temurin` and upgrade to Java 11.0.18 patch. * Upgrade to Spring Boot 2.6.14. * Upgrade to Gradle 7.6. @@ -148,12 +219,15 @@ All notable changes to this project will be documented in this file. ## [[7.2.1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v7.2.1) 2022-12-13 -* Replace `sessionId` implementation with a hash of the public configuration. From a consumer point of view, a constant hash received from the `POST /ping` response indicates that the scheduler configuration has not changed. With such constant hash, either the scheduler has restarted or not, the consumer does not need to reboot. +* Replace `sessionId` implementation with a hash of the public configuration. From a consumer point of view, a constant + hash received from the `POST /ping` response indicates that the scheduler configuration has not changed. With such + constant hash, either the scheduler has restarted or not, the consumer does not need to reboot. ## [[7.2.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v7.2.0) 2022-12-09 * Increments of jenkins-library up to version 2.2.3. Enable SonarCloud analyses on branches and Pull Requests. -* Update `iexec-common` version to [6.1.0](https://github.com/iExecBlockchainComputing/iexec-common/releases/tag/v6.1.0). +* Update `iexec-common` version + to [6.1.0](https://github.com/iExecBlockchainComputing/iexec-common/releases/tag/v6.1.0). ## [[7.1.3]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v7.1.3) 2022-12-07 @@ -187,6 +261,7 @@ All notable changes to this project will be documented in this file. Highly improved throughput of the iExec protocol. What has changed since v6.0.0? + * Fix task status deadlock. Chances to reach RUNNING task status for given states of replicates are now increased. * Fix race condition on replicate attribution. * Upgrade Jacoco/Sonarqube reporting and plugins. @@ -205,10 +280,12 @@ What has changed since v6.0.0? * Fix Recovering for the Retryable updateReplicateStatus(..) method. * Add checks before locally upgrading to the INITIALIZED status * Remove 2-blocks waiting time before supplying a new replicate -* Fix OptimisticLockingFailureException happening when 2 detectors detect the same change at the same time, leading to race updates on a same task +* Fix OptimisticLockingFailureException happening when 2 detectors detect the same change at the same time, leading to + race updates on a same task * Reuse socket when sending multiple requests to a blockchain node. * Replay fromBlockNumber now lives in a dedicated configuration: - A configuration document did store two different states. Updates on different states at the same time might lead to race conditions when saving to database. Now each state has its own document to avoid race conditions when saving. + A configuration document did store two different states. Updates on different states at the same time might lead to + race conditions when saving to database. Now each state has its own document to avoid race conditions when saving. * Fix TaskRepositoy.findByChainDealIdAndTaskIndex() to return unique result. ## [[6.4.2]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/6.4.2) 2021-12-14 @@ -252,16 +329,20 @@ What has changed since v6.0.0? ## [[6.1.4]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/6.1.4) 2021-10-14 -* Fixed OptimisticLockingFailureException happening when 2 detectors detect the same change at the same time, leading to race updates on a same task. +* Fixed OptimisticLockingFailureException happening when 2 detectors detect the same change at the same time, leading to + race updates on a same task. ## [[6.1.3]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/6.1.3) 2021-10-05 -* Bump iexec-common dependency (iexec-common@5.5.1) featuring socket reuse when sending multiple requests to a blockchain node. +* Bump iexec-common dependency (iexec-common@5.5.1) featuring socket reuse when sending multiple requests to a + blockchain node. ## [[6.1.2]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/6.1.2) 2021-10-01 Bugfix - Replay fromBlockNumber now lives in a dedicated configuration: -* A configuration document did store two different states. Updates on different states at the same time might lead to race conditions when saving to database. Now each state has its own document to avoid race conditions when saving. + +* A configuration document did store two different states. Updates on different states at the same time might lead to + race conditions when saving to database. Now each state has its own document to avoid race conditions when saving. ## [[6.0.1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/6.0.1) 2021-09-28 @@ -270,12 +351,14 @@ Bugfix - Replay fromBlockNumber now lives in a dedicated configuration: ## [[6.0.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/v6.0.0) 2021-07-29 What's new? + * Added Prometheus actuators * Moved TEE workflow configuration to dedicated service ## [[5.1.1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/5.1.1) 2021-04-12 What is patched? + * Updated management port and actuator endpoints. ## [[5.1.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/5.1.0) 2021-03-26 @@ -283,6 +366,7 @@ What is patched? What's new? Fix WebSockets problem: + * Enhance different task stages detection. When detecting unnotified contribute/reveal, we use the initialization block instead of the current block to lookup for the contribute/reveal metadata. * Use a dedicated TaskScheduler for STOMP WebSocket heartbeats. @@ -292,6 +376,7 @@ Fix WebSockets problem: * feature/task-replay. Also: + * Use the deal block as a landmark for a task. * Keep the computing task list consistent. * Worker should be instructed to contribute whenever it is possible (e.g. app/data download failure). @@ -301,25 +386,30 @@ Also: ## [[5.0.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/5.0.0) 2020-07-22 What's new? + * Dapp developers can browse worker computing logs over iexec-core API. * Task result link is standardized and generic. It supports Ethereum, IPFS & Dropbox "storage" providers. -* Result storage feature is moved to new [iExec Result Proxy](https://github.com/iExecBlockchainComputing/iexec-result-proxy). +* Result storage feature is moved to + new [iExec Result Proxy](https://github.com/iExecBlockchainComputing/iexec-result-proxy). * Full compatibility with new [iExec Secret Management Service](https://github.com/iExecBlockchainComputing/iexec-sms). * Compatibility with latest PoCo 5.1.0 smart contracts. ## [[4.0.1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/4.0.1) 2020-02-25 What's fixed? + * More resistance to unsync Ethereum nodes. ## [[4.0.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/4.0.0) 2019-12-13 What's new? + * Native-token sidechain compatibility. * GPU workers support. * Log aggregation. What's fixed? + * Database indexes. * JWT/challenge validity duration. * Worker freed after contribution timeout. @@ -327,12 +417,15 @@ What's fixed? ## [[3.2.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/3.2.0) 2019-09-17 What is new? + * Bag Of Tasks (BoT): Bag Of Tasks Deals can now be processed by the middleware. -* Use of iexec input files: Sometimes external resources are needed to run a computation without using a dataset, that is what input files are for. +* Use of iexec input files: Sometimes external resources are needed to run a computation without using a dataset, that + is what input files are for. * Whitelisting: Now the core can define a whitelist of workers that are allowed to connect to the pool. * Https: Workers can connect to the core using https. What is patched? + * The project has been updated to java 11 and web3j 4.3.0. * Internal refactoring to handle replicates update better. * Limit workers that ask for replicates too often. @@ -341,10 +434,12 @@ What is patched? ## [[3.1.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/3.1.0) 2019-07-11 What's new? + * Full end-to-end encryption inside a Trusted Execution Environment (TEE) powered by Intel(R) SGX. * Implemented the Proof-of-Contribution (PoCo) Sarmenta's formula for a greater task dispatching. What's patched? + * Reopen task worflow is back. * A single FAILED replicate status when a completion is impossible. * WORKER_LOST is not set for replicates which are already FAILED. @@ -355,18 +450,22 @@ What's patched? This release is a patch to fix an issue following the release of version 3.0.0. When asking for a replicate, a worker sends the latest available block number from the node it is is connected to. -If that node is a little bit behind the node the core is connected to, the worker will have a disadvantage compare to workers connected to more up-to-date nodes. +If that node is a little bit behind the node the core is connected to, the worker will have a disadvantage compare to +workers connected to more up-to-date nodes. To avoid this disadvantage, now the core waits for a few blocks before sending replicates to workers. ## [[3.0.0]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/3.0.0) 2019-05-15 This release contains the core set of changes in the 3.0.0-alpha-X releases and some other features/fixes. In summary this version introduces: + * A new architecture: the core has been completely re-architectured from the version 2. * Latest PoCo use. * Better management of transaction with the ethereum blockchain. -* Failover mechanisms: in case some workers are lost or restarted when working on a specific task, internal mechanisms will redistribute the task or use as much as possible the work performed by the lost / restarted workers. -* iExec End-To-End Encryption with Secret Management Service (SMS): from this version, inputs and outputs of the job can be fully encrypted using the Secret Management Service. +* Failover mechanisms: in case some workers are lost or restarted when working on a specific task, internal mechanisms + will redistribute the task or use as much as possible the work performed by the lost / restarted workers. +* iExec End-To-End Encryption with Secret Management Service (SMS): from this version, inputs and outputs of the job can + be fully encrypted using the Secret Management Service. * Decentralized oracle: If the result is needed by a smart contract, it is available directly on the blockchain. * IPFS: data can be retrieved from IPFS and public results can be published on IPFS. @@ -386,8 +485,8 @@ For further information on this version, please read our documentation. * Some general refactoring on the detectors. * Bug fix regarding the new PoCo version. * The core now also checks the errors on the blockchain sent by the workers. -Enhancement regarding the result repo. -Updated PoCo chain version 3.0.21. + Enhancement regarding the result repo. + Updated PoCo chain version 3.0.21. ## [[3.0.0-alpha1]](https://github.com/iExecBlockchainComputing/iexec-core/releases/tag/3.0.0-alpha1) 2019-01-25 diff --git a/Dockerfile b/Dockerfile index d152e521c..9a1fc438b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,19 @@ -FROM eclipse-temurin:11.0.20_8-jre-focal +FROM eclipse-temurin:11.0.21_9-jre-focal ARG jar RUN test -n "$jar" RUN apt-get update \ - && apt-get install -y curl \ + && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/* +RUN groupadd --system appuser \ + && useradd -g appuser -s /sbin/nologin -c "Docker image user" appuser + +WORKDIR /app COPY $jar iexec-core.jar +RUN chown -R appuser:appuser /app -ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/iexec-core.jar"] +USER appuser +ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "iexec-core.jar"] diff --git a/Jenkinsfile b/Jenkinsfile index 769af645d..480233793 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,8 +1,4 @@ -@Library('global-jenkins-library@2.7.3') _ +@Library('global-jenkins-library@2.7.4') _ buildJavaProject( - buildInfo: getBuildInfo(), - integrationTestsEnvVars: [], shouldPublishJars: true, - shouldPublishDockerImages: true, - dockerfileDir: '.', - buildContext: '.') + shouldPublishDockerImages: true) diff --git a/build.gradle b/build.gradle index 3dd32c36f..9072640bc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,13 @@ plugins { id 'java' id 'io.freefair.lombok' version '8.2.2' - id 'org.springframework.boot' version '2.7.14' - id 'io.spring.dependency-management' version '1.1.3' + id 'org.springframework.boot' version '2.7.17' + id 'io.spring.dependency-management' version '1.1.4' id 'jacoco' id 'org.sonarqube' version '4.2.1.3168' id 'maven-publish' } -group = 'com.iexec.core' - ext { springCloudVersion = '2021.0.8' jjwtVersion = '0.11.5' @@ -21,21 +19,30 @@ if (!project.hasProperty('gitBranch')) { ext.gitBranch = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim() } -if (gitBranch != 'main' && gitBranch != 'master' && ! (gitBranch ==~ '(release|hotfix|support)/.*')) { - version += '-NEXT-SNAPSHOT' -} - -repositories { - mavenLocal() - mavenCentral() - maven { - url "https://docker-regis-adm.iex.ec/repository/maven-public/" - credentials { - username nexusUser - password nexusPassword +allprojects { + group = 'com.iexec.core' + if (gitBranch != 'main' && gitBranch != 'master' && !(gitBranch ==~ '(release|hotfix|support)/.*')) { + version += '-NEXT-SNAPSHOT' + } + repositories { + mavenLocal() + mavenCentral() + maven { + url "https://docker-regis-adm.iex.ec/repository/maven-public/" + credentials { + username nexusUser + password nexusPassword + } + } + maven { url "https://jitpack.io" } + } + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) } + sourceCompatibility = "11" + targetCompatibility = "11" } - maven { url "https://jitpack.io" } } dependencyManagement { @@ -51,6 +58,7 @@ dependencies { implementation "com.iexec.blockchain:iexec-blockchain-adapter-api-library:$iexecBlockchainAdapterVersion" implementation "com.iexec.result-proxy:iexec-result-proxy-library:$iexecResultVersion" implementation "com.iexec.sms:iexec-sms-library:$iexecSmsVersion" + implementation project(':iexec-core-library') // spring implementation "org.springframework.boot:spring-boot-starter" @@ -66,7 +74,7 @@ dependencies { implementation "org.springframework.retry:spring-retry" // Spring Doc - implementation 'org.springdoc:springdoc-openapi-ui:1.6.3' + implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' // apache commons.lang3 implementation 'org.apache.commons:commons-lang3' @@ -104,12 +112,6 @@ dependencies { testImplementation "org.testcontainers:mongodb:$testContainersVersion" } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } -} - jar { enabled = true archiveClassifier.set('library') @@ -122,7 +124,7 @@ springBoot { tasks.named("bootJar") { manifest { attributes("Implementation-Title": "iExec Core Scheduler", - "Implementation-Version": project.version) + "Implementation-Version": project.version) } } @@ -141,7 +143,7 @@ jacocoTestReport { xml.required = true } } -tasks.test.finalizedBy tasks.jacocoTestReport +tasks.test.finalizedBy tasks.jacocoTestReport tasks.sonarqube.dependsOn tasks.jacocoTestReport publishing { @@ -162,14 +164,13 @@ publishing { } } -ext.jarPathForOCI = relativePath(tasks.bootJar.outputs.files.singleFile) +ext.jarPathForOCI = relativePath(tasks.bootJar.outputs.files.singleFile) ext.gitShortCommit = 'git rev-parse --short=8 HEAD'.execute().text.trim() -ext.ociImageName = 'local/' + ['bash', '-c', 'basename $(git config --get remote.origin.url) .git'].execute().text.trim() +ext.ociImageName = 'local/' + ['bash', '-c', 'basename $(git config --get remote.origin.url) .git'].execute().text.trim() tasks.register('buildImage', Exec) { group 'Build' description 'Builds an OCI image from a Dockerfile.' dependsOn bootJar - commandLine("sh", "-c", "docker build --build-arg jar=$jarPathForOCI -t $ociImageName:$gitShortCommit . &&" + - "docker tag $ociImageName:$gitShortCommit $ociImageName:dev") + commandLine 'docker', 'build', '--build-arg', 'jar=' + jarPathForOCI, '-t', ociImageName + ':dev', '.' } diff --git a/gradle.properties b/gradle.properties index d3c77ac17..e989fd3c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ -version=8.2.3 -iexecCommonVersion=8.3.0 -iexecCommonsPocoVersion=3.1.0 -iexecBlockchainAdapterVersion=8.2.0 -iexecResultVersion=8.2.0 -iexecSmsVersion=8.3.0 +version=8.3.0 +iexecCommonVersion=8.3.1 +iexecCommonsPocoVersion=3.2.0 +iexecBlockchainAdapterVersion=8.3.0 +iexecResultVersion=8.3.0 +iexecSmsVersion=8.4.0 nexusUser nexusPassword diff --git a/iexec-core-library/build.gradle b/iexec-core-library/build.gradle new file mode 100644 index 000000000..385b40bdf --- /dev/null +++ b/iexec-core-library/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'java-library' + id 'io.freefair.lombok' + id 'jacoco' + id 'maven-publish' +} + +dependencies { + implementation "com.iexec.commons:iexec-commons-poco:$iexecCommonsPocoVersion" + implementation "com.iexec.common:iexec-common:$iexecCommonVersion" + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +java { + sourceCompatibility = "11" + targetCompatibility = "11" + withJavadocJar() + withSourcesJar() +} + + +tasks.withType(Test).configureEach { + finalizedBy tasks.jacocoTestReport + useJUnitPlatform() +} + +// sonarqube code coverage requires jacoco XML report +jacocoTestReport { + reports { + xml.required = true + } +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + } + } + repositories { + maven { + credentials { + username nexusUser + password nexusPassword + } + url = project.hasProperty("nexusUrl")? project.nexusUrl: '' + } + } +} diff --git a/iexec-core-library/src/main/java/com/iexec/core/api/SchedulerClient.java b/iexec-core-library/src/main/java/com/iexec/core/api/SchedulerClient.java new file mode 100644 index 000000000..81e7294cc --- /dev/null +++ b/iexec-core-library/src/main/java/com/iexec/core/api/SchedulerClient.java @@ -0,0 +1,107 @@ +/* + * Copyright 2023-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.api; + +import com.iexec.common.config.PublicConfiguration; +import com.iexec.common.config.WorkerModel; +import com.iexec.common.replicate.ComputeLogs; +import com.iexec.common.replicate.ReplicateStatusUpdate; +import com.iexec.common.replicate.ReplicateTaskSummary; +import com.iexec.commons.poco.eip712.entity.EIP712Challenge; +import com.iexec.commons.poco.notification.TaskNotification; +import com.iexec.commons.poco.notification.TaskNotificationType; +import com.iexec.core.logs.TaskLogsModel; +import com.iexec.core.metric.PlatformMetric; +import com.iexec.core.task.TaskModel; +import feign.Headers; +import feign.Param; +import feign.RequestLine; + +import java.security.Signature; +import java.util.List; + +public interface SchedulerClient { + @RequestLine("GET /metrics") + PlatformMetric getMetrics(); + + @RequestLine("GET /version") + String getCoreVersion(); + + // region /workers + @RequestLine("GET /workers/challenge?walletAddress={walletAddress}") + String getChallenge(@Param("walletAddress") String walletAddress); + + @RequestLine("POST /workers/login?walletAddress={walletAddress}") + String login(@Param("walletAddress") String walletAddress, Signature signature); + + @RequestLine("POST /workers/ping") + @Headers("Authorization: {authorization}") + String ping(@Param("authorization") String authorization); + + @RequestLine("POST /workers/register") + @Headers("Authorization: {authorization}") + void registerWorker(@Param("authorization") String authorization, WorkerModel model); + + @RequestLine("GET /workers/config") + PublicConfiguration getPublicConfiguration(); + + @RequestLine("GET /workers/computing") + @Headers("Authorization: {authorization}") + List getComputingTasks(@Param("authorization") String authorization); + // endregion + + // region /replicates + @RequestLine("GET /replicates/available?blockNumber={blockNumber}") + @Headers("Authorization: {authorization}") + ReplicateTaskSummary getAvailableReplicateTaskSummary( + @Param("authorization") String authorization, @Param("blockNumber") long blockNumber); + + @RequestLine("GET /replicates/interrupted?blockNumber={blockNumber}") + @Headers("Authorization: {authorization}") + List getMissedTaskNotifications( + @Param("authorization") String authorization, @Param("blockNumber") long blockNumber); + + @RequestLine("POST /replicates/{chainTaskId}/updateStatus") + @Headers("Authorization: {authorization}") + TaskNotificationType updateReplicateStatus( + @Param("authorization") String authorization, + @Param("chainTaskId") String chainTaskId, + ReplicateStatusUpdate replicateStatusUpdate + ); + // endregion + + // region /tasks + @RequestLine("GET /tasks/{chainTaskId}") + TaskModel getTask(@Param("chainTaskId") String chainTaskId); + + @RequestLine("GET /tasks/logs/challenge?address={address}") + EIP712Challenge getTaskLogsChallenge(@Param("address") String address); + + @Headers("Authorization: {authorization}") + @RequestLine("GET /tasks/{chainTaskId}/logs") + TaskLogsModel getTaskLogs( + @Param("chainTaskId") String chainTaskId, + @Param("authorization") String authorization); + + @Headers("Authorization: {authorization}") + @RequestLine("GET /tasks/{chainTaskId}/replicates/{walletAddress}/logs") + ComputeLogs getComputeLogs( + @Param("chainTaskId") String chainTaskId, + @Param("walletAddress") String walletAddress, + @Param("authorization") String authorization); + // endregion +} diff --git a/src/main/java/com/iexec/core/version/VersionService.java b/iexec-core-library/src/main/java/com/iexec/core/api/SchedulerClientBuilder.java similarity index 55% rename from src/main/java/com/iexec/core/version/VersionService.java rename to iexec-core-library/src/main/java/com/iexec/core/api/SchedulerClientBuilder.java index ba3cc9f1d..2362df281 100644 --- a/src/main/java/com/iexec/core/version/VersionService.java +++ b/iexec-core-library/src/main/java/com/iexec/core/api/SchedulerClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2023-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,22 +14,17 @@ * limitations under the License. */ -package com.iexec.core.version; +package com.iexec.core.api; -import org.springframework.boot.info.BuildProperties; -import org.springframework.stereotype.Service; +import com.iexec.common.utils.FeignBuilder; +import feign.Logger; -@Service -public class VersionService { +public class SchedulerClientBuilder { - private final BuildProperties buildProperties; + private SchedulerClientBuilder() {} - VersionService (BuildProperties buildProperties) { - this.buildProperties = buildProperties; + public static SchedulerClient getInstance(Logger.Level logLevel, String url) { + return FeignBuilder.createBuilder(logLevel) + .target(SchedulerClient.class, url); } - - public String getVersion() { - return buildProperties.getVersion(); - } - } diff --git a/src/main/java/com/iexec/core/metric/PlatformMetric.java b/iexec-core-library/src/main/java/com/iexec/core/logs/TaskLogsModel.java similarity index 59% rename from src/main/java/com/iexec/core/metric/PlatformMetric.java rename to iexec-core-library/src/main/java/com/iexec/core/logs/TaskLogsModel.java index 77d9c0c61..87313c1ef 100644 --- a/src/main/java/com/iexec/core/metric/PlatformMetric.java +++ b/iexec-core-library/src/main/java/com/iexec/core/logs/TaskLogsModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2023-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,28 +14,23 @@ * limitations under the License. */ -package com.iexec.core.metric; +package com.iexec.core.logs; +import com.iexec.common.replicate.ComputeLogs; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; -import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; @Data -@AllArgsConstructor @Builder -public class PlatformMetric { - - private int aliveWorkers; - private int aliveTotalCpu; - private int aliveAvailableCpu; - private int aliveTotalGpu; - private int aliveAvailableGpu; - private int completedTasks; - private long dealEventsCount; - private long dealsCount; - private long replayDealsCount; - private BigInteger latestBlockNumberWithDeal; - +@NoArgsConstructor +@AllArgsConstructor +public class TaskLogsModel { + String chainTaskId; + @Builder.Default + List computeLogsList = new ArrayList<>(); } diff --git a/iexec-core-library/src/main/java/com/iexec/core/metric/PlatformMetric.java b/iexec-core-library/src/main/java/com/iexec/core/metric/PlatformMetric.java new file mode 100644 index 000000000..dcc475829 --- /dev/null +++ b/iexec-core-library/src/main/java/com/iexec/core/metric/PlatformMetric.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.metric; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.iexec.core.task.TaskStatus; +import lombok.Builder; +import lombok.Value; + +import java.math.BigInteger; +import java.util.LinkedHashMap; + +@Value +@Builder +@JsonDeserialize(builder = PlatformMetric.PlatformMetricBuilder.class) +public class PlatformMetric { + int aliveWorkers; + int aliveTotalCpu; + int aliveAvailableCpu; + int aliveTotalGpu; + int aliveAvailableGpu; + LinkedHashMap currentTaskStatusesCount; + long dealEventsCount; + long dealsCount; + long replayDealsCount; + BigInteger latestBlockNumberWithDeal; + + @JsonPOJOBuilder(withPrefix = "") + public static class PlatformMetricBuilder { + } +} diff --git a/iexec-core-library/src/main/java/com/iexec/core/replicate/ReplicateModel.java b/iexec-core-library/src/main/java/com/iexec/core/replicate/ReplicateModel.java new file mode 100644 index 000000000..f96a3c2d5 --- /dev/null +++ b/iexec-core-library/src/main/java/com/iexec/core/replicate/ReplicateModel.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.replicate; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.iexec.common.replicate.ReplicateStatus; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ReplicateModel { + private String self; + private String chainTaskId; + private String walletAddress; + private ReplicateStatus currentStatus; + private List statusUpdateList; + private String resultLink; + private String chainCallbackData; + private String contributionHash; + private String appLogs; + private Integer appExitCode; //null means unset + private String teeSessionGenerationError; // null means unset +} diff --git a/src/main/java/com/iexec/core/replicate/ReplicateStatusUpdateModel.java b/iexec-core-library/src/main/java/com/iexec/core/replicate/ReplicateStatusUpdateModel.java similarity index 96% rename from src/main/java/com/iexec/core/replicate/ReplicateStatusUpdateModel.java rename to iexec-core-library/src/main/java/com/iexec/core/replicate/ReplicateStatusUpdateModel.java index 7a1af1236..6d74d1b7a 100644 --- a/src/main/java/com/iexec/core/replicate/ReplicateStatusUpdateModel.java +++ b/iexec-core-library/src/main/java/com/iexec/core/replicate/ReplicateStatusUpdateModel.java @@ -40,7 +40,7 @@ public class ReplicateStatusUpdateModel { public static ReplicateStatusUpdateModel fromEntity(ReplicateStatusUpdate update) { if (update == null) { - return new ReplicateStatusUpdateModel(); + return ReplicateStatusUpdateModel.builder().build(); } final ReplicateStatusDetails details = update.getDetails(); diff --git a/src/main/java/com/iexec/core/task/TaskModel.java b/iexec-core-library/src/main/java/com/iexec/core/task/TaskModel.java similarity index 57% rename from src/main/java/com/iexec/core/task/TaskModel.java rename to iexec-core-library/src/main/java/com/iexec/core/task/TaskModel.java index ea6a08f63..c25b051c4 100644 --- a/src/main/java/com/iexec/core/task/TaskModel.java +++ b/iexec-core-library/src/main/java/com/iexec/core/task/TaskModel.java @@ -29,7 +29,6 @@ @AllArgsConstructor @Builder public class TaskModel { - private String chainTaskId; private List replicates; private long maxExecutionTime; @@ -48,26 +47,4 @@ public class TaskModel { private String resultLink; private String chainCallbackData; private List dateStatusList; - - public static TaskModel fromEntity(Task entity) { - return TaskModel.builder() - .chainTaskId(entity.getChainTaskId()) - .maxExecutionTime(entity.getMaxExecutionTime()) - .tag(entity.getTag()) - .dappType(entity.getDappType()) - .dappName(entity.getDappName()) - .commandLine(entity.getCommandLine()) - .initializationBlockNumber(entity.getInitializationBlockNumber()) - .currentStatus(entity.getCurrentStatus()) - .trust(entity.getTrust()) - .uploadingWorkerWalletAddress(entity.getUploadingWorkerWalletAddress()) - .consensus(entity.getConsensus()) - .contributionDeadline(entity.getContributionDeadline()) - .revealDeadline(entity.getRevealDeadline()) - .finalDeadline(entity.getFinalDeadline()) - .resultLink(entity.getResultLink()) - .chainCallbackData(entity.getChainCallbackData()) - .dateStatusList(entity.getDateStatusList()) - .build(); - } } diff --git a/src/main/java/com/iexec/core/task/TaskStatus.java b/iexec-core-library/src/main/java/com/iexec/core/task/TaskStatus.java similarity index 100% rename from src/main/java/com/iexec/core/task/TaskStatus.java rename to iexec-core-library/src/main/java/com/iexec/core/task/TaskStatus.java diff --git a/src/main/java/com/iexec/core/task/TaskStatusChange.java b/iexec-core-library/src/main/java/com/iexec/core/task/TaskStatusChange.java similarity index 67% rename from src/main/java/com/iexec/core/task/TaskStatusChange.java rename to iexec-core-library/src/main/java/com/iexec/core/task/TaskStatusChange.java index a24225bb7..fea575fde 100644 --- a/src/main/java/com/iexec/core/task/TaskStatusChange.java +++ b/iexec-core-library/src/main/java/com/iexec/core/task/TaskStatusChange.java @@ -17,34 +17,23 @@ package com.iexec.core.task; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.*; +import com.iexec.commons.poco.chain.ChainReceipt; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Date; -import com.iexec.commons.poco.chain.ChainReceipt; - @Data @NoArgsConstructor @Builder @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class TaskStatusChange { - - private Date date; + @Builder.Default + private Date date = new Date(); private TaskStatus status; - private ChainReceipt chainReceipt; - - public TaskStatusChange(TaskStatus status){ - this(status, null); - } - - public TaskStatusChange(TaskStatus status, ChainReceipt chainReceipt){ - this.date = new Date(); - this.status = status; - this.chainReceipt = chainReceipt; - } - - public TaskStatusChange(Date date, TaskStatus status) { - this(date, status, null); - } + @Builder.Default + private ChainReceipt chainReceipt = null; } diff --git a/iexec-core-library/src/test/java/com/iexec/core/api/SchedulerClientTest.java b/iexec-core-library/src/test/java/com/iexec/core/api/SchedulerClientTest.java new file mode 100644 index 000000000..58db33c9c --- /dev/null +++ b/iexec-core-library/src/test/java/com/iexec/core/api/SchedulerClientTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.api; + +import feign.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SchedulerClientTest { + + @Test + void instantiationTest() { + Assertions.assertNotNull(SchedulerClientBuilder.getInstance(Logger.Level.FULL, "localhost")); + } + +} diff --git a/iexec-core-library/src/test/java/com/iexec/core/metric/PlatformMetricTests.java b/iexec-core-library/src/test/java/com/iexec/core/metric/PlatformMetricTests.java new file mode 100644 index 000000000..c36b32608 --- /dev/null +++ b/iexec-core-library/src/test/java/com/iexec/core/metric/PlatformMetricTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.metric; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iexec.core.task.TaskStatus; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.LinkedHashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PlatformMetricTests { + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + void shouldSerializeAndDeserialize() throws JsonProcessingException { + final PlatformMetric platformMetric = PlatformMetric.builder() + .aliveWorkers(4) + .aliveTotalCpu(12) + .aliveAvailableCpu(7) + .aliveTotalGpu(0) + .aliveAvailableGpu(0) + .currentTaskStatusesCount(createCurrentTaskStatusesCount()) + .dealEventsCount(3000) + .dealsCount(1100) + .latestBlockNumberWithDeal(BigInteger.valueOf(1_000_000L)) + .build(); + assertEquals(platformMetric, mapper.readValue(mapper.writeValueAsString(platformMetric), PlatformMetric.class)); + } + + private LinkedHashMap createCurrentTaskStatusesCount() { + final LinkedHashMap expectedCurrentTaskStatusesCount = new LinkedHashMap<>(TaskStatus.values().length); + expectedCurrentTaskStatusesCount.put(TaskStatus.RECEIVED, 1L); + expectedCurrentTaskStatusesCount.put(TaskStatus.INITIALIZING, 2L); + expectedCurrentTaskStatusesCount.put(TaskStatus.INITIALIZED, 3L); + expectedCurrentTaskStatusesCount.put(TaskStatus.INITIALIZE_FAILED, 4L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RUNNING, 5L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RUNNING_FAILED, 6L); + expectedCurrentTaskStatusesCount.put(TaskStatus.CONTRIBUTION_TIMEOUT, 7L); + expectedCurrentTaskStatusesCount.put(TaskStatus.CONSENSUS_REACHED, 8L); + expectedCurrentTaskStatusesCount.put(TaskStatus.REOPENING, 9L); + expectedCurrentTaskStatusesCount.put(TaskStatus.REOPENED, 10L); + expectedCurrentTaskStatusesCount.put(TaskStatus.REOPEN_FAILED, 11L); + expectedCurrentTaskStatusesCount.put(TaskStatus.AT_LEAST_ONE_REVEALED, 12L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RESULT_UPLOADING, 13L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RESULT_UPLOADED, 14L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RESULT_UPLOAD_TIMEOUT, 15L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINALIZING, 16L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINALIZED, 17L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINALIZE_FAILED, 18L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINAL_DEADLINE_REACHED, 19L); + expectedCurrentTaskStatusesCount.put(TaskStatus.COMPLETED, 20L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FAILED, 21L); + + return expectedCurrentTaskStatusesCount; + } +} diff --git a/iexec-core-library/src/test/java/com/iexec/core/replicate/ReplicateStatusUpdateTests.java b/iexec-core-library/src/test/java/com/iexec/core/replicate/ReplicateStatusUpdateTests.java new file mode 100644 index 000000000..2167901df --- /dev/null +++ b/iexec-core-library/src/test/java/com/iexec/core/replicate/ReplicateStatusUpdateTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.replicate; + +import com.iexec.common.replicate.ReplicateStatus; +import com.iexec.common.replicate.ReplicateStatusUpdate; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ReplicateStatusUpdateTests { + @Test + void shouldGenerateModelFromNull() { + assertEquals(ReplicateStatusUpdateModel.builder().build(), ReplicateStatusUpdateModel.fromEntity(null)); + } + + @Test + void shouldGenerateModeFromNonNull() { + final Date now = new Date(); + final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() + .status(ReplicateStatus.COMPLETED) + .date(now) + .build(); + final ReplicateStatusUpdateModel expectedStatusUpdateModel = ReplicateStatusUpdateModel + .builder() + .status(ReplicateStatus.COMPLETED) + .date(now) + .build(); + assertEquals(expectedStatusUpdateModel, ReplicateStatusUpdateModel.fromEntity(statusUpdate)); + } +} diff --git a/iexec-core-library/src/test/java/com/iexec/core/task/TaskStatusTests.java b/iexec-core-library/src/test/java/com/iexec/core/task/TaskStatusTests.java new file mode 100644 index 000000000..e0697fc3b --- /dev/null +++ b/iexec-core-library/src/test/java/com/iexec/core/task/TaskStatusTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2023-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.task; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TaskStatusTests { + + // region contribution phase + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"INITIALIZED", "RUNNING"}) + void isInContributionPhase(TaskStatus taskStatus) { + assertTrue(TaskStatus.isInContributionPhase(taskStatus)); + } + + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"INITIALIZED", "RUNNING"}) + void isNotInContributionPhase(TaskStatus taskStatus) { + assertFalse(TaskStatus.isInContributionPhase(taskStatus)); + } + // endregion + + // region reveal phase + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"CONSENSUS_REACHED", "AT_LEAST_ONE_REVEALED", "RESULT_UPLOADING", "RESULT_UPLOADED"}) + void isInRevealPhase(TaskStatus taskStatus) { + assertTrue(TaskStatus.isInRevealPhase(taskStatus)); + } + + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"CONSENSUS_REACHED", "AT_LEAST_ONE_REVEALED", "RESULT_UPLOADING", "RESULT_UPLOADED"}) + void isNotInRevealPhase(TaskStatus taskStatus) { + assertFalse(TaskStatus.isInRevealPhase(taskStatus)); + } + // endregion + + // region result upload phase + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"RESULT_UPLOADING"}) + void isInResultUploadPhase(TaskStatus taskStatus) { + assertTrue(TaskStatus.isInResultUploadPhase(taskStatus)); + } + + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"RESULT_UPLOADING"}) + void isNotInResultUploadPhase(TaskStatus taskStatus) { + assertFalse(TaskStatus.isInResultUploadPhase(taskStatus)); + } + // endregion + + // region completion phase + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"FINALIZING", "FINALIZED", "COMPLETED"}) + void isInCompletionPhase(TaskStatus taskStatus) { + assertTrue(TaskStatus.isInCompletionPhase(taskStatus)); + } + + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"FINALIZING", "FINALIZED", "COMPLETED"}) + void isNotInCompletionPhase(TaskStatus taskStatus) { + assertFalse(TaskStatus.isInCompletionPhase(taskStatus)); + } + // endregion + + // region + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"COMPLETED", "FAILED"}) + void isFinalStatus(TaskStatus taskStatus) { + assertTrue(TaskStatus.isFinalStatus(taskStatus)); + } + + @ParameterizedTest + @EnumSource(value = TaskStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"COMPLETED", "FAILED"}) + void isNotFinalStatus(TaskStatus taskStatus) { + assertFalse(TaskStatus.isFinalStatus(taskStatus)); + } + // endregion + +} diff --git a/settings.gradle b/settings.gradle index 249e4089d..0fa137f7b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ rootProject.name = 'iexec-core' +include 'iexec-core-library' diff --git a/src/main/java/com/iexec/core/Application.java b/src/main/java/com/iexec/core/Application.java index b5d4f586c..a21c6879d 100644 --- a/src/main/java/com/iexec/core/Application.java +++ b/src/main/java/com/iexec/core/Application.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,11 @@ import io.changock.runner.spring.v5.config.EnableChangock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; - +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @EnableChangock @SpringBootApplication +@ConfigurationPropertiesScan public class Application { public static void main(String[] args) { diff --git a/src/main/java/com/iexec/core/chain/ChainConfig.java b/src/main/java/com/iexec/core/chain/ChainConfig.java index f2350b719..be156f5af 100644 --- a/src/main/java/com/iexec/core/chain/ChainConfig.java +++ b/src/main/java/com/iexec/core/chain/ChainConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,16 +30,16 @@ @NoArgsConstructor public class ChainConfig { - @Value("#{blockchainAdapterService.publicChainConfig.chainId}") - private Integer chainId; + @Value("#{publicChainConfig.chainId}") + private int chainId; - @Value("#{blockchainAdapterService.publicChainConfig.isSidechain()}") + @Value("#{publicChainConfig.isSidechain()}") private boolean isSidechain; - @Value("#{blockchainAdapterService.publicChainConfig.iexecHubContractAddress}") + @Value("#{publicChainConfig.iexecHubContractAddress}") private String hubAddress; - @Value("#{blockchainAdapterService.publicChainConfig.blockTime}") + @Value("#{publicChainConfig.blockTime}") private Duration blockTime; @Value("${chain.privateAddress}") diff --git a/src/main/java/com/iexec/core/chain/DealWatcherService.java b/src/main/java/com/iexec/core/chain/DealWatcherService.java index b843670da..d0f8f6659 100644 --- a/src/main/java/com/iexec/core/chain/DealWatcherService.java +++ b/src/main/java/com/iexec/core/chain/DealWatcherService.java @@ -26,6 +26,8 @@ import com.iexec.core.task.Task; import com.iexec.core.task.TaskService; import com.iexec.core.task.event.TaskCreatedEvent; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; import io.reactivex.disposables.Disposable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -51,6 +53,10 @@ @Service public class DealWatcherService { + public static final String METRIC_DEALS_EVENTS_COUNT = "iexec.core.deals.events"; + public static final String METRIC_DEALS_COUNT = "iexec.core.deals"; + public static final String METRIC_DEALS_REPLAY_COUNT = "iexec.core.deals.replay"; + public static final String METRIC_DEALS_LAST_BLOCK = "iexec.core.deals.last.block"; private static final List CORRECT_TEE_TRUSTS = List.of(BigInteger.ZERO, BigInteger.ONE); private final ChainConfig chainConfig; @@ -72,6 +78,11 @@ public class DealWatcherService { @Getter private long replayDealsCount = 0; + private final Counter dealEventsCounter; + private final Counter dealsCounter; + private final Counter replayDealsCounter; + private final Counter latestBlockNumberWithDealCounter; + public DealWatcherService(ChainConfig chainConfig, IexecHubService iexecHubService, ConfigurationService configurationService, @@ -84,6 +95,11 @@ public DealWatcherService(ChainConfig chainConfig, this.applicationEventPublisher = applicationEventPublisher; this.taskService = taskService; this.web3jService = web3jService; + + this.dealEventsCounter = Metrics.counter(METRIC_DEALS_EVENTS_COUNT); + this.dealsCounter = Metrics.counter(METRIC_DEALS_COUNT); + this.replayDealsCounter = Metrics.counter(METRIC_DEALS_REPLAY_COUNT); + this.latestBlockNumberWithDealCounter = Metrics.counter(METRIC_DEALS_LAST_BLOCK); } /** @@ -111,6 +127,7 @@ public void stop() { /** * Dispose of a {@link Disposable} subscription if it exists and is not already disposed. + * * @param subscription Deal event subscription to dispose of. */ private void disposeSubscription(Disposable subscription) { @@ -122,7 +139,7 @@ private void disposeSubscription(Disposable subscription) { /** * Subscribe to onchain deal events from * a given block to the latest block. - * + * * @param from start block * @return disposable subscription */ @@ -137,14 +154,16 @@ Disposable subscribeToDealEventFromOneBlockToLatest(BigInteger from) { /** * Update last seen block in the database * and run {@link DealEvent} handler. - * + * * @param dealEvent */ private void onDealEvent(DealEvent dealEvent, String watcher) { if ("replay".equals(watcher)) { replayDealsCount++; + replayDealsCounter.increment(); } else { dealsCount++; + dealsCounter.increment(); } String dealId = dealEvent.getChainDealId(); BigInteger dealBlock = dealEvent.getBlockNumber(); @@ -254,9 +273,9 @@ void replayDealEvent() { /** * Subscribe to onchain deal events for * a fixed range of blocks. - * + * * @param from start block - * @param to end block + * @param to end block * @return disposable subscription */ private Disposable subscribeToDealEventInRange(BigInteger from, BigInteger to) { @@ -282,9 +301,12 @@ EthFilter createDealEventFilter(BigInteger from, BigInteger to) { Optional schedulerNoticeToDealEvent(IexecHubContract.SchedulerNoticeEventResponse schedulerNotice) { dealEventsCount++; + dealEventsCounter.increment(); BigInteger noticeBlockNumber = schedulerNotice.log.getBlockNumber(); if (latestBlockNumberWithDeal.compareTo(noticeBlockNumber) < 0) { + double deltaBlocksNumber = noticeBlockNumber.subtract(latestBlockNumberWithDeal).doubleValue(); latestBlockNumberWithDeal = noticeBlockNumber; + latestBlockNumberWithDealCounter.increment(deltaBlocksNumber); } log.info("Received new deal [blockNumber:{}, chainDealId:{}, dealEventsCount:{}]", schedulerNotice.log.getBlockNumber(), BytesUtils.bytesToString(schedulerNotice.dealid), dealEventsCount); diff --git a/src/main/java/com/iexec/core/chain/IexecHubService.java b/src/main/java/com/iexec/core/chain/IexecHubService.java index cabb3bb98..a9597a3e9 100644 --- a/src/main/java/com/iexec/core/chain/IexecHubService.java +++ b/src/main/java/com/iexec/core/chain/IexecHubService.java @@ -39,8 +39,9 @@ import java.util.concurrent.ThreadPoolExecutor; import static com.iexec.common.utils.DateTimeUtils.now; +import static com.iexec.commons.poco.chain.ChainContributionStatus.CONTRIBUTED; +import static com.iexec.commons.poco.chain.ChainContributionStatus.REVEALED; import static com.iexec.commons.poco.contract.generated.IexecHubContract.*; -import static com.iexec.commons.poco.contract.generated.IexecHubContract.TASKFINALIZE_EVENT; import static com.iexec.commons.poco.utils.BytesUtils.stringToBytes; @Slf4j @@ -124,6 +125,7 @@ public boolean isBeforeContributionDeadline(ChainDeal chainDeal) { *
  • maxCategoryTime: duration of the deal's category. *
  • nbOfCategoryUnits: number of category units dedicated * for the contribution phase. + * * * @param chainDeal * @return @@ -146,6 +148,7 @@ public Date getChainDealContributionDeadline(ChainDeal chainDeal) { *
  • maxCategoryTime: duration of the deal's category. *
  • 10: number of category units dedicated * for the hole execution. + * * * @param chainDeal * @return @@ -255,7 +258,33 @@ private ChainReceipt buildChainReceipt(TransactionReceipt receipt) { .build(); } - // region Get event blocks + // region check contribution status + public boolean repeatIsContributedTrue(String chainTaskId, String walletAddress) { + return web3jService.repeatCheck(NB_BLOCKS_TO_WAIT_PER_RETRY, MAX_RETRIES, + "isContributedTrue", this::isContributed, chainTaskId, walletAddress); + } + + public boolean repeatIsRevealedTrue(String chainTaskId, String walletAddress) { + return web3jService.repeatCheck(NB_BLOCKS_TO_WAIT_PER_RETRY, MAX_RETRIES, + "isRevealedTrue", this::isRevealed, chainTaskId, walletAddress); + } + + public boolean isContributed(String... args) { + return getChainContribution(args[0], args[1]) + .map(ChainContribution::getStatus) + .filter(chainStatus -> chainStatus == CONTRIBUTED || chainStatus == REVEALED) + .isPresent(); + } + + public boolean isRevealed(String... args) { + return getChainContribution(args[0], args[1]) + .map(ChainContribution::getStatus) + .filter(chainStatus -> chainStatus == REVEALED) + .isPresent(); + } + // endregion + + // region get event blocks public ChainReceipt getContributionBlock(String chainTaskId, String workerWallet, long fromBlock) { diff --git a/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfig.java b/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfig.java index 3d33e6b17..93213152c 100644 --- a/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfig.java +++ b/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 IEXEC BLOCKCHAIN TECH + * Copyright 2021-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,37 +18,38 @@ import com.iexec.blockchain.api.BlockchainAdapterApiClient; import com.iexec.blockchain.api.BlockchainAdapterApiClientBuilder; +import com.iexec.blockchain.api.BlockchainAdapterService; +import com.iexec.common.config.PublicChainConfig; import feign.Logger; +import lombok.Data; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -@Configuration +import java.time.Duration; + +@Data +@ConstructorBinding +@ConfigurationProperties(prefix = "blockchain-adapter") public class BlockchainAdapterClientConfig { - @Value("${blockchain-adapter.protocol}") - private String protocol; - @Value("${blockchain-adapter.host}") - private String host; - @Value("${blockchain-adapter.port}") - private int port; + public static final int WATCH_PERIOD_SECONDS = 1;//To tune + public static final int MAX_ATTEMPTS = 50; + + private final String protocol; + private final String host; + private final int port; + // TODO improve property names before next major version @Value("${blockchain-adapter.user.name}") - private String username; + private final String username; @Value("${blockchain-adapter.user.password}") - private String password; + private final String password; public String getUrl() { return buildHostUrl(protocol, host, port); } - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - private String buildHostUrl(String protocol, String host, int port) { return protocol + "://" + host + ":" + port; } @@ -56,7 +57,22 @@ private String buildHostUrl(String protocol, String host, int port) { @Bean public BlockchainAdapterApiClient blockchainAdapterClient() { return BlockchainAdapterApiClientBuilder.getInstanceWithBasicAuth( - Logger.Level.NONE, getUrl(), getUsername(), getPassword()); + Logger.Level.NONE, getUrl(), username, password); + } + + @Bean + public BlockchainAdapterService blockchainAdapterService(BlockchainAdapterApiClient blockchainAdapterClient) { + return new BlockchainAdapterService(blockchainAdapterClient, Duration.ofSeconds(WATCH_PERIOD_SECONDS), MAX_ATTEMPTS); + } + + @Bean + public PublicChainConfig publicChainConfig(BlockchainAdapterApiClient apiClient) { + return apiClient.getPublicChainConfig(); + } + + @Bean + public int getChainId(PublicChainConfig publicChainConfig) { + return publicChainConfig.getChainId(); } } diff --git a/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterService.java b/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterService.java deleted file mode 100644 index 86cac41ba..000000000 --- a/src/main/java/com/iexec/core/chain/adapter/BlockchainAdapterService.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2021 IEXEC BLOCKCHAIN TECH - * - * 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.iexec.core.chain.adapter; - -import com.iexec.blockchain.api.BlockchainAdapterApiClient; -import com.iexec.common.chain.adapter.CommandStatus; -import com.iexec.common.chain.adapter.args.TaskFinalizeArgs; -import com.iexec.common.config.PublicChainConfig; -import feign.FeignException; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Service; - -import java.util.Optional; -import java.util.function.Function; - -import static java.util.concurrent.TimeUnit.SECONDS; - -@Slf4j -@Service -public class BlockchainAdapterService { - - public static final int WATCH_PERIOD_SECONDS = 1;//To tune - public static final int MAX_ATTEMPTS = 50; - - private final BlockchainAdapterApiClient blockchainAdapterClient; - private PublicChainConfig publicChainConfig; - - public BlockchainAdapterService(BlockchainAdapterApiClient blockchainAdapterClient) { - this.blockchainAdapterClient = blockchainAdapterClient; - } - - /** - * Request on-chain initialization of the task. - * - * @param chainDealId ID of the deal - * @param taskIndex index of the task in the deal - * @return chain task ID is initialization is properly requested - */ - public Optional requestInitialize(String chainDealId, int taskIndex) { - try { - String chainTaskId = blockchainAdapterClient.requestInitializeTask(chainDealId, taskIndex); - if (!StringUtils.isEmpty(chainTaskId)) { - log.info("Requested initialize [chainTaskId:{}, chainDealId:{}, " + - "taskIndex:{}]", chainTaskId, chainDealId, taskIndex); - return Optional.of(chainTaskId); - } - } catch (Exception e) { - log.error("Failed to requestInitialize [chainDealId:{}, " + - "taskIndex:{}]", chainDealId, taskIndex, e); - } - return Optional.empty(); - } - - /** - * Verify if the initialize task command is completed on-chain. - * - * @param chainTaskId ID of the task - * @return true if the tx is mined, false if reverted or empty for other - * cases (too long since still RECEIVED or PROCESSING, adapter error) - */ - public Optional isInitialized(String chainTaskId) { - return isCommandCompleted(blockchainAdapterClient::getStatusForInitializeTaskRequest, - chainTaskId, SECONDS.toMillis(WATCH_PERIOD_SECONDS), MAX_ATTEMPTS); - } - - /** - * Request on-chain finalization of the task. - * - * @param chainTaskId ID of the deal - * @param resultLink link of the result to be published on-chain - * @param callbackData optional data for on-chain callback - * @return chain task ID is initialization is properly requested - */ - public Optional requestFinalize(String chainTaskId, - String resultLink, - String callbackData) { - try { - String finalizeResponse = blockchainAdapterClient.requestFinalizeTask(chainTaskId, - new TaskFinalizeArgs(resultLink, callbackData)); - if (!StringUtils.isEmpty(finalizeResponse)) { - log.info("Requested finalize [chainTaskId:{}, resultLink:{}, callbackData:{}]", - chainTaskId, resultLink, callbackData); - return Optional.of(chainTaskId); - } - } catch (Exception e) { - log.error("Failed to requestFinalize [chainTaskId:{}, resultLink:{}, callbackData:{}]", - chainTaskId, resultLink, callbackData, e); - } - return Optional.empty(); - } - - /** - * Verify if the finalize task command is completed on-chain. - * - * @param chainTaskId ID of the task - * @return true if the tx is mined, false if reverted or empty for other - * cases (too long since still RECEIVED or PROCESSING, adapter error) - */ - public Optional isFinalized(String chainTaskId) { - return isCommandCompleted(blockchainAdapterClient::getStatusForFinalizeTaskRequest, - chainTaskId, SECONDS.toMillis(WATCH_PERIOD_SECONDS), MAX_ATTEMPTS); - } - - /** - * Verify if a command sent to the adapter is completed on-chain. - * - * @param getCommandStatusFunction method for checking the command is completed - * @param chainTaskId ID of the task - * @param period period in ms between checks - * @param maxAttempts maximum number of attempts for checking - * @return true if the tx is mined, false if reverted or empty for other - * cases (too long since still RECEIVED or PROCESSING, adapter error) - */ - Optional isCommandCompleted( - Function getCommandStatusFunction, - String chainTaskId, - long period, int maxAttempts) { - for(int attempt = 0; attempt < maxAttempts; attempt++) { - try { - CommandStatus status = getCommandStatusFunction.apply(chainTaskId); - if (CommandStatus.SUCCESS.equals(status) || CommandStatus.FAILURE.equals(status)) { - return Optional.of(status.equals(CommandStatus.SUCCESS)); - } - // RECEIVED, PROCESSING - log.warn("Waiting command completion [chainTaskId:{}, status:{}, period:{}ms, attempt:{}, maxAttempts:{}]", - chainTaskId, status, period, attempt, maxAttempts); - Thread.sleep(period); - } catch (Exception e) { - log.error("Unexpected error while waiting command completion [chainTaskId:{}, period:{}ms, attempt:{}, maxAttempts:{}]", - chainTaskId, period, attempt, maxAttempts, e); - return Optional.empty(); - } - } - log.error("Reached max retry while waiting command completion [chainTaskId:{}, maxAttempts:{}]", - chainTaskId, maxAttempts); - return Optional.empty(); - } - - /** - * Retrieve and store the public chain config. - */ - public PublicChainConfig getPublicChainConfig() { - if (publicChainConfig != null) { - return publicChainConfig; - } - try { - publicChainConfig = blockchainAdapterClient.getPublicChainConfig(); - log.info("Received public chain config [publicChainConfig:{}]", publicChainConfig); - return publicChainConfig; - } catch (FeignException e) { - log.error("Failed to get public chain config:", e); - } - return null; - } - - @Bean - public int getChainId() { - return getPublicChainConfig().getChainId(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/iexec/core/config/OpenApiConfig.java b/src/main/java/com/iexec/core/config/OpenApiConfig.java index fe51ac6e0..46582aaf3 100644 --- a/src/main/java/com/iexec/core/config/OpenApiConfig.java +++ b/src/main/java/com/iexec/core/config/OpenApiConfig.java @@ -16,19 +16,20 @@ package com.iexec.core.config; -import com.iexec.core.version.VersionService; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import org.springframework.boot.info.BuildProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiConfig { - private final VersionService versionService; + public static final String TITLE = "iExec Core Scheduler"; + private final BuildProperties buildProperties; - public OpenApiConfig(VersionService versionService) { - this.versionService = versionService; + public OpenApiConfig(BuildProperties buildProperties) { + this.buildProperties = buildProperties; } /* @@ -38,8 +39,8 @@ public OpenApiConfig(VersionService versionService) { public OpenAPI api() { return new OpenAPI().info( new Info() - .title("iExec Core Scheduler") - .version(versionService.getVersion()) + .title(TITLE) + .version(buildProperties.getVersion()) ); } } diff --git a/src/main/java/com/iexec/core/configuration/ResultRepositoryConfiguration.java b/src/main/java/com/iexec/core/configuration/ResultRepositoryConfiguration.java index 9e5a68bdc..2b446bdaf 100644 --- a/src/main/java/com/iexec/core/configuration/ResultRepositoryConfiguration.java +++ b/src/main/java/com/iexec/core/configuration/ResultRepositoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,27 +19,18 @@ import com.iexec.resultproxy.api.ResultProxyClient; import com.iexec.resultproxy.api.ResultProxyClientBuilder; import feign.Logger; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.beans.factory.annotation.Value; +import lombok.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Component; -@Component -@Getter -@AllArgsConstructor -@NoArgsConstructor +@Value +@ConstructorBinding +@ConfigurationProperties(prefix = "result-repository") public class ResultRepositoryConfiguration { - - @Value("${resultRepository.protocol}") - private String protocol; - - @Value("${resultRepository.host}") - private String host; - - @Value("${resultRepository.port}") - private String port; + String protocol; + String host; + String port; public String getResultRepositoryURL() { return protocol + "://" + host + ":" + port; diff --git a/src/main/java/com/iexec/core/configuration/WorkerConfiguration.java b/src/main/java/com/iexec/core/configuration/WorkerConfiguration.java index ca15227f5..cd11ab0cb 100644 --- a/src/main/java/com/iexec/core/configuration/WorkerConfiguration.java +++ b/src/main/java/com/iexec/core/configuration/WorkerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,36 +16,17 @@ package com.iexec.core.configuration; -import lombok.*; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import lombok.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; -import java.util.Arrays; import java.util.List; -@Setter -@Component +@Value +@ConstructorBinding +@ConfigurationProperties(prefix = "workers") public class WorkerConfiguration { - - @Value("${workers.askForReplicatePeriod}") - private long askForReplicatePeriod; - - @Value("${workers.requiredWorkerVersion}") - private String requiredWorkerVersion; - - @Value("${workers.whitelist}") - private String[] whitelist; - - // getters are overridden since the whitelist should return a list, not an array - public long getAskForReplicatePeriod() { - return askForReplicatePeriod; - } - - public String getRequiredWorkerVersion() { - return requiredWorkerVersion; - } - - public List getWhitelist() { - return Arrays.asList(whitelist); - } + long askForReplicatePeriod; + String requiredWorkerVersion; + List whitelist; } diff --git a/src/main/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetector.java b/src/main/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetector.java index d4eabe5dc..47d0eb5e2 100644 --- a/src/main/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetector.java +++ b/src/main/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2023-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetector.java b/src/main/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetector.java index 19c0f9884..a52dce614 100644 --- a/src/main/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetector.java +++ b/src/main/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetector.java b/src/main/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetector.java index cfd8cc3bd..b495730f7 100644 --- a/src/main/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetector.java +++ b/src/main/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/iexec/core/detector/replicate/UnnotifiedAbstractDetector.java b/src/main/java/com/iexec/core/detector/replicate/UnnotifiedAbstractDetector.java index 202759f5d..c81f39061 100644 --- a/src/main/java/com/iexec/core/detector/replicate/UnnotifiedAbstractDetector.java +++ b/src/main/java/com/iexec/core/detector/replicate/UnnotifiedAbstractDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,8 +30,7 @@ import java.util.List; -import static com.iexec.common.replicate.ReplicateStatus.WORKER_LOST; -import static com.iexec.common.replicate.ReplicateStatus.getMissingStatuses; +import static com.iexec.common.replicate.ReplicateStatus.*; @Slf4j public abstract class UnnotifiedAbstractDetector { @@ -71,9 +70,9 @@ protected UnnotifiedAbstractDetector(TaskService taskService, /** * Detects the following issues: *
      - *
    • `onchainDone` status only if replicates are in `offchainOngoing` status;
    • - *
    • `onchainDone` if replicates are not in `offchainDone` status.
    • - *
    + *
  • `onchainDone` status only if replicates are in `offchainOngoing` status + *
  • `onchainDone` if replicates are not in `offchainDone` status + * * The second detection is not always ran, depending on the detector run occurrences. */ void detectOnChainChanges() { @@ -95,24 +94,11 @@ void detectOnchainDoneWhenOffchainOngoing() { this.onchainDone, this.offchainOngoing, this.detectorRate); for (Task task : taskService.findByCurrentStatus(detectWhenOffChainTaskStatuses)) { - for (Replicate replicate : replicatesService.getReplicates(task.getChainTaskId())) { - final ReplicateStatus lastRelevantStatus = replicate.getLastRelevantStatus(); - if (lastRelevantStatus != offchainOngoing) { - continue; - } - - final boolean statusTrueOnChain = iexecHubService.isStatusTrueOnChain( - task.getChainTaskId(), - replicate.getWalletAddress(), - onchainDone - ); - - if (statusTrueOnChain) { - log.info("Detected confirmed missing update (replicate) [is:{}, should:{}, taskId:{}]", - lastRelevantStatus, onchainDone, task.getChainTaskId()); - updateReplicateStatuses(task, replicate); - } - } + replicatesService.getReplicates(task.getChainTaskId()).stream() + .filter(replicate -> replicate.getLastRelevantStatus() == offchainOngoing) + .filter(this::checkDetectionIsValid) + .filter(this::detectStatusReachedOnChain) + .forEach(replicate -> updateReplicateStatuses(task, replicate)); } } @@ -121,30 +107,52 @@ void detectOnchainDoneWhenOffchainOngoing() { * (worker didn't notify any status) * We want to detect them: * - Frequently but no so often since it's eth node resource consuming and less probable - * - When we receive a "can't do " relative to the `onchainDone` status (e.g.: `CANNOT_REVEAL`) + * - When we receive a "can't do <action>" relative to the `onchainDone` status (e.g.: `CANNOT_REVEAL`) */ public void detectOnchainDone() { log.debug("Detect onchain {} [retryIn:{}]", onchainDone, this.detectorRate * LESS_OFTEN_DETECTOR_FREQUENCY); for (Task task : taskService.findByCurrentStatus(detectWhenOffChainTaskStatuses)) { - for (Replicate replicate : replicatesService.getReplicates(task.getChainTaskId())) { - final ReplicateStatus lastRelevantStatus = replicate.getLastRelevantStatus(); - - if (lastRelevantStatus == offchainDone) { - continue; - } - - final boolean statusTrueOnChain = iexecHubService.isStatusTrueOnChain( - task.getChainTaskId(), - replicate.getWalletAddress(), - onchainDone - ); - - if (statusTrueOnChain) { - log.info("Detected confirmed missing update (replicate) [is:{}, should:{}, taskId:{}]", - lastRelevantStatus, onchainDone, task.getChainTaskId()); - updateReplicateStatuses(task, replicate); - } - } + replicatesService.getReplicates(task.getChainTaskId()).stream() + .filter(replicate -> replicate.getLastRelevantStatus() != offchainDone) + .filter(this::checkDetectionIsValid) + .filter(this::detectStatusReachedOnChain) + .forEach(replicate -> updateReplicateStatuses(task, replicate)); + } + } + + /** + * Checks replicate eligibility to a detection against an {@code offchainDone} status. + *

    + * All replicates are eligible to detection against {@code REVEALED}, but not against {@code CONTRIBUTED} or + * {@code CONTRIBUTE_AND_FINALIZED}. + * + * @param replicate The replicate to check + * @return {@literal true} if the replicate is eligible, {@literal false} otherwise + */ + private boolean checkDetectionIsValid(Replicate replicate) { + final boolean isEligibleToContributeAndFinalize = iexecHubService.getTaskDescription(replicate.getChainTaskId()) + .isEligibleToContributeAndFinalize(); + return offchainDone == REVEALED + || (!isEligibleToContributeAndFinalize && offchainDone == CONTRIBUTED) + || (isEligibleToContributeAndFinalize && offchainDone == CONTRIBUTE_AND_FINALIZE_DONE); + } + + /** + * Checks if {@code onchainDone} status has been reached on blockchain network. + * + * @param replicate Replicate whose on-chain status will be checked + * @return {@literal true} if given status has been found on-chain, {@literal false} otherwise. + */ + private boolean detectStatusReachedOnChain(Replicate replicate) { + final String chainTaskId = replicate.getChainTaskId(); + final String walletAddress = replicate.getWalletAddress(); + switch (onchainDone) { + case CONTRIBUTED: + return iexecHubService.isContributed(chainTaskId, walletAddress); + case REVEALED: + return iexecHubService.isRevealed(chainTaskId, walletAddress); + default: + return false; } } @@ -159,14 +167,13 @@ public void detectOnchainDone() { private void updateReplicateStatuses(Task task, Replicate replicate) { final String chainTaskId = task.getChainTaskId(); final long initBlockNumber = task.getInitializationBlockNumber(); - - final ReplicateStatus retrieveFrom = replicate.getCurrentStatus().equals(WORKER_LOST) - ? replicate.getLastButOneStatus() - : replicate.getCurrentStatus(); - final List statusesToUpdate = getMissingStatuses(retrieveFrom, offchainDone); - + final ReplicateStatus lastRelevantStatus = replicate.getLastRelevantStatus(); + final List statusesToUpdate = getMissingStatuses(lastRelevantStatus, offchainDone); final String wallet = replicate.getWalletAddress(); + log.info("Detected confirmed missing update (replicate) [is:{}, should:{}, taskId:{}]", + lastRelevantStatus, onchainDone, task.getChainTaskId()); + for (ReplicateStatus statusToUpdate : statusesToUpdate) { // add details to the update if needed ReplicateStatusDetails details = null; diff --git a/src/main/java/com/iexec/core/logs/TaskLogs.java b/src/main/java/com/iexec/core/logs/TaskLogs.java index edad27d66..f41830de5 100644 --- a/src/main/java/com/iexec/core/logs/TaskLogs.java +++ b/src/main/java/com/iexec/core/logs/TaskLogs.java @@ -16,21 +16,19 @@ package com.iexec.core.logs; -import java.util.ArrayList; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonIgnore; - import com.iexec.common.replicate.ComputeLogs; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; @Document @Data @@ -64,7 +62,14 @@ public TaskLogs(String chainTaskId, List computeLogsList) { public boolean containsWalletAddress(String walletAddress) { return computeLogsList.stream().anyMatch( - computeLog -> computeLog.getWalletAddress().equals(walletAddress) + computeLog -> computeLog.getWalletAddress().equals(walletAddress) ); } + + public TaskLogsModel generateModel() { + return TaskLogsModel.builder() + .chainTaskId(chainTaskId) + .computeLogsList(computeLogsList) + .build(); + } } diff --git a/src/main/java/com/iexec/core/logs/TaskLogsRepository.java b/src/main/java/com/iexec/core/logs/TaskLogsRepository.java index 082598a6d..156a6eba3 100644 --- a/src/main/java/com/iexec/core/logs/TaskLogsRepository.java +++ b/src/main/java/com/iexec/core/logs/TaskLogsRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package com.iexec.core.logs; -import java.util.List; -import java.util.Optional; - import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; +import java.util.List; +import java.util.Optional; + public interface TaskLogsRepository extends MongoRepository { Optional findOneByChainTaskId(String chainTaskId); diff --git a/src/main/java/com/iexec/core/logs/TaskLogsService.java b/src/main/java/com/iexec/core/logs/TaskLogsService.java index 22984a0d2..83a6faa84 100644 --- a/src/main/java/com/iexec/core/logs/TaskLogsService.java +++ b/src/main/java/com/iexec/core/logs/TaskLogsService.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package com.iexec.core.logs; -import java.util.List; -import java.util.Optional; - import com.iexec.common.replicate.ComputeLogs; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Optional; + @Service public class TaskLogsService { diff --git a/src/main/java/com/iexec/core/metric/MetricService.java b/src/main/java/com/iexec/core/metric/MetricService.java index b297d5a81..91f980065 100644 --- a/src/main/java/com/iexec/core/metric/MetricService.java +++ b/src/main/java/com/iexec/core/metric/MetricService.java @@ -17,24 +17,26 @@ package com.iexec.core.metric; import com.iexec.core.chain.DealWatcherService; -import com.iexec.core.task.TaskService; import com.iexec.core.task.TaskStatus; +import com.iexec.core.task.event.TaskStatusesCountUpdatedEvent; import com.iexec.core.worker.WorkerService; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import java.util.LinkedHashMap; + @Service public class MetricService { - private final DealWatcherService dealWatcherService; private final WorkerService workerService; - private final TaskService taskService; + private LinkedHashMap currentTaskStatusesCount; public MetricService(DealWatcherService dealWatcherService, - WorkerService workerService, - TaskService taskService) { + WorkerService workerService) { this.dealWatcherService = dealWatcherService; this.workerService = workerService; - this.taskService = taskService; + + this.currentTaskStatusesCount = new LinkedHashMap<>(); } public PlatformMetric getPlatformMetrics() { @@ -44,7 +46,7 @@ public PlatformMetric getPlatformMetrics() { .aliveAvailableCpu(workerService.getAliveAvailableCpu()) .aliveTotalGpu(workerService.getAliveTotalGpu()) .aliveAvailableGpu(workerService.getAliveAvailableGpu()) - .completedTasks(taskService.findByCurrentStatus(TaskStatus.COMPLETED).size()) + .currentTaskStatusesCount(currentTaskStatusesCount) .dealEventsCount(dealWatcherService.getDealEventsCount()) .dealsCount(dealWatcherService.getDealsCount()) .replayDealsCount(dealWatcherService.getReplayDealsCount()) @@ -52,4 +54,8 @@ public PlatformMetric getPlatformMetrics() { .build(); } + @EventListener + void onTaskStatusesCountUpdateEvent(TaskStatusesCountUpdatedEvent event) { + this.currentTaskStatusesCount = event.getCurrentTaskStatusesCount(); + } } diff --git a/src/main/java/com/iexec/core/replicate/Replicate.java b/src/main/java/com/iexec/core/replicate/Replicate.java index da15cf238..dcc5b43d0 100644 --- a/src/main/java/com/iexec/core/replicate/Replicate.java +++ b/src/main/java/com/iexec/core/replicate/Replicate.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.iexec.common.replicate.*; import com.iexec.commons.poco.chain.ChainReceipt; +import com.iexec.core.exception.MultipleOccurrencesOfFieldNotAllowed; import lombok.Data; import lombok.NoArgsConstructor; @@ -100,7 +101,7 @@ public boolean updateStatus(ReplicateStatus newStatus, ReplicateStatusModifier m } public boolean updateStatus(ReplicateStatus newStatus, ReplicateStatusCause cause, - ReplicateStatusModifier modifier, ChainReceipt chainReceipt) { + ReplicateStatusModifier modifier, ChainReceipt chainReceipt) { ReplicateStatusDetails details = ReplicateStatusDetails.builder() .chainReceipt(chainReceipt) .cause(cause) @@ -159,4 +160,48 @@ boolean isStatusBeforeWorkerLostEqualsTo(ReplicateStatus status) { && statusUpdateList.get(size - 1).getStatus().equals(WORKER_LOST) && statusUpdateList.get(size - 2).getStatus().equals(status); } + + public ReplicateModel generateModel() { + final List modelStatusUpdateList = new ArrayList<>(); + + Integer appExitCode = null; + String teeSessionGenerationError = null; + for (ReplicateStatusUpdate replicateStatusUpdate : statusUpdateList) { + modelStatusUpdateList.add(ReplicateStatusUpdateModel.fromEntity(replicateStatusUpdate)); + ReplicateStatusDetails details = replicateStatusUpdate.getDetails(); + if (details != null) { + final Integer detailsExitCode = details.getExitCode(); + if (detailsExitCode != null) { + if (appExitCode != null) { + throw new MultipleOccurrencesOfFieldNotAllowed("exitCode"); + } + appExitCode = detailsExitCode; + } + + final String detailsTeeSessionGenerationError = details.getTeeSessionGenerationError(); + if (detailsTeeSessionGenerationError != null) { + if (teeSessionGenerationError != null) { + throw new MultipleOccurrencesOfFieldNotAllowed("teeSessionGenerationError"); + } + teeSessionGenerationError = detailsTeeSessionGenerationError; + } + + if (appExitCode != null && teeSessionGenerationError != null) { + break; + } + } + } + + return ReplicateModel.builder() + .chainTaskId(chainTaskId) + .walletAddress(walletAddress) + .currentStatus(getCurrentStatus()) + .statusUpdateList(modelStatusUpdateList) + .resultLink(resultLink) + .chainCallbackData(chainCallbackData) + .contributionHash(contributionHash) + .appExitCode(appExitCode) + .teeSessionGenerationError(teeSessionGenerationError) + .build(); + } } diff --git a/src/main/java/com/iexec/core/replicate/ReplicateModel.java b/src/main/java/com/iexec/core/replicate/ReplicateModel.java deleted file mode 100644 index cf0f20033..000000000 --- a/src/main/java/com/iexec/core/replicate/ReplicateModel.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2022 IEXEC BLOCKCHAIN TECH - * - * 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.iexec.core.replicate; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.iexec.common.replicate.ReplicateStatus; -import com.iexec.common.replicate.ReplicateStatusDetails; -import com.iexec.common.replicate.ReplicateStatusUpdate; -import com.iexec.core.exception.MultipleOccurrencesOfFieldNotAllowed; -import lombok.*; - -import java.util.ArrayList; -import java.util.List; - -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class ReplicateModel { - - private String self; - private String chainTaskId; - private String walletAddress; - private ReplicateStatus currentStatus; - private List statusUpdateList; - private String resultLink; - private String chainCallbackData; - private String contributionHash; - private String appLogs; - private Integer appExitCode; //null means unset - private String teeSessionGenerationError; // null means unset - - public static ReplicateModel fromEntity(Replicate entity) { - final List statusUpdateList = new ArrayList<>(); - - Integer appExitCode = null; - String teeSessionGenerationError = null; - for (ReplicateStatusUpdate replicateStatusUpdate : entity.getStatusUpdateList()) { - statusUpdateList.add(ReplicateStatusUpdateModel.fromEntity(replicateStatusUpdate)); - ReplicateStatusDetails details = replicateStatusUpdate.getDetails(); - if (details != null) { - final Integer detailsExitCode = details.getExitCode(); - if (detailsExitCode != null) { - if (appExitCode != null) { - throw new MultipleOccurrencesOfFieldNotAllowed("exitCode"); - } - appExitCode = detailsExitCode; - } - - final String detailsTeeSessionGenerationError = details.getTeeSessionGenerationError(); - if (detailsTeeSessionGenerationError != null) { - if (teeSessionGenerationError != null) { - throw new MultipleOccurrencesOfFieldNotAllowed("teeSessionGenerationError"); - } - teeSessionGenerationError = detailsTeeSessionGenerationError; - } - - if (appExitCode != null && teeSessionGenerationError != null) { - break; - } - } - } - - return ReplicateModel.builder() - .chainTaskId(entity.getChainTaskId()) - .walletAddress(entity.getWalletAddress()) - .currentStatus(entity.getCurrentStatus()) - .statusUpdateList(statusUpdateList) - .resultLink(entity.getResultLink()) - .chainCallbackData(entity.getChainCallbackData()) - .contributionHash(entity.getContributionHash()) - .appExitCode(appExitCode) - .teeSessionGenerationError(teeSessionGenerationError) - .build(); - } -} diff --git a/src/main/java/com/iexec/core/replicate/ReplicateSupplyService.java b/src/main/java/com/iexec/core/replicate/ReplicateSupplyService.java index 4da8738cf..e33fd681f 100644 --- a/src/main/java/com/iexec/core/replicate/ReplicateSupplyService.java +++ b/src/main/java/com/iexec/core/replicate/ReplicateSupplyService.java @@ -420,7 +420,7 @@ private Optional recoverReplicateInRevealPhase(Task task, if (didReplicateStartRevealing && didReplicateRevealOnChain) { ReplicateStatusDetails details = new ReplicateStatusDetails(blockNumber); replicatesService.updateReplicateStatus(chainTaskId, walletAddress, REVEALED, details); - taskUpdateRequestManager.publishRequest(chainTaskId).join(); + taskUpdateRequestManager.publishRequest(chainTaskId); } // we read the replicate from db to consider the changes added in the previous case diff --git a/src/main/java/com/iexec/core/replicate/ReplicatesService.java b/src/main/java/com/iexec/core/replicate/ReplicatesService.java index 04dd9995b..d9a88d45b 100644 --- a/src/main/java/com/iexec/core/replicate/ReplicatesService.java +++ b/src/main/java/com/iexec/core/replicate/ReplicatesService.java @@ -47,12 +47,12 @@ @Service public class ReplicatesService { - private ReplicatesRepository replicatesRepository; - private IexecHubService iexecHubService; - private ApplicationEventPublisher applicationEventPublisher; - private Web3jService web3jService; - private ResultService resultService; - private TaskLogsService taskLogsService; + private final ReplicatesRepository replicatesRepository; + private final IexecHubService iexecHubService; + private final ApplicationEventPublisher applicationEventPublisher; + private final Web3jService web3jService; + private final ResultService resultService; + private final TaskLogsService taskLogsService; private final ContextualLockRunner replicatesUpdateLockRunner = new ContextualLockRunner<>(10, TimeUnit.MINUTES); @@ -165,20 +165,12 @@ public Optional getReplicateWithResultUploadedStatus(String chainTask * @return {@link ReplicateStatusUpdateError#NO_ERROR} if this update is OK, * another {@link ReplicateStatusUpdateError} containing the error reason otherwise. */ - public ReplicateStatusUpdateError canUpdateReplicateStatus(String chainTaskId, - String walletAddress, + public ReplicateStatusUpdateError canUpdateReplicateStatus(Replicate replicate, ReplicateStatusUpdate statusUpdate, UpdateReplicateStatusArgs updateReplicateStatusArgs) { - Optional oReplicateList = getReplicatesList(chainTaskId); - if (oReplicateList.isEmpty() || oReplicateList.get().getReplicateOfWorker(walletAddress).isEmpty()) { - log.error("Cannot update replicate, could not get replicate [chainTaskId:{}, UpdateRequest:{}]", - chainTaskId, statusUpdate); - return ReplicateStatusUpdateError.UNKNOWN_REPLICATE; - } - - ReplicatesList replicatesList = oReplicateList.get(); - Replicate replicate = replicatesList.getReplicateOfWorker(walletAddress).orElseThrow(); // "get" could be used there but triggers a warning - ReplicateStatus newStatus = statusUpdate.getStatus(); + final String chainTaskId = replicate.getChainTaskId(); + final String walletAddress = replicate.getWalletAddress(); + final ReplicateStatus newStatus = statusUpdate.getStatus(); boolean hasAlreadyTransitionedToStatus = replicate.containsStatus(newStatus); if (hasAlreadyTransitionedToStatus) { @@ -243,27 +235,19 @@ public UpdateReplicateStatusArgs computeUpdateReplicateStatusArgs(String chainTa ChainContribution chainContribution = null; String resultLink = null; String chainCallbackData = null; - TaskDescription taskDescription = null; + final TaskDescription taskDescription = iexecHubService.getTaskDescription(chainTaskId); - switch (statusUpdate.getStatus()) { - case CONTRIBUTED: - workerWeight = iexecHubService.getWorkerWeight(walletAddress); - chainContribution = iexecHubService.getChainContribution(chainTaskId, walletAddress).orElse(null); - break; - case RESULT_UPLOADED: - ReplicateStatusDetails details = statusUpdate.getDetails(); - if (details != null) { - resultLink = details.getResultLink(); - chainCallbackData = details.getChainCallbackData(); - } - taskDescription = iexecHubService.getTaskDescription(chainTaskId); - break; - case COMPUTED: - case RESULT_UPLOAD_FAILED: - taskDescription = iexecHubService.getTaskDescription(chainTaskId); - break; - default: - break; + if (statusUpdate.getStatus() == CONTRIBUTED || statusUpdate.getStatus() == CONTRIBUTE_AND_FINALIZE_DONE) { + workerWeight = iexecHubService.getWorkerWeight(walletAddress); + chainContribution = iexecHubService.getChainContribution(chainTaskId, walletAddress).orElse(null); + } + + if (statusUpdate.getStatus() == RESULT_UPLOADED || statusUpdate.getStatus() == CONTRIBUTE_AND_FINALIZE_DONE) { + final ReplicateStatusDetails details = statusUpdate.getDetails(); + if (details != null) { + resultLink = details.getResultLink(); + chainCallbackData = details.getChainCallbackData(); + } } return UpdateReplicateStatusArgs.builder() @@ -418,28 +402,36 @@ Either updateReplicateStatusWi log.info("Replicate update request [status:{}, chainTaskId:{}, walletAddress:{}, details:{}]", statusUpdate.getStatus(), chainTaskId, walletAddress, statusUpdate.getDetailsWithoutLogs()); - final ReplicateStatusUpdateError error = canUpdateReplicateStatus(chainTaskId, walletAddress, statusUpdate, updateReplicateStatusArgs); + final Optional oReplicatesList = getReplicatesList(chainTaskId); + final Optional oReplicate = oReplicatesList + .flatMap(replicatesList -> replicatesList.getReplicateOfWorker(walletAddress)); + if (oReplicatesList.isEmpty() || oReplicate.isEmpty()) { + log.error("Cannot update replicate, could not get replicate [chainTaskId:{}, UpdateRequest:{}]", + chainTaskId, statusUpdate); + return Either.left(ReplicateStatusUpdateError.UNKNOWN_REPLICATE); + } + final ReplicatesList replicatesList = oReplicatesList.get(); + final Replicate replicate = oReplicate.get(); + final ReplicateStatus newStatus = statusUpdate.getStatus(); + + final ReplicateStatusUpdateError error = canUpdateReplicateStatus(replicate, statusUpdate, updateReplicateStatusArgs); if (ReplicateStatusUpdateError.NO_ERROR != error) { return Either.left(error); } - ReplicatesList replicatesList = getReplicatesList(chainTaskId).orElseThrow(); // "get" could be used there but triggers a warning - Replicate replicate = replicatesList.getReplicateOfWorker(walletAddress).orElseThrow(); // "get" could be used there but triggers a warning - ReplicateStatus newStatus = statusUpdate.getStatus(); - - if (newStatus.equals(CONTRIBUTED)) { + if (newStatus == CONTRIBUTED || newStatus == CONTRIBUTE_AND_FINALIZE_DONE) { replicate.setContributionHash(updateReplicateStatusArgs.getChainContribution().getResultHash()); replicate.setWorkerWeight(updateReplicateStatusArgs.getWorkerWeight()); } - if (newStatus.equals(RESULT_UPLOADED)) { + if (newStatus == RESULT_UPLOADED || newStatus == CONTRIBUTE_AND_FINALIZE_DONE) { replicate.setResultLink(updateReplicateStatusArgs.getResultLink()); replicate.setChainCallbackData(updateReplicateStatusArgs.getChainCallbackData()); } if (statusUpdate.getDetails() != null && - (newStatus.equals(COMPUTED) || (newStatus.equals(COMPUTE_FAILED) - && ReplicateStatusCause.APP_COMPUTE_FAILED.equals(statusUpdate.getDetails().getCause())))) { + (newStatus == COMPUTED || (newStatus == COMPUTE_FAILED + && ReplicateStatusCause.APP_COMPUTE_FAILED == statusUpdate.getDetails().getCause()))) { final ComputeLogs computeLogs = statusUpdate.getDetails().tailLogs().getComputeLogs(); taskLogsService.addComputeLogs(chainTaskId, computeLogs); statusUpdate.getDetails().setComputeLogs(null);//using null here to keep light replicate @@ -618,13 +610,11 @@ public boolean isResultUploaded(TaskDescription task) { } public boolean didReplicateContributeOnchain(String chainTaskId, String walletAddress) { - return iexecHubService.isStatusTrueOnChain( - chainTaskId, walletAddress, getChainStatus(ReplicateStatus.CONTRIBUTED)); + return iexecHubService.isContributed(chainTaskId, walletAddress); } public boolean didReplicateRevealOnchain(String chainTaskId, String walletAddress) { - return iexecHubService.isStatusTrueOnChain( - chainTaskId, walletAddress, getChainStatus(ReplicateStatus.REVEALED)); + return iexecHubService.isRevealed(chainTaskId, walletAddress); } public void setRevealTimeoutStatusIfNeeded(String chainTaskId, Replicate replicate) { diff --git a/src/main/java/com/iexec/core/security/WebSecurityConfig.java b/src/main/java/com/iexec/core/security/WebSecurityConfig.java index 4abacb311..908d5bbfc 100644 --- a/src/main/java/com/iexec/core/security/WebSecurityConfig.java +++ b/src/main/java/com/iexec/core/security/WebSecurityConfig.java @@ -16,18 +16,19 @@ package com.iexec.core.security; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +public class WebSecurityConfig { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // Disable CSRF (cross site request forgery) http.csrf().disable(); @@ -36,5 +37,6 @@ protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().permitAll(); + return http.build(); } } diff --git a/src/main/java/com/iexec/core/task/Task.java b/src/main/java/com/iexec/core/task/Task.java index 1d5b6b6c9..b96f1797c 100644 --- a/src/main/java/com/iexec/core/task/Task.java +++ b/src/main/java/com/iexec/core/task/Task.java @@ -35,6 +35,7 @@ import java.util.Optional; import static com.iexec.core.task.TaskStatus.CONSENSUS_REACHED; +import static com.iexec.core.task.TaskStatus.RECEIVED; @Document @Getter @@ -54,7 +55,7 @@ unique = true) public class Task { - public static final String CURRENT_STATUS_FIELD_NAME = "currentStatus"; + public static final String CURRENT_STATUS_FIELD_NAME = "currentStatus"; public static final String CONTRIBUTION_DEADLINE_FIELD_NAME = "contributionDeadline"; @Id @@ -98,7 +99,10 @@ public Task(String dappName, String commandLine, int trust) { this.commandLine = commandLine; this.trust = trust; this.dateStatusList = new ArrayList<>(); - this.dateStatusList.add(new TaskStatusChange(TaskStatus.RECEIVED)); + TaskStatusChange taskStatusChange = TaskStatusChange.builder() + .status(RECEIVED) + .build(); + this.dateStatusList.add(taskStatusChange); this.currentStatus = TaskStatus.RECEIVED; } @@ -122,7 +126,11 @@ public void changeStatus(TaskStatus status) { public void changeStatus(TaskStatus status, ChainReceipt chainReceipt) { setCurrentStatus(status); - this.getDateStatusList().add(new TaskStatusChange(status, chainReceipt)); + TaskStatusChange taskStatusChange = TaskStatusChange.builder() + .status(status) + .chainReceipt(chainReceipt) + .build(); + this.getDateStatusList().add(taskStatusChange); } @JsonIgnore @@ -178,4 +186,26 @@ public boolean inCompletionPhase() { public boolean isTeeTask() { return TeeUtils.isTeeTag(getTag()); } + + TaskModel generateModel() { + return TaskModel.builder() + .chainTaskId(chainTaskId) + .maxExecutionTime(maxExecutionTime) + .tag(tag) + .dappType(dappType) + .dappName(dappName) + .commandLine(commandLine) + .initializationBlockNumber(initializationBlockNumber) + .currentStatus(currentStatus) + .trust(trust) + .uploadingWorkerWalletAddress(uploadingWorkerWalletAddress) + .consensus(consensus) + .contributionDeadline(contributionDeadline) + .revealDeadline(revealDeadline) + .finalDeadline(finalDeadline) + .resultLink(resultLink) + .chainCallbackData(chainCallbackData) + .dateStatusList(dateStatusList) + .build(); + } } diff --git a/src/main/java/com/iexec/core/task/TaskController.java b/src/main/java/com/iexec/core/task/TaskController.java index 04dce180d..f883911c2 100644 --- a/src/main/java/com/iexec/core/task/TaskController.java +++ b/src/main/java/com/iexec/core/task/TaskController.java @@ -24,6 +24,7 @@ import com.iexec.commons.poco.utils.SignatureUtils; import com.iexec.core.chain.IexecHubService; import com.iexec.core.logs.TaskLogs; +import com.iexec.core.logs.TaskLogsModel; import com.iexec.core.logs.TaskLogsService; import com.iexec.core.replicate.Replicate; import com.iexec.core.replicate.ReplicateModel; @@ -70,7 +71,7 @@ public ResponseEntity getChallenge(@RequestParam("address") Str @GetMapping("/tasks/{chainTaskId}") public ResponseEntity getTask(@PathVariable("chainTaskId") String chainTaskId) { return taskService.getTaskByChainTaskId(chainTaskId).map(task -> { - TaskModel taskModel = TaskModel.fromEntity(task); + TaskModel taskModel = task.generateModel(); if (replicatesService.hasReplicatesList(chainTaskId)) { taskModel.setReplicates(replicatesService.getReplicates(chainTaskId) .stream() @@ -99,7 +100,7 @@ public ResponseEntity getTaskReplicate(@PathVariable("chainTaskI * @return replicate model */ ReplicateModel buildReplicateModel(Replicate replicate) { - ReplicateModel replicateModel = ReplicateModel.fromEntity(replicate); + ReplicateModel replicateModel = replicate.generateModel(); if (replicate.isAppComputeLogsPresent()) { String logs = linkTo(methodOn(TaskController.class) .getComputeLogs( @@ -123,7 +124,7 @@ ReplicateModel buildReplicateModel(Replicate replicate) { "/tasks/{chainTaskId}/stdout", // @Deprecated "/tasks/{chainTaskId}/logs" }) - public ResponseEntity getTaskLogs( + public ResponseEntity getTaskLogs( @PathVariable("chainTaskId") String chainTaskId, @RequestHeader("Authorization") String authorization) { SignedChallenge signedChallenge = SignedChallenge.createFromString(authorization); @@ -131,7 +132,7 @@ public ResponseEntity getTaskLogs( return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } String taskLogsRequester = signedChallenge.getWalletAddress(); - if(!isTaskRequester(taskLogsRequester, chainTaskId)) { + if (!isTaskRequester(taskLogsRequester, chainTaskId)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } Signature signature = new Signature(Numeric.cleanHexPrefix(signedChallenge.getChallengeSignature())); @@ -141,6 +142,7 @@ public ResponseEntity getTaskLogs( return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } return taskLogsService.getTaskLogs(chainTaskId) + .map(TaskLogs::generateModel) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @@ -174,8 +176,9 @@ public ResponseEntity getComputeLogs( /** * Checks if requester address from bearer token is the same as the address used to buy the task execution. + * * @param logsRequester Wallet address of requester asking task or compute logs - * @param chainTaskId Task for which outputs are requested + * @param chainTaskId Task for which outputs are requested * @return true if the user requesting computation outputs was the one to buy the task, false otherwise */ private boolean isTaskRequester(String logsRequester, String chainTaskId) { diff --git a/src/main/java/com/iexec/core/task/TaskRepository.java b/src/main/java/com/iexec/core/task/TaskRepository.java index 766014baf..e9465fffd 100644 --- a/src/main/java/com/iexec/core/task/TaskRepository.java +++ b/src/main/java/com/iexec/core/task/TaskRepository.java @@ -63,7 +63,7 @@ interface TaskRepository extends MongoRepository { * @return The first task matching with the criteria, according to the {@code sort} parameter. */ Optional findFirstByCurrentStatusInAndTagNotInAndChainTaskIdNotIn(List statuses, List excludedTags, List excludedChainTaskIds, Sort sort); - + @Query("{ 'currentStatus': {$nin: ?0} }") List findByCurrentStatusNotIn(List statuses); @@ -71,4 +71,6 @@ interface TaskRepository extends MongoRepository { List findChainTaskIdsByFinalDeadlineBefore(Date date); List findByCurrentStatusInAndContributionDeadlineAfter(List status, Date date); + + long countByCurrentStatus(TaskStatus currentStatus); } diff --git a/src/main/java/com/iexec/core/task/TaskService.java b/src/main/java/com/iexec/core/task/TaskService.java index dcaae8e42..f4d7d245a 100644 --- a/src/main/java/com/iexec/core/task/TaskService.java +++ b/src/main/java/com/iexec/core/task/TaskService.java @@ -21,11 +21,14 @@ import com.iexec.commons.poco.tee.TeeUtils; import com.iexec.core.chain.IexecHubService; import com.iexec.core.replicate.ReplicatesList; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -37,13 +40,22 @@ @Slf4j @Service public class TaskService { + + public static final String METRIC_TASKS_COMPLETED_COUNT = "iexec.core.tasks.completed"; private final TaskRepository taskRepository; private final IexecHubService iexecHubService; + private final Counter completedTasksCounter; public TaskService(TaskRepository taskRepository, IexecHubService iexecHubService) { this.taskRepository = taskRepository; this.iexecHubService = iexecHubService; + this.completedTasksCounter = Metrics.counter(METRIC_TASKS_COMPLETED_COUNT); + } + + @PostConstruct + void init() { + completedTasksCounter.increment(findByCurrentStatus(TaskStatus.COMPLETED).size()); } /** @@ -97,13 +109,21 @@ public Optional addTask( /** * Updates a task if it already exists in DB. * Otherwise, will not do anything. + * * @param task Task to update. * @return An {@link Optional} if task exists, {@link Optional#empty()} otherwise. */ public Optional updateTask(Task task) { - return taskRepository + + Optional optionalTask = taskRepository .findByChainTaskId(task.getChainTaskId()) .map(existingTask -> taskRepository.save(task)); + + if (optionalTask.isPresent() && optionalTask.get().getCurrentStatus() == TaskStatus.COMPLETED) { + completedTasksCounter.increment(); + } + + return optionalTask; } public Optional getTaskByChainTaskId(String chainTaskId) { @@ -135,7 +155,7 @@ public List findByCurrentStatus(List statusList) { * * @param shouldExcludeTeeTasks Whether TEE tasks should be retrieved * as well as standard tasks. - * @param excludedChainTaskIds Tasks to exclude from retrieval. + * @param excludedChainTaskIds Tasks to exclude from retrieval. * @return The first task which is {@link TaskStatus#INITIALIZED} * or {@link TaskStatus#RUNNING}, * or {@link Optional#empty()} if no task meets the requirements. @@ -167,7 +187,7 @@ public Optional getPrioritizedInitializedOrRunningTask( * * * @param statuses The task status should be one of this list. - * @param excludedTag The task tag should not be this tag + * @param excludedTags The task tag should not be these tags * - use {@literal null} if no tag should be excluded. * @param excludedChainTaskIds The chain task ID should not be one of this list. * @param sort How to prioritize tasks. @@ -240,4 +260,12 @@ public boolean isConsensusReached(ReplicatesList replicatesList) { int offChainWinners = replicatesList.getNbValidContributedWinners(chainTask.getConsensusValue()); return offChainWinners >= onChainWinners; } + + public long getCompletedTasksCount() { + return (long) completedTasksCounter.count(); + } + + public long countByCurrentStatus(TaskStatus status) { + return taskRepository.countByCurrentStatus(status); + } } diff --git a/src/main/java/com/iexec/core/task/event/TaskStatusesCountUpdatedEvent.java b/src/main/java/com/iexec/core/task/event/TaskStatusesCountUpdatedEvent.java new file mode 100644 index 000000000..913ada4e2 --- /dev/null +++ b/src/main/java/com/iexec/core/task/event/TaskStatusesCountUpdatedEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024-2024 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.task.event; + +import com.iexec.core.task.TaskStatus; +import lombok.Value; + +import java.util.LinkedHashMap; + +@Value +public class TaskStatusesCountUpdatedEvent { + LinkedHashMap currentTaskStatusesCount; +} diff --git a/src/main/java/com/iexec/core/task/update/TaskUpdateManager.java b/src/main/java/com/iexec/core/task/update/TaskUpdateManager.java index d3f81660e..d41a61359 100644 --- a/src/main/java/com/iexec/core/task/update/TaskUpdateManager.java +++ b/src/main/java/com/iexec/core/task/update/TaskUpdateManager.java @@ -16,13 +16,13 @@ package com.iexec.core.task.update; +import com.iexec.blockchain.api.BlockchainAdapterService; import com.iexec.common.replicate.ReplicateStatus; import com.iexec.commons.poco.chain.ChainReceipt; import com.iexec.commons.poco.chain.ChainTask; import com.iexec.commons.poco.chain.ChainTaskStatus; import com.iexec.commons.poco.task.TaskDescription; import com.iexec.core.chain.IexecHubService; -import com.iexec.core.chain.adapter.BlockchainAdapterService; import com.iexec.core.replicate.Replicate; import com.iexec.core.replicate.ReplicatesList; import com.iexec.core.replicate.ReplicatesService; @@ -33,13 +33,20 @@ import com.iexec.core.task.event.*; import com.iexec.core.worker.Worker; import com.iexec.core.worker.WorkerService; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Metrics; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -48,6 +55,8 @@ @Service @Slf4j class TaskUpdateManager { + public static final String METRIC_TASKS_STATUSES_COUNT = "iexec.core.tasks.count"; + private final TaskService taskService; private final IexecHubService iexecHubService; private final ReplicatesService replicatesService; @@ -56,6 +65,8 @@ class TaskUpdateManager { private final BlockchainAdapterService blockchainAdapterService; private final SmsService smsService; + private final LinkedHashMap currentTaskStatusesCount; + public TaskUpdateManager(TaskService taskService, IexecHubService iexecHubService, ReplicatesService replicatesService, @@ -70,9 +81,48 @@ public TaskUpdateManager(TaskService taskService, this.workerService = workerService; this.blockchainAdapterService = blockchainAdapterService; this.smsService = smsService; + + this.currentTaskStatusesCount = Arrays.stream(TaskStatus.values()) + .collect(Collectors.toMap( + Function.identity(), + status -> new AtomicLong(), + (a, b) -> b, + LinkedHashMap::new)); + + for (TaskStatus status : TaskStatus.values()) { + Gauge.builder(METRIC_TASKS_STATUSES_COUNT, () -> currentTaskStatusesCount.get(status).get()) + .tags( + "period", "current", + "status", status.name() + ).register(Metrics.globalRegistry); + } + } + + @PostConstruct + Future init() { + final ExecutorService taskStatusesCountExecutor = Executors.newSingleThreadExecutor(); + final Future future = taskStatusesCountExecutor.submit( + this::initializeCurrentTaskStatusesCount, + null // Trick to get a `Future` instead of a `Future` + ); + taskStatusesCountExecutor.shutdown(); + return future; + } + + /** + * The following could take a bit of time, depending on how many tasks are in DB. + * It is expected to take ~1.7s for 1,000,000 tasks and to be linear (so, ~17s for 10,000,000 tasks). + * As we use AtomicLongs, the final count should be accurate - no race conditions to expect, + * even though new deals are detected during the count. + */ + private void initializeCurrentTaskStatusesCount() { + currentTaskStatusesCount + .entrySet() + .parallelStream() + .forEach(entry -> entry.getValue().addAndGet(taskService.countByCurrentStatus(entry.getKey()))); + publishTaskStatusesCountUpdate(); } - @SuppressWarnings("DuplicateBranchesInSwitch") void updateTask(String chainTaskId) { Optional optional = taskService.getTaskByChainTaskId(chainTaskId); if (optional.isEmpty()) { @@ -84,11 +134,10 @@ void updateTask(String chainTaskId) { boolean isFinalDeadlinePossible = !TaskStatus.getStatusesWhereFinalDeadlineIsImpossible().contains(currentStatus); if (isFinalDeadlinePossible && new Date().after(task.getFinalDeadline())) { - updateTaskStatusAndSave(task, FINAL_DEADLINE_REACHED); // Eventually should fire a "final deadline reached" notification to worker, // but here let's just trigger an toFailed(task) leading to a failed status // which will itself fire a generic "abort" notification - toFailed(task); + toFailed(task, FINAL_DEADLINE_REACHED); return; } @@ -103,25 +152,16 @@ void updateTask(String chainTaskId) { initialized2Running(task); initializedOrRunning2ContributionTimeout(task); break; - case INITIALIZE_FAILED: - toFailed(task); - break; case RUNNING: running2Finalized2Completed(task); // running2Finalized2Completed must be the first call to prevent other transition execution running2ConsensusReached(task); running2RunningFailed(task); initializedOrRunning2ContributionTimeout(task); break; - case RUNNING_FAILED: - toFailed(task); - break; case CONSENSUS_REACHED: consensusReached2AtLeastOneReveal2ResultUploading(task); consensusReached2Reopening(task); break; - case CONTRIBUTION_TIMEOUT: - toFailed(task); - break; case AT_LEAST_ONE_REVEALED: requestUpload(task); break; @@ -131,9 +171,6 @@ void updateTask(String chainTaskId) { case REOPENED: updateTaskStatusAndSave(task, INITIALIZED); break; - case REOPEN_FAILED: - toFailed(task); - break; case RESULT_UPLOADING: resultUploading2Uploaded(task); resultUploading2UploadTimeout(task); @@ -141,18 +178,18 @@ void updateTask(String chainTaskId) { case RESULT_UPLOADED: resultUploaded2Finalizing(task); break; - case RESULT_UPLOAD_TIMEOUT: - toFailed(task); - break; case FINALIZING: finalizing2Finalized2Completed(task); break; case FINALIZED: finalizedToCompleted(task); break; + case INITIALIZE_FAILED: + case RUNNING_FAILED: + case CONTRIBUTION_TIMEOUT: + case REOPEN_FAILED: + case RESULT_UPLOAD_TIMEOUT: case FINALIZE_FAILED: - toFailed(task); - break; case FINAL_DEADLINE_REACHED: toFailed(task); break; @@ -162,24 +199,49 @@ void updateTask(String chainTaskId) { } } - Task updateTaskStatusAndSave(Task task, TaskStatus newStatus) { - return updateTaskStatusAndSave(task, newStatus, null); + /** + * Creates one or several task status changes for the task before committing all of them to the database. + * + * @param task The task + * @param statuses List of statuses to append to the task {@code dateStatusList} + */ + void updateTaskStatusesAndSave(Task task, TaskStatus... statuses) { + TaskStatus initialStatus = task.getCurrentStatus(); + TaskStatus lastStatus = statuses[statuses.length - 1]; + for (TaskStatus newStatus : statuses) { + log.info("Create TaskStatusChange succeeded [chainTaskId:{}, currentStatus:{}, newStatus:{}]", + task.getChainTaskId(), task.getCurrentStatus(), newStatus); + task.changeStatus(newStatus, null); + } + saveTask(task, initialStatus, lastStatus); } - Task updateTaskStatusAndSave(Task task, TaskStatus newStatus, ChainReceipt chainReceipt) { - TaskStatus currentStatus = task.getCurrentStatus(); + void updateTaskStatusAndSave(Task task, TaskStatus newStatus) { + updateTaskStatusAndSave(task, newStatus, null); + } + + void updateTaskStatusAndSave(Task task, TaskStatus newStatus, ChainReceipt chainReceipt) { + TaskStatus initialStatus = task.getCurrentStatus(); task.changeStatus(newStatus, chainReceipt); - Optional savedTask = taskService.updateTask(task); + saveTask(task, initialStatus, newStatus); + } + /** + * Saves the task to the database. + * + * @param task The task + * @param currentStatus The current status in database + * @param newStatus The new status in database after save + */ + void saveTask(Task task, TaskStatus currentStatus, TaskStatus newStatus) { + Optional savedTask = taskService.updateTask(task); // `savedTask.isPresent()` should always be true if the task exists in the repository. if (savedTask.isPresent()) { + updateMetricsAfterStatusUpdate(currentStatus, newStatus); log.info("UpdateTaskStatus succeeded [chainTaskId:{}, currentStatus:{}, newStatus:{}]", task.getChainTaskId(), currentStatus, newStatus); - return savedTask.get(); } else { - log.warn("UpdateTaskStatus failed. Chain Task is probably unknown." + - " [chainTaskId:{}, currentStatus:{}, wishedStatus:{}]", + log.warn("UpdateTaskStatus failed. Chain Task is probably unknown [chainTaskId:{}, currentStatus:{}, wishedStatus:{}]", task.getChainTaskId(), currentStatus, newStatus); - return null; } } @@ -208,8 +270,7 @@ void received2Initializing(Task task) { Optional smsUrl = smsService.getVerifiedSmsUrl(task.getChainTaskId(), task.getTag()); if (smsUrl.isEmpty()) { log.error("Couldn't get verified SMS url [chainTaskId: {}]", task.getChainTaskId()); - updateTaskStatusAndSave(task, INITIALIZE_FAILED); - updateTaskStatusAndSave(task, FAILED); + toFailed(task, INITIALIZE_FAILED); return; } task.setSmsUrl(smsUrl.get()); //SMS URL source of truth for the task @@ -224,10 +285,9 @@ void received2Initializing(Task task) { task.getChainTaskId()); final Optional enclaveChallenge = smsService.getEnclaveChallenge(chainTaskId, task.getSmsUrl()); if (enclaveChallenge.isEmpty()) { - log.error("Can't initialize task, enclave challenge is empty" + - " [chainTaskId:{}]", chainTaskId); - updateTaskStatusAndSave(task, INITIALIZE_FAILED); - updateTask(chainTaskId); + log.error("Can't initialize task, enclave challenge is empty [chainTaskId:{}]", + chainTaskId); + toFailed(task, INITIALIZE_FAILED); return; } task.setEnclaveChallenge(enclaveChallenge.get()); @@ -237,8 +297,7 @@ void received2Initializing(Task task) { }, () -> { log.error("Failed to request initialize on blockchain [chainTaskId:{}]", task.getChainTaskId()); - updateTaskStatusAndSave(task, INITIALIZE_FAILED); - updateTaskStatusAndSave(task, FAILED); + toFailed(task, INITIALIZE_FAILED); }); } @@ -250,7 +309,7 @@ void initializing2Initialized(Task task) { blockchainAdapterService .isInitialized(task.getChainTaskId()) .ifPresentOrElse(isSuccess -> { - if (isSuccess != null && isSuccess) { + if (Boolean.TRUE.equals(isSuccess)) { log.info("Initialized on blockchain (tx mined) [chainTaskId:{}]", task.getChainTaskId()); //Without receipt, using deal block for initialization block @@ -261,8 +320,7 @@ void initializing2Initialized(Task task) { } log.error("Initialization failed on blockchain (tx reverted) [chainTaskId:{}]", task.getChainTaskId()); - updateTaskStatusAndSave(task, INITIALIZE_FAILED); - updateTaskStatusAndSave(task, FAILED); + toFailed(task, INITIALIZE_FAILED); }, () -> log.error("Unable to check initialization on blockchain " + "(likely too long), should use a detector [chainTaskId:{}]", task.getChainTaskId())); @@ -362,12 +420,11 @@ void running2Finalized2Completed(Task task) { } else if (nbReplicatesWithContributeAndFinalizeStatus > 1) { log.error("Too many replicates in ContributeAndFinalize status" + " [chainTaskId:{}, nbReplicates:{}]", chainTaskId, nbReplicatesWithContributeAndFinalizeStatus); - toFailed(task); + toFailed(task, RUNNING_FAILED); return; } - updateTaskStatusAndSave(task, FINALIZED); - finalizedToCompleted(task); + toFinalizedToCompleted(task); } void initializedOrRunning2ContributionTimeout(Task task) { @@ -376,8 +433,7 @@ void initializedOrRunning2ContributionTimeout(Task task) { boolean isNowAfterContributionDeadline = task.getContributionDeadline() != null && new Date().after(task.getContributionDeadline()); if (isInitializedOrRunningTask && isNowAfterContributionDeadline) { - updateTaskStatusAndSave(task, CONTRIBUTION_TIMEOUT); - updateTaskStatusAndSave(task, FAILED); + updateTaskStatusesAndSave(task, CONTRIBUTION_TIMEOUT, FAILED); applicationEventPublisher.publishEvent(new ContributionTimeoutEvent(task.getChainTaskId())); } } @@ -428,8 +484,7 @@ void running2RunningFailed(Task task) { // If all alive workers have failed on this task, its computation should be stopped. // It could denote that the task is wrong // - e.g. failing script, dataset can't be retrieved, app can't be downloaded, ... - updateTaskStatusAndSave(task, RUNNING_FAILED); - updateTaskStatusAndSave(task, FAILED); + updateTaskStatusesAndSave(task, RUNNING_FAILED, FAILED); applicationEventPublisher.publishEvent(new TaskRunningFailedEvent(task.getChainTaskId())); } @@ -550,9 +605,8 @@ void resultUploading2UploadTimeout(Task task) { && new Date().after(task.getFinalDeadline()); if (isTaskInResultUploading && isNowAfterFinalDeadline) { - updateTaskStatusAndSave(task, RESULT_UPLOAD_TIMEOUT); applicationEventPublisher.publishEvent(new ResultUploadTimeoutEvent(task.getChainTaskId())); - updateTaskStatusAndSave(task, FAILED); + toFailed(task, RESULT_UPLOAD_TIMEOUT); } } @@ -615,8 +669,7 @@ void resultUploaded2Finalizing(Task task) { }, () -> { log.error("Failed to request finalize on blockchain [chainTaskId:{}]", task.getChainTaskId()); - updateTaskStatusAndSave(task, FINALIZE_FAILED); - updateTaskStatusAndSave(task, FAILED); + toFailed(task, FINALIZE_FAILED); }); } @@ -624,20 +677,18 @@ void finalizing2Finalized2Completed(Task task) { blockchainAdapterService .isFinalized(task.getChainTaskId()) .ifPresentOrElse(isSuccess -> { - if (isSuccess != null && isSuccess) { - log.info("Finalized on blockchain (tx mined)" + - "[chainTaskId:{}]", task.getChainTaskId()); - updateTaskStatusAndSave(task, FINALIZED, null); - finalizedToCompleted(task); + if (Boolean.TRUE.equals(isSuccess)) { + log.info("Finalized on blockchain (tx mined) [chainTaskId:{}]", + task.getChainTaskId()); + toFinalizedToCompleted(task); return; } - log.error("Finalization failed on blockchain (tx reverted)" + - "[chainTaskId:{}]", task.getChainTaskId()); - updateTaskStatusAndSave(task, FINALIZE_FAILED); - updateTaskStatusAndSave(task, FAILED); + log.error("Finalization failed on blockchain (tx reverted) [chainTaskId:{}]", + task.getChainTaskId()); + toFailed(task, FINALIZE_FAILED); }, () -> log.error("Unable to check finalization on blockchain " + - "(likely too long), should use a detector " + - "[chainTaskId:{}]", task.getChainTaskId())); + "(likely too long), should use a detector [chainTaskId:{}]", + task.getChainTaskId())); } void finalizedToCompleted(Task task) { @@ -648,8 +699,46 @@ void finalizedToCompleted(Task task) { applicationEventPublisher.publishEvent(new TaskCompletedEvent(task)); } + void toFinalizedToCompleted(Task task) { + updateTaskStatusesAndSave(task, FINALIZED, COMPLETED); + applicationEventPublisher.publishEvent(new TaskCompletedEvent(task)); + } + void toFailed(Task task) { updateTaskStatusAndSave(task, FAILED); applicationEventPublisher.publishEvent(new TaskFailedEvent(task.getChainTaskId())); } + + void toFailed(Task task, TaskStatus reason) { + updateTaskStatusesAndSave(task, reason, FAILED); + applicationEventPublisher.publishEvent(new TaskFailedEvent(task.getChainTaskId())); + } + + void updateMetricsAfterStatusUpdate(TaskStatus previousStatus, TaskStatus newStatus) { + currentTaskStatusesCount.get(previousStatus).decrementAndGet(); + currentTaskStatusesCount.get(newStatus).incrementAndGet(); + publishTaskStatusesCountUpdate(); + } + + @EventListener(TaskCreatedEvent.class) + void onTaskCreatedEvent() { + currentTaskStatusesCount.get(RECEIVED).incrementAndGet(); + publishTaskStatusesCountUpdate(); + } + + private void publishTaskStatusesCountUpdate() { + // Copying the map here ensures the original values can't be updated from outside this class. + // As this data should be read only, no need for any atomic class. + final LinkedHashMap currentTaskStatusesCountToPublish = currentTaskStatusesCount + .entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entrySet -> entrySet.getValue().get(), + (a, b) -> b, + LinkedHashMap::new + )); + final TaskStatusesCountUpdatedEvent event = new TaskStatusesCountUpdatedEvent(currentTaskStatusesCountToPublish); + applicationEventPublisher.publishEvent(event); + } } diff --git a/src/main/java/com/iexec/core/task/update/TaskUpdateRequestManager.java b/src/main/java/com/iexec/core/task/update/TaskUpdateRequestManager.java index 3cda6d4a2..16ff8b80c 100644 --- a/src/main/java/com/iexec/core/task/update/TaskUpdateRequestManager.java +++ b/src/main/java/com/iexec/core/task/update/TaskUpdateRequestManager.java @@ -23,8 +23,8 @@ import org.springframework.stereotype.Component; import java.util.Optional; -import java.util.concurrent.*; -import java.util.function.Supplier; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import static com.iexec.common.chain.CategoriesUtils.LONGEST_TASK_TIMEOUT; @@ -42,7 +42,6 @@ public class TaskUpdateRequestManager { */ private static final int TASK_UPDATE_THREADS_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; - private final ExecutorService executorService = Executors.newFixedThreadPool(1); private final ContextualLockRunner taskExecutionLockRunner = new ContextualLockRunner<>(LONGEST_TASK_TIMEOUT.getSeconds(), TimeUnit.SECONDS); @@ -69,36 +68,36 @@ public TaskUpdateRequestManager(TaskService taskService, } /** - * Publish TaskUpdateRequest async - * @param chainTaskId - * @return + * Publish a TaskUpdateRequest if no request is already waiting for this task. + * This request will be dealt with asynchronously. + *

    + * As of now, we do sequential requests to the DB which can cause a big load. + * We should aim to have some batch requests to unload the scheduler. + * + * @param chainTaskId ID of the task to publish the request for. + * @return {@literal true} if request has been published, + * {@literal false} otherwise. */ - public CompletableFuture publishRequest(String chainTaskId) { - Supplier publishRequest = () -> { - if (chainTaskId.isEmpty()){ - return false; - } - if (queue.containsTask(chainTaskId)){ - log.debug("Request already published [chainTaskId:{}]", chainTaskId); - return false; - } - final Optional oTask = taskService.getTaskByChainTaskId(chainTaskId); - if (oTask.isEmpty()) { - log.warn("No such task. [chainTaskId: {}]", chainTaskId); - return false; - } + public synchronized boolean publishRequest(String chainTaskId) { + if (chainTaskId.isEmpty()) { + return false; + } + if (queue.containsTask(chainTaskId)) { + log.debug("Request already published [chainTaskId:{}]", chainTaskId); + return false; + } + final Optional oTask = taskService.getTaskByChainTaskId(chainTaskId); + if (oTask.isEmpty()) { + log.warn("No such task. [chainTaskId: {}]", chainTaskId); + return false; + } - final Task task = oTask.get(); - taskUpdateExecutor.execute(new TaskUpdate(task, this::updateTask)); - log.debug("Published task update request" + - " [chainTaskId:{}, currentStatus:{}, contributionDeadline:{}, queueSize:{}]", - chainTaskId, task.getChainTaskId(), task.getContributionDeadline(), queue.size()); - return true; - }; - // TODO: find a better way to publish request. - // As of now, we do sequential requests to the DB which can cause a big load. - // We should aim to have some batch requests to unload the scheduler. - return CompletableFuture.supplyAsync(publishRequest, executorService); + final Task task = oTask.get(); + taskUpdateExecutor.execute(new TaskUpdate(task, this::updateTask)); + log.debug("Published task update request" + + " [chainTaskId:{}, currentStatus:{}, contributionDeadline:{}, queueSize:{}]", + chainTaskId, task.getCurrentStatus(), task.getContributionDeadline(), queue.size()); + return true; } private void updateTask(String chainTaskId) { diff --git a/src/main/java/com/iexec/core/version/VersionController.java b/src/main/java/com/iexec/core/version/VersionController.java index 450b029bf..cfb9d7d21 100644 --- a/src/main/java/com/iexec/core/version/VersionController.java +++ b/src/main/java/com/iexec/core/version/VersionController.java @@ -16,22 +16,42 @@ package com.iexec.core.version; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Metrics; +import org.springframework.boot.info.BuildProperties; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import javax.annotation.PostConstruct; + @RestController public class VersionController { - private final VersionService versionService; + public static final String METRIC_INFO_GAUGE_NAME = "iexec.version.info"; + public static final String METRIC_INFO_GAUGE_DESC = "A metric to expose version and application name."; + public static final String METRIC_INFO_LABEL_APP_NAME = "iexecAppName"; + public static final String METRIC_INFO_LABEL_APP_VERSION = "iexecAppVersion"; + // Must be static final to avoid garbage collect and side effect on gauge + public static final int METRIC_VALUE = 1; + private final BuildProperties buildProperties; + + public VersionController(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } - public VersionController(VersionService versionService) { - this.versionService = versionService; + @PostConstruct + void initializeGaugeVersion() { + Gauge.builder(METRIC_INFO_GAUGE_NAME, METRIC_VALUE, n -> METRIC_VALUE) + .description(METRIC_INFO_GAUGE_DESC) + .tags(METRIC_INFO_LABEL_APP_VERSION, buildProperties.getVersion(), + METRIC_INFO_LABEL_APP_NAME, buildProperties.getName()) + .register(Metrics.globalRegistry); } @GetMapping("/version") public ResponseEntity getVersion() { - return ResponseEntity.ok(versionService.getVersion()); + return ResponseEntity.ok(buildProperties.getVersion()); } } diff --git a/src/main/java/com/iexec/core/worker/WorkerService.java b/src/main/java/com/iexec/core/worker/WorkerService.java index bddb3ecd2..3e0b19d99 100644 --- a/src/main/java/com/iexec/core/worker/WorkerService.java +++ b/src/main/java/com/iexec/core/worker/WorkerService.java @@ -18,13 +18,17 @@ import com.iexec.common.utils.ContextualLockRunner; import com.iexec.core.configuration.WorkerConfiguration; +import io.micrometer.core.instrument.Metrics; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import javax.annotation.PostConstruct; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import static com.iexec.common.utils.DateTimeUtils.addMinutesToDate; @@ -40,9 +44,15 @@ @Service public class WorkerService { + public static final String METRIC_WORKERS_GAUGE = "iexec.core.workers"; + public static final String METRIC_CPU_TOTAL_GAUGE = "iexec.core.cpu.total"; + public static final String METRIC_CPU_AVAILABLE_GAUGE = "iexec.core.cpu.available"; private final WorkerRepository workerRepository; private final WorkerConfiguration workerConfiguration; private final ContextualLockRunner contextualLockRunner; + private AtomicInteger aliveWorkersGauge; + private AtomicInteger aliveTotalCpuGauge; + private AtomicInteger aliveAvailableCpuGauge; @Getter private final ConcurrentHashMap workerStatsMap = new ConcurrentHashMap<>(); @@ -64,6 +74,38 @@ public WorkerService(WorkerRepository workerRepository, this.contextualLockRunner = new ContextualLockRunner<>(); } + @PostConstruct + void init() { + aliveWorkersGauge = Metrics.gauge(METRIC_WORKERS_GAUGE, new AtomicInteger(getAliveWorkers().size())); + aliveTotalCpuGauge = Metrics.gauge(METRIC_CPU_TOTAL_GAUGE, new AtomicInteger(getAliveTotalCpu())); + aliveAvailableCpuGauge = Metrics.gauge(METRIC_CPU_AVAILABLE_GAUGE, new AtomicInteger(getAliveAvailableCpu())); + } + + /** + * updateMetrics is used to update all workers metrics + */ + @Scheduled(fixedDelayString = "${cron.metrics.refresh.period}", initialDelayString = "${cron.metrics.refresh.period}") + void updateMetrics() { + // Fusion of methods getAliveTotalCpu and getAliveAvailableCpu to prevent making 3 calls to getAliveWorkers + int availableCpus = 0; + int totalCpus = 0; + List workers = getAliveWorkers(); + for (Worker worker : workers) { + if (worker.isGpuEnabled()) { + continue; + } + int workerCpuNb = worker.getCpuNb(); + int computingReplicateNb = worker.getComputingChainTaskIds().size(); + int availableCpu = workerCpuNb - computingReplicateNb; + totalCpus += workerCpuNb; + availableCpus += availableCpu; + } + + aliveWorkersGauge.set(workers.size()); + aliveTotalCpuGauge.set(totalCpus); + aliveAvailableCpuGauge.set(availableCpus); + } + // region Read methods public Optional getWorker(String walletAddress) { return workerRepository.findByWalletAddress(walletAddress); @@ -79,7 +121,8 @@ public boolean isAllowedToJoin(String workerAddress) { } public boolean isWorkerAllowedToAskReplicate(String walletAddress) { - Date lastReplicateDemandDate = workerStatsMap.get(walletAddress).getLastReplicateDemandDate(); + Date lastReplicateDemandDate = workerStatsMap.computeIfAbsent(walletAddress, WorkerStats::new) + .getLastReplicateDemandDate(); if (lastReplicateDemandDate == null) { return true; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e1b7e69d1..077a5aa16 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,21 +19,22 @@ mongock: cron: # all in milliseconds + metrics.refresh.period: 20000 #20s deal.replay: 60000 # 1m detector: - worker-lost: 30000 # 30s + worker-lost: 30000 # 30s chain: - unstarted-tx: 300000 # 5m - initialize: 30000 # 30s - contribute: 30000 # 30s - reveal: 30000 # 30s + unstarted-tx: 300000 # 5m + initialize: 30000 # 30s + contribute: 30000 # 30s + reveal: 30000 # 30s contribute-and-finalize: 30000 # 30s - finalize: 30000 # 30s - final-deadline: 30000 # 30s + finalize: 30000 # 30s + final-deadline: 30000 # 30s timeout: - contribute: 120000 # 2m + contribute: 120000 # 2m reveal: ${REVEAL_TIMEOUT_PERIOD:120000} # 2m - result-upload: 30000 # 30s + result-upload: 30000 # 30s workers: askForReplicatePeriod: ${IEXEC_ASK_REPLICATE_PERIOD:5000} diff --git a/src/test/java/com/iexec/core/chain/DealWatcherServiceTests.java b/src/test/java/com/iexec/core/chain/DealWatcherServiceTests.java index 0e3bf4fb0..35298f920 100644 --- a/src/test/java/com/iexec/core/chain/DealWatcherServiceTests.java +++ b/src/test/java/com/iexec/core/chain/DealWatcherServiceTests.java @@ -26,13 +26,22 @@ import com.iexec.core.task.Task; import com.iexec.core.task.TaskService; import com.iexec.core.task.event.TaskCreatedEvent; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.reactivex.Flowable; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.*; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import org.web3j.protocol.core.methods.response.Log; @@ -77,6 +86,11 @@ private IexecHubContract.SchedulerNoticeEventResponse createSchedulerNotice(BigI return schedulerNotice; } + @BeforeAll + static void initRegistry() { + Metrics.globalRegistry.add(new SimpleMeterRegistry()); + } + @BeforeEach void init() { MockitoAnnotations.openMocks(this); @@ -84,6 +98,29 @@ void init() { when(chainConfig.getPoolAddress()).thenReturn("0x1"); } + @AfterEach + void afterEach() { + Metrics.globalRegistry.clear(); + } + + @Test + void shouldReturnZeroForAllCountersWhereNothingHasAppended() { + Counter dealsCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_COUNT).counter(); + Counter dealsEventsCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_EVENTS_COUNT).counter(); + Counter dealsReplayCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_REPLAY_COUNT).counter(); + Counter lastBlockCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_LAST_BLOCK).counter(); + + Assertions.assertThat(dealsCounter).isNotNull(); + Assertions.assertThat(dealsEventsCounter).isNotNull(); + Assertions.assertThat(dealsReplayCounter).isNotNull(); + Assertions.assertThat(lastBlockCounter).isNotNull(); + + Assertions.assertThat(dealsCounter.count()).isZero(); + Assertions.assertThat(dealsEventsCounter.count()).isZero(); + Assertions.assertThat(dealsReplayCounter.count()).isZero(); + Assertions.assertThat(lastBlockCounter.count()).isZero(); + } + @Test void shouldRunAndStop() { BigInteger blockNumber = BigInteger.TEN; @@ -111,6 +148,16 @@ void shouldUpdateLastSeenBlockWhenOneDeal() { dealWatcherService.subscribeToDealEventFromOneBlockToLatest(from); verify(configurationService).setLastSeenBlockWithDeal(blockOfDeal); + Counter lastBlockCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_LAST_BLOCK).counter(); + Counter dealsCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_COUNT).counter(); + Counter dealsEventsCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_EVENTS_COUNT).counter(); + Assertions.assertThat(lastBlockCounter).isNotNull(); + Assertions.assertThat(dealsCounter).isNotNull(); + Assertions.assertThat(dealsEventsCounter).isNotNull(); + Assertions.assertThat(lastBlockCounter.count()).isEqualTo(blockOfDeal.doubleValue()); + Assertions.assertThat(dealsCounter.count()).isEqualTo(1); + Assertions.assertThat(dealsEventsCounter.count()).isEqualTo(1); + } @Test @@ -139,7 +186,7 @@ void shouldUpdateLastSeenBlockWhenOneDealAndCreateTask() { when(iexecHubService.getChainDeal(BytesUtils.bytesToString(schedulerNotice.dealid))).thenReturn(Optional.of(chainDeal)); when(iexecHubService.isBeforeContributionDeadline(chainDeal)).thenReturn(true); when(taskService.addTask(any(), anyInt(), anyLong(), any(), any(), anyInt(), anyLong(), any(), any(), any())) - .thenReturn(Optional.of(task)); + .thenReturn(Optional.of(task)); when(configurationService.getLastSeenBlockWithDeal()).thenReturn(from); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(TaskCreatedEvent.class); @@ -185,9 +232,9 @@ void shouldUpdateLastSeenBlockWhenOneDealAndNotCreateTaskSinceBotSizeIsZero() { IexecHubContract.SchedulerNoticeEventResponse schedulerNotice = createSchedulerNotice(blockOfDeal); ChainDeal chainDeal = ChainDeal.builder() - .botFirst(BigInteger.valueOf(0)) - .botSize(BigInteger.valueOf(0)) - .build(); + .botFirst(BigInteger.valueOf(0)) + .botSize(BigInteger.valueOf(0)) + .build(); when(iexecHubService.getDealEventObservable(any())).thenReturn(Flowable.just(schedulerNotice)); when(iexecHubService.getChainDeal(BytesUtils.bytesToString(schedulerNotice.dealid))).thenReturn(Optional.of(chainDeal)); @@ -206,9 +253,9 @@ void shouldUpdateLastSeenBlockWhenOneDealButNotCreateTaskSinceExceptionThrown() IexecHubContract.SchedulerNoticeEventResponse schedulerNotice = createSchedulerNotice(blockOfDeal); ChainDeal chainDeal = ChainDeal.builder() - .botFirst(BigInteger.valueOf(0)) - .botSize(BigInteger.valueOf(1)) - .build(); + .botFirst(BigInteger.valueOf(0)) + .botSize(BigInteger.valueOf(1)) + .build(); when(iexecHubService.getDealEventObservable(any())).thenReturn(Flowable.just(schedulerNotice)); when(iexecHubService.getChainDeal(BytesUtils.bytesToString(schedulerNotice.dealid))).thenReturn(Optional.of(chainDeal)); @@ -266,6 +313,12 @@ void shouldReplayAllEventInRange() { dealWatcherService.replayDealEvent(); verify(iexecHubService).getChainDeal(any()); + Counter lastBlockCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_LAST_BLOCK).counter(); + Counter dealsReplayCounter = Metrics.globalRegistry.find(DealWatcherService.METRIC_DEALS_REPLAY_COUNT).counter(); + Assertions.assertThat(lastBlockCounter).isNotNull(); + Assertions.assertThat(dealsReplayCounter).isNotNull(); + Assertions.assertThat(lastBlockCounter.count()).isEqualTo(blockOfDeal.doubleValue()); + Assertions.assertThat(dealsReplayCounter.count()).isEqualTo(1); } @Test diff --git a/src/test/java/com/iexec/core/chain/IexecHubServiceTests.java b/src/test/java/com/iexec/core/chain/IexecHubServiceTests.java index 41d40ded3..9c24882a6 100644 --- a/src/test/java/com/iexec/core/chain/IexecHubServiceTests.java +++ b/src/test/java/com/iexec/core/chain/IexecHubServiceTests.java @@ -1,14 +1,13 @@ package com.iexec.core.chain; -import com.iexec.commons.poco.chain.ChainReceipt; -import com.iexec.commons.poco.chain.ChainTask; -import com.iexec.commons.poco.chain.ChainTaskStatus; +import com.iexec.commons.poco.chain.*; import com.iexec.commons.poco.contract.generated.IexecHubContract; import com.iexec.commons.poco.utils.BytesUtils; import io.reactivex.Flowable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -29,7 +28,7 @@ class IexecHubServiceTests { - private static final String TRANSACTION_HASH = "transactionHash"; + private static final String TRANSACTION_HASH = "transactionHash"; @Mock private CredentialsService credentialsService; @@ -72,7 +71,41 @@ void shouldTaskNotBeInCompletedStatusOnChain() { assertThat(iexecHubService.isTaskInCompletedStatusOnChain(CHAIN_TASK_ID)).isFalse(); } - // region Get event blocks + // region check contribution status + @ParameterizedTest + @EnumSource(value = ChainContributionStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"CONTRIBUTED", "REVEALED"}) + void shouldBeContributed(ChainContributionStatus status) { + final ChainContribution chainContribution = ChainContribution.builder().status(status).build(); + when(iexecHubService.getChainContribution(anyString(), anyString())).thenReturn(Optional.of(chainContribution)); + assertThat(iexecHubService.isContributed(CHAIN_TASK_ID, WORKER_ADDRESS)).isTrue(); + } + + @ParameterizedTest + @EnumSource(value = ChainContributionStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"CONTRIBUTED", "REVEALED"}) + void shouldNotBeContributed(ChainContributionStatus status) { + final ChainContribution chainContribution = ChainContribution.builder().status(status).build(); + when(iexecHubService.getChainContribution(anyString(), anyString())).thenReturn(Optional.of(chainContribution)); + assertThat(iexecHubService.isContributed(CHAIN_TASK_ID, WORKER_ADDRESS)).isFalse(); + } + + @ParameterizedTest + @EnumSource(value = ChainContributionStatus.class, mode = EnumSource.Mode.INCLUDE, names = {"REVEALED"}) + void shouldBeRevealed(ChainContributionStatus status) { + final ChainContribution chainContribution = ChainContribution.builder().status(status).build(); + when(iexecHubService.getChainContribution(anyString(), anyString())).thenReturn(Optional.of(chainContribution)); + assertThat(iexecHubService.isRevealed(CHAIN_TASK_ID, WORKER_ADDRESS)).isTrue(); + } + + @ParameterizedTest + @EnumSource(value = ChainContributionStatus.class, mode = EnumSource.Mode.EXCLUDE, names = {"REVEALED"}) + void shouldNotBeRevealed(ChainContributionStatus status) { + final ChainContribution chainContribution = ChainContribution.builder().status(status).build(); + when(iexecHubService.getChainContribution(anyString(), anyString())).thenReturn(Optional.of(chainContribution)); + assertThat(iexecHubService.isRevealed(CHAIN_TASK_ID, WORKER_ADDRESS)).isFalse(); + } + // endregion + + // region get event blocks @Test void shouldGetContributionBlock() { final int fromBlock = 0; diff --git a/src/test/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfigTests.java b/src/test/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfigTests.java new file mode 100644 index 000000000..0093c010e --- /dev/null +++ b/src/test/java/com/iexec/core/chain/adapter/BlockchainAdapterClientConfigTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2021-2023 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.chain.adapter; + +import com.iexec.common.config.PublicChainConfig; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Slf4j +@ExtendWith(SpringExtension.class) +@EnableConfigurationProperties(value = BlockchainAdapterClientConfig.class) +@Testcontainers +class BlockchainAdapterClientConfigTests { + private static final int WIREMOCK_PORT = 8080; + + @Autowired + private PublicChainConfig chainConfig; + + @Container + static final GenericContainer wmServer = new GenericContainer<>("wiremock/wiremock:3.3.1") + .withClasspathResourceMapping("wiremock", "/home/wiremock", BindMode.READ_ONLY) + .withExposedPorts(WIREMOCK_PORT); + + @DynamicPropertySource + static void registerProperties(DynamicPropertyRegistry registry) { + registry.add("blockchain-adapter.protocol", () -> "http"); + registry.add("blockchain-adapter.host", () -> "localhost"); + registry.add("blockchain-adapter.port", () -> wmServer.getMappedPort(WIREMOCK_PORT)); + } + + @Test + void checkChainConfigInitialization() { + PublicChainConfig expectedConfig = PublicChainConfig.builder() + .chainId(255) + .chainNodeUrl("http://localhost:8545") + .blockTime(Duration.ofSeconds(5L)) + .sidechain(true) + .build(); + assertThat(chainConfig).isEqualTo(expectedConfig); + } + +} diff --git a/src/test/java/com/iexec/core/chain/adapter/BlockchainAdapterServiceTests.java b/src/test/java/com/iexec/core/chain/adapter/BlockchainAdapterServiceTests.java deleted file mode 100644 index 9ef110344..000000000 --- a/src/test/java/com/iexec/core/chain/adapter/BlockchainAdapterServiceTests.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2021 IEXEC BLOCKCHAIN TECH - * - * 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.iexec.core.chain.adapter; - -import com.iexec.blockchain.api.BlockchainAdapterApiClient; -import com.iexec.common.chain.adapter.CommandStatus; -import com.iexec.common.chain.adapter.args.TaskFinalizeArgs; -import com.iexec.common.config.PublicChainConfig; -import feign.FeignException; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -import static org.mockito.Mockito.*; - -class BlockchainAdapterServiceTests { - - public static final String CHAIN_TASK_ID = "CHAIN_TASK_ID"; - public static final String CHAIN_DEAL_ID = "CHAIN_DEAL_ID"; - public static final int TASK_INDEX = 0; - public static final String LINK = "link"; - public static final String CALLBACK = "callback"; - public static final int PERIOD = 10; - public static final int MAX_ATTEMPTS = 3; - - @Mock - private BlockchainAdapterApiClient blockchainAdapterClient; - - @InjectMocks - private BlockchainAdapterService blockchainAdapterService; - - @BeforeEach - void init() { - MockitoAnnotations.openMocks(this); - } - - //Initialize - - @Test - void requestInitialize() { - when(blockchainAdapterClient.requestInitializeTask(CHAIN_DEAL_ID, TASK_INDEX)) - .thenReturn(CHAIN_TASK_ID); - - Assertions.assertThat(blockchainAdapterService.requestInitialize(CHAIN_DEAL_ID, TASK_INDEX)) - .isPresent(); - } - - @Test - void requestInitializeFailedSinceNot200() { - when(blockchainAdapterClient.requestInitializeTask(CHAIN_DEAL_ID, TASK_INDEX)) - .thenThrow(FeignException.BadRequest.class); - - Assertions.assertThat(blockchainAdapterService.requestInitialize(CHAIN_DEAL_ID, TASK_INDEX)) - .isEmpty(); - } - - @Test - void requestInitializeFailedSinceNoBody() { - when(blockchainAdapterClient.requestInitializeTask(CHAIN_DEAL_ID, TASK_INDEX)) - .thenReturn(""); - - Assertions.assertThat(blockchainAdapterService.requestInitialize(CHAIN_DEAL_ID, TASK_INDEX)) - .isEmpty(); - } - - @Test - void isInitialized() { - when(blockchainAdapterClient.getStatusForInitializeTaskRequest(CHAIN_TASK_ID)) - .thenReturn(CommandStatus.SUCCESS); - Assertions.assertThat(blockchainAdapterService.isInitialized(CHAIN_TASK_ID)) - .isEqualTo(Optional.of(true)); - } - - // Finalize - - @Test - void requestFinalize() { - when(blockchainAdapterClient.requestFinalizeTask(CHAIN_TASK_ID, new TaskFinalizeArgs(LINK, CALLBACK))) - .thenReturn(CHAIN_TASK_ID); - - Assertions.assertThat(blockchainAdapterService.requestFinalize(CHAIN_TASK_ID, LINK, CALLBACK)) - .isPresent(); - } - - @Test - void requestFinalizeFailedSinceNot200() { - when(blockchainAdapterClient.requestFinalizeTask(CHAIN_TASK_ID, new TaskFinalizeArgs(LINK, CALLBACK))) - .thenThrow(FeignException.BadRequest.class); - - Assertions.assertThat(blockchainAdapterService.requestFinalize(CHAIN_TASK_ID, LINK, CALLBACK)) - .isEmpty(); - } - - @Test - void requestFinalizeFailedSinceNoBody() { - when(blockchainAdapterClient.requestFinalizeTask(CHAIN_TASK_ID, new TaskFinalizeArgs(LINK, CALLBACK))) - .thenReturn(""); - - Assertions.assertThat(blockchainAdapterService.requestFinalize(CHAIN_TASK_ID, LINK, CALLBACK)) - .isEmpty(); - } - - @Test - void isFinalized() { - when(blockchainAdapterClient.getStatusForFinalizeTaskRequest(CHAIN_TASK_ID)) - .thenReturn(CommandStatus.SUCCESS); - Assertions.assertThat(blockchainAdapterService.isFinalized(CHAIN_TASK_ID)) - .isEqualTo(Optional.of(true)); - } - - // Testing ability to pull & wait - - @Test - void isCommandCompletedWithSuccess() { - when(blockchainAdapterClient.getStatusForInitializeTaskRequest(CHAIN_TASK_ID)) - .thenReturn(CommandStatus.RECEIVED) - .thenReturn(CommandStatus.PROCESSING) - .thenReturn(CommandStatus.SUCCESS); - - Optional commandCompleted = blockchainAdapterService - .isCommandCompleted(blockchainAdapterClient::getStatusForInitializeTaskRequest, - CHAIN_TASK_ID, PERIOD, MAX_ATTEMPTS); - Assertions.assertThat(commandCompleted).isPresent(); - Assertions.assertThat(commandCompleted.get()).isTrue(); - } - - @Test - void isCommandCompletedWithFailure() { - when(blockchainAdapterClient.getStatusForInitializeTaskRequest(CHAIN_TASK_ID)) - .thenReturn(CommandStatus.RECEIVED) - .thenReturn(CommandStatus.PROCESSING) - .thenReturn(CommandStatus.FAILURE); - - Optional commandCompleted = blockchainAdapterService - .isCommandCompleted(blockchainAdapterClient::getStatusForInitializeTaskRequest, - CHAIN_TASK_ID, PERIOD, MAX_ATTEMPTS); - Assertions.assertThat(commandCompleted).isPresent(); - Assertions.assertThat(commandCompleted.get()).isFalse(); - } - - @Test - void isCommandCompletedWithMaxAttempts() { - when(blockchainAdapterClient.getStatusForInitializeTaskRequest(CHAIN_TASK_ID)) - .thenReturn(CommandStatus.PROCESSING); - Optional commandCompleted = blockchainAdapterService - .isCommandCompleted(blockchainAdapterClient::getStatusForInitializeTaskRequest, - CHAIN_TASK_ID, PERIOD, MAX_ATTEMPTS); - Assertions.assertThat(commandCompleted).isEmpty(); - } - - // region getPublicChainConfig - @Test - void shouldGetPublicChainConfigOnlyOnce() { - final PublicChainConfig expectedChainConfig = PublicChainConfig.builder().build(); - when(blockchainAdapterClient.getPublicChainConfig()) - .thenReturn(expectedChainConfig); - - final PublicChainConfig actualChainConfig = - blockchainAdapterService.getPublicChainConfig(); - - Assertions.assertThat(actualChainConfig).isEqualTo(expectedChainConfig); - Assertions.assertThat(blockchainAdapterService.getPublicChainConfig()) - .isEqualTo(expectedChainConfig); - - // When calling `blockchainAdapterService.getPublicChainConfig()` again, - // it should retrieve the cached value. - blockchainAdapterService.getPublicChainConfig(); - verify(blockchainAdapterClient, times(1)).getPublicChainConfig(); - } - - @Test - void shouldNotGetPublicChainConfigSinceNotFound() { - when(blockchainAdapterClient.getPublicChainConfig()) - .thenThrow(FeignException.NotFound.class); - - final PublicChainConfig actualChainConfig = - blockchainAdapterService.getPublicChainConfig(); - Assertions.assertThat(actualChainConfig).isNull(); - } - - // endregion -} \ No newline at end of file diff --git a/src/test/java/com/iexec/core/config/OpenApiConfigTest.java b/src/test/java/com/iexec/core/config/OpenApiConfigTest.java new file mode 100644 index 000000000..bdeb73c5f --- /dev/null +++ b/src/test/java/com/iexec/core/config/OpenApiConfigTest.java @@ -0,0 +1,41 @@ +package com.iexec.core.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@Import(ProjectInfoAutoConfiguration.class) +class OpenApiConfigTest { + + @Autowired + private BuildProperties buildProperties; + + private OpenApiConfig openApiConfig; + + @BeforeEach + void setUp() { + openApiConfig = new OpenApiConfig(buildProperties); + } + + @Test + void shouldReturnOpenAPIObjectWithCorrectInfo() { + OpenAPI api = openApiConfig.api(); + assertThat(api).isNotNull(). + extracting(OpenAPI::getInfo).isNotNull(). + extracting( + Info::getVersion, + Info::getTitle + ) + .containsExactly(buildProperties.getVersion(), OpenApiConfig.TITLE); + } +} diff --git a/src/test/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetectorTests.java b/src/test/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetectorTests.java index f429d42ba..03c0fc586 100644 --- a/src/test/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetectorTests.java +++ b/src/test/java/com/iexec/core/detector/replicate/ContributionAndFinalizationUnnotifiedDetectorTests.java @@ -1,10 +1,27 @@ +/* + * Copyright 2023-2024 IEXEC BLOCKCHAIN TECH + * + * 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.iexec.core.detector.replicate; import com.iexec.common.replicate.ReplicateStatus; import com.iexec.common.replicate.ReplicateStatusDetails; import com.iexec.common.replicate.ReplicateStatusUpdate; -import com.iexec.commons.poco.chain.ChainContributionStatus; import com.iexec.commons.poco.chain.ChainReceipt; +import com.iexec.commons.poco.task.TaskDescription; +import com.iexec.commons.poco.utils.BytesUtils; import com.iexec.core.chain.IexecHubService; import com.iexec.core.chain.Web3jService; import com.iexec.core.configuration.CronConfiguration; @@ -15,15 +32,18 @@ import com.iexec.core.task.TaskStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.*; +import org.springframework.test.util.ReflectionTestUtils; -import java.util.Arrays; +import java.math.BigInteger; import java.util.Collections; import static com.iexec.common.replicate.ReplicateStatus.*; import static com.iexec.common.replicate.ReplicateStatusModifier.WORKER; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; class ContributionAndFinalizationUnnotifiedDetectorTests { @@ -52,9 +72,24 @@ class ContributionAndFinalizationUnnotifiedDetectorTests { @BeforeEach void init() { MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(detector, "detectorRate", 1000); + when(iexecHubService.getTaskDescription(anyString())).thenReturn(TaskDescription.builder() + .trust(BigInteger.ONE) + .isTeeTask(true) + .callback(BytesUtils.EMPTY_ADDRESS) + .build()); } - // region Detector aggregator + private Replicate getReplicateWithStatus(ReplicateStatus replicateStatus) { + Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); + ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() + .modifier(WORKER).status(replicateStatus).build(); + replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + return replicate; + } + + // region detectOnChainChanges + /** * When running {@link ContributionAndFinalizationUnnotifiedDetector#detectOnChainChanges} 10 times, * {@link ReplicatesService#updateReplicateStatus(String, String, ReplicateStatus, ReplicateStatusDetails)} should be called 11 times: @@ -68,12 +103,9 @@ void shouldDetectBothChangesOnChain() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTE_AND_FINALIZE_ONGOING).modifier(WORKER).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - + Replicate replicate = getReplicateWithStatus(CONTRIBUTE_AND_FINALIZE_ONGOING); when(replicatesService.getReplicates(CHAIN_TASK_ID)).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, ChainContributionStatus.REVEALED)).thenReturn(true); + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getFinalizeBlock(CHAIN_TASK_ID, 0)) .thenReturn(ChainReceipt.builder() @@ -94,20 +126,19 @@ void shouldDetectBothChangesOnChain() { any(ReplicateStatusDetails.class) ); } + // endregion - //region detectOnchainDoneWhenOffchainOngoing (ContributeAndFinalizeOngoing) + // region detectOnchainDoneWhenOffchainOngoing (ContributeAndFinalizeOngoing) + @Test - void shouldDetectUnNotifiedContributeAndFinalizeDoneAfterContributeAndFinalizeOngoing() { + void shouldDetectMissedUpdateSinceOffChainOngoing() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTE_AND_FINALIZE_ONGOING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); + Replicate replicate = getReplicateWithStatus(CONTRIBUTE_AND_FINALIZE_ONGOING); when(replicatesService.getReplicates(CHAIN_TASK_ID)).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, ChainContributionStatus.REVEALED)).thenReturn(true); + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getFinalizeBlock(CHAIN_TASK_ID, 0)) .thenReturn(ChainReceipt.builder() @@ -128,53 +159,47 @@ void shouldDetectUnNotifiedContributeAndFinalizeDoneAfterContributeAndFinalizeOn } @Test - void shouldDetectUnNotifiedContributeAndFinalizeDoneSinceBeforeContributeAndFinalizeOngoing() { + void shouldNotDetectMissedUpdateSinceNotOffChainOngoing() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(COMPUTED).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); + Replicate replicate = getReplicateWithStatus(COMPUTED); when(replicatesService.getReplicates(CHAIN_TASK_ID)).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, ChainContributionStatus.REVEALED)).thenReturn(true); + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(true); detector.detectOnchainDoneWhenOffchainOngoing(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } @Test - void shouldNotDetectUnNotifiedContributeAndFinalizeDoneSinceNotFinalizedOnChain() { + void shouldNotDetectMissedUpdateSinceNotOnChainDone() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTE_AND_FINALIZE_ONGOING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); + Replicate replicate = getReplicateWithStatus(CONTRIBUTE_AND_FINALIZE_ONGOING); when(replicatesService.getReplicates(CHAIN_TASK_ID)).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, ChainContributionStatus.REVEALED)).thenReturn(false); + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(false); detector.detectOnchainDoneWhenOffchainOngoing(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + // endregion - //region detectOnchainDone (REVEALED) - @Test - void shouldDetectUnNotifiedContributeAndFinalizeOngoing() { - Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); + // region detectOnchainDone (REVEALED) - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTE_AND_FINALIZE_ONGOING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + @ParameterizedTest + @EnumSource(value = ReplicateStatus.class, names = {"COMPUTED", "CONTRIBUTE_AND_FINALIZE_ONGOING"}) + void shouldDetectMissedUpdateSinceOnChainDoneNotOffChainDone(ReplicateStatus replicateStatus) { + Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); + Replicate replicate = getReplicateWithStatus(replicateStatus); when(replicatesService.getReplicates(CHAIN_TASK_ID)).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, ChainContributionStatus.REVEALED)).thenReturn(true); + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getFinalizeBlock(CHAIN_TASK_ID, 0)).thenReturn(ChainReceipt.builder() .blockNumber(10L) @@ -193,21 +218,41 @@ void shouldDetectUnNotifiedContributeAndFinalizeOngoing() { } @Test - void shouldNotDetectUnNotifiedContributedSinceContributeAndFinalizeDone() { + void shouldNotDetectMissedUpdateSinceOnChainDoneAndOffChainDone() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTE_AND_FINALIZE_DONE).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); + Replicate replicate = getReplicateWithStatus(CONTRIBUTE_AND_FINALIZE_DONE); when(replicatesService.getReplicates(CHAIN_TASK_ID)).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(CHAIN_TASK_ID, WALLET_ADDRESS, ChainContributionStatus.REVEALED)).thenReturn(true); + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_ADDRESS)).thenReturn(true); detector.detectOnchainDone(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + + @Test + void shouldNotDetectMissedUpdateSinceOnChainDoneAndNotEligibleToContributeAndFinalize() { + when(iexecHubService.getTaskDescription(CHAIN_TASK_ID)).thenReturn( + TaskDescription.builder().trust(BigInteger.ONE).isTeeTask(true).callback("0x2").build()); + Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); + + Replicate replicate = getReplicateWithStatus(CONTRIBUTING); + when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); + when(iexecHubService.isContributed(any(), any())).thenReturn(true); + when(web3jService.getLatestBlockNumber()).thenReturn(11L); + when(iexecHubService.getContributionBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() + .blockNumber(10L) + .txHash("0xabcef") + .build()); + + detector.detectOnchainDone(); + + Mockito.verify(replicatesService, never()) + .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); + } + // endregion } diff --git a/src/test/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetectorTests.java b/src/test/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetectorTests.java index 26d99fcdc..3a9996987 100644 --- a/src/test/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetectorTests.java +++ b/src/test/java/com/iexec/core/detector/replicate/ContributionUnnotifiedDetectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import com.iexec.common.replicate.ReplicateStatusDetails; import com.iexec.common.replicate.ReplicateStatusUpdate; import com.iexec.commons.poco.chain.ChainReceipt; +import com.iexec.commons.poco.task.TaskDescription; +import com.iexec.commons.poco.utils.BytesUtils; import com.iexec.core.chain.IexecHubService; import com.iexec.core.chain.Web3jService; import com.iexec.core.configuration.CronConfiguration; @@ -30,21 +32,24 @@ import com.iexec.core.task.TaskStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.*; +import org.springframework.test.util.ReflectionTestUtils; -import java.util.Arrays; +import java.math.BigInteger; import java.util.Collections; import static com.iexec.common.replicate.ReplicateStatus.*; import static com.iexec.common.replicate.ReplicateStatusModifier.WORKER; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; class ContributionUnnotifiedDetectorTests { private final static String CHAIN_TASK_ID = "chainTaskId"; private final static String WALLET_ADDRESS = "0x1"; - private final static int DETECTOR_PERIOD = 1000; @Mock private TaskService taskService; @@ -68,9 +73,24 @@ class ContributionUnnotifiedDetectorTests { @BeforeEach void init() { MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(contributionDetector, "detectorRate", 1000); + when(iexecHubService.getTaskDescription(anyString())).thenReturn(TaskDescription.builder() + .trust(BigInteger.ONE) + .isTeeTask(true) + .callback("0x1") + .build()); } - // region Detector aggregator + private Replicate getReplicateWithStatus(ReplicateStatus replicateStatus) { + Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); + ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() + .modifier(WORKER).status(replicateStatus).build(); + replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + return replicate; + } + + // region detectOnChainChanges + /** * When running {@link ContributionUnnotifiedDetector#detectOnChainChanges} 10 times, * {@link ReplicatesService#updateReplicateStatus(String, String, ReplicateStatus, ReplicateStatusDetails)} should be called 11 times: @@ -84,12 +104,9 @@ void shouldDetectBothChangesOnChain() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTING).modifier(WORKER).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - + Replicate replicate = getReplicateWithStatus(CONTRIBUTING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isContributed(any(), any())).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getContributionBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() .blockNumber(10L) @@ -103,21 +120,19 @@ void shouldDetectBothChangesOnChain() { Mockito.verify(replicatesService, Mockito.times(11)) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + // endregion - //region detectOnchainDoneWhenOffchainOngoing (CONTRIBUTING) + // region detectOnchainDoneWhenOffchainOngoing (CONTRIBUTING) + @Test - void shouldDetectUnNotifiedContributedAfterContributing() { + void shouldDetectMissedUpdateSinceOffChainOngoing() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - when(cronConfiguration.getContribute()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(CONTRIBUTING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isContributed(any(), any())).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getContributionBlock(anyString(), anyString(), anyLong())) .thenReturn(ChainReceipt.builder().blockNumber(10L).txHash("0xabcef").build()); @@ -129,56 +144,47 @@ void shouldDetectUnNotifiedContributedAfterContributing() { } @Test - void shouldDetectUnNotifiedContributedAfterContributingSinceBeforeContributing() { + void shouldNotDetectMissedUpdateSinceNotOffChainOngoing() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(COMPUTED).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - when(cronConfiguration.getContribute()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(COMPUTED); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isContributed(any(), any())).thenReturn(true); contributionDetector.detectOnchainDoneWhenOffchainOngoing(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } @Test - void shouldNotDetectUnNotifiedContributedAfterContributingSinceNotContributedOnChain() { + void shouldNotDetectMissedUpdateSinceNotOnChainDone() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - // when(cronConfiguration.getContribute()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(CONTRIBUTING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(false); + when(iexecHubService.isContributed(any(), any())).thenReturn(false); contributionDetector.detectOnchainDoneWhenOffchainOngoing(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + // endregion // region detectOnchainDone (CONTRIBUTED) - @Test - void shouldDetectUnNotifiedContributed1() { - Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + @ParameterizedTest + @EnumSource(value = ReplicateStatus.class, names = {"COMPUTED", "CONTRIBUTING"}) + void shouldDetectMissedUpdateSinceOnChainDoneNotOffChainDone(ReplicateStatus replicateStatus) { + Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - when(cronConfiguration.getContribute()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(replicateStatus); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isContributed(any(), any())).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getContributionBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() .blockNumber(10L) @@ -192,45 +198,40 @@ void shouldDetectUnNotifiedContributed1() { } @Test - void shouldDetectUnNotifiedContributed2() { + void shouldNotDetectMissedUpdateSinceOnChainDoneAndOffChainDone() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - when(cronConfiguration.getContribute()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(CONTRIBUTED); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); - when(web3jService.getLatestBlockNumber()).thenReturn(11L); - when(iexecHubService.getContributionBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() - .blockNumber(10L) - .txHash("0xabcef") - .build()); - + when(iexecHubService.isContributed(any(), any())).thenReturn(true); contributionDetector.detectOnchainDone(); - Mockito.verify(replicatesService, Mockito.times(1))//Missed CONTRIBUTING & CONTRIBUTED + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } @Test - void shouldNotDetectUnNotifiedContributedSinceContributed() { + void shouldNotDetectMissedUpdateSinceOnChainDoneAndEligibleToContributeAndFinalize() { + when(iexecHubService.getTaskDescription(CHAIN_TASK_ID)).thenReturn( + TaskDescription.builder().trust(BigInteger.ONE).isTeeTask(true).callback(BytesUtils.EMPTY_ADDRESS).build()); Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(Arrays.asList(TaskStatus.INITIALIZED, TaskStatus.RUNNING))).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(CONTRIBUTED).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + when(taskService.findByCurrentStatus(TaskStatus.getWaitingContributionStatuses())).thenReturn(Collections.singletonList(task)); - when(cronConfiguration.getContribute()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(CONTRIBUTING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isContributed(any(), any())).thenReturn(true); + when(web3jService.getLatestBlockNumber()).thenReturn(11L); + when(iexecHubService.getContributionBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() + .blockNumber(10L) + .txHash("0xabcef") + .build()); + contributionDetector.detectOnchainDone(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + // endregion } diff --git a/src/test/java/com/iexec/core/detector/replicate/ReplicateResultUploadTimeoutDetectorTests.java b/src/test/java/com/iexec/core/detector/replicate/ReplicateResultUploadTimeoutDetectorTests.java index 9f7e3a471..4a335935a 100644 --- a/src/test/java/com/iexec/core/detector/replicate/ReplicateResultUploadTimeoutDetectorTests.java +++ b/src/test/java/com/iexec/core/detector/replicate/ReplicateResultUploadTimeoutDetectorTests.java @@ -20,7 +20,9 @@ import com.iexec.common.replicate.ReplicateStatusModifier; import com.iexec.core.replicate.Replicate; import com.iexec.core.replicate.ReplicatesService; -import com.iexec.core.task.*; +import com.iexec.core.task.Task; +import com.iexec.core.task.TaskService; +import com.iexec.core.task.TaskStatus; import com.iexec.core.task.TaskStatusChange; import com.iexec.core.task.update.TaskUpdateRequestManager; import org.junit.jupiter.api.BeforeEach; @@ -35,7 +37,7 @@ import java.util.Date; import java.util.Optional; -import static com.iexec.common.replicate.ReplicateStatus.*; +import static com.iexec.common.replicate.ReplicateStatus.RESULT_UPLOAD_FAILED; import static com.iexec.common.utils.DateTimeUtils.addMinutesToDate; import static org.mockito.Mockito.when; @@ -47,7 +49,7 @@ class ReplicateResultUploadTimeoutDetectorTests { @Mock private TaskService taskService; - + @Mock private ReplicatesService replicatesService; @@ -103,9 +105,9 @@ void shouldDetectOneReplicateWithResultUploadingLongAgo() { replicate1.updateStatus(ReplicateStatus.COMPUTED, ReplicateStatusModifier.WORKER); replicate1.updateStatus(ReplicateStatus.RESULT_UPLOADING, ReplicateStatusModifier.POOL_MANAGER); - TaskStatusChange change1 = new TaskStatusChange(fourMinutesAgo, TaskStatus.INITIALIZED); - TaskStatusChange change2 = new TaskStatusChange(threeMinutesAgo, TaskStatus.RUNNING); - TaskStatusChange change3 = new TaskStatusChange(twoMinutesAgo, TaskStatus.RESULT_UPLOADING); + TaskStatusChange change1 = TaskStatusChange.builder().date(fourMinutesAgo).status(TaskStatus.INITIALIZED).build(); + TaskStatusChange change2 = TaskStatusChange.builder().date(threeMinutesAgo).status(TaskStatus.RUNNING).build(); + TaskStatusChange change3 = TaskStatusChange.builder().date(twoMinutesAgo).status(TaskStatus.RESULT_UPLOADING).build(); task.setUploadingWorkerWalletAddress(WALLET_WORKER_1); task.setDateStatusList(Arrays.asList(change1, change2, change3)); @@ -138,9 +140,9 @@ void shouldNotDetectReplicatePreviouslyDetected() { // we suppose that the status has already been set in a previous detect replicate.updateStatus(ReplicateStatus.RESULT_UPLOAD_FAILED, ReplicateStatusModifier.POOL_MANAGER); - TaskStatusChange change1 = new TaskStatusChange(fourMinutesAgo, TaskStatus.INITIALIZED); - TaskStatusChange change2 = new TaskStatusChange(threeMinutesAgo, TaskStatus.RUNNING); - TaskStatusChange change3 = new TaskStatusChange(twoMinutesAgo, TaskStatus.RESULT_UPLOADING); + TaskStatusChange change1 = TaskStatusChange.builder().date(fourMinutesAgo).status(TaskStatus.INITIALIZED).build(); + TaskStatusChange change2 = TaskStatusChange.builder().date(threeMinutesAgo).status(TaskStatus.RUNNING).build(); + TaskStatusChange change3 = TaskStatusChange.builder().date(twoMinutesAgo).status(TaskStatus.RESULT_UPLOADING).build(); task.setUploadingWorkerWalletAddress(WALLET_WORKER_1); task.setDateStatusList(Arrays.asList(change1, change2, change3)); diff --git a/src/test/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetectorTests.java b/src/test/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetectorTests.java index cfe6a05bf..5b48741f8 100644 --- a/src/test/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetectorTests.java +++ b/src/test/java/com/iexec/core/detector/replicate/RevealUnnotifiedDetectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH + * Copyright 2020-2024 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import com.iexec.common.replicate.ReplicateStatusDetails; import com.iexec.common.replicate.ReplicateStatusUpdate; import com.iexec.commons.poco.chain.ChainReceipt; +import com.iexec.commons.poco.task.TaskDescription; import com.iexec.core.chain.IexecHubService; import com.iexec.core.chain.Web3jService; import com.iexec.core.configuration.CronConfiguration; @@ -30,20 +31,23 @@ import com.iexec.core.task.TaskStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.*; +import org.springframework.test.util.ReflectionTestUtils; import java.util.Collections; import static com.iexec.common.replicate.ReplicateStatus.*; import static com.iexec.common.replicate.ReplicateStatusModifier.WORKER; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; class RevealUnnotifiedDetectorTests { private final static String CHAIN_TASK_ID = "chainTaskId"; private final static String WALLET_ADDRESS = "0x1"; - private final static int DETECTOR_PERIOD = 1000; @Mock private TaskService taskService; @@ -67,9 +71,20 @@ class RevealUnnotifiedDetectorTests { @BeforeEach void init() { MockitoAnnotations.openMocks(this); + ReflectionTestUtils.setField(revealDetector, "detectorRate", 1000); + when(iexecHubService.getTaskDescription(anyString())).thenReturn(TaskDescription.builder().build()); } - // region Detector aggregator + private Replicate getReplicateWithStatus(ReplicateStatus replicateStatus) { + Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); + ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() + .modifier(WORKER).status(replicateStatus).build(); + replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); + return replicate; + } + + // region detectOnChainChanges + /** * When running {@link RevealUnnotifiedDetector#detectOnChainChanges} 10 times, * {@link ReplicatesService#updateReplicateStatus(String, String, ReplicateStatus, ReplicateStatusDetails)} should be called 11 times: @@ -83,12 +98,9 @@ void shouldDetectBothChangesOnChain() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(REVEALING).modifier(WORKER).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - + Replicate replicate = getReplicateWithStatus(REVEALING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isRevealed(any(), any())).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getRevealBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() .blockNumber(10L) @@ -102,21 +114,19 @@ void shouldDetectBothChangesOnChain() { Mockito.verify(replicatesService, Mockito.times(11)) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + // endregion - //region detectOnchainDoneWhenOffchainOngoing (REVEALING) + // region detectOnchainDoneWhenOffchainOngoing (REVEALING) + @Test - void shouldDetectUnNotifiedRevealedAfterRevealing() { + void shouldDetectMissedUpdateSinceOffChainOngoing() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().status(REVEALING).modifier(WORKER).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - - when(cronConfiguration.getReveal()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(REVEALING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isRevealed(any(), any())).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getRevealBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() .blockNumber(10L) @@ -130,54 +140,46 @@ void shouldDetectUnNotifiedRevealedAfterRevealing() { } @Test - void shouldDetectUnNotifiedRevealedAfterRevealingSinceBeforeRevealing() { + void shouldNotDetectMissedUpdateSinceNotOffChainOngoing() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().modifier(WORKER).status(CONTRIBUTING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - - when(cronConfiguration.getReveal()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(CONTRIBUTING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isRevealed(any(), any())).thenReturn(true); revealDetector.detectOnchainDoneWhenOffchainOngoing(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } @Test - void shouldNotDetectUnNotifiedRevealedAfterRevealingSinceNotRevealedOnChain() { + void shouldNotDetectMissedUpdateSinceNotOnChainDone() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().modifier(WORKER).status(REVEALING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - when(cronConfiguration.getReveal()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(REVEALING); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(false); + when(iexecHubService.isRevealed(any(), any())).thenReturn(false); revealDetector.detectOnchainDoneWhenOffchainOngoing(); - Mockito.verify(replicatesService, Mockito.times(0)) + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } + // endregion // region detectOnchainDone (REVEALED) - @Test - void shouldDetectUnNotifiedRevealed1() { + + @ParameterizedTest + @EnumSource(value = ReplicateStatus.class, names = {"CONTRIBUTED", "REVEALING"}) + void shouldDetectMissedUpdateSinceOnChainDoneNotOffChainDone(ReplicateStatus replicateStatus) { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().modifier(WORKER).status(REVEALING).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - - when(cronConfiguration.getReveal()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(replicateStatus); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); + when(iexecHubService.isRevealed(any(), any())).thenReturn(true); when(web3jService.getLatestBlockNumber()).thenReturn(11L); when(iexecHubService.getRevealBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() .blockNumber(10L) @@ -191,50 +193,18 @@ void shouldDetectUnNotifiedRevealed1() { } @Test - void shouldDetectUnNotifiedRevealed2() { + void shouldNotDetectMissedUpdateSinceOnChainDoneAndOffChainDone() { Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusDetails details = new ReplicateStatusDetails(10L); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() - .modifier(WORKER) - .status(CONTRIBUTED) - .details(details) - .build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - - when(cronConfiguration.getReveal()).thenReturn(DETECTOR_PERIOD); + Replicate replicate = getReplicateWithStatus(REVEALED); when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); - when(web3jService.getLatestBlockNumber()).thenReturn(11L); - when(iexecHubService.getRevealBlock(anyString(), anyString(), anyLong())).thenReturn(ChainReceipt.builder() - .blockNumber(10L) - .txHash("0xabcef") - .build()); - + when(iexecHubService.isRevealed(any(), any())).thenReturn(true); revealDetector.detectOnchainDone(); - Mockito.verify(replicatesService, Mockito.times(1))//Missed REVEALING & REVEALED + Mockito.verify(replicatesService, never()) .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } - @Test - void shouldNotDetectUnNotifiedRevealedSinceRevealed() { - Task task = Task.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(taskService.findByCurrentStatus(TaskStatus.getWaitingRevealStatuses())).thenReturn(Collections.singletonList(task)); - - Replicate replicate = new Replicate(WALLET_ADDRESS, CHAIN_TASK_ID); - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder().modifier(WORKER).status(REVEALED).build(); - replicate.setStatusUpdateList(Collections.singletonList(statusUpdate)); - - when(cronConfiguration.getReveal()).thenReturn(DETECTOR_PERIOD); - when(replicatesService.getReplicates(any())).thenReturn(Collections.singletonList(replicate)); - when(iexecHubService.isStatusTrueOnChain(any(), any(), any())).thenReturn(true); - revealDetector.detectOnchainDone(); - - Mockito.verify(replicatesService, Mockito.times(0)) - .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); - } // endregion } diff --git a/src/test/java/com/iexec/core/metric/MetricServiceTests.java b/src/test/java/com/iexec/core/metric/MetricServiceTests.java index bd6ccfd0e..79966819e 100644 --- a/src/test/java/com/iexec/core/metric/MetricServiceTests.java +++ b/src/test/java/com/iexec/core/metric/MetricServiceTests.java @@ -17,10 +17,11 @@ package com.iexec.core.metric; import com.iexec.core.chain.DealWatcherService; -import com.iexec.core.task.TaskService; import com.iexec.core.task.TaskStatus; +import com.iexec.core.task.event.TaskStatusesCountUpdatedEvent; import com.iexec.core.worker.Worker; import com.iexec.core.worker.WorkerService; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -28,6 +29,7 @@ import org.mockito.MockitoAnnotations; import java.math.BigInteger; +import java.util.LinkedHashMap; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -39,8 +41,6 @@ class MetricServiceTests { private DealWatcherService dealWatcherService; @Mock private WorkerService workerService; - @Mock - private TaskService taskService; @InjectMocks private MetricService metricService; @@ -52,30 +52,61 @@ void init() { @Test void shouldGetPlatformMetrics() { + final LinkedHashMap expectedCurrentTaskStatusesCount = createExpectedCurrentTaskStatusesCount(); + List aliveWorkers = List.of(new Worker()); when(workerService.getAliveWorkers()).thenReturn(aliveWorkers); when(workerService.getAliveTotalCpu()).thenReturn(1); when(workerService.getAliveAvailableCpu()).thenReturn(1); when(workerService.getAliveTotalGpu()).thenReturn(1); when(workerService.getAliveAvailableGpu()).thenReturn(1); - when(taskService.findByCurrentStatus(TaskStatus.COMPLETED)) - .thenReturn(List.of()); when(dealWatcherService.getDealEventsCount()).thenReturn(10L); when(dealWatcherService.getDealsCount()).thenReturn(8L); when(dealWatcherService.getReplayDealsCount()).thenReturn(2L); when(dealWatcherService.getLatestBlockNumberWithDeal()).thenReturn(BigInteger.valueOf(255L)); PlatformMetric metric = metricService.getPlatformMetrics(); - assertThat(metric.getAliveWorkers()).isEqualTo(aliveWorkers.size()); - assertThat(metric.getAliveTotalCpu()).isEqualTo(1); - assertThat(metric.getAliveAvailableCpu()).isEqualTo(1); - assertThat(metric.getAliveTotalGpu()).isEqualTo(1); - assertThat(metric.getAliveAvailableGpu()).isEqualTo(1); - assertThat(metric.getCompletedTasks()).isZero(); - assertThat(metric.getDealEventsCount()).isEqualTo(10); - assertThat(metric.getDealsCount()).isEqualTo(8); - assertThat(metric.getReplayDealsCount()).isEqualTo(2); - assertThat(metric.getLatestBlockNumberWithDeal()).isEqualTo(255); + Assertions.assertAll( + () -> assertThat(metric.getAliveWorkers()).isEqualTo(aliveWorkers.size()), + () -> assertThat(metric.getAliveTotalCpu()).isEqualTo(1), + () -> assertThat(metric.getAliveAvailableCpu()).isEqualTo(1), + () -> assertThat(metric.getAliveTotalGpu()).isEqualTo(1), + () -> assertThat(metric.getAliveAvailableGpu()).isEqualTo(1), + () -> assertThat(metric.getCurrentTaskStatusesCount()).isEqualTo(expectedCurrentTaskStatusesCount), + () -> assertThat(metric.getDealEventsCount()).isEqualTo(10), + () -> assertThat(metric.getDealsCount()).isEqualTo(8), + () -> assertThat(metric.getReplayDealsCount()).isEqualTo(2), + () -> assertThat(metric.getLatestBlockNumberWithDeal()).isEqualTo(255) + ); + } + + private LinkedHashMap createExpectedCurrentTaskStatusesCount() { + final LinkedHashMap expectedCurrentTaskStatusesCount = new LinkedHashMap<>(TaskStatus.values().length); + expectedCurrentTaskStatusesCount.put(TaskStatus.RECEIVED, 1L); + expectedCurrentTaskStatusesCount.put(TaskStatus.INITIALIZING, 2L); + expectedCurrentTaskStatusesCount.put(TaskStatus.INITIALIZED, 3L); + expectedCurrentTaskStatusesCount.put(TaskStatus.INITIALIZE_FAILED, 4L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RUNNING, 5L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RUNNING_FAILED, 6L); + expectedCurrentTaskStatusesCount.put(TaskStatus.CONTRIBUTION_TIMEOUT, 7L); + expectedCurrentTaskStatusesCount.put(TaskStatus.CONSENSUS_REACHED, 8L); + expectedCurrentTaskStatusesCount.put(TaskStatus.REOPENING, 9L); + expectedCurrentTaskStatusesCount.put(TaskStatus.REOPENED, 10L); + expectedCurrentTaskStatusesCount.put(TaskStatus.REOPEN_FAILED, 11L); + expectedCurrentTaskStatusesCount.put(TaskStatus.AT_LEAST_ONE_REVEALED, 12L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RESULT_UPLOADING, 13L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RESULT_UPLOADED, 14L); + expectedCurrentTaskStatusesCount.put(TaskStatus.RESULT_UPLOAD_TIMEOUT, 15L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINALIZING, 16L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINALIZED, 17L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINALIZE_FAILED, 18L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FINAL_DEADLINE_REACHED, 19L); + expectedCurrentTaskStatusesCount.put(TaskStatus.COMPLETED, 20L); + expectedCurrentTaskStatusesCount.put(TaskStatus.FAILED, 21L); + + metricService.onTaskStatusesCountUpdateEvent(new TaskStatusesCountUpdatedEvent(expectedCurrentTaskStatusesCount)); + + return expectedCurrentTaskStatusesCount; } } diff --git a/src/test/java/com/iexec/core/replicate/ReplicateControllerTests.java b/src/test/java/com/iexec/core/replicate/ReplicateControllerTests.java index 34f9a4d9e..25113f681 100644 --- a/src/test/java/com/iexec/core/replicate/ReplicateControllerTests.java +++ b/src/test/java/com/iexec/core/replicate/ReplicateControllerTests.java @@ -54,7 +54,7 @@ class ReplicateControllerTests { private static final ReplicateTaskSummary REPLICATE_TASK_SUMMARY = ReplicateTaskSummary.builder() .workerpoolAuthorization(AUTH) .smsUrl(SMS_URL) - .build(); + .build(); private static final ReplicateStatusUpdate UPDATE = ReplicateStatusUpdate.builder() .status(ReplicateStatus.STARTED) .build(); @@ -229,11 +229,11 @@ void shouldUpdateReplicate() { .thenReturn(true); when(replicatesService.computeUpdateReplicateStatusArgs(CHAIN_TASK_ID, WALLET_ADDRESS, UPDATE)) .thenReturn(UPDATE_ARGS); - when(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_ADDRESS, UPDATE, UPDATE_ARGS)) + when(replicatesService.canUpdateReplicateStatus(new Replicate(CHAIN_TASK_ID, WALLET_ADDRESS), UPDATE, UPDATE_ARGS)) .thenReturn(ReplicateStatusUpdateError.NO_ERROR); when(replicatesService.updateReplicateStatus(CHAIN_TASK_ID, WALLET_ADDRESS, UPDATE, UPDATE_ARGS)) .thenReturn(Either.right(TaskNotificationType.PLEASE_DOWNLOAD_APP)); - + ResponseEntity response = replicatesController.updateReplicateStatus(TOKEN, CHAIN_TASK_ID, UPDATE); @@ -258,7 +258,7 @@ void shouldUpdateReplicateAndSetWalletAddress() { .thenReturn(true); when(replicatesService.computeUpdateReplicateStatusArgs(CHAIN_TASK_ID, WALLET_ADDRESS, updateWithLogs)) .thenReturn(UPDATE_ARGS); - when(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_ADDRESS, updateWithLogs, UPDATE_ARGS)) + when(replicatesService.canUpdateReplicateStatus(new Replicate(CHAIN_TASK_ID, WALLET_ADDRESS), updateWithLogs, UPDATE_ARGS)) .thenReturn(ReplicateStatusUpdateError.NO_ERROR); when(replicatesService.updateReplicateStatus(CHAIN_TASK_ID, WALLET_ADDRESS, updateWithLogs, UPDATE_ARGS)) .thenReturn(Either.right((TaskNotificationType.PLEASE_DOWNLOAD_APP))); @@ -279,7 +279,7 @@ void shouldUpdateReplicateAndSetWalletAddress() { void shouldNotUpdateReplicateSinceUnauthorized() { when(jwtTokenProvider.getWalletAddressFromBearerToken(TOKEN)) .thenReturn(""); - + ResponseEntity response = replicatesController.updateReplicateStatus(TOKEN, CHAIN_TASK_ID, UPDATE); @@ -321,7 +321,7 @@ void shouldReturnPleaseAbortSinceCantUpdate(ReplicateStatusUpdateError error) { .thenReturn(UPDATE_ARGS); when(replicatesService.updateReplicateStatus(CHAIN_TASK_ID, WALLET_ADDRESS, UPDATE, UPDATE_ARGS)) .thenReturn(Either.left(error)); - + ResponseEntity response = replicatesController.updateReplicateStatus(TOKEN, CHAIN_TASK_ID, UPDATE); diff --git a/src/test/java/com/iexec/core/replicate/ReplicateModelTest.java b/src/test/java/com/iexec/core/replicate/ReplicateModelTests.java similarity index 70% rename from src/test/java/com/iexec/core/replicate/ReplicateModelTest.java rename to src/test/java/com/iexec/core/replicate/ReplicateModelTests.java index 47502e52d..4dca4fea5 100644 --- a/src/test/java/com/iexec/core/replicate/ReplicateModelTest.java +++ b/src/test/java/com/iexec/core/replicate/ReplicateModelTests.java @@ -26,10 +26,7 @@ import java.util.Collections; import java.util.List; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class ReplicateModelTest { +class ReplicateModelTests { public static final String CHAIN_TASK_ID = "task"; public static final String WALLET_ADDRESS = "wallet"; @@ -40,6 +37,7 @@ class ReplicateModelTest { .build(); public static final ReplicateStatusUpdate STATUS_UPDATE = ReplicateStatusUpdate.builder() .details(STATUS_UPDATE_DETAILS) + .status(CURRENT_STATUS) .build(); public static final List STATUS_UPDATE_LIST = Collections.singletonList(STATUS_UPDATE); public static final String RESULT_LINK = "link"; @@ -48,16 +46,10 @@ class ReplicateModelTest { @Test void shouldConvertFromEntityToDto() { - Replicate entity = mock(Replicate.class); - when(entity.getChainTaskId()).thenReturn(CHAIN_TASK_ID); - when(entity.getWalletAddress()).thenReturn(WALLET_ADDRESS); - when(entity.getCurrentStatus()).thenReturn(CURRENT_STATUS); - when(entity.getStatusUpdateList()).thenReturn(STATUS_UPDATE_LIST); - when(entity.getResultLink()).thenReturn(RESULT_LINK); - when(entity.getChainCallbackData()).thenReturn(CHAIN_CALLBACK_DATA); - when(entity.getContributionHash()).thenReturn(CONTRIBUTION_HASH); - - ReplicateModel dto = ReplicateModel.fromEntity(entity); + Replicate entity = createReplicate(); + entity.setStatusUpdateList(STATUS_UPDATE_LIST); + + ReplicateModel dto = entity.generateModel(); Assertions.assertEquals(entity.getChainTaskId(), dto.getChainTaskId()); Assertions.assertEquals(entity.getWalletAddress(), dto.getWalletAddress()); Assertions.assertEquals(entity.getCurrentStatus(), dto.getCurrentStatus()); @@ -81,21 +73,16 @@ void shouldNotConvertFromEntityToDtoSinceMultipleAppExitCode() { final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .details(statusUpdateDetails) + .status(CURRENT_STATUS) .build(); final List statusUpdateList = List.of(statusUpdate, statusUpdate); - Replicate entity = mock(Replicate.class); - when(entity.getChainTaskId()).thenReturn(CHAIN_TASK_ID); - when(entity.getWalletAddress()).thenReturn(WALLET_ADDRESS); - when(entity.getCurrentStatus()).thenReturn(CURRENT_STATUS); - when(entity.getStatusUpdateList()).thenReturn(statusUpdateList); - when(entity.getResultLink()).thenReturn(RESULT_LINK); - when(entity.getChainCallbackData()).thenReturn(CHAIN_CALLBACK_DATA); - when(entity.getContributionHash()).thenReturn(CONTRIBUTION_HASH); + Replicate entity = createReplicate(); + entity.setStatusUpdateList(statusUpdateList); final MultipleOccurrencesOfFieldNotAllowed exception = Assertions - .assertThrows(MultipleOccurrencesOfFieldNotAllowed.class, () -> ReplicateModel.fromEntity(entity)); + .assertThrows(MultipleOccurrencesOfFieldNotAllowed.class, entity::generateModel); Assertions.assertEquals("exitCode", exception.getFieldName()); } @@ -107,21 +94,26 @@ void shouldNotConvertFromEntityToDtoSinceMultipleTeeError() { final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .details(statusUpdateDetails) + .status(CURRENT_STATUS) .build(); final List statusUpdateList = List.of(statusUpdate, statusUpdate); - Replicate entity = mock(Replicate.class); - when(entity.getChainTaskId()).thenReturn(CHAIN_TASK_ID); - when(entity.getWalletAddress()).thenReturn(WALLET_ADDRESS); - when(entity.getCurrentStatus()).thenReturn(CURRENT_STATUS); - when(entity.getStatusUpdateList()).thenReturn(statusUpdateList); - when(entity.getResultLink()).thenReturn(RESULT_LINK); - when(entity.getChainCallbackData()).thenReturn(CHAIN_CALLBACK_DATA); - when(entity.getContributionHash()).thenReturn(CONTRIBUTION_HASH); + Replicate entity = createReplicate(); + entity.setStatusUpdateList(statusUpdateList); final MultipleOccurrencesOfFieldNotAllowed exception = Assertions - .assertThrows(MultipleOccurrencesOfFieldNotAllowed.class, () -> ReplicateModel.fromEntity(entity)); + .assertThrows(MultipleOccurrencesOfFieldNotAllowed.class, entity::generateModel); Assertions.assertEquals("teeSessionGenerationError", exception.getFieldName()); } -} \ No newline at end of file + + private Replicate createReplicate() { + Replicate replicate = new Replicate(); + replicate.setChainTaskId(CHAIN_TASK_ID); + replicate.setWalletAddress(WALLET_ADDRESS); + replicate.setResultLink(RESULT_LINK); + replicate.setChainCallbackData(CHAIN_CALLBACK_DATA); + replicate.setContributionHash(CONTRIBUTION_HASH); + return replicate; + } +} diff --git a/src/test/java/com/iexec/core/replicate/ReplicateServiceTests.java b/src/test/java/com/iexec/core/replicate/ReplicateServiceTests.java index 790c56dbd..7b1413521 100644 --- a/src/test/java/com/iexec/core/replicate/ReplicateServiceTests.java +++ b/src/test/java/com/iexec/core/replicate/ReplicateServiceTests.java @@ -18,7 +18,6 @@ import com.iexec.common.replicate.*; import com.iexec.commons.poco.chain.ChainContribution; -import com.iexec.commons.poco.chain.ChainContributionStatus; import com.iexec.commons.poco.notification.TaskNotificationType; import com.iexec.commons.poco.task.TaskDescription; import com.iexec.commons.poco.utils.BytesUtils; @@ -337,6 +336,7 @@ void shouldGetCorrectNbReplicatesContainingMultipleStatus() { assertThat(shouldBe0).isZero(); } + // region getRandomReplicateWithRevealStatus @Test void shouldGetReplicateWithRevealStatus() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); @@ -385,9 +385,11 @@ void shouldNotGetReplicateWithRevealStatusWithNonEmptyList() { Optional optional = replicatesService.getRandomReplicateWithRevealStatus(CHAIN_TASK_ID); assertThat(optional).isEmpty(); } + //endregion + //region updateReplicateStatus @Test - void shouldUpdateReplicateStatusWithoutStdout(){ + void shouldUpdateReplicateStatusWithoutStdout() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); @@ -426,7 +428,7 @@ void shouldUpdateReplicateStatusWithoutStdout(){ } @Test - void shouldUpdateReplicateStatusWithStdoutIfComputed(){ + void shouldUpdateReplicateStatusWithStdoutIfComputed() { String stdout = "This is an stdout message !"; Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(COMPUTING, ReplicateStatusModifier.WORKER); @@ -456,7 +458,7 @@ void shouldUpdateReplicateStatusWithStdoutIfComputed(){ } @Test - void shouldUpdateReplicateStatusWithStdoutIfAppComputeFailed(){ + void shouldUpdateReplicateStatusWithStdoutIfAppComputeFailed() { String stdout = "This is an stdout message !"; Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(COMPUTING, ReplicateStatusModifier.WORKER); @@ -489,7 +491,7 @@ void shouldUpdateReplicateStatusWithStdoutIfAppComputeFailed(){ } @Test - void shouldNotUpdateReplicateStatusSinceNoReplicateList(){ + void shouldNotUpdateReplicateStatusSinceNoReplicateList() { when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.empty()); replicatesService.updateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, new ReplicateStatusUpdate(REVEALING)); @@ -500,7 +502,7 @@ void shouldNotUpdateReplicateStatusSinceNoReplicateList(){ } @Test - void shouldNotUpdateReplicateStatusSinceNoMatchingReplicate(){ + void shouldNotUpdateReplicateStatusSinceNoMatchingReplicate() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTED, ReplicateStatusModifier.WORKER); ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); @@ -541,7 +543,7 @@ void shouldNotUpdateReplicateStatusSinceInvalidWorkflowTransition() { } @Test - void shouldNotUpdateReplicateStatusSinceWrongOnChainStatus(){ + void shouldNotUpdateReplicateStatusSinceWrongOnChainStatus() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); @@ -712,8 +714,8 @@ void shouldNotSetContributionHashSinceRevealing() { when(web3jService.isBlockAvailable(anyLong())).thenReturn(true); when(iexecHubService.repeatIsRevealedTrue(anyString(), anyString())).thenReturn(true); when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_WORKER_1)).thenReturn(Optional.of(ChainContribution.builder() - .resultHash("hash") - .build())); + .resultHash("hash") + .build())); when(replicatesRepository.save(replicatesList)).thenReturn(replicatesList); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ReplicateUpdatedEvent.class); @@ -766,8 +768,9 @@ void shouldUpdateToResultUploaded() { assertThat(capturedEvent.getWalletAddress()).isEqualTo(WALLET_WORKER_1); assertThat(capturedEvent.getReplicateStatusUpdate().getStatus()).isEqualTo(RESULT_UPLOADED); } + // endregion - // getReplicateWithResultUploadedStatus + // region getReplicateWithResultUploadedStatus @Test void should() { @@ -785,10 +788,11 @@ void should() { .getReplicateWithResultUploadedStatus(CHAIN_TASK_ID) .get() .getWalletAddress()) - .isEqualTo(WALLET_WORKER_2); + .isEqualTo(WALLET_WORKER_2); } + // endregion - // isResultUploaded + // region isResultUploaded @Test void shouldCheckResultServiceAndReturnTrue() { @@ -861,18 +865,13 @@ void shouldReturnTrueForTeeTask() { assertThat(isResultUploaded).isTrue(); verify(resultService, never()).isResultUploaded(CHAIN_TASK_ID); } + // endregion // didReplicateContributeOnchain @Test void shouldReturnFindReplicateContributedOnchain() { - when( - iexecHubService.isStatusTrueOnChain( - CHAIN_TASK_ID, - WALLET_WORKER_1, - ChainContributionStatus.CONTRIBUTED - ) - ).thenReturn(true); + when(iexecHubService.isContributed(CHAIN_TASK_ID, WALLET_WORKER_1)).thenReturn(true); assertThat( replicatesService.didReplicateContributeOnchain( CHAIN_TASK_ID, @@ -884,14 +883,8 @@ void shouldReturnFindReplicateContributedOnchain() { // didReplicateRevealOnchain @Test - void shouldFindReplicatedReveledOnchain() { - when( - iexecHubService.isStatusTrueOnChain( - CHAIN_TASK_ID, - WALLET_WORKER_1, - ChainContributionStatus.REVEALED - ) - ).thenReturn(true); + void shouldFindReplicatedRevealedOnchain() { + when(iexecHubService.isRevealed(CHAIN_TASK_ID, WALLET_WORKER_1)).thenReturn(true); assertThat( replicatesService.didReplicateRevealOnchain( CHAIN_TASK_ID, @@ -912,56 +905,33 @@ void shouldSetTriggerReplicateUpdateIfRevealTimeout() { verify(replicatesRepository).findByChainTaskId(CHAIN_TASK_ID); } - // canUpdateReplicateStatus + // region canUpdateReplicateStatus @Test void shouldAuthorizeUpdate() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTED, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(REVEALING) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, UPDATE_ARGS)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, UPDATE_ARGS)) .isEqualTo(ReplicateStatusUpdateError.NO_ERROR); } - @Test - void shouldNotAuthorizeUpdateSinceNoMatchingReplicate() { - Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); - replicate.updateStatus(CONTRIBUTED, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - - ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() - .modifier(WORKER) - .status(REVEALING) - .build(); - - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_2, statusUpdate, UPDATE_ARGS)) - .isEqualTo(ReplicateStatusUpdateError.UNKNOWN_REPLICATE); - } - @Test void shouldNotAuthorizeUpdateSinceAlreadyReported() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTED, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTED) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, UPDATE_ARGS)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, UPDATE_ARGS)) .isEqualTo(ReplicateStatusUpdateError.ALREADY_REPORTED); } @@ -969,16 +939,13 @@ void shouldNotAuthorizeUpdateSinceAlreadyReported() { void shouldNotAuthorizeUpdateSinceBadWorkflowTransition() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(REVEALING, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTED) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, UPDATE_ARGS)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, UPDATE_ARGS)) .isEqualTo(ReplicateStatusUpdateError.BAD_WORKFLOW_TRANSITION); } @@ -986,16 +953,13 @@ void shouldNotAuthorizeUpdateSinceBadWorkflowTransition() { void shouldNotAuthorizeUpdateSinceContributeFailed() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTE_FAILED) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, UPDATE_ARGS)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, UPDATE_ARGS)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1003,16 +967,13 @@ void shouldNotAuthorizeUpdateSinceContributeFailed() { void shouldNotAuthorizeUpdateSinceRevealFailed() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(REVEALING, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(REVEAL_FAILED) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, UPDATE_ARGS)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, UPDATE_ARGS)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1020,9 +981,6 @@ void shouldNotAuthorizeUpdateSinceRevealFailed() { void shouldAuthorizeUpdateOnResultUploadFailed() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) @@ -1033,7 +991,7 @@ void shouldAuthorizeUpdateOnResultUploadFailed() { .taskDescription(TaskDescription.builder().build()) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateReplicateStatusArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateReplicateStatusArgs)) .isEqualTo(ReplicateStatusUpdateError.NO_ERROR); } @@ -1041,9 +999,6 @@ void shouldAuthorizeUpdateOnResultUploadFailed() { void shouldNotAuthorizeUpdateOnResultUploadFailedSinceResultUploadedWithCallback() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) @@ -1054,7 +1009,7 @@ void shouldNotAuthorizeUpdateOnResultUploadFailedSinceResultUploadedWithCallback .taskDescription(TaskDescription.builder().callback("callback").build()) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateReplicateStatusArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateReplicateStatusArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1062,9 +1017,6 @@ void shouldNotAuthorizeUpdateOnResultUploadFailedSinceResultUploadedWithCallback void shouldNotAuthorizeUpdateOnResultUploadFailedSinceResultUploadedWithTee() { Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); - - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) @@ -1075,7 +1027,7 @@ void shouldNotAuthorizeUpdateOnResultUploadFailedSinceResultUploadedWithTee() { .taskDescription(TaskDescription.builder().isTeeTask(true).build()) .build(); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateReplicateStatusArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateReplicateStatusArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1084,16 +1036,14 @@ void shouldNotAuthorizeUpdateOnContributedSinceNoBlockAvailable() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTED) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(web3jService.isBlockAvailable(anyLong())).thenReturn(false); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, UPDATE_ARGS)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, UPDATE_ARGS)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1102,7 +1052,6 @@ void shouldNotAuthorizeUpdateOnContributedSinceWorkerWeightNotValid() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTED) @@ -1112,11 +1061,10 @@ void shouldNotAuthorizeUpdateOnContributedSinceWorkerWeightNotValid() { .workerWeight(0) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(web3jService.isBlockAvailable(anyLong())).thenReturn(true); when(iexecHubService.repeatIsContributedTrue(anyString(), anyString())).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1125,7 +1073,6 @@ void shouldNotAuthorizeUpdateOnContributedSinceNoChainContribution() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTED) @@ -1136,11 +1083,10 @@ void shouldNotAuthorizeUpdateOnContributedSinceNoChainContribution() { .chainContribution(null) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(web3jService.isBlockAvailable(anyLong())).thenReturn(true); when(iexecHubService.repeatIsContributedTrue(anyString(), anyString())).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1149,7 +1095,6 @@ void shouldNotAuthorizeUpdateOnContributedSinceNoChainContributionResultHash() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTED) @@ -1160,11 +1105,10 @@ void shouldNotAuthorizeUpdateOnContributedSinceNoChainContributionResultHash() { .chainContribution(ChainContribution.builder().resultHash("").build()) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(web3jService.isBlockAvailable(anyLong())).thenReturn(true); when(iexecHubService.repeatIsContributedTrue(anyString(), anyString())).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1173,7 +1117,6 @@ void shouldAuthorizeUpdateOnResultUploaded() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(RESULT_UPLOADED) @@ -1184,11 +1127,10 @@ void shouldAuthorizeUpdateOnResultUploaded() { .taskDescription(TaskDescription.builder().callback("callback").build()) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(web3jService.isBlockAvailable(anyLong())).thenReturn(true); when(iexecHubService.repeatIsContributedTrue(anyString(), anyString())).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.NO_ERROR); } @@ -1197,7 +1139,6 @@ void shouldNotAuthorizeUpdateOnResultUploadedSinceNoChainCallbackData() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(RESULT_UPLOADED) @@ -1207,9 +1148,7 @@ void shouldNotAuthorizeUpdateOnResultUploadedSinceNoChainCallbackData() { .taskDescription(TaskDescription.builder().callback("callback").build()) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1218,7 +1157,6 @@ void shouldNotAuthorizeUpdateOnResultUploadedSinceNoResultLink() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(RESULT_UPLOADED) @@ -1228,10 +1166,9 @@ void shouldNotAuthorizeUpdateOnResultUploadedSinceNoResultLink() { .taskDescription(TaskDescription.builder().chainTaskId(CHAIN_TASK_ID).build()) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(resultService.isResultUploaded(CHAIN_TASK_ID)).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1240,7 +1177,6 @@ void shouldNotAuthorizeUpdateOnResultUploadedSinceResultNotUploaded() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(RESULT_UPLOADING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(RESULT_UPLOADED) @@ -1250,10 +1186,9 @@ void shouldNotAuthorizeUpdateOnResultUploadedSinceResultNotUploaded() { .taskDescription(TaskDescription.builder().build()) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - when(resultService.isResultUploaded(anyString())).thenReturn(false); + when(resultService.isResultUploaded(any())).thenReturn(false); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1262,15 +1197,12 @@ void shouldAuthorizeUpdateOnContributeAndFinalizeOnGoing() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(COMPUTED, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTE_AND_FINALIZE_ONGOING) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, null)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, null)) .isEqualTo(ReplicateStatusUpdateError.NO_ERROR); } @@ -1279,7 +1211,6 @@ void shouldAuthorizeUpdateOnContributeAndFinalizeDone() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTE_AND_FINALIZE_ONGOING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTE_AND_FINALIZE_DONE) @@ -1290,13 +1221,12 @@ void shouldAuthorizeUpdateOnContributeAndFinalizeDone() { .isTeeTask(true) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(iexecHubService.repeatIsRevealedTrue(CHAIN_TASK_ID, WALLET_WORKER_1)) .thenReturn(true); when(iexecHubService.getTaskDescription(CHAIN_TASK_ID)).thenReturn(task); when(iexecHubService.isTaskInCompletedStatusOnChain(CHAIN_TASK_ID)).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, null)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, null)) .isEqualTo(ReplicateStatusUpdateError.NO_ERROR); } @@ -1305,18 +1235,16 @@ void shouldNotAuthorizeUpdateOnContributeAndFinalizeDoneWhenNotRevealed() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTE_AND_FINALIZE_ONGOING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTE_AND_FINALIZE_DONE) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(iexecHubService.repeatIsRevealedTrue(CHAIN_TASK_ID, WALLET_WORKER_1)) .thenReturn(false); when(iexecHubService.isTaskInCompletedStatusOnChain(CHAIN_TASK_ID)).thenReturn(true); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, null)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, null)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1325,23 +1253,20 @@ void shouldNotAuthorizeUpdateOnContributeAndFinalizeDoneWhenNotUploaded() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTE_AND_FINALIZE_ONGOING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTE_AND_FINALIZE_DONE) .build(); final TaskDescription task = TaskDescription.builder().chainTaskId(CHAIN_TASK_ID).build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(iexecHubService.repeatIsRevealedTrue(CHAIN_TASK_ID, WALLET_WORKER_1)) .thenReturn(true); when(iexecHubService.getTaskDescription(CHAIN_TASK_ID)).thenReturn(task); when(resultService.isResultUploaded(CHAIN_TASK_ID)).thenReturn(false); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, null)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, null)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); - verify(replicatesRepository).findByChainTaskId(CHAIN_TASK_ID); verify(iexecHubService).repeatIsRevealedTrue(CHAIN_TASK_ID, WALLET_WORKER_1); verify(iexecHubService).getTaskDescription(CHAIN_TASK_ID); verify(resultService).isResultUploaded(CHAIN_TASK_ID); @@ -1352,18 +1277,16 @@ void shouldNotAuthorizeUpdateOnContributeAndFinalizeDoneWhenTaskNotCompleted() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(CONTRIBUTE_AND_FINALIZE_ONGOING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(CONTRIBUTE_AND_FINALIZE_DONE) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); when(iexecHubService.repeatIsRevealedTrue(CHAIN_TASK_ID, WALLET_WORKER_1)) .thenReturn(true); when(iexecHubService.isTaskInCompletedStatusOnChain(CHAIN_TASK_ID)).thenReturn(false); - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, null)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, null)) .isEqualTo(ReplicateStatusUpdateError.GENERIC_CANT_UPDATE); } @@ -1372,7 +1295,6 @@ void shouldNotAuthorizeUpdateOnComputedWhenTaskDescriptionIsNull() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(COMPUTING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(COMPUTED) @@ -1381,9 +1303,7 @@ void shouldNotAuthorizeUpdateOnComputedWhenTaskDescriptionIsNull() { .builder() .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.UNKNOWN_TASK); } @@ -1393,7 +1313,6 @@ void shouldAuthorizeUpdateOnComputedWhenTaskDescriptionIsFilled() { final Replicate replicate = new Replicate(WALLET_WORKER_1, CHAIN_TASK_ID); replicate.updateStatus(COMPUTING, ReplicateStatusModifier.WORKER); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, Collections.singletonList(replicate)); final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() .modifier(WORKER) .status(COMPUTED) @@ -1403,13 +1322,13 @@ void shouldAuthorizeUpdateOnComputedWhenTaskDescriptionIsFilled() { .taskDescription(TaskDescription.builder().build()) .build(); - when(replicatesRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - - assertThat(replicatesService.canUpdateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate, updateArgs)) + assertThat(replicatesService.canUpdateReplicateStatus(replicate, statusUpdate, updateArgs)) .isEqualTo(ReplicateStatusUpdateError.NO_ERROR); } - // computeUpdateReplicateStatusArgs + // endregion + + // region computeUpdateReplicateStatusArgs @Test void computeUpdateReplicateStatusArgsContributed() { @@ -1430,7 +1349,7 @@ void computeUpdateReplicateStatusArgsContributed() { when(iexecHubService.getWorkerWeight(WALLET_WORKER_1)).thenReturn(expectedWorkerWeight); when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_WORKER_1)) - .thenReturn(Optional.of(ChainContribution.builder().build())); + .thenReturn(Optional.of(expectedChainContribution)); assertThat(replicatesService.computeUpdateReplicateStatusArgs(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate)) .isEqualTo(UpdateReplicateStatusArgs.builder() @@ -1441,8 +1360,6 @@ void computeUpdateReplicateStatusArgsContributed() { @Test void computeUpdateReplicateStatusArgsResultUploaded() { - final int unexpectedWorkerWeight = 1; - final ChainContribution unexpectedChainContribution = ChainContribution.builder().build(); final String expectedResultLink = "resultLink"; final String expectedChainCallbackData = "chainCallbackData"; final TaskDescription expectedTaskDescription = TaskDescription.builder().build(); @@ -1457,9 +1374,6 @@ void computeUpdateReplicateStatusArgsResultUploaded() { .details(details) .build(); - when(iexecHubService.getWorkerWeight(WALLET_WORKER_1)).thenReturn(unexpectedWorkerWeight); - when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_WORKER_1)) - .thenReturn(Optional.of(unexpectedChainContribution)); when(iexecHubService.getTaskDescription(CHAIN_TASK_ID)) .thenReturn(expectedTaskDescription); @@ -1471,6 +1385,42 @@ void computeUpdateReplicateStatusArgsResultUploaded() { .chainCallbackData(expectedChainCallbackData) .taskDescription(expectedTaskDescription) .build()); + + verify(iexecHubService, never()).getWorkerWeight(WALLET_WORKER_1); + verify(iexecHubService, never()).getChainContribution(CHAIN_TASK_ID, WALLET_WORKER_1); + } + + @Test + void computeUpdateReplicateStatusArgsContributeAndFinalizeDone() { + final int expectedWorkerWeight = 1; + final ChainContribution expectedChainContribution = ChainContribution.builder().build(); + final String expectedResultLink = "resultLink"; + final String expectedChainCallbackData = "chainCallbackData"; + final TaskDescription expectedTaskDescription = TaskDescription.builder().build(); + + final ReplicateStatusDetails details = ReplicateStatusDetails.builder() + .resultLink(expectedResultLink) + .chainCallbackData(expectedChainCallbackData) + .build(); + final ReplicateStatusUpdate statusUpdate = ReplicateStatusUpdate.builder() + .modifier(WORKER) + .status(CONTRIBUTE_AND_FINALIZE_DONE) + .details(details) + .build(); + + when(iexecHubService.getTaskDescription(CHAIN_TASK_ID)).thenReturn(expectedTaskDescription); + when(iexecHubService.getWorkerWeight(WALLET_WORKER_1)).thenReturn(expectedWorkerWeight); + when(iexecHubService.getChainContribution(CHAIN_TASK_ID, WALLET_WORKER_1)) + .thenReturn(Optional.of(expectedChainContribution)); + + assertThat(replicatesService.computeUpdateReplicateStatusArgs(CHAIN_TASK_ID, WALLET_WORKER_1, statusUpdate)) + .isEqualTo(UpdateReplicateStatusArgs.builder() + .workerWeight(expectedWorkerWeight) + .chainContribution(expectedChainContribution) + .resultLink(expectedResultLink) + .chainCallbackData(expectedChainCallbackData) + .taskDescription(expectedTaskDescription) + .build()); } @Test @@ -1505,4 +1455,5 @@ void computeUpdateReplicateStatusArgsResultUploadFailed() { .build()); } + // endregion } diff --git a/src/test/java/com/iexec/core/replicate/ReplicateSupplyServiceTests.java b/src/test/java/com/iexec/core/replicate/ReplicateSupplyServiceTests.java index 0bd5d703d..e175c840b 100644 --- a/src/test/java/com/iexec/core/replicate/ReplicateSupplyServiceTests.java +++ b/src/test/java/com/iexec/core/replicate/ReplicateSupplyServiceTests.java @@ -631,7 +631,6 @@ private void assertTaskAccessForNewReplicateNotDeadLocking(String chainTaskId) { private void assertTaskAccessForNewReplicateLockNeverUsed(String chainTaskId) { final Lock lock = replicateSupplyService.taskAccessForNewReplicateLocks.get(chainTaskId); assertThat(lock).isNull(); - ; } // Tests on getMissedTaskNotifications() @@ -672,13 +671,14 @@ void shouldNotGetInterruptedReplicateSinceEnclaveChallengeNeededButNotGenerated( .updateReplicateStatus(any(), any(), any(), any(ReplicateStatusDetails.class)); } - + /** + * CONTRIBUTING + !onChain => RecoveryAction.CONTRIBUTE + */ @Test - // CREATED, ..., CAN_CONTRIBUTE => RecoveryAction.CONTRIBUTE - void shouldTellReplicateToContributeWhenComputing() { + void shouldTellReplicateToContributeSinceNotDoneOnchain() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.RUNNING); - Optional replicate = getStubReplicate(ReplicateStatus.COMPUTING); + Optional replicate = getStubReplicate(ReplicateStatus.CONTRIBUTING); when(workerService.getChainTaskIds(WALLET_WORKER_1)).thenReturn(ids); when(taskService.getTasksByChainTaskIds(ids)).thenReturn(taskList); @@ -686,6 +686,9 @@ void shouldTellReplicateToContributeWhenComputing() { when(signatureService.createAuthorization(WALLET_WORKER_1, CHAIN_TASK_ID, ENCLAVE_CHALLENGE)) .thenReturn(getStubAuth()); + when(replicatesService.didReplicateContributeOnchain(CHAIN_TASK_ID, WALLET_WORKER_1)) + .thenReturn(false); + List missedTaskNotifications = replicateSupplyService.getMissedTaskNotifications(3L, WALLET_WORKER_1); @@ -694,15 +697,17 @@ void shouldTellReplicateToContributeWhenComputing() { assertThat(taskNotificationType).isEqualTo(TaskNotificationType.PLEASE_CONTRIBUTE); Mockito.verify(replicatesService, times(1)) - .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); + .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * CREATED, ..., CAN_CONTRIBUTE => RecoveryAction.CONTRIBUTE + */ @Test - // CONTRIBUTING + !onChain => RecoveryAction.CONTRIBUTE - void shouldTellReplicateToContributeSinceNotDoneOnchain() { + void shouldTellReplicateToContributeWhenComputing() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.RUNNING); - Optional replicate = getStubReplicate(ReplicateStatus.CONTRIBUTING); + Optional replicate = getStubReplicate(ReplicateStatus.COMPUTING); when(workerService.getChainTaskIds(WALLET_WORKER_1)).thenReturn(ids); when(taskService.getTasksByChainTaskIds(ids)).thenReturn(taskList); @@ -710,9 +715,6 @@ void shouldTellReplicateToContributeSinceNotDoneOnchain() { when(signatureService.createAuthorization(WALLET_WORKER_1, CHAIN_TASK_ID, ENCLAVE_CHALLENGE)) .thenReturn(getStubAuth()); - when(replicatesService.didReplicateContributeOnchain(CHAIN_TASK_ID, WALLET_WORKER_1)) - .thenReturn(false); - List missedTaskNotifications = replicateSupplyService.getMissedTaskNotifications(3L, WALLET_WORKER_1); @@ -721,12 +723,14 @@ void shouldTellReplicateToContributeSinceNotDoneOnchain() { assertThat(taskNotificationType).isEqualTo(TaskNotificationType.PLEASE_CONTRIBUTE); Mockito.verify(replicatesService, times(1)) - .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING + .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); } + /** + * CONTRIBUTING + done onChain => updateStatus to CONTRIBUTED + * Task not in CONSENSUS_REACHED => RecoveryAction.WAIT + */ @Test - // CONTRIBUTING + done onChain => updateStatus to CONTRIBUTED - // Task not in CONSENSUS_REACHED => RecoveryAction.WAIT void shouldTellReplicateToWaitSinceContributedOnchain() { long blockNumber = 3; // ChainReceipt chainReceipt = new ChainReceipt(blockNumber, ""); @@ -763,74 +767,80 @@ void shouldTellReplicateToWaitSinceContributedOnchain() { any(ReplicateStatusDetails.class)); } + /** + * any status + Task in CONTRIBUTION_TIMEOUT => RecoveryAction.ABORT_CONTRIBUTION_TIMEOUT + */ @Test - // CONTRIBUTING + done onChain => updateStatus to CONTRIBUTED - // Task in CONSENSUS_REACHED => RecoveryAction.REVEAL - void shouldTellReplicateToRevealSinceConsensusReached() { + void shouldTellReplicateToAbortSinceContributionTimeout() { long blockNumber = 3; - // ChainReceipt chainReceipt = new ChainReceipt(blockNumber, ""); List ids = List.of(CHAIN_TASK_ID); - List taskList = getStubTaskList(TaskStatus.RUNNING); + List taskList = getStubTaskList(TaskStatus.CONTRIBUTION_TIMEOUT); Optional replicate1 = getStubReplicate(ReplicateStatus.CONTRIBUTING); - Optional replicate2 = getStubReplicate(ReplicateStatus.CONTRIBUTED); - List replicates = List.of(replicate1.get(), replicate2.get()); - final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, replicates); when(workerService.getChainTaskIds(WALLET_WORKER_1)).thenReturn(ids); when(taskService.getTasksByChainTaskIds(ids)).thenReturn(taskList); - when(replicatesService.getReplicate(CHAIN_TASK_ID, WALLET_WORKER_1)) - .thenReturn(replicate1) - .thenReturn(replicate2); + when(replicatesService.getReplicate(CHAIN_TASK_ID, WALLET_WORKER_1)).thenReturn(replicate1); when(signatureService.createAuthorization(WALLET_WORKER_1, CHAIN_TASK_ID, ENCLAVE_CHALLENGE)) .thenReturn(getStubAuth()); - when(replicatesService.didReplicateContributeOnchain(CHAIN_TASK_ID, WALLET_WORKER_1)) - .thenReturn(true); - when(replicatesService.getReplicatesList(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); - when(taskService.isConsensusReached(replicatesList)).thenReturn(true); List missedTaskNotifications = replicateSupplyService.getMissedTaskNotifications(blockNumber, WALLET_WORKER_1); assertThat(missedTaskNotifications).isNotEmpty(); TaskNotificationType taskNotificationType = missedTaskNotifications.get(0).getTaskNotificationType(); - assertThat(taskNotificationType).isEqualTo(TaskNotificationType.PLEASE_REVEAL); + assertThat(taskNotificationType).isEqualTo(TaskNotificationType.PLEASE_ABORT); + TaskNotificationExtra notificationExtra = missedTaskNotifications.get(0).getTaskNotificationExtra(); + assertThat(notificationExtra.getTaskAbortCause()).isEqualTo(TaskAbortCause.CONTRIBUTION_TIMEOUT); Mockito.verify(replicatesService, times(1)) - .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); - Mockito.verify(replicatesService, times(1)) - .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatus.class), // RECOVERING - any(ReplicateStatusDetails.class)); + .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * CONTRIBUTING + done onChain => updateStatus to CONTRIBUTED + * Task in CONSENSUS_REACHED => RecoveryAction.REVEAL + */ @Test - // any status + Task in CONTRIBUTION_TIMEOUT => RecoveryAction.ABORT_CONTRIBUTION_TIMEOUT - void shouldTellReplicateToAbortSinceContributionTimeout() { + void shouldTellReplicateToRevealSinceConsensusReached() { long blockNumber = 3; + // ChainReceipt chainReceipt = new ChainReceipt(blockNumber, ""); List ids = List.of(CHAIN_TASK_ID); - List taskList = getStubTaskList(TaskStatus.CONTRIBUTION_TIMEOUT); + List taskList = getStubTaskList(TaskStatus.RUNNING); Optional replicate1 = getStubReplicate(ReplicateStatus.CONTRIBUTING); + Optional replicate2 = getStubReplicate(ReplicateStatus.CONTRIBUTED); + List replicates = List.of(replicate1.get(), replicate2.get()); + final ReplicatesList replicatesList = new ReplicatesList(CHAIN_TASK_ID, replicates); when(workerService.getChainTaskIds(WALLET_WORKER_1)).thenReturn(ids); when(taskService.getTasksByChainTaskIds(ids)).thenReturn(taskList); - when(replicatesService.getReplicate(CHAIN_TASK_ID, WALLET_WORKER_1)).thenReturn(replicate1); + when(replicatesService.getReplicate(CHAIN_TASK_ID, WALLET_WORKER_1)) + .thenReturn(replicate1) + .thenReturn(replicate2); when(signatureService.createAuthorization(WALLET_WORKER_1, CHAIN_TASK_ID, ENCLAVE_CHALLENGE)) .thenReturn(getStubAuth()); + when(replicatesService.didReplicateContributeOnchain(CHAIN_TASK_ID, WALLET_WORKER_1)) + .thenReturn(true); + when(replicatesService.getReplicatesList(CHAIN_TASK_ID)).thenReturn(Optional.of(replicatesList)); + when(taskService.isConsensusReached(replicatesList)).thenReturn(true); List missedTaskNotifications = replicateSupplyService.getMissedTaskNotifications(blockNumber, WALLET_WORKER_1); assertThat(missedTaskNotifications).isNotEmpty(); TaskNotificationType taskNotificationType = missedTaskNotifications.get(0).getTaskNotificationType(); - assertThat(taskNotificationType).isEqualTo(TaskNotificationType.PLEASE_ABORT); - TaskNotificationExtra notificationExtra = missedTaskNotifications.get(0).getTaskNotificationExtra(); - assertThat(notificationExtra.getTaskAbortCause()).isEqualTo(TaskAbortCause.CONTRIBUTION_TIMEOUT); + assertThat(taskNotificationType).isEqualTo(TaskNotificationType.PLEASE_REVEAL); Mockito.verify(replicatesService, times(1)) - .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING + .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); + Mockito.verify(replicatesService, times(1)) + .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatus.class), // RECOVERING + any(ReplicateStatusDetails.class)); } + /** + * !CONTRIBUTED + Task in CONSENSUS_REACHED => RecoveryAction.ABORT_CONSENSUS_REACHED + */ @Test - // !CONTRIBUTED + Task in CONSENSUS_REACHED => RecoveryAction.ABORT_CONSENSUS_REACHED void shouldTellReplicateToWaitSinceConsensusReachedAndItDidNotContribute() { long blockNumber = 3; List ids = List.of(CHAIN_TASK_ID); @@ -857,8 +867,10 @@ void shouldTellReplicateToWaitSinceConsensusReachedAndItDidNotContribute() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * CONTRIBUTED + Task in REVEAL phase => RecoveryAction.REVEAL + */ @Test - // CONTRIBUTED + Task in REVEAL phase => RecoveryAction.REVEAL void shouldTellReplicateToRevealSinceContributed() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.AT_LEAST_ONE_REVEALED); @@ -881,8 +893,10 @@ void shouldTellReplicateToRevealSinceContributed() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * REVEALING + !onChain => RecoveryAction.REVEAL + */ @Test - // REVEALING + !onChain => RecoveryAction.REVEAL void shouldTellReplicateToRevealSinceNotDoneOnchain() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.AT_LEAST_ONE_REVEALED); @@ -908,9 +922,11 @@ void shouldTellReplicateToRevealSinceNotDoneOnchain() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * REVEALING + done onChain => updateStatus to REVEALED + * no RESULT_UPLOAD_REQUESTED => RecoveryAction.WAIT + */ @Test - // REVEALING + done onChain => updateStatus to REVEALED - // no RESULT_UPLOAD_REQUESTED => RecoveryAction.WAIT void shouldTellReplicateToWaitSinceRevealed() { long blockNumber = 3; // ChainReceipt chainReceipt = new ChainReceipt(blockNumber, ""); @@ -928,10 +944,7 @@ void shouldTellReplicateToWaitSinceRevealed() { .thenReturn(getStubAuth()); when(replicatesService.didReplicateRevealOnchain(CHAIN_TASK_ID, WALLET_WORKER_1)) .thenReturn(true); - - CompletableFuture future = new CompletableFuture<>(); - when(taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID)).thenReturn(future); - future.complete(true); + when(taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID)).thenReturn(true); List missedTaskNotifications = replicateSupplyService.getMissedTaskNotifications(blockNumber, WALLET_WORKER_1); @@ -947,9 +960,11 @@ void shouldTellReplicateToWaitSinceRevealed() { any(ReplicateStatusDetails.class)); } + /** + * REVEALING + done onChain => updateStatus to REVEALED + * RESULT_UPLOAD_REQUESTED => RecoveryAction.UPLOAD_RESULT + */ @Test - // REVEALING + done onChain => updateStatus to REVEALED - // RESULT_UPLOAD_REQUESTED => RecoveryAction.UPLOAD_RESULT void shouldTellReplicateToUploadResultSinceRequestedAfterRevealing() { long blockNumber = 3; // ChainReceipt chainReceipt = new ChainReceipt(blockNumber, ""); @@ -967,9 +982,7 @@ void shouldTellReplicateToUploadResultSinceRequestedAfterRevealing() { .thenReturn(getStubAuth()); when(replicatesService.didReplicateRevealOnchain(CHAIN_TASK_ID, WALLET_WORKER_1)) .thenReturn(true); - CompletableFuture future = new CompletableFuture<>(); - when(taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID)).thenReturn(future); - future.complete(true); + when(taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID)).thenReturn(true); List missedTaskNotifications = replicateSupplyService.getMissedTaskNotifications(blockNumber, WALLET_WORKER_1); @@ -985,8 +998,10 @@ void shouldTellReplicateToUploadResultSinceRequestedAfterRevealing() { any(ReplicateStatusDetails.class)); } + /** + * RESULT_UPLOAD_REQUESTED => RecoveryAction.UPLOAD_RESULT + */ @Test - // RESULT_UPLOAD_REQUESTED => RecoveryAction.UPLOAD_RESULT void shouldTellReplicateToUploadResultSinceRequested() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.RESULT_UPLOADING); @@ -1009,8 +1024,10 @@ void shouldTellReplicateToUploadResultSinceRequested() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * RESULT_UPLOADING + not done yet => RecoveryAction.UPLOAD_RESULT + */ @Test - // RESULT_UPLOADING + not done yet => RecoveryAction.UPLOAD_RESULT void shouldTellReplicateToUploadResultSinceNotDoneYet() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.RESULT_UPLOADING); @@ -1035,9 +1052,11 @@ void shouldTellReplicateToUploadResultSinceNotDoneYet() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * RESULT_UPLOADING + done => update to ReplicateStatus.RESULT_UPLOADED + * RecoveryAction.WAIT + */ @Test - // RESULT_UPLOADING + done => update to ReplicateStatus.RESULT_UPLOADED - // RecoveryAction.WAIT void shouldTellReplicateToWaitSinceDetectedResultUpload() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.RESULT_UPLOADING); @@ -1065,8 +1084,10 @@ void shouldTellReplicateToWaitSinceDetectedResultUpload() { .updateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, RESULT_UPLOADED); } + /** + * RESULT_UPLOADED => RecoveryAction.WAIT + */ @Test - // RESULT_UPLOADED => RecoveryAction.WAIT void shouldTellReplicateToWaitSinceItUploadedResult() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.RESULT_UPLOADING); @@ -1094,8 +1115,10 @@ void shouldTellReplicateToWaitSinceItUploadedResult() { .updateReplicateStatus(CHAIN_TASK_ID, WALLET_WORKER_1, RESULT_UPLOADED); } + /** + * REVEALED + Task in completion phase => RecoveryAction.WAIT + */ @Test - // REVEALED + Task in completion phase => RecoveryAction.WAIT void shouldTellReplicateToWaitForCompletionSinceItRevealed() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.FINALIZING); @@ -1120,8 +1143,10 @@ void shouldTellReplicateToWaitForCompletionSinceItRevealed() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * REVEALED + RESULT_UPLOADED + Task in completion phase => RecoveryAction.WAIT + */ @Test - // REVEALED + RESULT_UPLOADED + Task in completion phase => RecoveryAction.WAIT void shouldTellReplicateToWaitForCompletionSinceItRevealedAndUploaded() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.FINALIZING); @@ -1178,8 +1203,10 @@ void shouldTellReplicateToCompleteSinceItRevealed() { .updateReplicateStatus(anyString(), anyString(), any(ReplicateStatusUpdate.class)); // RECOVERING } + /** + * !REVEALED + Task in completion phase => null / nothing + */ @Test - // !REVEALED + Task in completion phase => null / nothing void shouldNotTellReplicateToWaitForCompletionSinceItDidNotReveal() { List ids = List.of(CHAIN_TASK_ID); List taskList = getStubTaskList(TaskStatus.FINALIZING); diff --git a/src/test/java/com/iexec/core/task/TaskControllerTests.java b/src/test/java/com/iexec/core/task/TaskControllerTests.java index 163552e33..c0e1a72c2 100644 --- a/src/test/java/com/iexec/core/task/TaskControllerTests.java +++ b/src/test/java/com/iexec/core/task/TaskControllerTests.java @@ -23,6 +23,7 @@ import com.iexec.commons.poco.task.TaskDescription; import com.iexec.core.chain.IexecHubService; import com.iexec.core.logs.TaskLogs; +import com.iexec.core.logs.TaskLogsModel; import com.iexec.core.logs.TaskLogsService; import com.iexec.core.replicate.Replicate; import com.iexec.core.replicate.ReplicateModel; @@ -57,12 +58,18 @@ class TaskControllerTests { private String requesterAddress; private String signature; - @Mock private EIP712ChallengeService challengeService; - @Mock private IexecHubService iexecHubService; - @Mock private ReplicatesService replicatesService; - @Mock private TaskService taskService; - @Mock private TaskLogsService taskLogsService; - @InjectMocks private TaskController taskController; + @Mock + private EIP712ChallengeService challengeService; + @Mock + private IexecHubService iexecHubService; + @Mock + private ReplicatesService replicatesService; + @Mock + private TaskService taskService; + @Mock + private TaskLogsService taskLogsService; + @InjectMocks + private TaskController taskController; @BeforeEach @SneakyThrows @@ -98,8 +105,8 @@ void shouldGetChallenge() { //region getTask @Test void shouldGetTaskModel() { - Task taskEntity = mock(Task.class); - when(taskEntity.getChainTaskId()).thenReturn(TASK_ID); + Task taskEntity = new Task(); + taskEntity.setChainTaskId(TASK_ID); when(taskService.getTaskByChainTaskId(TASK_ID)) .thenReturn(Optional.of(taskEntity)); @@ -113,8 +120,8 @@ void shouldGetTaskModel() { @Test void shouldGetTaskModelWithReplicates() { - Task taskEntity = mock(Task.class); - when(taskEntity.getChainTaskId()).thenReturn(TASK_ID); + Task taskEntity = new Task(); + taskEntity.setChainTaskId(TASK_ID); when(taskService.getTaskByChainTaskId(TASK_ID)) .thenReturn(Optional.of(taskEntity)); when(replicatesService.hasReplicatesList(TASK_ID)) @@ -165,10 +172,10 @@ void shouldNotGetReplicate() { //region buildReplicateModel @Test void shouldBuildReplicateModel() { - Replicate entity = mock(Replicate.class); - when(entity.getChainTaskId()).thenReturn(TASK_ID); - when(entity.getWalletAddress()).thenReturn(WORKER_ADDRESS); - when(entity.isAppComputeLogsPresent()).thenReturn(false); + Replicate entity = new Replicate(); + entity.setChainTaskId(TASK_ID); + entity.setWalletAddress(WORKER_ADDRESS); + entity.setAppComputeLogsPresent(false); ReplicateModel dto = taskController.buildReplicateModel(entity); assertEquals(TASK_ID, dto.getChainTaskId()); @@ -178,10 +185,10 @@ void shouldBuildReplicateModel() { @Test void shouldBuildReplicateModelWithComputeLogs() { - Replicate entity = mock(Replicate.class); - when(entity.getChainTaskId()).thenReturn(TASK_ID); - when(entity.getWalletAddress()).thenReturn(WORKER_ADDRESS); - when(entity.isAppComputeLogsPresent()).thenReturn(true); + Replicate entity = new Replicate(); + entity.setChainTaskId(TASK_ID); + entity.setWalletAddress(WORKER_ADDRESS); + entity.setAppComputeLogsPresent(true); ReplicateModel dto = taskController.buildReplicateModel(entity); assertEquals(TASK_ID, dto.getChainTaskId()); @@ -201,7 +208,7 @@ void shouldGetTaskLogsWhenAuthenticated() { when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(description); when(challengeService.getChallenge(requesterAddress)).thenReturn(challenge); when(taskLogsService.getTaskLogs(TASK_ID)).thenReturn(Optional.of(taskStdout)); - ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); + ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); assertEquals(HttpStatus.OK, response.getStatusCode()); verify(iexecHubService).getTaskDescription(TASK_ID); } @@ -215,7 +222,7 @@ void shouldFailToGetTaskLogsWithNotFoundStatus() { when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(description); when(challengeService.getChallenge(requesterAddress)).thenReturn(challenge); when(taskLogsService.getTaskLogs(TASK_ID)).thenReturn(Optional.empty()); - ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); + ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); verify(iexecHubService).getTaskDescription(TASK_ID); } @@ -229,14 +236,14 @@ void shouldFailToGetTaskLogsWhenBadChallenge() { when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(description); when(challengeService.getChallenge(requesterAddress)).thenReturn(challenge); when(taskLogsService.getComputeLogs(TASK_ID, WORKER_ADDRESS)).thenReturn(Optional.empty()); - ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); + ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); verify(iexecHubService).getTaskDescription(TASK_ID); } @Test void shouldFailToGetTaskLogsWhenInvalidAuthorization() { - ResponseEntity response = taskController.getTaskLogs(TASK_ID, ""); + ResponseEntity response = taskController.getTaskLogs(TASK_ID, ""); assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); verifyNoInteractions(iexecHubService); } @@ -250,7 +257,7 @@ void shouldFailToGetTaskLogsWhenNotTaskRequester() { String authorization = String.join("_", challenge.getHash(), signature, requesterAddress); when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(description); when(challengeService.getChallenge(requesterAddress)).thenReturn(challenge); - ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); + ResponseEntity response = taskController.getTaskLogs(TASK_ID, authorization); assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); verify(iexecHubService).getTaskDescription(TASK_ID); } diff --git a/src/test/java/com/iexec/core/task/TaskModelTests.java b/src/test/java/com/iexec/core/task/TaskModelTests.java index 78494efb5..8ddf5aaf8 100644 --- a/src/test/java/com/iexec/core/task/TaskModelTests.java +++ b/src/test/java/com/iexec/core/task/TaskModelTests.java @@ -66,7 +66,7 @@ void shouldConvertFromEntityToDto() { .dateStatusList(DATE_STATUS_LIST) .build(); - TaskModel dto = TaskModel.fromEntity(entity); + TaskModel dto = entity.generateModel(); Assertions.assertEquals(entity.getChainTaskId(), dto.getChainTaskId()); Assertions.assertEquals(entity.getMaxExecutionTime(), dto.getMaxExecutionTime()); Assertions.assertEquals(entity.getTag(), dto.getTag()); diff --git a/src/test/java/com/iexec/core/task/TaskServiceTests.java b/src/test/java/com/iexec/core/task/TaskServiceTests.java index 8a6c93e1d..599743c8a 100644 --- a/src/test/java/com/iexec/core/task/TaskServiceTests.java +++ b/src/test/java/com/iexec/core/task/TaskServiceTests.java @@ -21,10 +21,18 @@ import com.iexec.core.chain.IexecHubService; import com.iexec.core.replicate.ReplicatesList; import com.iexec.core.replicate.ReplicatesService; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Sort; @@ -32,8 +40,7 @@ import java.time.temporal.ChronoUnit; import java.util.*; -import static com.iexec.core.task.TaskStatus.INITIALIZED; -import static com.iexec.core.task.TaskStatus.RUNNING; +import static com.iexec.core.task.TaskStatus.*; import static com.iexec.core.task.TaskTestsUtils.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -58,9 +65,20 @@ class TaskServiceTests { @InjectMocks private TaskService taskService; + @BeforeAll + static void initRegistry() { + Metrics.globalRegistry.add(new SimpleMeterRegistry()); + } + @BeforeEach void init() { MockitoAnnotations.openMocks(this); + taskService.init(); + } + + @AfterEach + void afterEach() { + Metrics.globalRegistry.clear(); } @Test @@ -217,8 +235,13 @@ void shouldFindTaskExpired() { // region updateTask() @Test - void shouldUpdateTask() { + void shouldUpdateTaskAndMetrics() { + Counter counter = Metrics.globalRegistry.find(TaskService.METRIC_TASKS_COMPLETED_COUNT).counter(); + Assertions.assertThat(counter).isNotNull(); + Assertions.assertThat(counter.count()).isZero(); + Task task = getStubTask(maxExecutionTime); + task.setCurrentStatus(TaskStatus.COMPLETED); when(taskRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(task)); when(taskRepository.save(task)).thenReturn(task); @@ -227,6 +250,29 @@ void shouldUpdateTask() { assertThat(optional) .isPresent() .isEqualTo(Optional.of(task)); + counter = Metrics.globalRegistry.find(TaskService.METRIC_TASKS_COMPLETED_COUNT).counter(); + Assertions.assertThat(counter).isNotNull(); + Assertions.assertThat(counter.count()).isOne(); + } + + @Test + void shouldUpdateTaskButNotMetricsWhenTaskIsNotCompleted() { + Counter counter = Metrics.globalRegistry.find(TaskService.METRIC_TASKS_COMPLETED_COUNT).counter(); + Assertions.assertThat(counter).isNotNull(); + Assertions.assertThat(counter.count()).isZero(); + + Task task = getStubTask(maxExecutionTime); + when(taskRepository.findByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(task)); + when(taskRepository.save(task)).thenReturn(task); + + Optional optional = taskService.updateTask(task); + + assertThat(optional) + .isPresent() + .isEqualTo(Optional.of(task)); + counter = Metrics.globalRegistry.find(TaskService.METRIC_TASKS_COMPLETED_COUNT).counter(); + Assertions.assertThat(counter).isNotNull(); + Assertions.assertThat(counter.count()).isZero(); } @Test @@ -311,4 +357,24 @@ void shouldConsensusBeReached() { Mockito.verify(iexecHubService).getChainTask(any()); } // endregion + + // region getCompletedTasksCount + @Test + void shouldGet0CompletedTasksCountWhenNoTaskCompleted() { + final long completedTasksCount = taskService.getCompletedTasksCount(); + assertThat(completedTasksCount).isZero(); + } + + @Test + void shouldGet3CompletedTasksCount() { + final TaskService taskService = new TaskService(taskRepository, iexecHubService); + final Task task = Task.builder().currentStatus(COMPLETED).build(); + when(taskRepository.findByCurrentStatus(COMPLETED)) + .thenReturn(List.of(task, task, task)); + taskService.init(); + + final long completedTasksCount = taskService.getCompletedTasksCount(); + assertThat(completedTasksCount).isEqualTo(3); + } + // endregion } diff --git a/src/test/java/com/iexec/core/task/update/TaskUpdateManagerTest.java b/src/test/java/com/iexec/core/task/update/TaskUpdateManagerTest.java index 5ecaeec3b..d0a6c8588 100644 --- a/src/test/java/com/iexec/core/task/update/TaskUpdateManagerTest.java +++ b/src/test/java/com/iexec/core/task/update/TaskUpdateManagerTest.java @@ -16,6 +16,7 @@ package com.iexec.core.task.update; +import com.iexec.blockchain.api.BlockchainAdapterService; import com.iexec.common.replicate.ReplicateStatus; import com.iexec.common.replicate.ReplicateStatusModifier; import com.iexec.common.replicate.ReplicateStatusUpdate; @@ -28,7 +29,6 @@ import com.iexec.commons.poco.utils.BytesUtils; import com.iexec.core.chain.IexecHubService; import com.iexec.core.chain.Web3jService; -import com.iexec.core.chain.adapter.BlockchainAdapterService; import com.iexec.core.configuration.ResultRepositoryConfiguration; import com.iexec.core.detector.replicate.RevealTimeoutDetector; import com.iexec.core.replicate.Replicate; @@ -39,8 +39,14 @@ import com.iexec.core.task.TaskService; import com.iexec.core.task.TaskStatus; import com.iexec.core.task.event.PleaseUploadEvent; +import com.iexec.core.task.event.TaskStatusesCountUpdatedEvent; import com.iexec.core.worker.Worker; import com.iexec.core.worker.WorkerService; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -48,18 +54,19 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.util.ReflectionTestUtils; import java.math.BigInteger; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import static com.iexec.core.task.TaskStatus.*; import static com.iexec.core.task.TaskTestsUtils.*; +import static com.iexec.core.task.update.TaskUpdateManager.METRIC_TASKS_STATUSES_COUNT; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -104,12 +111,44 @@ class TaskUpdateManagerTest { @InjectMocks private TaskUpdateManager taskUpdateManager; + @BeforeAll + static void initRegistry() { + Metrics.globalRegistry.add(new SimpleMeterRegistry()); + } + @BeforeEach void init() { MockitoAnnotations.openMocks(this); } - // Tests on consensusReached2Reopening transition + @AfterEach + void afterEach() { + Metrics.globalRegistry.clear(); + } + + // region init + @Test + void shouldBuildGaugesAndFireEvent() throws ExecutionException, InterruptedException { + for (final TaskStatus status : TaskStatus.values()) { + // Give a unique initial count for each status + when(taskService.countByCurrentStatus(status)).thenReturn((long) status.ordinal()); + } + + taskUpdateManager.init().get(); + + for (final TaskStatus status : TaskStatus.values()) { + final Gauge gauge = getCurrentTasksCountGauge(status); + assertThat(gauge).isNotNull() + // Check the gauge value is equal to the unique count for each status + .extracting(Gauge::value) + .isEqualTo(((double) status.ordinal())); + } + + verify(applicationEventPublisher, times(1)).publishEvent(any(TaskStatusesCountUpdatedEvent.class)); + } + // endregion + + // region consensusReached2Reopening @Test void shouldNotUpgrade2ReopenedSinceCurrentStatusWrong() { @@ -180,7 +219,7 @@ void shouldNotUpgrade2ReopenedSinceCantReopenOnChain() { } @Test - void shouldNotUpgrade2ReopenedSinceNotEnoughtGas() { + void shouldNotUpgrade2ReopenedSinceNotEnoughGas() { Task task = getStubTask(maxExecutionTime); task.changeStatus(CONSENSUS_REACHED); @@ -196,9 +235,8 @@ void shouldNotUpgrade2ReopenedSinceNotEnoughtGas() { assertThat(task.getCurrentStatus()).isEqualTo(CONSENSUS_REACHED); } - @Test - void shouldNotUpgrade2ReopenedBut2ReopendedFailedSinceTxFailed() { + void shouldNotUpgrade2ReopenedBut2ReopenFailedSinceTxFailed() { Task task = getStubTask(maxExecutionTime); task.changeStatus(CONSENSUS_REACHED); @@ -241,7 +279,9 @@ void shouldUpgrade2Reopened() { assertThat(task.getDateStatusList().get(4).getStatus()).isEqualTo(INITIALIZED); } - // Tests on received2Initializing transition + // endregion + + // region received2Initializing @Test void shouldNotUpdateReceived2InitializingSinceChainTaskIdIsNotEmpty() { @@ -336,7 +376,7 @@ void shouldNotUpdateReceived2InitializingSinceNoSmsClient() { } @Test - void shouldUpdateInitializing2InitailizeFailedSinceChainTaskIdIsEmpty() { + void shouldUpdateInitializing2InitializeFailedSinceChainTaskIdIsEmpty() { Task task = getStubTask(maxExecutionTime); task.changeStatus(RECEIVED); task.setChainTaskId(CHAIN_TASK_ID); @@ -413,7 +453,6 @@ void shouldUpdateReceived2Initializing2InitializedOnStandard() { verify(taskService, times(2)).updateTask(task); //initializing & initialized } - @Test void shouldUpdateReceived2Initializing2InitializedOnTee() { Task task = getStubTask(maxExecutionTime); @@ -483,10 +522,12 @@ void shouldNotUpdateReceived2Initializing2InitializedOnTeeSinceCannotRetrieveSms assertThat(task.getSmsUrl()).isNull(); verify(smsService, times(1)).getVerifiedSmsUrl(CHAIN_TASK_ID, tag); verify(smsService, times(0)).getEnclaveChallenge(anyString(), anyString()); - verify(taskService, times(2)).updateTask(task); // INITIALIZE_FAILED & FAILED + verify(taskService, times(1)).updateTask(task); // INITIALIZE_FAILED & FAILED } - // Tests on initializing2Initialized transition + // endregion + + // region initializing2Initialized @Test void shouldUpdateInitializing2Initialized() { @@ -535,7 +576,9 @@ void shouldNotUpdateInitializing2InitializedSinceFailedToCheck() { assertThat(task.getCurrentStatus()).isEqualTo(INITIALIZING); } - // Tests on initialized2Running transition + // endregion + + // region initialized2Running @Test void shouldUpdateInitialized2Running() { // 1 RUNNING out of 2 @@ -595,7 +638,9 @@ void shouldNotUpdateInitialized2RunningSinceComputedIsMoreThanNeeded() { assertThat(task.getCurrentStatus()).isEqualTo(INITIALIZED); } - // initializedOrRunning2ContributionTimeout + // endregion + + // region initializedOrRunning2ContributionTimeout @Test void shouldNotUpdateInitializedOrRunning2ContributionTimeoutSinceBeforeTimeout() { @@ -660,7 +705,6 @@ void shouldNotReSendNotificationWhenAlreadyInContributionTimeout() { .publishEvent(any()); } - @Test void shouldUpdateFromInitializedOrRunning2ContributionTimeout() { Date now = new Date(); @@ -684,7 +728,9 @@ void shouldUpdateFromInitializedOrRunning2ContributionTimeout() { assertThat(task.getLastButOneStatus()).isEqualTo(CONTRIBUTION_TIMEOUT); } - // Tests on running2Finalized2Completed transition + // endregion + + // region running2Finalized2Completed @Test void shouldNotUpdateRunning2Finalized2CompletedWhenTaskNotRunning() { @@ -756,7 +802,9 @@ void shouldNotUpdateRunning2Finalized2CompletedWhenMoreThanOneReplicatesOnContri assertThat(task.getCurrentStatus()).isEqualTo(FAILED); } - // Tests on running2ConsensusReached transition + // endregion + + // region running2ConsensusReached @Test void shouldUpdateRunning2ConsensusReached() { @@ -839,7 +887,10 @@ void shouldNOTUpdateRunning2ConsensusReachedSinceWinnerContributorsDiffers() { assertThat(task.getCurrentStatus()).isEqualTo(RUNNING); } - // Tests on running2RunningFailed transition + // endregion + + // region running2RunningFailed + @Test void shouldUpdateRunning2RunningFailedOn1Worker() { Task task = getStubTask(maxExecutionTime); @@ -1078,7 +1129,9 @@ void shouldNotUpdateRunning2AllWorkersFailedSinceOneStillHasToBeLaunched() { assertThat(task.getDateOfStatus(RUNNING_FAILED)).isEmpty(); } - // Tests on consensusReached2AtLeastOneReveal2UploadRequested transition + // endregion + + // region consensusReached2AtLeastOneReveal2UploadRequested @Test void shouldUpdateConsensusReached2AtLeastOneReveal2Uploading() { @@ -1114,7 +1167,10 @@ void shouldNOTUpdateConsensusReached2AtLeastOneRevealSinceNoRevealedReplicate() assertThat(task.getCurrentStatus()).isEqualTo(CONSENSUS_REACHED); } - // Tests on AT_LEAST_ONE_REVEALED + // endregion + + // region requestUpload + @Test void shouldRequestUploadAfterOneRevealed() { Task task = getStubTask(maxExecutionTime); @@ -1148,7 +1204,10 @@ void shouldNotRequestUploadAfterOneRevealedAsWorkerLost() { assertThat(task.getCurrentStatus()).isEqualTo(AT_LEAST_ONE_REVEALED); } - // Tests on reopening2Reopened transition + // endregion + + // region reopening2Reopened + @Test void shouldUpdateReopening2Reopened() { final Task task = getStubTask(maxExecutionTime); @@ -1195,7 +1254,10 @@ void shouldNotUpdateReopening2ReopenedSinceUnknownChainTask() { verify(replicatesService, times(0)).setRevealTimeoutStatusIfNeeded(eq(CHAIN_TASK_ID), any()); } - // Tests on REOPENED + // endregion + + // region reopened2Initialized + @Test void shouldUpdateReopened() { final Task task = getStubTask(maxExecutionTime); @@ -1208,7 +1270,9 @@ void shouldUpdateReopened() { assertThat(task.getCurrentStatus()).isEqualTo(INITIALIZED); } - // Test on resultUploading2Uploaded2Finalizing2Finalized + // endregion + + // region resultUploading2Uploaded2Finalizing2Finalized @Test void shouldUpdateResultUploading2Uploaded2Finalizing2Finalized() { //one worker uploaded @@ -1246,7 +1310,9 @@ void shouldUpdateResultUploading2Uploaded2Finalizing2Finalized() { //one worker assertThat(lastButThreeStatus).isEqualTo(RESULT_UPLOADED); } - // Tests on finalizing2Finalized transition + // endregion + + // region finalizing2Finalized @Test void shouldUpdateFinalized2Completed() { @@ -1276,6 +1342,8 @@ void shouldUpdateFinalizing2FinalizedFailed() { assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 3).getStatus()).isEqualTo(FINALIZING); } + // endregion + @Test void shouldUpdateResultUploading2UploadedButNot2Finalizing() { //one worker uploaded Task task = getStubTask(maxExecutionTime); @@ -1560,7 +1628,8 @@ void shouldNotUpdateResultUploaded2FinalizingSinceCantRequestFinalize() { assertThat(task.getCurrentStatus()).isEqualTo(FAILED); } - // Tests on finalizedToCompleted transition + // region finalizingToFinalizedToCompleted + @Test void shouldUpdateFinalizing2Finalized2Completed() { Task task = getStubTask(maxExecutionTime); @@ -1576,6 +1645,40 @@ void shouldUpdateFinalizing2Finalized2Completed() { assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 3).getStatus()).isEqualTo(FINALIZING); } + @Test + void shouldNotUpdateFinalizing2FinalizedSinceNotFinalized() { + Task task = getStubTask(maxExecutionTime); + task.setChainTaskId(CHAIN_TASK_ID); + task.changeStatus(FINALIZING); + + when(taskService.getTaskByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(task)); + when(blockchainAdapterService.isFinalized(CHAIN_TASK_ID)).thenReturn(Optional.of(false)); + + taskUpdateManager.updateTask(CHAIN_TASK_ID); + assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 4).getStatus()).isEqualTo(RECEIVED); + assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 3).getStatus()).isEqualTo(FINALIZING); + assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 2).getStatus()).isEqualTo(FINALIZE_FAILED); + assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 1).getStatus()).isEqualTo(FAILED); + assertThat(task.getCurrentStatus()).isEqualTo(FAILED); + } + + @Test + void shouldNotUpdateFinalizing2FinalizedSinceFailedToCheck() { + Task task = getStubTask(maxExecutionTime); + task.setChainTaskId(CHAIN_TASK_ID); + task.changeStatus(FINALIZING); + + when(taskService.getTaskByChainTaskId(CHAIN_TASK_ID)).thenReturn(Optional.of(task)); + when(blockchainAdapterService.isFinalized(CHAIN_TASK_ID)).thenReturn(Optional.empty()); + + taskUpdateManager.updateTask(CHAIN_TASK_ID); + assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 2).getStatus()).isEqualTo(RECEIVED); + assertThat(task.getDateStatusList().get(task.getDateStatusList().size() - 1).getStatus()).isEqualTo(FINALIZING); + assertThat(task.getCurrentStatus()).isEqualTo(FINALIZING); + } + + // endregion + // 3 replicates in RUNNING 0 in COMPUTED @Test void shouldUpdateTaskToRunningFromWorkersInRunning() { @@ -1806,7 +1909,8 @@ void shouldUpdateToFailed() { } } - // Tests on requestUpload + // region requestUpload + @Test void shouldRequestUpload() { Task task = getStubTask(maxExecutionTime); @@ -1884,7 +1988,9 @@ void shouldNotRequestUploadSinceUploadInProgress() { .publishEvent(any(PleaseUploadEvent.class)); } - // publishRequest + // endregion + + // region publishRequest @Test void shouldTriggerUpdateTaskAsynchronously() { @@ -1892,6 +1998,64 @@ void shouldTriggerUpdateTaskAsynchronously() { verify(taskUpdateRequestManager).publishRequest(CHAIN_TASK_ID); } + // endregion + + // region onTaskCreatedEvent + @Test + void shouldIncrementCurrentReceivedGaugeWhenTaskReceived() { + when(taskService.countByCurrentStatus(RECEIVED)).thenReturn(0L); + + // Init gauges + taskUpdateManager.init(); + + final Gauge currentReceivedTasks = getCurrentTasksCountGauge(RECEIVED); + assertThat(currentReceivedTasks.value()).isZero(); + + taskUpdateManager.onTaskCreatedEvent(); + + assertThat(currentReceivedTasks.value()).isOne(); + } + // endregion + + // region updateMetricsAfterStatusUpdate + @Test + void shouldUpdateMetricsAfterStatusUpdate() throws ExecutionException, InterruptedException { + when(taskService.countByCurrentStatus(RECEIVED)).thenReturn(1L); + when(taskService.countByCurrentStatus(INITIALIZING)).thenReturn(0L); + + // Init gauges + taskUpdateManager.init().get(); + + final Gauge currentReceivedTasks = getCurrentTasksCountGauge(RECEIVED); + final Gauge currentInitializingTasks = getCurrentTasksCountGauge(INITIALIZING); + + assertThat(currentReceivedTasks.value()).isOne(); + assertThat(currentInitializingTasks.value()).isZero(); + + taskUpdateManager.updateMetricsAfterStatusUpdate(RECEIVED, INITIALIZING); + + assertThat(currentReceivedTasks.value()).isZero(); + assertThat(currentInitializingTasks.value()).isOne(); + // Called a first time during init, then a second time during update + verify(applicationEventPublisher, times(2)).publishEvent(any(TaskStatusesCountUpdatedEvent.class)); + } + // endregion + + // region onTaskCreatedEvent + @Test + void shouldUpdateCurrentReceivedCountAndFireEvent() { + final AtomicLong receivedCount = + (AtomicLong) ((LinkedHashMap) ReflectionTestUtils.getField(taskUpdateManager, "currentTaskStatusesCount")) + .get(RECEIVED); + final long initialCount = receivedCount.get(); + + taskUpdateManager.onTaskCreatedEvent(); + + assertThat(receivedCount.get() - initialCount).isOne(); + verify(applicationEventPublisher, times(1)).publishEvent(any(TaskStatusesCountUpdatedEvent.class)); + } + // endregion + // region utils private void mockTaskDescriptionFromTask(Task task) { final TaskDescription taskDescription = TaskDescription.builder() @@ -1902,5 +2066,14 @@ private void mockTaskDescriptionFromTask(Task task) { .build(); when(iexecHubService.getTaskDescription(task.getChainTaskId())).thenReturn(taskDescription); } + + Gauge getCurrentTasksCountGauge(TaskStatus status) { + return Metrics.globalRegistry + .find(METRIC_TASKS_STATUSES_COUNT) + .tags( + "period", "current", + "status", status.name() + ).gauge(); + } // endregion } diff --git a/src/test/java/com/iexec/core/task/update/TaskUpdateRequestManagerTests.java b/src/test/java/com/iexec/core/task/update/TaskUpdateRequestManagerTests.java index d8543b48e..feefa7701 100644 --- a/src/test/java/com/iexec/core/task/update/TaskUpdateRequestManagerTests.java +++ b/src/test/java/com/iexec/core/task/update/TaskUpdateRequestManagerTests.java @@ -41,18 +41,16 @@ void shouldPublishRequest() throws ExecutionException, InterruptedException { when(taskService.getTaskByChainTaskId(CHAIN_TASK_ID)) .thenReturn(Optional.of(Task.builder().chainTaskId(CHAIN_TASK_ID).build())); - CompletableFuture booleanCompletableFuture = taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID); - booleanCompletableFuture.join(); + boolean publishRequestStatus = taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID); - Assertions.assertThat(booleanCompletableFuture.get()).isTrue(); + Assertions.assertThat(publishRequestStatus).isTrue(); } @Test void shouldNotPublishRequestSinceEmptyTaskId() throws ExecutionException, InterruptedException { - CompletableFuture booleanCompletableFuture = taskUpdateRequestManager.publishRequest(""); - booleanCompletableFuture.join(); + boolean publishRequestStatus = taskUpdateRequestManager.publishRequest(""); - Assertions.assertThat(booleanCompletableFuture.get()).isFalse(); + Assertions.assertThat(publishRequestStatus).isFalse(); } @Test @@ -63,10 +61,9 @@ void shouldNotPublishRequestSinceItemAlreadyAdded() throws ExecutionException, I buildTaskUpdate(CHAIN_TASK_ID, null, null, null) ); - CompletableFuture booleanCompletableFuture = taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID); - booleanCompletableFuture.join(); + boolean publishRequestStatus = taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID); - Assertions.assertThat(booleanCompletableFuture.get()).isFalse(); + Assertions.assertThat(publishRequestStatus).isFalse(); } @Test @@ -74,10 +71,9 @@ void shouldNotPublishRequestSinceTaskDoesNotExist() throws ExecutionException, I when(taskService.getTaskByChainTaskId(CHAIN_TASK_ID)) .thenReturn(Optional.empty()); - CompletableFuture booleanCompletableFuture = taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID); - booleanCompletableFuture.join(); + boolean publishRequestStatus = taskUpdateRequestManager.publishRequest(CHAIN_TASK_ID); - Assertions.assertThat(booleanCompletableFuture.get()).isFalse(); + Assertions.assertThat(publishRequestStatus).isFalse(); } // endregion @@ -232,7 +228,7 @@ void shouldGetInOrderForStatusAndContributionDeadline() throws InterruptedExcept ); } // endregion - + private TaskUpdate buildTaskUpdate(String chainTaskId, TaskStatus status, Date contributionDeadline, diff --git a/src/test/java/com/iexec/core/version/VersionControllerTests.java b/src/test/java/com/iexec/core/version/VersionControllerTests.java index 2911d2533..d01c5c0d4 100644 --- a/src/test/java/com/iexec/core/version/VersionControllerTests.java +++ b/src/test/java/com/iexec/core/version/VersionControllerTests.java @@ -16,32 +16,67 @@ package com.iexec.core.version; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.annotation.Import; import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.when; +@ExtendWith(SpringExtension.class) +@Import(ProjectInfoAutoConfiguration.class) class VersionControllerTests { - @Mock - private VersionService versionService; - @InjectMocks + private VersionController versionController; + @Autowired + private BuildProperties buildProperties; + + @BeforeAll + static void initRegistry() { + Metrics.globalRegistry.add(new SimpleMeterRegistry()); + } + @BeforeEach void init() { - MockitoAnnotations.openMocks(this); + versionController = new VersionController(buildProperties); + versionController.initializeGaugeVersion(); } - @ParameterizedTest - @ValueSource(strings={"x.y.z", "x.y.z-rc", "x.y.z-NEXT-SNAPSHOT"}) - void testVersionController(String version) { - when(versionService.getVersion()).thenReturn(version); - assertEquals(ResponseEntity.ok(version), versionController.getVersion()); + @AfterEach + void afterEach() { + Metrics.globalRegistry.clear(); + } + + @Test + void testVersionController() { + assertEquals(ResponseEntity.ok(buildProperties.getVersion()), versionController.getVersion()); + } + + @Test + void shouldReturnInfoGauge() { + final Gauge info = Metrics.globalRegistry.find(VersionController.METRIC_INFO_GAUGE_NAME).gauge(); + assertThat(info) + .isNotNull() + .extracting(Gauge::getId) + .isNotNull() + .extracting( + id -> id.getTag(VersionController.METRIC_INFO_LABEL_APP_NAME), + id -> id.getTag(VersionController.METRIC_INFO_LABEL_APP_VERSION) + ) + .containsExactly(buildProperties.getName(), buildProperties.getVersion()); + assertThat(info.value()).isEqualTo(VersionController.METRIC_VALUE); } } + diff --git a/src/test/java/com/iexec/core/version/VersionServiceTests.java b/src/test/java/com/iexec/core/version/VersionServiceTests.java deleted file mode 100644 index 6ef1c6ceb..000000000 --- a/src/test/java/com/iexec/core/version/VersionServiceTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020-2023 IEXEC BLOCKCHAIN TECH - * - * 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.iexec.core.version; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.springframework.boot.info.BuildProperties; - -class VersionServiceTests { - - @Mock - private BuildProperties buildProperties; - - @InjectMocks - private VersionService versionService; - - @BeforeEach - public void preflight() { - MockitoAnnotations.openMocks(this); - } - - @ParameterizedTest - @ValueSource(strings={"x.y.z", "x.y.z-rc", "x.y.z-NEXT-SNAPSHOT"}) - void testVersions(String version) { - Mockito.when(buildProperties.getVersion()).thenReturn(version); - Assertions.assertEquals(version, versionService.getVersion()); - } - -} diff --git a/src/test/java/com/iexec/core/worker/WorkerServiceTests.java b/src/test/java/com/iexec/core/worker/WorkerServiceTests.java index c5d59fd59..9fda5aa5f 100644 --- a/src/test/java/com/iexec/core/worker/WorkerServiceTests.java +++ b/src/test/java/com/iexec/core/worker/WorkerServiceTests.java @@ -17,6 +17,12 @@ package com.iexec.core.worker; import com.iexec.core.configuration.WorkerConfiguration; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -46,13 +52,41 @@ class WorkerServiceTests { @InjectMocks private WorkerService workerService; + + @BeforeAll + static void initRegistry() { + Metrics.globalRegistry.add(new SimpleMeterRegistry()); + } + @BeforeEach void init() { MockitoAnnotations.openMocks(this); } + @AfterEach + void afterEach() { + Metrics.globalRegistry.clear(); + } + // getWorker + @Test + void shouldReturnZeroForAllCountersWhereNothingHasAppended() { + workerService.init(); + Gauge aliveWorkersGauge = Metrics.globalRegistry.find(WorkerService.METRIC_WORKERS_GAUGE).gauge(); + Gauge aliveTotalCpuGauge = Metrics.globalRegistry.find(WorkerService.METRIC_CPU_TOTAL_GAUGE).gauge(); + Gauge aliveAvailableCpuGauge = Metrics.globalRegistry.find(WorkerService.METRIC_CPU_AVAILABLE_GAUGE).gauge(); + + Assertions.assertThat(aliveWorkersGauge).isNotNull(); + Assertions.assertThat(aliveTotalCpuGauge).isNotNull(); + Assertions.assertThat(aliveAvailableCpuGauge).isNotNull(); + + Assertions.assertThat(aliveWorkersGauge.value()).isZero(); + Assertions.assertThat(aliveTotalCpuGauge.value()).isZero(); + Assertions.assertThat(aliveAvailableCpuGauge.value()).isZero(); + + } + @Test void shouldGetWorker() { String workerName = "worker1"; @@ -98,7 +132,6 @@ void shouldNotAddNewWorker() { when(workerRepository.save(Mockito.any())).thenReturn(newWorker); Worker addedWorker = workerService.addWorker(newWorker); - assertThat(addedWorker).isNotEqualTo(existingWorker); assertThat(addedWorker.getId()).isEqualTo(existingWorker.getId()); } @@ -608,15 +641,31 @@ void shouldGetZeroAvailableCpuIfNoWorkerAlive() { void shouldGetTotalAliveCpu() { Worker worker1 = Worker.builder() .cpuNb(4) + .computingChainTaskIds(List.of("T1", "T2", "T3")) .build(); Worker worker2 = Worker.builder() .cpuNb(2) + .computingChainTaskIds(List.of("T4")) .build(); List list = List.of(worker1, worker2); when(workerRepository.findByWalletAddressIn(any())).thenReturn(list); + workerService.init(); + workerService.updateMetrics(); assertThat(workerService.getAliveTotalCpu()) .isEqualTo(worker1.getCpuNb() + worker2.getCpuNb()); + + Gauge aliveWorkersGauge = Metrics.globalRegistry.find(WorkerService.METRIC_WORKERS_GAUGE).gauge(); + Gauge aliveTotalCpuGauge = Metrics.globalRegistry.find(WorkerService.METRIC_CPU_TOTAL_GAUGE).gauge(); + Gauge aliveAvailableCpuGauge = Metrics.globalRegistry.find(WorkerService.METRIC_CPU_AVAILABLE_GAUGE).gauge(); + + Assertions.assertThat(aliveWorkersGauge).isNotNull(); + Assertions.assertThat(aliveTotalCpuGauge).isNotNull(); + Assertions.assertThat(aliveAvailableCpuGauge).isNotNull(); + + Assertions.assertThat(aliveWorkersGauge.value()).isEqualTo(list.size()); + Assertions.assertThat(aliveTotalCpuGauge.value()).isEqualTo(worker1.getCpuNb() + worker2.getCpuNb()); + Assertions.assertThat(aliveAvailableCpuGauge.value()).isEqualTo(2); } // getAliveTotalGpu diff --git a/src/test/resources/wiremock/mappings/chain-config.json b/src/test/resources/wiremock/mappings/chain-config.json new file mode 100644 index 000000000..fef4f4d95 --- /dev/null +++ b/src/test/resources/wiremock/mappings/chain-config.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/config/chain" + }, + "response": { + "status": 200, + "body": "{\"chainId\":255,\"chainNodeUrl\":\"http://localhost:8545\",\"blockTime\":\"PT5s\",\"sidechain\":\"true\"}", + "headers": { + "Content-Type": "application/json" + } + } +}