diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml index 1b8c0229..cde7b4b0 100644 --- a/.github/workflows/build-master.yml +++ b/.github/workflows/build-master.yml @@ -1,9 +1,10 @@ + name: Build master on: push: branches: - - "master" + - 'master' workflow_dispatch: concurrency: @@ -14,6 +15,7 @@ permissions: contents: write jobs: + check_source: name: "Run code checks" uses: ./.github/workflows/_shared-check.yaml @@ -26,54 +28,53 @@ jobs: ref: ${{ github.sha }} release: "snapshot" docker: true - docker_repository: "ethpandaops/assertoor" + docker_repository: "noku-team/assertoor" docker_tag_prefix: "master" additional_tags: "['master','master-latest']" secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - + update_project_wiki: name: Generate project documentation needs: [check_source] runs-on: ubuntu-latest steps: - - name: Checkout base code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - path: code - - name: Generate wiki from docs - env: - WIKI_TOKEN: ${{ secrets.WIKI_TOKEN }} - run: | - git clone https://$WIKI_TOKEN@github.com/${{ github.event.repository.owner.name }}/${{ github.event.repository.name }}.wiki.git ./wiki - - - name: Generate wiki from docs - run: | - touch ./wiki.md - - for filename in code/docs/*.md; do - while IFS= read -r line; do - if [[ "$line" =~ ^"#!!" ]]; then - bash -c "cd code && ${line:3}" >> ./wiki.md - else - echo "$line" >> ./wiki.md - fi - done <<< $(cat $filename) - echo "" >> ./wiki.md - done - - cp ./wiki.md ./wiki/Home.md - - - name: Push to wiki - env: - WIKI_TOKEN: ${{ secrets.WIKI_TOKEN }} - run: | - cd wiki - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add . - git diff-index --quiet HEAD || git commit -m "Add changes" && git push + - name: Checkout base code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: code + - name: Generate wiki from docs + env: + WIKI_TOKEN: ${{ secrets.WIKI_TOKEN }} + run: | + git clone https://$WIKI_TOKEN@github.com/${{ github.event.repository.owner.name }}/${{ github.event.repository.name }}.wiki.git ./wiki + + - name: Generate wiki from docs + run: | + touch ./wiki.md + + for filename in code/docs/*.md; do + while IFS= read -r line; do + if [[ "$line" =~ ^"#!!" ]]; then + bash -c "cd code && ${line:3}" >> ./wiki.md + else + echo "$line" >> ./wiki.md + fi + done <<< $(cat $filename) + echo "" >> ./wiki.md + done + + cp ./wiki.md ./wiki/Home.md + - name: Push to wiki + env: + WIKI_TOKEN: ${{ secrets.WIKI_TOKEN }} + run: | + cd wiki + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add . + git diff-index --quiet HEAD || git commit -m "Add changes" && git push create_snapshot_release: name: Create snapshot release @@ -84,164 +85,146 @@ jobs: - name: "Download build artifacts" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - # (re)create snapshot binary release - - name: Update snapshot tag & remove previous snapshot release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - try { - var snapshotTag = "snapshot"; - var snapshotRelease = await github.repos.getReleaseByTag({ + # (re)create snapshot binary release + - name: Update snapshot tag & remove previous snapshot release + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + try { + var snapshotTag = "snapshot"; + var snapshotRelease = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: snapshotTag + }); + if(snapshotRelease && snapshotRelease.data && snapshotRelease.data.tag_name == snapshotTag) { + console.log("delete previous snapshot release"); + await github.rest.repos.deleteRelease({ owner: context.repo.owner, repo: context.repo.repo, - tag: snapshotTag + release_id: snapshotRelease.data.id }); - if(snapshotRelease && snapshotRelease.data && snapshotRelease.data.tag_name == snapshotTag) { - console.log("delete previous snapshot release"); - await github.repos.deleteRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - tag: snapshotTag - }); - if(snapshotRelease && snapshotRelease.data && snapshotRelease.data.tag_name == snapshotTag) { - console.log("delete previous snapshot release"); - await github.repos.deleteRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: snapshotRelease.data.id - }); - } - - var snapshotRef = await github.git.getRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "tags/" + snapshotTag - }); - if(snapshotRef && snapshotRef.data && snapshotRef.data.ref) { - if(snapshotRef.data.object.sha !== context.sha) { - await github.git.updateRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "tags/" + snapshotTag, - sha: context.sha, - }); - } - } - else { - await github.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "tags/" + snapshotTag, - sha: context.sha, - }); - } - } catch (e) { - console.log(e) - } - else { - await github.git.createRef({ + } + + var snapshotRef = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "tags/" + snapshotTag + }); + if(snapshotRef && snapshotRef.data && snapshotRef.data.ref) { + if(snapshotRef.data.object.sha !== context.sha) { + await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: "tags/" + snapshotTag, sha: context.sha, }); } - } catch (e) { - console.log(e) } - - name: Create snapshot release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 - id: create_release - with: - draft: false - prerelease: true - release_name: "Dev Snapshot" - tag_name: "snapshot" - body: | - ## Latest automatically built executables. (Unstable development snapshot) - Built from master branch (commit: ${{ github.sha }}) - - Please read the [wiki](https://github.com/ethpandaops/assertoor/wiki) for setup / configuration instructions. - - ### Release Artifacts - | Release File | Description | - | ------------- | ------------- | - | [assertoor_snapshot_windows_amd64.zip](https://github.com/ethpandaops/assertoor/releases/download/snapshot/assertoor_snapshot_windows_amd64.zip) | assertoor executables for windows/amd64 | - | [assertoor_snapshot_linux_amd64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/snapshot/assertoor_snapshot_linux_amd64.tar.gz) | assertoor executables for linux/amd64 | - | [assertoor_snapshot_linux_arm64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/snapshot/assertoor_snapshot_linux_arm64.tar.gz) | assertoor executables for linux/arm64 | - | [assertoor_snapshot_darwin_amd64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/snapshot/assertoor_snapshot_darwin_amd64.tar.gz) | assertoor executable for macos/amd64 | - | [assertoor_snapshot_darwin_arm64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/snapshot/assertoor_snapshot_darwin_arm64.tar.gz) | assertoor executable for macos/arm64 | - env: - GITHUB_TOKEN: ${{ github.token }} - - # generate & upload release artifacts - - name: "Generate release package: assertoor_snapshot_windows_amd64.zip" - run: | - cd assertoor_windows_amd64 - zip -r -q ../assertoor_snapshot_windows_amd64.zip . - - name: "Upload snapshot release artifact: assertoor_snapshot_windows_amd64.zip" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_snapshot_windows_amd64.zip - asset_name: assertoor_snapshot_windows_amd64.zip - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: "Generate release package: assertoor_snapshot_linux_amd64.tar.gz" - run: | - cd assertoor_linux_amd64 - tar -czf ../assertoor_snapshot_linux_amd64.tar.gz . - - name: "Upload snapshot release artifact: assertoor_snapshot_linux_amd64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_snapshot_linux_amd64.tar.gz - asset_name: assertoor_snapshot_linux_amd64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: "Generate release package: assertoor_snapshot_linux_arm64.tar.gz" - run: | - cd assertoor_linux_arm64 - tar -czf ../assertoor_snapshot_linux_arm64.tar.gz . - - name: "Upload snapshot release artifact: assertoor_snapshot_linux_arm64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_snapshot_linux_arm64.tar.gz - asset_name: assertoor_snapshot_linux_arm64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: "Generate release package: assertoor_snapshot_darwin_amd64.tar.gz" - run: | - cd assertoor_darwin_amd64 - tar -czf ../assertoor_snapshot_darwin_amd64.tar.gz . - - name: "Upload snapshot release artifact: assertoor_snapshot_darwin_amd64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_snapshot_darwin_amd64.tar.gz - asset_name: assertoor_snapshot_darwin_amd64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: "Generate release package: assertoor_snapshot_darwin_arm64.tar.gz" - run: | - cd assertoor_darwin_arm64 - tar -czf ../assertoor_snapshot_darwin_arm64.tar.gz . - - name: "Upload snapshot release artifact: assertoor_snapshot_darwin_arm64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_snapshot_darwin_arm64.tar.gz - asset_name: assertoor_snapshot_darwin_arm64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} + else { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "tags/" + snapshotTag, + sha: context.sha, + }); + } + } catch (e) { + console.log(e) + } + - name: Create snapshot release + uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 + id: create_release + with: + draft: false + prerelease: true + release_name: "Dev Snapshot" + tag_name: "snapshot" + body: | + ## Latest automatically built executables. (Unstable development snapshot) + Built from master branch (commit: ${{ github.sha }}) + + Please read the [wiki](https://github.com/noku-team/assertoor/wiki) for setup / configuration instructions. + + ### Release Artifacts + | Release File | Description | + | ------------- | ------------- | + | [assertoor_snapshot_windows_amd64.zip](https://github.com/noku-team/assertoor/releases/download/snapshot/assertoor_snapshot_windows_amd64.zip) | assertoor executables for windows/amd64 | + | [assertoor_snapshot_linux_amd64.tar.gz](https://github.com/noku-team/assertoor/releases/download/snapshot/assertoor_snapshot_linux_amd64.tar.gz) | assertoor executables for linux/amd64 | + | [assertoor_snapshot_linux_arm64.tar.gz](https://github.com/noku-team/assertoor/releases/download/snapshot/assertoor_snapshot_linux_arm64.tar.gz) | assertoor executables for linux/arm64 | + | [assertoor_snapshot_darwin_amd64.tar.gz](https://github.com/noku-team/assertoor/releases/download/snapshot/assertoor_snapshot_darwin_amd64.tar.gz) | assertoor executable for macos/amd64 | + | [assertoor_snapshot_darwin_arm64.tar.gz](https://github.com/noku-team/assertoor/releases/download/snapshot/assertoor_snapshot_darwin_arm64.tar.gz) | assertoor executable for macos/arm64 | + env: + GITHUB_TOKEN: ${{ github.token }} + + # generate & upload release artifacts + - name: "Generate release package: assertoor_snapshot_windows_amd64.zip" + run: | + cd assertoor_windows_amd64 + zip -r -q ../assertoor_snapshot_windows_amd64.zip . + - name: "Upload snapshot release artifact: assertoor_snapshot_windows_amd64.zip" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_snapshot_windows_amd64.zip + asset_name: assertoor_snapshot_windows_amd64.zip + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_snapshot_linux_amd64.tar.gz" + run: | + cd assertoor_linux_amd64 + tar -czf ../assertoor_snapshot_linux_amd64.tar.gz . + - name: "Upload snapshot release artifact: assertoor_snapshot_linux_amd64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_snapshot_linux_amd64.tar.gz + asset_name: assertoor_snapshot_linux_amd64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_snapshot_linux_arm64.tar.gz" + run: | + cd assertoor_linux_arm64 + tar -czf ../assertoor_snapshot_linux_arm64.tar.gz . + - name: "Upload snapshot release artifact: assertoor_snapshot_linux_arm64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_snapshot_linux_arm64.tar.gz + asset_name: assertoor_snapshot_linux_arm64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_snapshot_darwin_amd64.tar.gz" + run: | + cd assertoor_darwin_amd64 + tar -czf ../assertoor_snapshot_darwin_amd64.tar.gz . + - name: "Upload snapshot release artifact: assertoor_snapshot_darwin_amd64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_snapshot_darwin_amd64.tar.gz + asset_name: assertoor_snapshot_darwin_amd64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_snapshot_darwin_arm64.tar.gz" + run: | + cd assertoor_darwin_arm64 + tar -czf ../assertoor_snapshot_darwin_arm64.tar.gz . + - name: "Upload snapshot release artifact: assertoor_snapshot_darwin_arm64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_snapshot_darwin_arm64.tar.gz + asset_name: assertoor_snapshot_darwin_arm64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 038bb8a9..57f0a78a 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,3 +1,4 @@ + name: Build Release on: @@ -18,133 +19,133 @@ jobs: ref: ${{ github.sha }} release: "v${{ inputs.version }}" docker: true - docker_repository: "ethpandaops/assertoor" + docker_repository: "noku-team/assertoor" docker_tag_prefix: "v${{ inputs.version }}" additional_tags: "['v${{ inputs.version }}','latest']" secrets: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - + create_release: name: Create Release needs: [build_binaries] runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 100 - ref: ${{ github.sha }} - - name: "Generate release changelog" - id: changelog - run: | - git fetch --tags - prev_tag=$(git tag --sort=-version:refname | grep -e "^v[0-9.]*$" | head -n 1) - echo "previous release: $prev_tag" - if [ "$prev_tag" ]; then - changelog=$(git log --oneline --no-decorate $prev_tag..HEAD) - else - changelog=$(git log --oneline --no-decorate) - fi - echo "changelog<> $GITHUB_OUTPUT - echo " - ${changelog//$'\n'/$'\n' - }" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 100 + ref: ${{ github.sha }} + - name: "Generate release changelog" + id: changelog + run: | + git fetch --tags + prev_tag=$(git tag --sort=-version:refname | grep -e "^v[0-9.]*$" | head -n 1) + echo "previous release: $prev_tag" + if [ "$prev_tag" ]; then + changelog=$(git log --oneline --no-decorate $prev_tag..HEAD) + else + changelog=$(git log --oneline --no-decorate) + fi + echo "changelog<> $GITHUB_OUTPUT + echo " - ${changelog//$'\n'/$'\n' - }" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT # download build artifacts - name: "Download build artifacts" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - # create draft release - - name: Create latest release - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 - id: create_release - with: - draft: true - prerelease: false - release_name: "v${{ inputs.version }}" - tag_name: "v${{ inputs.version }}" - body: | - ### Changes - ${{ steps.changelog.outputs.changelog }} - - ### Release Artifacts - Please read through the [wiki](https://github.com/ethpandaops/assertoor/wiki) for setup & configuration instructions. - | Release File | Description | - | ------------- | ------------- | - | [assertoor_${{ inputs.version }}_windows_amd64.zip](https://github.com/ethpandaops/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_windows_amd64.zip) | assertoor executables for windows/amd64 | - | [assertoor_${{ inputs.version }}_linux_amd64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_linux_amd64.tar.gz) | assertoor executables for linux/amd64 | - | [assertoor_${{ inputs.version }}_linux_arm64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_linux_arm64.tar.gz) | assertoor executables for linux/arm64 | - | [assertoor_${{ inputs.version }}_darwin_amd64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_darwin_amd64.tar.gz) | assertoor executable for macos/amd64 | - | [assertoor_${{ inputs.version }}_darwin_arm64.tar.gz](https://github.com/ethpandaops/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_darwin_arm64.tar.gz) | assertoor executable for macos/arm64 | - env: - GITHUB_TOKEN: ${{ github.token }} - - # generate & upload release artifacts - - name: "Generate release package: assertoor_${{ inputs.version }}_windows_amd64.zip" - run: | - cd assertoor_windows_amd64 - zip -r -q ../assertoor_${{ inputs.version }}_windows_amd64.zip . - - name: "Upload release artifact: assertoor_${{ inputs.version }}_windows_amd64.zip" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_${{ inputs.version }}_windows_amd64.zip - asset_name: assertoor_${{ inputs.version }}_windows_amd64.zip - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: "Generate release package: assertoor_${{ inputs.version }}_linux_amd64.tar.gz" - run: | - cd assertoor_linux_amd64 - tar -czf ../assertoor_${{ inputs.version }}_linux_amd64.tar.gz . - - name: "Upload release artifact: assertoor_${{ inputs.version }}_linux_amd64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_${{ inputs.version }}_linux_amd64.tar.gz - asset_name: assertoor_${{ inputs.version }}_linux_amd64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} + # create draft release + - name: Create latest release + uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 + id: create_release + with: + draft: true + prerelease: false + release_name: "v${{ inputs.version }}" + tag_name: "v${{ inputs.version }}" + body: | + ### Changes + ${{ steps.changelog.outputs.changelog }} - - name: "Generate release package: assertoor_${{ inputs.version }}_linux_arm64.tar.gz" - run: | - cd assertoor_linux_arm64 - tar -czf ../assertoor_${{ inputs.version }}_linux_arm64.tar.gz . - - name: "Upload release artifact: assertoor_${{ inputs.version }}_linux_arm64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_${{ inputs.version }}_linux_arm64.tar.gz - asset_name: assertoor_${{ inputs.version }}_linux_arm64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} + ### Release Artifacts + Please read through the [wiki](https://github.com/noku-team/assertoor/wiki) for setup & configuration instructions. + | Release File | Description | + | ------------- | ------------- | + | [assertoor_${{ inputs.version }}_windows_amd64.zip](https://github.com/noku-team/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_windows_amd64.zip) | assertoor executables for windows/amd64 | + | [assertoor_${{ inputs.version }}_linux_amd64.tar.gz](https://github.com/noku-team/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_linux_amd64.tar.gz) | assertoor executables for linux/amd64 | + | [assertoor_${{ inputs.version }}_linux_arm64.tar.gz](https://github.com/noku-team/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_linux_arm64.tar.gz) | assertoor executables for linux/arm64 | + | [assertoor_${{ inputs.version }}_darwin_amd64.tar.gz](https://github.com/noku-team/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_darwin_amd64.tar.gz) | assertoor executable for macos/amd64 | + | [assertoor_${{ inputs.version }}_darwin_arm64.tar.gz](https://github.com/noku-team/assertoor/releases/download/v${{ inputs.version }}/assertoor_${{ inputs.version }}_darwin_arm64.tar.gz) | assertoor executable for macos/arm64 | + env: + GITHUB_TOKEN: ${{ github.token }} - - name: "Generate release package: assertoor_${{ inputs.version }}_darwin_amd64.tar.gz" - run: | - cd assertoor_darwin_amd64 - tar -czf ../assertoor_${{ inputs.version }}_darwin_amd64.tar.gz . - - name: "Upload release artifact: assertoor_${{ inputs.version }}_darwin_amd64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_${{ inputs.version }}_darwin_amd64.tar.gz - asset_name: assertoor_${{ inputs.version }}_darwin_amd64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} + # generate & upload release artifacts + - name: "Generate release package: assertoor_${{ inputs.version }}_windows_amd64.zip" + run: | + cd assertoor_windows_amd64 + zip -r -q ../assertoor_${{ inputs.version }}_windows_amd64.zip . + - name: "Upload release artifact: assertoor_${{ inputs.version }}_windows_amd64.zip" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_${{ inputs.version }}_windows_amd64.zip + asset_name: assertoor_${{ inputs.version }}_windows_amd64.zip + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_${{ inputs.version }}_linux_amd64.tar.gz" + run: | + cd assertoor_linux_amd64 + tar -czf ../assertoor_${{ inputs.version }}_linux_amd64.tar.gz . + - name: "Upload release artifact: assertoor_${{ inputs.version }}_linux_amd64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_${{ inputs.version }}_linux_amd64.tar.gz + asset_name: assertoor_${{ inputs.version }}_linux_amd64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_${{ inputs.version }}_linux_arm64.tar.gz" + run: | + cd assertoor_linux_arm64 + tar -czf ../assertoor_${{ inputs.version }}_linux_arm64.tar.gz . + - name: "Upload release artifact: assertoor_${{ inputs.version }}_linux_arm64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_${{ inputs.version }}_linux_arm64.tar.gz + asset_name: assertoor_${{ inputs.version }}_linux_arm64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: "Generate release package: assertoor_${{ inputs.version }}_darwin_amd64.tar.gz" + run: | + cd assertoor_darwin_amd64 + tar -czf ../assertoor_${{ inputs.version }}_darwin_amd64.tar.gz . + - name: "Upload release artifact: assertoor_${{ inputs.version }}_darwin_amd64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_${{ inputs.version }}_darwin_amd64.tar.gz + asset_name: assertoor_${{ inputs.version }}_darwin_amd64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} - - name: "Generate release package: assertoor_${{ inputs.version }}_darwin_arm64.tar.gz" - run: | - cd assertoor_darwin_arm64 - tar -czf ../assertoor_${{ inputs.version }}_darwin_arm64.tar.gz . - - name: "Upload release artifact: assertoor_${{ inputs.version }}_darwin_arm64.tar.gz" - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./assertoor_${{ inputs.version }}_darwin_arm64.tar.gz - asset_name: assertoor_${{ inputs.version }}_darwin_arm64.tar.gz - asset_content_type: application/octet-stream - env: - GITHUB_TOKEN: ${{ github.token }} + - name: "Generate release package: assertoor_${{ inputs.version }}_darwin_arm64.tar.gz" + run: | + cd assertoor_darwin_arm64 + tar -czf ../assertoor_${{ inputs.version }}_darwin_arm64.tar.gz . + - name: "Upload release artifact: assertoor_${{ inputs.version }}_darwin_arm64.tar.gz" + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./assertoor_${{ inputs.version }}_darwin_arm64.tar.gz + asset_name: assertoor_${{ inputs.version }}_darwin_arm64.tar.gz + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/pkg/coordinator/tasks/run_shell/task.go b/pkg/coordinator/tasks/run_shell/task.go index 69f92018..f6a66fa3 100644 --- a/pkg/coordinator/tasks/run_shell/task.go +++ b/pkg/coordinator/tasks/run_shell/task.go @@ -396,7 +396,7 @@ func (t *Task) storeTaskResults(summaryFile *resultFile, resultDir string) { TaskID: uint64(t.ctx.Index), Type: "result", Index: fileIdx, - Name: file.Name(), + Name: fmt.Sprintf("%v%v", prefix, file.Name()), Size: uint64(len(data)), Data: data, }); err3 != nil { diff --git a/pkg/coordinator/tasks/tx_pool_latency_analysis/README.md b/pkg/coordinator/tasks/tx_pool_latency_analysis/README.md index a675b5c5..45513029 100644 --- a/pkg/coordinator/tasks/tx_pool_latency_analysis/README.md +++ b/pkg/coordinator/tasks/tx_pool_latency_analysis/README.md @@ -9,36 +9,39 @@ The `tx_pool_latency_analysis` task evaluates latency of transaction processing - **`privateKey`**: The private key of the account to use for sending transactions. -- **`txCount`**: - The total number of transactions to send. +- **`tps`**: + The total number of transactions to send in one second. + +- **`duration_s`**: + The test duration (the number of transactions to send is calculated as `tps * duration_s`). - **`measureInterval`**: The interval at which the script logs progress (e.g., every 100 transactions). -- **`highLatency`**: - The expected average transaction latency in milliseconds. - -- **`failOnHighLatency`**: - Whether the task should fail if the measured latency exceeds `highLatency`. - - ### Outputs - **`tx_count`**: The total number of transactions sent. -- **`avg_latency_ms`**: - The average latency of the transactions in milliseconds. +- **`min_latency_mus`**: + The min latency of the transactions in microseconds. + +- **`max_latency_mus`**: + The max latency of the transactions in microseconds. + +- **`tx_pool_latency_hdr_plot`**: + The HDR plot of the transaction pool latency. ### Defaults ```yaml - name: tx_pool_latency_analysis config: - txCount: 15000 + tps: 100 + duration_s: 10 measureInterval: 1000 - highLatency: 5000 - failOnHighLatency: false configVars: - privateKey: "tx_pool_latency_analysis" + privateKey: "walletPrivkey" ``` + + diff --git a/pkg/coordinator/tasks/tx_pool_latency_analysis/config.go b/pkg/coordinator/tasks/tx_pool_latency_analysis/config.go index f8b0ea48..5bfed789 100644 --- a/pkg/coordinator/tasks/tx_pool_latency_analysis/config.go +++ b/pkg/coordinator/tasks/tx_pool_latency_analysis/config.go @@ -3,19 +3,17 @@ package txpoollatencyanalysis type Config struct { PrivateKey string `yaml:"privateKey" json:"privateKey"` - TxCount int `yaml:"txCount" json:"txCount"` + TPS int `yaml:"tps" json:"tps"` + Duration_s int `yaml:"duration_s" json:"duration_s"` MeasureInterval int `yaml:"measureInterval" json:"measureInterval"` - HighLatency int64 `yaml:"highLatency" json:"highLatency"` - FailOnHighLatency bool `yaml:"failOnHighLatency" json:"failOnHighLatency"` SecondsBeforeRunning int64 `yaml:"secondsBeforeRunning" json:"secondsBeforeRunning"` } func DefaultConfig() Config { return Config{ - TxCount: 1000, + TPS: 100, + Duration_s: 60, MeasureInterval: 100, - HighLatency: 5000, // in microseconds - FailOnHighLatency: true, SecondsBeforeRunning: 0, } } diff --git a/pkg/coordinator/tasks/tx_pool_latency_analysis/task.go b/pkg/coordinator/tasks/tx_pool_latency_analysis/task.go index 0d411d2b..31100ae6 100644 --- a/pkg/coordinator/tasks/tx_pool_latency_analysis/task.go +++ b/pkg/coordinator/tasks/tx_pool_latency_analysis/task.go @@ -7,7 +7,6 @@ import ( "fmt" "math/big" "math/rand" - "sort" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -28,7 +27,7 @@ var ( TaskName = "tx_pool_latency_analysis" TaskDescriptor = &types.TaskDescriptor{ Name: TaskName, - Description: "Checks the latency of transactions in the Ethereum TxPool", + Description: "Checks the TxPool transaction propagation latency", Config: DefaultConfig(), NewTask: NewTask, } @@ -102,6 +101,10 @@ func (t *Task) Execute(ctx context.Context) error { client := executionClients[rand.Intn(len(executionClients))] + t.logger.Infof("Measuring TxPool transaction propagation *latency*") + t.logger.Infof("Targeting client: %s, TPS: %d, Duration: %d seconds", + client.GetName(), t.config.TPS, t.config.Duration_s) + conn, err := t.getTcpConn(ctx, client) if err != nil { t.logger.Errorf("Failed to get wire eth TCP connection: %v", err) @@ -111,67 +114,176 @@ func (t *Task) Execute(ctx context.Context) error { defer conn.Close() - var totalLatency time.Duration - var latencies []time.Duration - - var txs []*ethtypes.Transaction - - for i := 0; i < t.config.TxCount; i++ { - tx, err := t.generateTransaction(ctx) - if err != nil { - t.logger.Errorf("Failed to create transaction: %v", err) - t.ctx.SetResult(types.TaskResultFailure) - return nil + // Wait for the specified seconds before starting the task + if t.config.SecondsBeforeRunning > 0 { + t.logger.Infof("Waiting for %d seconds before starting the task...", t.config.SecondsBeforeRunning) + select { + case <-time.After(time.Duration(t.config.SecondsBeforeRunning) * time.Second): + t.logger.Infof("Starting task after waiting.") + case <-ctx.Done(): + t.logger.Warnf("Task cancelled before starting.") + return ctx.Err() } + } - startTx := time.Now() + // Prepare to collect transaction latencies + var totNumberOfTxes int = t.config.TPS * t.config.Duration_s + var txs []*ethtypes.Transaction = make([]*ethtypes.Transaction, totNumberOfTxes) + var txStartTime []time.Time = make([]time.Time, totNumberOfTxes) + var testDeadline time.Time = time.Now().Add(time.Duration(t.config.Duration_s+60*30) * time.Second) + var latenciesMus = make([]int64, totNumberOfTxes) + + startTime := time.Now() + isFailed := false + sentTxCount := 0 + duplicatedP2PEventCount := 0 + coordinatedOmissionEventCount := 0 + + // Start generating and sending transactions + go func() { + startExecTime := time.Now() + endTime := startExecTime.Add(time.Second * time.Duration(t.config.Duration_s)) + + // Generate and send transactions + for i := 0; i < totNumberOfTxes; i++ { + // Calculate how much time we have left + remainingTime := time.Until(endTime) + + // Calculate sleep time to distribute remaining transactions evenly + sleepTime := remainingTime / time.Duration(totNumberOfTxes-i) + + // generate and send tx + go func(i int) { + + tx, err := t.generateTransaction(ctx, i) + if err != nil { + t.logger.Errorf("Failed to create transaction: %v", err) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true + return + } + + txStartTime[i] = time.Now() + err = client.GetRPCClient().SendTransaction(ctx, tx) + if err != nil { + t.logger.WithField("client", client.GetName()).Errorf("Failed to send transaction: %v", err) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true + return + } + + txs[i] = tx + sentTxCount++ + + // log transaction sending + if sentTxCount%t.config.MeasureInterval == 0 { + elapsed := time.Since(startTime) + t.logger.Infof("Sent %d transactions in %.2fs", sentTxCount, elapsed.Seconds()) + } + + }(i) + + // Sleep to control the TPS + if i < totNumberOfTxes-1 { + if sleepTime > 0 { + time.Sleep(sleepTime) + } else { + coordinatedOmissionEventCount++ + } + } - err = client.GetRPCClient().SendTransaction(ctx, tx) - if err != nil { - t.logger.Errorf("Failed to send transaction: %v. Nonce: %d. ", err, tx.Nonce()) - t.ctx.SetResult(types.TaskResultFailure) - return nil + select { + case <-ctx.Done(): + t.logger.Warnf("Task cancelled, stopping transaction generation.") + return + default: + // if testDeadline reached, stop sending txes + if isFailed { + return + } + if time.Now().After(testDeadline) { + t.logger.Infof("Reached duration limit, stopping transaction generation.") + return + } + } } + }() - txs = append(txs, tx) + // Wait P2P event messages + func() { + var receivedEvents int = 0 + for { + txes, err := conn.ReadTransactionMessages() + if err != nil { + t.logger.Errorf("Failed reading p2p events: %v", err) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true + return + } - // Create a context with timeout for reading transaction messages - readCtx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() + for _, tx := range *txes { + tx_data := tx.Data() + // read tx_data that is in the format "tx_index:" + var tx_index int + _, err := fmt.Sscanf(string(tx_data), "tx_index:%d", &tx_index) + if err != nil { + t.logger.Errorf("Failed to parse transaction data: %v", err) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true + return + } + if tx_index < 0 || tx_index >= totNumberOfTxes { + t.logger.Errorf("Transaction index out of range: %d", tx_index) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true + return + } + + // log the duplicated p2p events, and count duplicated p2p events + // todo: add a timeout of N seconds that activates if duplicatedP2PEventCount + receivedEvents >= totNumberOfTxes, if exceeded, exit the function + if latenciesMus[tx_index] != 0 { + duplicatedP2PEventCount++ + } + + latenciesMus[tx_index] = time.Since(txStartTime[tx_index]).Microseconds() + receivedEvents++ + + if receivedEvents%t.config.MeasureInterval == 0 { + t.logger.Infof("Received %d p2p events", receivedEvents) + } + } - done := make(chan error, 1) - go func() { - _, readErr := conn.ReadTransactionMessages() - done <- readErr - }() + if receivedEvents >= totNumberOfTxes { + t.logger.Infof("Reading of p2p events finished") + return + } - select { - case err = <-done: - if err != nil { - t.logger.Errorf("Failed to read transaction messages: %v", err) - t.ctx.SetResult(types.TaskResultFailure) - return nil + select { + case <-ctx.Done(): + t.logger.Warnf("Task cancelled, stopping reading p2p events.") + return + default: + // check test deadline + if time.Now().After(testDeadline) { + t.logger.Warnf("Reached duration limit, stopping reading p2p events.") + return + } } - case <-readCtx.Done(): - t.logger.Warnf("Timeout waiting for transaction message at index %d, retrying transaction", i) - i-- // Retry this transaction - continue } + }() - latency := time.Since(startTx) - latencies = append(latencies, latency) - totalLatency += latency + lastMeasureDelay := time.Since(startTime) + t.logger.Infof("Last measure delay since start time: %s", lastMeasureDelay) - if (i+1)%t.config.MeasureInterval == 0 { - avgSoFar := totalLatency.Microseconds() / int64(i+1) - t.logger.Infof("Processed %d transactions, current avg latency: %dmus.", i+1, avgSoFar) - } + if coordinatedOmissionEventCount > 0 { + t.logger.Warnf("Coordinated omission events: %d", coordinatedOmissionEventCount) } - avgLatency := totalLatency / time.Duration(t.config.TxCount) - t.logger.Infof("Average transaction latency: %dmus", avgLatency.Microseconds()) + if duplicatedP2PEventCount > 0 { + t.logger.Warnf("Duplicated p2p events: %d", duplicatedP2PEventCount) + } - // send to other clients, for speeding up tx mining + // Send txes to other clients, for speeding up tx mining for _, tx := range txs { for _, otherClient := range executionClients { if otherClient.GetName() == client.GetName() { @@ -182,22 +294,28 @@ func (t *Task) Execute(ctx context.Context) error { } } - // Convert latencies to microseconds for processing - latenciesMus := make([]int64, len(latencies)) - for i, latency := range latencies { - latenciesMus[i] = latency.Microseconds() + // Check if the context was cancelled or other errors occurred + if ctx.Err() != nil && !isFailed { + return nil + } + + // Check if we received all transactions p2p events + notReceivedP2PEventCount := 0 + for i := 0; i < totNumberOfTxes; i++ { + if latenciesMus[i] == 0 { + notReceivedP2PEventCount++ + // Assign a default value for missing P2P events + latenciesMus[i] = (time.Duration(t.config.Duration_s) * time.Second).Microseconds() + } + } + if notReceivedP2PEventCount > 0 { + t.logger.Warnf("Missed p2p events: %d (assigned latency=duration)", notReceivedP2PEventCount) } // Calculate statistics - var totalLatencyMus int64 var maxLatency int64 = 0 var minLatency int64 = 0 - if len(latenciesMus) > 0 { - minLatency = latenciesMus[0] - } - for _, lat := range latenciesMus { - totalLatencyMus += lat if lat > maxLatency { maxLatency = lat } @@ -205,49 +323,7 @@ func (t *Task) Execute(ctx context.Context) error { minLatency = lat } } - - // Calculate mean - var meanLatency float64 = 0 - if len(latenciesMus) > 0 { - meanLatency = float64(totalLatencyMus) / float64(len(latenciesMus)) - } - - // Sort for percentiles - sortedLatencies := make([]int64, len(latenciesMus)) - copy(sortedLatencies, latenciesMus) - sort.Slice(sortedLatencies, func(i, j int) bool { - return sortedLatencies[i] < sortedLatencies[j] - }) - - // Calculate percentiles - percentile50th := float64(0) - percentile90th := float64(0) - percentile95th := float64(0) - percentile99th := float64(0) - - if len(sortedLatencies) > 0 { - getPercentile := func(pct float64) float64 { - idx := int(float64(len(sortedLatencies)-1) * pct / 100) - return float64(sortedLatencies[idx]) - } - - percentile50th = getPercentile(50) - percentile90th = getPercentile(90) - percentile95th = getPercentile(95) - percentile99th = getPercentile(99) - } - - // Create statistics map for output - latenciesStats := map[string]float64{ - "total": float64(totalLatencyMus), - "mean": meanLatency, - "50th": percentile50th, - "90th": percentile90th, - "95th": percentile95th, - "99th": percentile99th, - "max": float64(maxLatency), - "min": float64(minLatency), - } + t.logger.Infof("Max latency: %d mus, Min latency: %d mus", maxLatency, minLatency) // Generate HDR plot plot, err := hdr.HdrPlot(latenciesMus) @@ -257,22 +333,22 @@ func (t *Task) Execute(ctx context.Context) error { return nil } - if t.config.FailOnHighLatency && avgLatency.Microseconds() > t.config.HighLatency { - t.logger.Errorf("Transaction latency too high: %dmus (expected <= %dmus)", avgLatency.Microseconds(), t.config.HighLatency) - t.ctx.SetResult(types.TaskResultFailure) - } else { - t.ctx.Outputs.SetVar("tx_count", t.config.TxCount) - t.ctx.Outputs.SetVar("avg_latency_mus", avgLatency.Microseconds()) - t.ctx.Outputs.SetVar("latencies", latenciesStats) + t.ctx.Outputs.SetVar("tx_count", totNumberOfTxes) + t.ctx.Outputs.SetVar("min_latency_mus", minLatency) + t.ctx.Outputs.SetVar("max_latency_mus", maxLatency) + t.ctx.Outputs.SetVar("duplicated_p2p_event_count", duplicatedP2PEventCount) + t.ctx.Outputs.SetVar("missed_p2p_event_count", notReceivedP2PEventCount) + t.ctx.Outputs.SetVar("coordinated_omission_event_count", coordinatedOmissionEventCount) - t.ctx.SetResult(types.TaskResultSuccess) - } + t.ctx.SetResult(types.TaskResultSuccess) outputs := map[string]interface{}{ - "tx_count": t.config.TxCount, - "avg_latency_mus": avgLatency.Microseconds(), - "tx_pool_latency_hdr_plot": plot, - "latencies": latenciesStats, + "tx_count": totNumberOfTxes, + "min_latency_mus": minLatency, + "max_latency_mus": maxLatency, + "tx_pool_latency_hdr_plot": plot, + "duplicated_p2p_event_count": duplicatedP2PEventCount, + "coordinated_omission_events_count": coordinatedOmissionEventCount, } outputsJSON, _ := json.Marshal(outputs) @@ -324,7 +400,7 @@ func (t *Task) getTcpConn(ctx context.Context, client *execution.Client) (*sentr return conn, nil } -func (t *Task) generateTransaction(ctx context.Context) (*ethtypes.Transaction, error) { +func (t *Task) generateTransaction(ctx context.Context, i int) (*ethtypes.Transaction, error) { tx, err := t.wallet.BuildTransaction(ctx, func(_ context.Context, nonce uint64, _ bind.SignerFn) (*ethtypes.Transaction, error) { addr := t.wallet.GetAddress() toAddr := &addr @@ -334,9 +410,7 @@ func (t *Task) generateTransaction(ctx context.Context) (*ethtypes.Transaction, feeCap := &helper.BigInt{Value: *big.NewInt(100000000000)} // 100 Gwei tipCap := &helper.BigInt{Value: *big.NewInt(1000000000)} // 1 Gwei - var txObj ethtypes.TxData - - txObj = ðtypes.DynamicFeeTx{ + txObj := ðtypes.DynamicFeeTx{ ChainID: t.ctx.Scheduler.GetServices().ClientPool().GetExecutionPool().GetBlockCache().GetChainID(), Nonce: nonce, GasTipCap: &tipCap.Value, @@ -344,7 +418,7 @@ func (t *Task) generateTransaction(ctx context.Context) (*ethtypes.Transaction, Gas: 50000, To: toAddr, Value: txAmount, - Data: []byte{}, + Data: []byte(fmt.Sprintf("tx_index:%d", i)), } return ethtypes.NewTx(txObj), nil diff --git a/pkg/coordinator/tasks/tx_pool_throughput_analysis/README.md b/pkg/coordinator/tasks/tx_pool_throughput_analysis/README.md index b59eef7b..7c1fc94c 100644 --- a/pkg/coordinator/tasks/tx_pool_throughput_analysis/README.md +++ b/pkg/coordinator/tasks/tx_pool_throughput_analysis/README.md @@ -9,23 +9,30 @@ The `tx_pool_throughput_analysis` task evaluates the throughput of transaction p - **`privateKey`**: The private key of the account to use for sending transactions. -- **`qps`**: +- **`tps`**: The total number of transactions to send in one second. +- **`duration_s`**: + The test duration (the number of transactions to send is calculated as `tps * duration_s`). + - **`measureInterval`**: The interval at which the script logs progress (e.g., every 100 transactions). ### Outputs -- **`total_time_mus`**: - The total time taken to send the transactions in microseconds. +- **`tx_count`**: + The total number of transactions sent. + +- **`mean_tps_throughput`**: + The mean throughput (tps) ### Defaults ```yaml - name: tx_pool_throughput_analysis config: - qps: 15000 + tps: 100 + duration_s: 10 measureInterval: 1000 configVars: privateKey: "walletPrivkey" diff --git a/pkg/coordinator/tasks/tx_pool_throughput_analysis/config.go b/pkg/coordinator/tasks/tx_pool_throughput_analysis/config.go index 60e0e70e..a6940c37 100644 --- a/pkg/coordinator/tasks/tx_pool_throughput_analysis/config.go +++ b/pkg/coordinator/tasks/tx_pool_throughput_analysis/config.go @@ -3,15 +3,17 @@ package txpoolcheck type Config struct { PrivateKey string `yaml:"privateKey" json:"privateKey"` - QPS int `yaml:"qps" json:"qps"` + TPS int `yaml:"tps" json:"tps"` + Duration_s int `yaml:"duration_s" json:"duration_s"` MeasureInterval int `yaml:"measureInterval" json:"measureInterval"` SecondsBeforeRunning int `yaml:"secondsBeforeRunning" json:"secondsBeforeRunning"` } func DefaultConfig() Config { return Config{ - QPS: 1000, - MeasureInterval: 100, + TPS: 100, + Duration_s: 60, + MeasureInterval: 100, SecondsBeforeRunning: 0, } } diff --git a/pkg/coordinator/tasks/tx_pool_throughput_analysis/task.go b/pkg/coordinator/tasks/tx_pool_throughput_analysis/task.go index 11f7f5dd..b2a0d86c 100644 --- a/pkg/coordinator/tasks/tx_pool_throughput_analysis/task.go +++ b/pkg/coordinator/tasks/tx_pool_throughput_analysis/task.go @@ -13,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/core/forkid" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/params" "github.com/noku-team/assertoor/pkg/coordinator/clients/execution" "github.com/noku-team/assertoor/pkg/coordinator/helper" @@ -27,7 +26,7 @@ var ( TaskName = "tx_pool_throughput_analysis" TaskDescriptor = &types.TaskDescriptor{ Name: TaskName, - Description: "Checks the throughput of transactions in the Ethereum TxPool", + Description: "Checks the TxPool transaction propagation throughput", Config: DefaultConfig(), NewTask: NewTask, } @@ -101,6 +100,10 @@ func (t *Task) Execute(ctx context.Context) error { client := executionClients[rand.Intn(len(executionClients))] + t.logger.Infof("Measuring TxPool transaction propagation *throughput*") + t.logger.Infof("Targeting client: %s, TPS: %d, Duration: %d seconds", + client.GetName(), t.config.TPS, t.config.Duration_s) + conn, err := t.getTcpConn(ctx, client) if err != nil { t.logger.Errorf("Failed to get wire eth TCP connection: %v", err) @@ -110,137 +113,176 @@ func (t *Task) Execute(ctx context.Context) error { defer conn.Close() - var txs []*ethtypes.Transaction + // Wait for the specified seconds before starting the task + if t.config.SecondsBeforeRunning > 0 { + t.logger.Infof("Waiting for %d seconds before starting the task...", t.config.SecondsBeforeRunning) + select { + case <-time.After(time.Duration(t.config.SecondsBeforeRunning) * time.Second): + t.logger.Infof("Starting task after waiting.") + case <-ctx.Done(): + t.logger.Warnf("Task cancelled before starting.") + return ctx.Err() + } + } + + // Prepare to collect transaction latencies + var totNumberOfTxes int = t.config.TPS * t.config.Duration_s + var txs []*ethtypes.Transaction = make([]*ethtypes.Transaction, totNumberOfTxes) + var txStartTime []time.Time = make([]time.Time, totNumberOfTxes) + var testDeadline time.Time = time.Now().Add(time.Duration(t.config.Duration_s+60*30) * time.Second) + var latenciesMus = make([]int64, totNumberOfTxes) startTime := time.Now() isFailed := false sentTxCount := 0 + duplicatedP2PEventCount := 0 + coordinatedOmissionEventCount := 0 + // Start generating and sending transactions go func() { startExecTime := time.Now() - endTime := startExecTime.Add(time.Second) + endTime := startExecTime.Add(time.Second * time.Duration(t.config.Duration_s)) - for i := range t.config.QPS { + // Generate and send transactions + for i := 0; i < totNumberOfTxes; i++ { // Calculate how much time we have left remainingTime := time.Until(endTime) // Calculate sleep time to distribute remaining transactions evenly - sleepTime := remainingTime / time.Duration(t.config.QPS-i) + sleepTime := remainingTime / time.Duration(totNumberOfTxes-i) - // generate and sign tx - go func() { - if ctx.Err() != nil && !isFailed { + // generate and send tx + go func(i int) { + + tx, err := t.generateTransaction(ctx, i) + if err != nil { + t.logger.Errorf("Failed to create transaction: %v", err) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true return } - tx, err := t.generateTransaction(ctx) + txStartTime[i] = time.Now() + err = client.GetRPCClient().SendTransaction(ctx, tx) if err != nil { - t.logger.Errorf("Failed to create transaction: %v", err) + t.logger.WithField("client", client.GetName()).Errorf("Failed to send transaction: %v", err) t.ctx.SetResult(types.TaskResultFailure) isFailed = true return } + txs[i] = tx sentTxCount++ + // log transaction sending if sentTxCount%t.config.MeasureInterval == 0 { elapsed := time.Since(startTime) t.logger.Infof("Sent %d transactions in %.2fs", sentTxCount, elapsed.Seconds()) } - err = client.GetRPCClient().SendTransaction(ctx, tx) - if err != nil { - t.logger.WithField("client", client.GetName()).Errorf("Failed to send transaction: %v", err) - t.ctx.SetResult(types.TaskResultFailure) - isFailed = true - return - } + }(i) - txs = append(txs, tx) - }() + // Sleep to control the TPS + if i < totNumberOfTxes-1 { + if sleepTime > 0 { + time.Sleep(sleepTime) + } else { + coordinatedOmissionEventCount++ + } + } - if isFailed { + select { + case <-ctx.Done(): + t.logger.Warnf("Task cancelled, stopping transaction generation.") return + default: + // if testDeadline reached, stop sending txes + if isFailed { + return + } + if time.Now().After(testDeadline) { + t.logger.Infof("Reached duration limit, stopping transaction generation.") + return + } } - - time.Sleep(sleepTime) } - - execTime := time.Since(startExecTime) - t.logger.Infof("Time to generate %d transactions: %v", t.config.QPS, execTime) }() - lastMeasureTime := time.Now() - gotTx := 0 - - if isFailed { - return nil - } - - for gotTx < t.config.QPS { - if isFailed { - return nil - } - - // Add a timeout of 180 seconds for reading transaction messages - readChan := make(chan struct { - txs *eth.TransactionsPacket - err error - }) - - go func() { - txs, err := conn.ReadTransactionMessages() - readChan <- struct { - txs *eth.TransactionsPacket - err error - }{txs, err} - }() - - select { - case result := <-readChan: - if result.err != nil { - t.logger.Errorf("Failed to read transaction messages: %v", result.err) + // Wait P2P event messages + func() { + var receivedEvents int = 0 + for { + txes, err := conn.ReadTransactionMessages() + if err != nil { + t.logger.Errorf("Failed reading p2p events: %v", err) t.ctx.SetResult(types.TaskResultFailure) - return nil - } - gotTx += len(*result.txs) - case <-time.After(180 * time.Second): - t.logger.Warnf("Timeout after 180 seconds while reading transaction messages. Re-sending transactions...") - - // Calculate how many transactions we're still missing - missingTxCount := t.config.QPS - gotTx - if missingTxCount <= 0 { - break + isFailed = true + return } - // Re-send transactions to the original client - for i := 0; i < missingTxCount && i < len(txs); i++ { - err = client.GetRPCClient().SendTransaction(ctx, txs[i]) + for _, tx := range *txes { + tx_data := tx.Data() + // read tx_data that is in the format "tx_index:" + var tx_index int + _, err := fmt.Sscanf(string(tx_data), "tx_index:%d", &tx_index) if err != nil { - t.logger.WithError(err).Errorf("Failed to re-send transaction message, error: %v", err) + t.logger.Errorf("Failed to parse transaction data: %v", err) + t.ctx.SetResult(types.TaskResultFailure) + isFailed = true + return + } + if tx_index < 0 || tx_index >= totNumberOfTxes { + t.logger.Errorf("Transaction index out of range: %d", tx_index) t.ctx.SetResult(types.TaskResultFailure) - return nil + isFailed = true + return + } + + // log the duplicated p2p events, and count duplicated p2p events + // todo: add a timeout of N seconds that activates if duplicatedP2PEventCount + receivedEvents >= totNumberOfTxes, if exceeded, exit the function + if latenciesMus[tx_index] != 0 { + duplicatedP2PEventCount++ + } + + latenciesMus[tx_index] = time.Since(txStartTime[tx_index]).Microseconds() + receivedEvents++ + + if receivedEvents%t.config.MeasureInterval == 0 { + t.logger.Infof("Received %d p2p events", receivedEvents) } } - t.logger.Infof("Re-sent %d transactions", missingTxCount) - continue - } + if receivedEvents >= totNumberOfTxes { + t.logger.Infof("Reading of p2p events finished") + return + } - if gotTx%t.config.MeasureInterval != 0 { - continue + select { + case <-ctx.Done(): + t.logger.Warnf("Task cancelled, stopping reading p2p events.") + return + default: + // check test deadline + if time.Now().After(testDeadline) { + t.logger.Warnf("Reached duration limit, stopping reading p2p events.") + return + } + } } + }() - t.logger.Infof("Got %d transactions", gotTx) - t.logger.Infof("Tx/s: (%d txs processed): %.2f / s \n", gotTx, float64(t.config.MeasureInterval)*float64(time.Second)/float64(time.Since(lastMeasureTime))) + lastMeasureDelay := time.Since(startTime) + t.logger.Infof("Last measure delay since start time: %s", lastMeasureDelay) - lastMeasureTime = time.Now() + if coordinatedOmissionEventCount > 0 { + t.logger.Warnf("Coordinated omission events: %d", coordinatedOmissionEventCount) } - totalTime := time.Since(startTime) - t.logger.Infof("Total time for %d transactions: %.2fs", sentTxCount, totalTime.Seconds()) + if duplicatedP2PEventCount > 0 { + t.logger.Warnf("Duplicated p2p events: %d", duplicatedP2PEventCount) + } - // send to other clients, for speeding up tx mining + // Send txes to other clients, for speeding up tx mining for _, tx := range txs { for _, otherClient := range executionClients { if otherClient.GetName() == client.GetName() { @@ -251,16 +293,43 @@ func (t *Task) Execute(ctx context.Context) error { } } + // Check if the context was cancelled or other errors occurred + if ctx.Err() != nil && !isFailed { + return nil + } + + // Check if we received all transactions p2p events + notReceivedP2PEventCount := 0 + for i := 0; i < totNumberOfTxes; i++ { + if latenciesMus[i] == 0 { + notReceivedP2PEventCount++ + // Assign a default value for missing P2P events + latenciesMus[i] = (time.Duration(t.config.Duration_s) * time.Second).Microseconds() + } + } + if notReceivedP2PEventCount > 0 { + t.logger.Warnf("Missed p2p events: %d (assigned latency=duration)", notReceivedP2PEventCount) + } + + // Calculate statistics + processed_tx_per_second := float64(sentTxCount) / lastMeasureDelay.Seconds() + + t.ctx.Outputs.SetVar("mean_tps_throughput", processed_tx_per_second) + t.logger.Infof("Processed %d transactions in %.2fs, mean throughput: %.2f tx/s", sentTxCount, lastMeasureDelay.Seconds(), processed_tx_per_second) + t.ctx.Outputs.SetVar("tx_count", totNumberOfTxes) + t.logger.Infof("Sent %d transactions in %.2fs", sentTxCount, lastMeasureDelay.Seconds()) + + t.ctx.SetResult(types.TaskResultSuccess) + outputs := map[string]interface{}{ - "total_time_mus": totalTime.Microseconds(), - "qps": t.config.QPS, + "tx_count": totNumberOfTxes, + "mean_tps_throughput": processed_tx_per_second, + "coordinated_omission_events_count": coordinatedOmissionEventCount, } + outputsJSON, _ := json.Marshal(outputs) t.logger.Infof("outputs_json: %s", string(outputsJSON)) - t.ctx.Outputs.SetVar("total_time_mus", totalTime.Milliseconds()) - t.ctx.SetResult(types.TaskResultSuccess) - return nil } @@ -307,7 +376,7 @@ func (t *Task) getTcpConn(ctx context.Context, client *execution.Client) (*sentr return conn, nil } -func (t *Task) generateTransaction(ctx context.Context) (*ethtypes.Transaction, error) { +func (t *Task) generateTransaction(ctx context.Context, i int) (*ethtypes.Transaction, error) { tx, err := t.wallet.BuildTransaction(ctx, func(_ context.Context, nonce uint64, _ bind.SignerFn) (*ethtypes.Transaction, error) { addr := t.wallet.GetAddress() toAddr := &addr @@ -317,9 +386,7 @@ func (t *Task) generateTransaction(ctx context.Context) (*ethtypes.Transaction, feeCap := &helper.BigInt{Value: *big.NewInt(100000000000)} // 100 Gwei tipCap := &helper.BigInt{Value: *big.NewInt(1000000000)} // 1 Gwei - var txObj ethtypes.TxData - - txObj = ðtypes.DynamicFeeTx{ + txObj := ðtypes.DynamicFeeTx{ ChainID: t.ctx.Scheduler.GetServices().ClientPool().GetExecutionPool().GetBlockCache().GetChainID(), Nonce: nonce, GasTipCap: &tipCap.Value, @@ -327,7 +394,7 @@ func (t *Task) generateTransaction(ctx context.Context) (*ethtypes.Transaction, Gas: 50000, To: toAddr, Value: txAmount, - Data: []byte{}, + Data: []byte(fmt.Sprintf("tx_index:%d", i)), } return ethtypes.NewTx(txObj), nil diff --git a/pkg/coordinator/utils/sentry/conn.go b/pkg/coordinator/utils/sentry/conn.go index afae7b6d..d9c5993e 100644 --- a/pkg/coordinator/utils/sentry/conn.go +++ b/pkg/coordinator/utils/sentry/conn.go @@ -1,6 +1,7 @@ package sentry import ( + "context" "crypto/ecdsa" "encoding/json" "errors" @@ -267,28 +268,54 @@ loop: } // readUntil reads eth protocol messages until a message of the target type is -// received. It returns an error if there is a disconnect, or if the context -// is cancelled before a message of the desired type can be read. -func readUntil[T any](conn *Conn) (*T, error) { - for { - received, err := conn.ReadEth() - if err != nil { - if err == errDisc { - return nil, errDisc +// received. It returns an error if there is a disconnect, timeout expires, +// or if the context is cancelled before a message of the desired type can be read. +func readUntil[T any](conn *Conn, ctx context.Context) (*T, error) { + resultCh := make(chan *T, 1) + errCh := make(chan error, 1) + + go func() { + defer close(resultCh) + defer close(errCh) + + for { + received, err := conn.ReadEth() + if err != nil { + if err == errDisc { + errCh <- errDisc + return + } + continue } - continue - } - switch res := received.(type) { - case *T: - return res, nil + switch res := received.(type) { + case *T: + resultCh <- res + return + } } + }() + + select { + case result := <-resultCh: + return result, nil + case err := <-errCh: + return nil, err + case <-ctx.Done(): + return nil, fmt.Errorf("timeoutExpired") } } // readTransactionMessages reads transaction messages from the connection. -func (conn *Conn) ReadTransactionMessages() (*eth.TransactionsPacket, error) { - return readUntil[eth.TransactionsPacket](conn) +// The timeout parameter is optional - if provided and > 0, the function will timeout after the specified duration. +func (conn *Conn) ReadTransactionMessages(timeout ...time.Duration) (*eth.TransactionsPacket, error) { + ctx := context.Background() + if len(timeout) > 0 && timeout[0] > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout[0]) + defer cancel() + } + return readUntil[eth.TransactionsPacket](conn, ctx) } // dialAs attempts to dial a given node and perform a handshake using the generated diff --git a/pkg/coordinator/web/api/get_task_result_api.go b/pkg/coordinator/web/api/get_task_result_api.go index 188f2ab4..c5a75c66 100644 --- a/pkg/coordinator/web/api/get_task_result_api.go +++ b/pkg/coordinator/web/api/get_task_result_api.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/gorilla/mux" "github.com/noku-team/assertoor/pkg/coordinator/db" "github.com/noku-team/assertoor/pkg/coordinator/types" + "github.com/gorilla/mux" ) // GetTaskResult godoc @@ -92,18 +92,20 @@ func (ah *APIHandler) GetTaskResult(w http.ResponseWriter, r *http.Request) { } // Check if view parameter is set - viewMode := r.URL.Query().Has("view") + downloadMode := r.URL.Query().Has("download") // Determine content type contentType := "application/octet-stream" - if viewMode { + if !downloadMode { ext := strings.ToLower(filepath.Ext(resultFile.Name)) switch ext { case ".txt", ".log", ".yaml", ".yml", ".json", ".md": contentType = "text/plain" case ".html", ".htm": contentType = "text/html" + case ".css": + contentType = "text/css" case ".png": contentType = "image/png" case ".jpg", ".jpeg": @@ -118,7 +120,7 @@ func (ah *APIHandler) GetTaskResult(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", contentType) - if !viewMode { + if downloadMode { w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", resultFile.Name)) } diff --git a/pkg/coordinator/web/handlers/test_run.go b/pkg/coordinator/web/handlers/test_run.go index 707c8949..97bbb2ef 100644 --- a/pkg/coordinator/web/handlers/test_run.go +++ b/pkg/coordinator/web/handlers/test_run.go @@ -252,4 +252,4 @@ func (fh *FrontendHandler) getTestRunPageData(runID uint64) (*TestRunPage, error } return pageData, nil -} \ No newline at end of file +} diff --git a/pkg/coordinator/web/server.go b/pkg/coordinator/web/server.go index 11e04682..a31a028c 100644 --- a/pkg/coordinator/web/server.go +++ b/pkg/coordinator/web/server.go @@ -7,11 +7,11 @@ import ( "net/http" "strings" - "github.com/gorilla/mux" coordinator_types "github.com/noku-team/assertoor/pkg/coordinator/types" "github.com/noku-team/assertoor/pkg/coordinator/web/api" "github.com/noku-team/assertoor/pkg/coordinator/web/handlers" "github.com/noku-team/assertoor/pkg/coordinator/web/types" + "github.com/gorilla/mux" "github.com/sirupsen/logrus" httpSwagger "github.com/swaggo/http-swagger" "github.com/urfave/negroni" @@ -96,6 +96,7 @@ func (ws *Server) ConfigureRoutes(frontendConfig *types.FrontendConfig, apiConfi ws.router.HandleFunc("/api/v1/test_runs/delete", apiHandler.PostTestRunsDelete).Methods("POST") ws.router.HandleFunc("/api/v1/test_run/{runId}/cancel", apiHandler.PostTestRunCancel).Methods("POST") ws.router.HandleFunc("/api/v1/test_run/{runId}/details", apiHandler.GetTestRunDetails).Methods("GET") + ws.router.HandleFunc("/api/v1/test_run/{runId}/task/{taskIndex}/details", apiHandler.GetTestRunTaskDetails).Methods("GET") ws.router.HandleFunc("/api/v1/test_run/{runId}/task/{taskId}/result/{resultType}/{fileId:.*}", apiHandler.GetTaskResult).Methods("GET") } } diff --git a/pkg/coordinator/web/templates/test_run/test_run.html b/pkg/coordinator/web/templates/test_run/test_run.html index 3d356839..95269429 100644 --- a/pkg/coordinator/web/templates/test_run/test_run.html +++ b/pkg/coordinator/web/templates/test_run/test_run.html @@ -1,6 +1,13 @@ {{ define "page" }} -
-

Test Run {{ .RunID }}: {{ .Name }}

+ + + + + + + +
+

Test Run {{ .RunID }}: {{ html "" }}{{ html "" }}

@@ -8,69 +15,64 @@

Test Run {{ .RunID }}: {{ .Name }}

- + - {{ if .IsStarted }} + {{ html "" }} - + - {{ end }} - {{ if .IsCompleted }} + {{ html "" }} + {{ html "" }} - + - {{ end }} + {{ html "" }} - +
Test ID: - {{ .TestID }} -
Test Status: - {{ if eq .Status "pending" }} + {{ html "" }} Pending - {{ else if eq .Status "running" }} + {{ html "" }} + {{ html "" }} Running - {{ else if eq .Status "success" }} + {{ html "" }} + {{ html "" }} Success - {{ else if eq .Status "failure" }} + {{ html "" }} + {{ html "" }} Failed - {{ else if eq .Status "aborted" }} + {{ html "" }} + {{ html "" }} Cancelled - {{ else }} - - {{ .Status }} - - {{ end }} + {{ html "" }} + {{ html "" }} + + {{ html "" }}
Start Time: - {{ formatDateTime .StartTime.UTC }} -
Finish Time: - {{ formatDateTime .StopTime.UTC }} -
Timeout: - {{ .Timeout }} -
@@ -89,241 +91,224 @@
Tasks
- - - {{ $isSecTrimmed := .IsSecTrimmed }} - {{ range $i, $task := .Tasks }} - + + {{ html "" }} +
- {{ range $l, $graph := $task.GraphLevels }} -
- {{ if gt $graph 1 }} + {{ html "" }} +
+ {{ html "" }}
- {{ end }} + {{ html "" }}
- {{ end }} - {{ if $task.HasChildren }} -
-
- {{ end }} + {{ html "" }}
- - {{ $task.Index }} - +
- {{ $task.Name }} - {{ $task.Title }} + + - {{ if $task.HasRunTime }}{{ $task.RunTime }}{{ else }}?{{ end }} - {{ if $task.HasTimeout }} / {{ $task.Timeout }}{{ end }} - {{ if $task.HasCustomRunTime}} - - ({{ $task.CustomRunTime}}) - - {{ end }} + + {{ html "" }} + / + {{ html "" }} + {{ html "" }} + + () + + {{ html "" }} - {{ if eq $task.Result "success" }} + {{ html "" }} - {{ else if eq $task.Result "failure" }} + {{ html "" }} + {{ html "" }} - {{ else }} + {{ html "" }} + {{ html "" }} - {{ end }} + {{ html "" }} - {{ if eq $task.Status "pending" }} + {{ html "" }} - {{ else if eq $task.Status "running" }} + {{ html "" }} + {{ html "" }} - {{ end }} + {{ html "" }} + + + - -
+ {{ html "" }} +
- {{ if $task.IsStarted }} + {{ html "" }} - + - {{ end }} - {{ if $task.IsCompleted }} + {{ html "" }} + {{ html "" }} - + - {{ end }} - {{ if not (eq $task.ResultError "") }} - - - - - {{ end }} + {{ html "" }}
Status: - {{ if eq $task.Status "pending" }} + {{ html "" }} Pending - {{ else if eq $task.Status "running" }} + {{ html "" }} + {{ html "" }} Running - {{ else if eq $task.Status "complete" }} + {{ html "" }} + {{ html "" }} Complete - {{ else }} + {{ html "" }} + {{ html "" }} - {{ $task.Status }} + - {{ end }} + {{ html "" }}
Result: - {{ if eq $task.Result "success" }} + {{ html "" }} Success - {{ else if eq $task.Result "failure" }} + {{ html "" }} + {{ html "" }} Failure - {{ else }} + {{ html "" }} + {{ html "" }} None - {{ end }} + {{ html "" }}
Start Time:{{ formatDateTime $task.StartTime.UTC }}
Finish Time:{{ formatDateTime $task.StopTime.UTC }}
Error Result: -
{{ .ResultError }}
-
- {{ if not $isSecTrimmed }} - {{ if $task.IsStarted }} -